diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d5cd3185..42e7d214f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +## [7.34.3] - 2024.06.18 + +### Fixed +* [`prop-types`]: null-check rootNode before calling getScope ([#3762][] @crnhrv) +* [`boolean-prop-naming`]: avoid a crash with a spread prop ([#3733][] @ljharb) +* [`jsx-boolean-value`]: `assumeUndefinedIsFalse` with `never` must not allow explicit `true` value ([#3757][] @6uliver) +* [`no-object-type-as-default-prop`]: enable rule for components with many parameters ([#3768][] @JulienR1) +* [`jsx-key`]: incorrect behavior for checkKeyMustBeforeSpread with map callbacks ([#3769][] @akulsr0) + +[7.34.3]: https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.34.2...v7.34.3 +[#3769]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3769 +[#3768]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3768 +[#3762]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3762 +[#3757]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3757 +[#3733]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3733 + ## [7.34.2] - 2024.05.24 ### Fixed diff --git a/lib/rules/boolean-prop-naming.js b/lib/rules/boolean-prop-naming.js index f64da93242..e6ea400025 100644 --- a/lib/rules/boolean-prop-naming.js +++ b/lib/rules/boolean-prop-naming.js @@ -399,7 +399,7 @@ module.exports = { } if (propType) { - [].concat(propType).forEach((prop) => { + [].concat(propType).filter(Boolean).forEach((prop) => { validatePropNaming( component.node, prop.properties || prop.members || prop.body diff --git a/lib/rules/jsx-boolean-value.js b/lib/rules/jsx-boolean-value.js index 1cff171147..4572e7ff8d 100644 --- a/lib/rules/jsx-boolean-value.js +++ b/lib/rules/jsx-boolean-value.js @@ -132,7 +132,6 @@ module.exports = { } if ( isNever(configuration, exceptions, propName) - && !configObject.assumeUndefinedIsFalse && value && value.type === 'JSXExpressionContainer' && value.expression.value === true diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js index feee7ad5a0..825d21f4bb 100644 --- a/lib/rules/jsx-key.js +++ b/lib/rules/jsx-key.js @@ -73,11 +73,31 @@ module.exports = { const reactPragma = pragmaUtil.getFromContext(context); const fragmentPragma = pragmaUtil.getFragmentFromContext(context); + function isKeyAfterSpread(attributes) { + let hasFoundSpread = false; + return attributes.some((attribute) => { + if (attribute.type === 'JSXSpreadAttribute') { + hasFoundSpread = true; + return false; + } + if (attribute.type !== 'JSXAttribute') { + return false; + } + return hasFoundSpread && propName(attribute) === 'key'; + }); + } + function checkIteratorElement(node) { - if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) { - report(context, messages.missingIterKey, 'missingIterKey', { - node, - }); + if (node.type === 'JSXElement') { + if (!hasProp(node.openingElement.attributes, 'key')) { + report(context, messages.missingIterKey, 'missingIterKey', { node }); + } else { + const attrs = node.openingElement.attributes; + + if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) { + report(context, messages.keyBeforeSpread, 'keyBeforeSpread', { node }); + } + } } else if (checkFragmentShorthand && node.type === 'JSXFragment') { report(context, messages.missingIterKeyUsePrag, 'missingIterKeyUsePrag', { node, @@ -115,20 +135,6 @@ module.exports = { return returnStatements; } - function isKeyAfterSpread(attributes) { - let hasFoundSpread = false; - return attributes.some((attribute) => { - if (attribute.type === 'JSXSpreadAttribute') { - hasFoundSpread = true; - return false; - } - if (attribute.type !== 'JSXAttribute') { - return false; - } - return hasFoundSpread && propName(attribute) === 'key'; - }); - } - /** * Checks if the given node is a function expression or arrow function, * and checks if there is a missing key prop in return statement's arguments diff --git a/lib/rules/no-object-type-as-default-prop.js b/lib/rules/no-object-type-as-default-prop.js index 7be98cb4b3..42012413e1 100644 --- a/lib/rules/no-object-type-as-default-prop.js +++ b/lib/rules/no-object-type-as-default-prop.js @@ -30,7 +30,7 @@ const messages = { function hasUsedObjectDestructuringSyntax(params) { return ( params != null - && params.length === 1 + && params.length >= 1 && params[0].type === 'ObjectPattern' ); } diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 6f879e2347..22063afb8e 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -372,7 +372,8 @@ module.exports = function propTypesInstructions(context, components, utils) { */ function resolveValueForIdentifierNode(node, rootNode, callback) { if ( - node + rootNode + && node && node.type === 'Identifier' ) { const scope = getScope(context, rootNode); diff --git a/package.json b/package.json index 4f5155c832..ba818e4eec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react", - "version": "7.34.2", + "version": "7.34.3", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", @@ -29,7 +29,7 @@ "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", @@ -45,12 +45,12 @@ "string.prototype.matchall": "^4.0.11" }, "devDependencies": { - "@babel/core": "^7.24.6", - "@babel/eslint-parser": "^7.24.6", - "@babel/plugin-syntax-decorators": "^7.24.6", - "@babel/plugin-syntax-do-expressions": "^7.24.6", - "@babel/plugin-syntax-function-bind": "^7.24.6", - "@babel/preset-react": "^7.24.6", + "@babel/core": "^7.24.7", + "@babel/eslint-parser": "^7.24.7", + "@babel/plugin-syntax-decorators": "^7.24.7", + "@babel/plugin-syntax-do-expressions": "^7.24.7", + "@babel/plugin-syntax-function-bind": "^7.24.7", + "@babel/preset-react": "^7.24.7", "@types/eslint": "=7.2.10", "@types/estree": "0.0.52", "@types/node": "^4.9.5", @@ -59,11 +59,11 @@ "babel-eslint": "^8 || ^9 || ^10.1.0", "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint-config-airbnb-base": "^15.0.0", - "eslint-doc-generator": "^1.7.0", + "eslint-doc-generator": "^1.7.1", "eslint-plugin-eslint-plugin": "^2.3.0 || ^3.5.3 || ^4.0.1 || ^5.0.5", "eslint-plugin-import": "^2.29.1", "eslint-remote-tester": "^3.0.1", - "eslint-remote-tester-repositories": "^1.0.1", + "eslint-remote-tester-repositories": "^2.0.0", "eslint-scope": "^3.7.3", "espree": "^3.5.4", "glob": "=10.3.7", diff --git a/tests/lib/rules/boolean-prop-naming.js b/tests/lib/rules/boolean-prop-naming.js index 2dec7ed70c..10ff9097f2 100644 --- a/tests/lib/rules/boolean-prop-naming.js +++ b/tests/lib/rules/boolean-prop-naming.js @@ -415,7 +415,6 @@ ruleTester.run('boolean-prop-naming', rule, { `, options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }], features: ['ts'], - errors: [], }, { code: ` @@ -426,7 +425,6 @@ ruleTester.run('boolean-prop-naming', rule, { `, options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }], features: ['types'], - errors: [], }, { code: ` @@ -439,7 +437,6 @@ ruleTester.run('boolean-prop-naming', rule, { `, options: [{ rule: '(is|has)[A-Z]([A-Za-z0-9]?)+' }], features: ['types'], - errors: [], }, { code: ` @@ -451,7 +448,6 @@ ruleTester.run('boolean-prop-naming', rule, { `, options: [{ rule: '^is[A-Z]([A-Za-z0-9]?)+' }], features: ['types'], - errors: [], }, { code: ` @@ -465,7 +461,6 @@ ruleTester.run('boolean-prop-naming', rule, { `, options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], features: ['types'], - errors: [], }, { code: ` @@ -479,7 +474,6 @@ ruleTester.run('boolean-prop-naming', rule, { `, options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], features: ['types'], - errors: [], }, { code: ` @@ -495,7 +489,21 @@ ruleTester.run('boolean-prop-naming', rule, { `, options: [{ rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], features: ['types'], - errors: [], + }, + { + code: ` + export const DataRow = (props: { label: string; value: string; } & React.HTMLAttributes) => { + const { label, value, ...otherProps } = props; + return ( +
+ {label} + {value} +
+ ); + }; + `, + options: [{ rule: '(^(is|has|should|without)[A-Z]([A-Za-z0-9]?)+|disabled|required|checked|defaultChecked)' }], + features: ['types'], }, ]), diff --git a/tests/lib/rules/jsx-boolean-value.js b/tests/lib/rules/jsx-boolean-value.js index e8cbbb3e7f..f4216f2ce1 100644 --- a/tests/lib/rules/jsx-boolean-value.js +++ b/tests/lib/rules/jsx-boolean-value.js @@ -52,6 +52,10 @@ ruleTester.run('jsx-boolean-value', rule, { code: ';', options: ['never', { assumeUndefinedIsFalse: true }], }, + { + code: ';', + options: ['never', { assumeUndefinedIsFalse: false }], + }, { code: ';', options: ['never', { assumeUndefinedIsFalse: true, always: ['foo'] }], @@ -145,6 +149,21 @@ ruleTester.run('jsx-boolean-value', rule, { }, ], }, + { + code: ';', + output: ';', + options: ['never', { assumeUndefinedIsFalse: true }], + errors: [ + { + messageId: 'omitBoolean', + data: { propName: 'foo' }, + }, + { + messageId: 'omitPropAndBoolean', + data: { propName: 'bak' }, + }, + ], + }, { code: ';', output: ';', diff --git a/tests/lib/rules/jsx-key.js b/tests/lib/rules/jsx-key.js index 17109e4215..748e9a2fd9 100644 --- a/tests/lib/rules/jsx-key.js +++ b/tests/lib/rules/jsx-key.js @@ -409,5 +409,20 @@ ruleTester.run('jsx-key', rule, { { messageId: 'missingIterKey' }, ], }, + { + code: ` + const TestCase = () => { + const list = [1, 2, 3, 4, 5]; + + return ( +
+ {list.map(x =>
)} +
+ ); + }; + `, + options: [{ checkKeyMustBeforeSpread: true }], + errors: [{ messageId: 'keyBeforeSpread' }], + }, ]), }); diff --git a/tests/lib/rules/no-object-type-as-default-prop.js b/tests/lib/rules/no-object-type-as-default-prop.js index e2660fb938..b515ef6a49 100644 --- a/tests/lib/rules/no-object-type-as-default-prop.js +++ b/tests/lib/rules/no-object-type-as-default-prop.js @@ -143,6 +143,11 @@ ruleTester.run('no-object-type-as-default-prop', rule, { return null; }; `, + ` + const Foo = ({bar = 1}, context) => { + return null; + }; + `, ` export default function NotAComponent({foo = {}}) {} ` @@ -183,6 +188,24 @@ ruleTester.run('no-object-type-as-default-prop', rule, { } `, errors: expectedViolations, + }, + { + code: ` + const Foo = ({ + a = {}, + b = ['one', 'two'], + c = /regex/i, + d = () => {}, + e = function() {}, + f = class {}, + g = new Thing(), + h = , + i = Symbol('foo') + }, context) => { + return null; + } + `, + errors: expectedViolations, } )), }); diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index df762d74ef..97206b7d44 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3361,6 +3361,20 @@ ruleTester.run('prop-types', rule, { `, features: ['ts', 'no-babel'], }, + { + code: ` + import React from "react"; + + const returnTypeProp = (someProp: any) => ({ someProp }); + + const SomeComponent: React.FunctionComponent< + ReturnType + > = ({ someProp }) => { + return
{someProp}
; + }; + `, + features: ['ts', 'no-babel'], + }, { code: ` export const EuiSuperSelectControl: ( @@ -7840,6 +7854,26 @@ ruleTester.run('prop-types', rule, { ], features: ['ts', 'no-babel'], }, + { + code: ` + import React from "react"; + + const returnTypeProp = (someProp: any) => ({ someProp }); + + const SomeComponent: React.FunctionComponent< + ReturnType + > = ({ someIncorrectProp }) => { + return
{someProp}
; + }; + `, + errors: [ + { + messageId: 'missingPropType', + data: { name: 'someIncorrectProp' }, + }, + ], + features: ['ts', 'no-babel'], + }, { code: ` import React from 'react';