From 0a8092f61a2fed237023a50e2382f59be0b5918c Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Tue, 9 Apr 2024 11:36:44 +0900 Subject: [PATCH 1/2] [types] add jsdoc type annotations --- CHANGELOG.md | 2 + lib/rules/button-has-type.js | 10 ++++- .../checked-requires-onchange-or-readonly.js | 3 +- lib/rules/forbid-elements.js | 9 ++--- lib/rules/forbid-foreign-prop-types.js | 9 ++++- lib/rules/forbid-prop-types.js | 19 ++++++--- lib/rules/hook-use-state.js | 1 + lib/rules/jsx-closing-bracket-location.js | 1 + lib/rules/jsx-curly-spacing.js | 1 + lib/rules/jsx-equals-spacing.js | 1 + lib/rules/jsx-fragments.js | 6 +-- lib/rules/jsx-indent-props.js | 1 + lib/rules/jsx-indent.js | 1 + lib/rules/jsx-no-bind.js | 8 ++-- lib/rules/jsx-no-leaked-render.js | 3 -- lib/rules/jsx-sort-default-props.js | 3 +- lib/rules/jsx-space-before-closing.js | 3 +- lib/rules/no-access-state-in-setstate.js | 39 +++++++++++++------ lib/rules/no-adjacent-inline-elements.js | 3 +- lib/rules/no-children-prop.js | 12 ++++-- lib/rules/no-danger-with-children.js | 2 + lib/rules/no-deprecated.js | 24 +++++++++--- lib/rules/no-find-dom-node.js | 9 +++-- lib/rules/no-invalid-html-attribute.js | 3 +- lib/rules/no-is-mounted.js | 6 ++- lib/rules/no-render-return-value.js | 3 +- lib/rules/no-unknown-property.js | 3 +- .../no-unused-class-component-methods.js | 3 +- lib/rules/no-unused-state.js | 8 +++- lib/rules/sort-default-props.js | 3 +- lib/rules/style-prop-object.js | 14 +++++-- lib/rules/void-dom-elements-no-children.js | 5 ++- 32 files changed, 154 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd1e7de37..43e05c494a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,10 +18,12 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange * [Tests] add @typescript-eslint/parser v6 ([#3629][] @HenryBrown0) * [Tests] add @typescript-eslint/parser v7 and v8 ([#3629][] @hampustagerud) * [Docs] [`no-danger`]: update broken link ([#3817][] @lucasrmendonca) +* [types] add jsdoc type annotations ([#3731][] @y-hsgw) [#3632]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3632 [#3812]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3812 +[#3731]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3731 [#3629]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3629 [#3817]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3817 [#3807]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3807 diff --git a/lib/rules/button-has-type.js b/lib/rules/button-has-type.js index d40596067d..a6fd41cbfe 100644 --- a/lib/rules/button-has-type.js +++ b/lib/rules/button-has-type.js @@ -28,6 +28,7 @@ const messages = { forbiddenValue: '"{{value}}" is an invalid value for button type attribute', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -149,14 +150,19 @@ module.exports = { } const props = node.arguments[1].properties; - const typeProp = props.find((prop) => prop.key && prop.key.name === 'type'); + const typeProp = props.find((prop) => ( + 'key' in prop + && prop.key + && 'name' in prop.key + && prop.key.name === 'type' + )); if (!typeProp) { reportMissing(node); return; } - checkExpression(node, typeProp.value); + checkExpression(node, 'value' in typeProp ? typeProp.value : undefined); }, }; }, diff --git a/lib/rules/checked-requires-onchange-or-readonly.js b/lib/rules/checked-requires-onchange-or-readonly.js index c719644e1c..d67449ea51 100644 --- a/lib/rules/checked-requires-onchange-or-readonly.js +++ b/lib/rules/checked-requires-onchange-or-readonly.js @@ -24,7 +24,7 @@ const defaultOptions = { }; /** - * @param {string[]} properties + * @param {object[]} properties * @param {string} keyName * @returns {Set} */ @@ -41,6 +41,7 @@ function extractTargetProps(properties, keyName) { ); } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/forbid-elements.js b/lib/rules/forbid-elements.js index 6d672cf70d..f08ef1d1e7 100644 --- a/lib/rules/forbid-elements.js +++ b/lib/rules/forbid-elements.js @@ -20,6 +20,7 @@ const messages = { forbiddenElement_message: '<{{element}}> is forbidden, {{message}}', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -105,13 +106,11 @@ module.exports = { return; } - const argType = argument.type; - - if (argType === 'Identifier' && /^[A-Z_]/.test(argument.name)) { + if (argument.type === 'Identifier' && /^[A-Z_]/.test(argument.name)) { reportIfForbidden(argument.name, argument); - } else if (argType === 'Literal' && /^[a-z][^.]*$/.test(argument.value)) { + } else if (argument.type === 'Literal' && /^[a-z][^.]*$/.test(String(argument.value))) { reportIfForbidden(argument.value, argument); - } else if (argType === 'MemberExpression') { + } else if (argument.type === 'MemberExpression') { reportIfForbidden(getText(context, argument), argument); } }, diff --git a/lib/rules/forbid-foreign-prop-types.js b/lib/rules/forbid-foreign-prop-types.js index 5d91bf07e3..7724049af4 100644 --- a/lib/rules/forbid-foreign-prop-types.js +++ b/lib/rules/forbid-foreign-prop-types.js @@ -13,6 +13,7 @@ const messages = { forbiddenPropType: 'Using propTypes from another component is not safe because they may be removed in production builds', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -108,7 +109,9 @@ module.exports = { && !ast.isAssignmentLHS(node) && !isAllowedAssignment(node) )) || ( + // @ts-expect-error The JSXText type is not present in the estree type definitions (node.property.type === 'Literal' || node.property.type === 'JSXText') + && 'value' in node.property && node.property.value === 'propTypes' && !ast.isAssignmentLHS(node) && !isAllowedAssignment(node) @@ -121,7 +124,11 @@ module.exports = { }, ObjectPattern(node) { - const propTypesNode = node.properties.find((property) => property.type === 'Property' && property.key.name === 'propTypes'); + const propTypesNode = node.properties.find((property) => ( + property.type === 'Property' + && 'name' in property.key + && property.key.name === 'propTypes' + )); if (propTypesNode) { report(context, messages.forbiddenPropType, 'forbiddenPropType', { diff --git a/lib/rules/forbid-prop-types.js b/lib/rules/forbid-prop-types.js index 0ef23c4715..df40706d00 100644 --- a/lib/rules/forbid-prop-types.js +++ b/lib/rules/forbid-prop-types.js @@ -26,6 +26,7 @@ const messages = { forbiddenPropType: 'Prop type "{{target}}" is forbidden', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -192,7 +193,9 @@ module.exports = { } if (node.specifiers.length >= 1) { const propTypesSpecifier = node.specifiers.find((specifier) => ( - specifier.imported && specifier.imported.name === 'PropTypes' + 'imported' in specifier + && specifier.imported + && specifier.imported.name === 'PropTypes' )); if (propTypesSpecifier) { propTypesPackageName = propTypesSpecifier.local.name; @@ -228,12 +231,13 @@ module.exports = { return; } - checkNode(node.parent.right); + checkNode('right' in node.parent && node.parent.right); }, CallExpression(node) { if ( - node.callee.object + node.callee.type === 'MemberExpression' + && node.callee.object && !isPropTypesPackage(node.callee.object) && !propsUtil.isPropTypesDeclaration(node.callee) ) { @@ -242,9 +246,12 @@ module.exports = { if ( node.arguments.length > 0 - && (node.callee.name === 'shape' || astUtil.getPropertyName(node.callee) === 'shape') + && ( + ('name' in node.callee && node.callee.name === 'shape') + || astUtil.getPropertyName(node.callee) === 'shape' + ) ) { - checkProperties(node.arguments[0].properties); + checkProperties('properties' in node.arguments[0] && node.arguments[0].properties); } }, @@ -267,7 +274,7 @@ module.exports = { ObjectExpression(node) { node.properties.forEach((property) => { - if (!property.key) { + if (!('key' in property) || !property.key) { return; } diff --git a/lib/rules/hook-use-state.js b/lib/rules/hook-use-state.js index 938802d82b..2d1cf681cd 100644 --- a/lib/rules/hook-use-state.js +++ b/lib/rules/hook-use-state.js @@ -26,6 +26,7 @@ const messages = { suggestMemo: 'Replace useState call with useMemo', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/jsx-closing-bracket-location.js b/lib/rules/jsx-closing-bracket-location.js index f3286826b3..eed661e944 100644 --- a/lib/rules/jsx-closing-bracket-location.js +++ b/lib/rules/jsx-closing-bracket-location.js @@ -20,6 +20,7 @@ const messages = { bracketLocation: 'The closing bracket must be {{location}}{{details}}', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/jsx-curly-spacing.js b/lib/rules/jsx-curly-spacing.js index 71bf901904..fda49381ce 100644 --- a/lib/rules/jsx-curly-spacing.js +++ b/lib/rules/jsx-curly-spacing.js @@ -35,6 +35,7 @@ const messages = { spaceNeededBefore: 'A space is required before \'{{token}}\'', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/jsx-equals-spacing.js b/lib/rules/jsx-equals-spacing.js index 3424961a9d..6920fb24bf 100644 --- a/lib/rules/jsx-equals-spacing.js +++ b/lib/rules/jsx-equals-spacing.js @@ -20,6 +20,7 @@ const messages = { needSpaceAfter: 'A space is required after \'=\'', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/jsx-fragments.js b/lib/rules/jsx-fragments.js index 4dadb076d7..6835331025 100644 --- a/lib/rules/jsx-fragments.js +++ b/lib/rules/jsx-fragments.js @@ -22,12 +22,12 @@ function replaceNode(source, node, text) { } const messages = { - fragmentsNotSupported: 'Fragments are only supported starting from React v16.2. ' - + 'Please disable the `react/jsx-fragments` rule in `eslint` settings or upgrade your version of React.', + fragmentsNotSupported: 'Fragments are only supported starting from React v16.2. Please disable the `react/jsx-fragments` rule in `eslint` settings or upgrade your version of React.', preferPragma: 'Prefer {{react}}.{{fragment}} over fragment shorthand', preferFragment: 'Prefer fragment shorthand over {{react}}.{{fragment}}', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -170,7 +170,7 @@ module.exports = { ImportDeclaration(node) { if (node.source && node.source.value === 'react') { node.specifiers.forEach((spec) => { - if (spec.imported && spec.imported.name === fragmentPragma) { + if ('imported' in spec && spec.imported && spec.imported.name === fragmentPragma) { if (spec.local) { fragmentNames.add(spec.local.name); } diff --git a/lib/rules/jsx-indent-props.js b/lib/rules/jsx-indent-props.js index 4e7c048511..8504878731 100644 --- a/lib/rules/jsx-indent-props.js +++ b/lib/rules/jsx-indent-props.js @@ -45,6 +45,7 @@ const messages = { wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js index 243a17b485..f27eca029d 100644 --- a/lib/rules/jsx-indent.js +++ b/lib/rules/jsx-indent.js @@ -50,6 +50,7 @@ const messages = { wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js index 93b53414ac..2e8aa687dc 100644 --- a/lib/rules/jsx-no-bind.js +++ b/lib/rules/jsx-no-bind.js @@ -25,6 +25,7 @@ const messages = { func: 'JSX props should not use functions', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -116,7 +117,7 @@ module.exports = { /** * @param {string | number} violationType - * @param {any} variableName + * @param {unknown} variableName * @param {string | number} blockStart */ function addVariableNameToSet(violationType, variableName, blockStart) { @@ -175,11 +176,10 @@ module.exports = { if ( blockAncestors.length > 0 && variableViolationType + && 'kind' in node.parent && node.parent.kind === 'const' // only support const right now ) { - addVariableNameToSet( - variableViolationType, node.id.name, blockAncestors[0].range[0] - ); + addVariableNameToSet(variableViolationType, 'name' in node.id ? node.id.name : undefined, blockAncestors[0].range[0]); } }, diff --git a/lib/rules/jsx-no-leaked-render.js b/lib/rules/jsx-no-leaked-render.js index 71c44e7b29..1e271b2a68 100644 --- a/lib/rules/jsx-no-leaked-render.js +++ b/lib/rules/jsx-no-leaked-render.js @@ -109,9 +109,6 @@ function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNod throw new TypeError('Invalid value for "validStrategies" option'); } -/** - * @type {import('eslint').Rule.RuleModule} - */ /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { diff --git a/lib/rules/jsx-sort-default-props.js b/lib/rules/jsx-sort-default-props.js index 0de8f221c3..f990f77189 100644 --- a/lib/rules/jsx-sort-default-props.js +++ b/lib/rules/jsx-sort-default-props.js @@ -25,6 +25,7 @@ const messages = { propsNotSorted: 'Default prop types declarations should be sorted alphabetically', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { deprecated: true, @@ -178,7 +179,7 @@ module.exports = { return; } - checkNode(node.parent.right); + checkNode('right' in node.parent && node.parent.right); }, Program() { diff --git a/lib/rules/jsx-space-before-closing.js b/lib/rules/jsx-space-before-closing.js index e923adfbd3..ffcc357d8b 100644 --- a/lib/rules/jsx-space-before-closing.js +++ b/lib/rules/jsx-space-before-closing.js @@ -23,6 +23,7 @@ const messages = { needSpaceBeforeClose: 'A space is required before closing bracket', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { deprecated: true, @@ -58,7 +59,7 @@ module.exports = { const sourceCode = getSourceCode(context); const leftToken = getTokenBeforeClosingBracket(node); - const closingSlash = sourceCode.getTokenAfter(leftToken); + const closingSlash = /** @type {import("eslint").AST.Token} */ (sourceCode.getTokenAfter(leftToken)); if (leftToken.loc.end.line !== closingSlash.loc.start.line) { return; diff --git a/lib/rules/no-access-state-in-setstate.js b/lib/rules/no-access-state-in-setstate.js index d1db7ba422..7126517112 100644 --- a/lib/rules/no-access-state-in-setstate.js +++ b/lib/rules/no-access-state-in-setstate.js @@ -19,6 +19,7 @@ const messages = { useCallback: 'Use callback in setState when referencing the previous state.', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -73,12 +74,12 @@ module.exports = { // Appends all the methods that are calling another // method containing this.state to the methods array methods.forEach((method) => { - if (node.callee.name === method.methodName) { + if ('name' in node.callee && node.callee.name === method.methodName) { let current = node.parent; while (current.type !== 'Program') { if (current.type === 'MethodDefinition') { methods.push({ - methodName: current.key.name, + methodName: 'name' in current.key ? current.key.name : undefined, node: method.node, }); break; @@ -93,7 +94,7 @@ module.exports = { let current = node.parent; while (current.type !== 'Program') { if (isFirstArgumentInSetStateCall(current, node)) { - const methodName = node.callee.name; + const methodName = 'name' in node.callee ? node.callee.name : undefined; methods.forEach((method) => { if (method.methodName === methodName) { report(context, messages.useCallback, 'useCallback', { @@ -110,10 +111,12 @@ module.exports = { MemberExpression(node) { if ( - node.property.name === 'state' + 'name' in node.property + && node.property.name === 'state' && node.object.type === 'ThisExpression' && isClassComponent(node) ) { + /** @type {import("eslint").Rule.Node} */ let current = node; while (current.type !== 'Program') { // Reporting if this.state is directly within this.setState @@ -127,13 +130,17 @@ module.exports = { // Storing all functions and methods that contains this.state if (current.type === 'MethodDefinition') { methods.push({ - methodName: current.key.name, + methodName: 'name' in current.key ? current.key.name : undefined, node, }); break; - } else if (current.type === 'FunctionExpression' && current.parent.key) { + } else if ( + current.type === 'FunctionExpression' + && 'key' in current.parent + && current.parent.key + ) { methods.push({ - methodName: current.parent.key.name, + methodName: 'name' in current.parent.key ? current.parent.key.name : undefined, node, }); break; @@ -144,7 +151,7 @@ module.exports = { vars.push({ node, scope: getScope(context, node), - variableName: current.id.name, + variableName: 'name' in current.id ? current.id.name : undefined, }); break; } @@ -156,13 +163,14 @@ module.exports = { Identifier(node) { // Checks if the identifier is a variable within an object + /** @type {import("eslint").Rule.Node} */ let current = node; while (current.parent.type === 'BinaryExpression') { current = current.parent; } if ( - current.parent.value === current - || current.parent.object === current + ('value' in current.parent && current.parent.value === current) + || ('object' in current.parent && current.parent.object === current) ) { while (current.type !== 'Program') { if (isFirstArgumentInSetStateCall(current, node)) { @@ -180,9 +188,16 @@ module.exports = { }, ObjectPattern(node) { - const isDerivedFromThis = node.parent.init && node.parent.init.type === 'ThisExpression'; + const isDerivedFromThis = 'init' in node.parent && node.parent.init && node.parent.init.type === 'ThisExpression'; node.properties.forEach((property) => { - if (property && property.key && property.key.name === 'state' && isDerivedFromThis) { + if ( + property + && 'key' in property + && property.key + && 'name' in property.key + && property.key.name === 'state' + && isDerivedFromThis + ) { vars.push({ node: property.key, scope: getScope(context, node), diff --git a/lib/rules/no-adjacent-inline-elements.js b/lib/rules/no-adjacent-inline-elements.js index 9d096d97b2..8110d8563b 100644 --- a/lib/rules/no-adjacent-inline-elements.js +++ b/lib/rules/no-adjacent-inline-elements.js @@ -77,6 +77,7 @@ const messages = { inlineElement: 'Child elements which render as inline HTML elements should be separated by a space or wrapped in block level elements.', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -118,7 +119,7 @@ module.exports = { if (node.arguments.length < 2 || !node.arguments[2]) { return; } - const children = node.arguments[2].elements; + const children = 'elements' in node.arguments[2] ? node.arguments[2].elements : undefined; validate(node, children); }, }; diff --git a/lib/rules/no-children-prop.js b/lib/rules/no-children-prop.js index 3ccaf53729..84ccbbf3a6 100644 --- a/lib/rules/no-children-prop.js +++ b/lib/rules/no-children-prop.js @@ -37,6 +37,7 @@ const messages = { passFunctionAsArgs: 'Do not pass a function as an additional argument to React.createElement. Instead, pass it as a prop.', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -86,11 +87,16 @@ module.exports = { return; } - const props = node.arguments[1].properties; - const childrenProp = props.find((prop) => prop.key && prop.key.name === 'children'); + const props = 'properties' in node.arguments[1] ? node.arguments[1].properties : undefined; + const childrenProp = props.find((prop) => ( + 'key' in prop + && prop.key + && 'name' in prop.key + && prop.key.name === 'children' + )); if (childrenProp) { - if (childrenProp.value && !isFunction(childrenProp.value)) { + if ('value' in childrenProp && childrenProp.value && !isFunction(childrenProp.value)) { report(context, messages.passChildrenAsArgs, 'passChildrenAsArgs', { node, }); diff --git a/lib/rules/no-danger-with-children.js b/lib/rules/no-danger-with-children.js index edf5073a71..3b01998502 100644 --- a/lib/rules/no-danger-with-children.js +++ b/lib/rules/no-danger-with-children.js @@ -17,6 +17,7 @@ const messages = { dangerWithChildren: 'Only set one of `children` or `props.dangerouslySetInnerHTML`', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -119,6 +120,7 @@ module.exports = { if ( node.callee && node.callee.type === 'MemberExpression' + && 'name' in node.callee.property && node.callee.property.name === 'createElement' && node.arguments.length > 1 ) { diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js index 462bd4c756..9f5ddf7082 100644 --- a/lib/rules/no-deprecated.js +++ b/lib/rules/no-deprecated.js @@ -115,6 +115,7 @@ const messages = { deprecated: '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}{{refs}}', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -226,18 +227,24 @@ module.exports = { if (!isReactImport) { return; } - node.specifiers.filter(((s) => s.imported)).forEach((specifier) => { - checkDeprecation(node, `${MODULES[node.source.value][0]}.${specifier.imported.name}`, specifier); + node.specifiers.filter(((s) => 'imported' in s && s.imported)).forEach((specifier) => { + // TODO, semver-major: remove `in` check as part of jsdoc->tsdoc migration + checkDeprecation(node, 'imported' in specifier && `${MODULES[node.source.value][0]}.${specifier.imported.name}`, specifier); }); }, VariableDeclarator(node) { const reactModuleName = getReactModuleName(node); - const isRequire = node.init && node.init.callee && node.init.callee.name === 'require'; + const isRequire = node.init + && 'callee' in node.init + && node.init.callee + && 'name' in node.init.callee + && node.init.callee.name === 'require'; const isReactRequire = node.init + && 'arguments' in node.init && node.init.arguments && node.init.arguments.length - && typeof MODULES[node.init.arguments[0].value] !== 'undefined'; + && typeof MODULES['value' in node.init.arguments[0] ? node.init.arguments[0].value : undefined] !== 'undefined'; const isDestructuring = node.id && node.id.type === 'ObjectPattern'; if ( @@ -246,8 +253,13 @@ module.exports = { ) { return; } - node.id.properties.filter((p) => p.type !== 'RestElement' && p.key).forEach((property) => { - checkDeprecation(node, `${reactModuleName || pragma}.${property.key.name}`, property); + + ('properties' in node.id ? node.id.properties : undefined).filter((p) => p.type !== 'RestElement' && p.key).forEach((property) => { + checkDeprecation( + node, + 'key' in property && 'name' in property.key && `${reactModuleName || pragma}.${property.key.name}`, + property + ); }); }, diff --git a/lib/rules/no-find-dom-node.js b/lib/rules/no-find-dom-node.js index 3d9ef52ef8..eaf9e53a46 100644 --- a/lib/rules/no-find-dom-node.js +++ b/lib/rules/no-find-dom-node.js @@ -16,6 +16,7 @@ const messages = { noFindDOMNode: 'Do not use findDOMNode. It doesn’t work with function components and is deprecated in StrictMode. See https://reactjs.org/docs/react-dom.html#finddomnode', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -35,12 +36,14 @@ module.exports = { CallExpression(node) { const callee = node.callee; - const isfindDOMNode = callee.name === 'findDOMNode' || ( - callee.property + const isFindDOMNode = ('name' in callee && callee.name === 'findDOMNode') || ( + 'property' in callee + && callee.property + && 'name' in callee.property && callee.property.name === 'findDOMNode' ); - if (!isfindDOMNode) { + if (!isFindDOMNode) { return; } diff --git a/lib/rules/no-invalid-html-attribute.js b/lib/rules/no-invalid-html-attribute.js index 0f018f8080..b8a14d1ab1 100644 --- a/lib/rules/no-invalid-html-attribute.js +++ b/lib/rules/no-invalid-html-attribute.js @@ -589,6 +589,7 @@ function checkCreateProps(context, node, attribute) { } } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -638,7 +639,7 @@ module.exports = { } // ignore non-HTML elements - if (!HTML_ELEMENTS.has(elemNameArg.value)) { + if (typeof elemNameArg.value === 'string' && !HTML_ELEMENTS.has(elemNameArg.value)) { return; } diff --git a/lib/rules/no-is-mounted.js b/lib/rules/no-is-mounted.js index fd99d5b683..02a100251a 100644 --- a/lib/rules/no-is-mounted.js +++ b/lib/rules/no-is-mounted.js @@ -17,6 +17,7 @@ const messages = { noIsMounted: 'Do not use isMounted', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -38,7 +39,10 @@ module.exports = { if (callee.type !== 'MemberExpression') { return; } - if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'isMounted') { + if ( + callee.object.type !== 'ThisExpression' + && (!('name' in callee.property) || callee.property.name !== 'isMounted') + ) { return; } const ancestors = getAncestors(context, node); diff --git a/lib/rules/no-render-return-value.js b/lib/rules/no-render-return-value.js index 61818a92aa..9e0f9d4046 100644 --- a/lib/rules/no-render-return-value.js +++ b/lib/rules/no-render-return-value.js @@ -17,6 +17,7 @@ const messages = { noReturnValue: 'Do not depend on the return value from {{node}}.render', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -56,7 +57,7 @@ module.exports = { if ( callee.object.type !== 'Identifier' || !calleeObjectName.test(callee.object.name) - || callee.property.name !== 'render' + || (!('name' in callee.property) || callee.property.name !== 'render') ) { return; } diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js index 50134264d1..dc5018007a 100644 --- a/lib/rules/no-unknown-property.js +++ b/lib/rules/no-unknown-property.js @@ -490,7 +490,7 @@ function tagNameHasDot(node) { /** * Get the standard name of the attribute. * @param {string} name - Name of the attribute. - * @param {string} context - eslint context + * @param {object} context - eslint context * @returns {string | undefined} The standard name of the attribute, or undefined if no standard name was found. */ function getStandardName(name, context) { @@ -516,6 +516,7 @@ const messages = { dataLowercaseRequired: 'React does not recognize data-* props with uppercase characters on a DOM element. Found \'{{name}}\', use \'{{lowerCaseName}}\' instead', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-unused-class-component-methods.js b/lib/rules/no-unused-class-component-methods.js index 96664d6c52..4356cc2a5e 100644 --- a/lib/rules/no-unused-class-component-methods.js +++ b/lib/rules/no-unused-class-component-methods.js @@ -98,6 +98,7 @@ const messages = { unusedWithClass: 'Unused method or property "{{name}}" of class "{{className}}"', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -248,7 +249,7 @@ module.exports = { node.id.properties .filter((prop) => prop.type === 'Property' && isKeyLiteralLike(prop, prop.key)) .forEach((prop) => { - addUsedProperty(prop.key); + addUsedProperty('key' in prop ? prop.key : undefined); }); } }, diff --git a/lib/rules/no-unused-state.js b/lib/rules/no-unused-state.js index 21d77748ec..a71d63ebeb 100644 --- a/lib/rules/no-unused-state.js +++ b/lib/rules/no-unused-state.js @@ -78,6 +78,7 @@ const messages = { unusedStateField: 'Unused state field: \'{{name}}\'', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -431,7 +432,11 @@ module.exports = { return; } - if (parent.key.name === 'getInitialState') { + if ( + 'key' in parent + && 'name' in parent.key + && parent.key.name === 'getInitialState' + ) { const body = node.body.body; const lastBodyNode = body[body.length - 1]; @@ -463,6 +468,7 @@ module.exports = { && unwrappedRight.type === 'ObjectExpression' ) { // Find the nearest function expression containing this assignment. + /** @type {import("eslint").Rule.Node} */ let fn = node; while (fn.type !== 'FunctionExpression' && fn.parent) { fn = fn.parent; diff --git a/lib/rules/sort-default-props.js b/lib/rules/sort-default-props.js index 50bd7ed53c..3f3c6ea9b3 100644 --- a/lib/rules/sort-default-props.js +++ b/lib/rules/sort-default-props.js @@ -22,6 +22,7 @@ const messages = { propsNotSorted: 'Default prop types declarations should be sorted alphabetically', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -172,7 +173,7 @@ module.exports = { return; } - checkNode(node.parent.right); + checkNode('right' in node.parent && node.parent.right); }, }; }, diff --git a/lib/rules/style-prop-object.js b/lib/rules/style-prop-object.js index b0fd7f76ce..f2c63e525f 100644 --- a/lib/rules/style-prop-object.js +++ b/lib/rules/style-prop-object.js @@ -18,6 +18,7 @@ const messages = { stylePropNotObject: 'Style prop value must be an object', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -80,7 +81,7 @@ module.exports = { isCreateElement(context, node) && node.arguments.length > 1 ) { - if (node.arguments[0].name) { + if ('name' in node.arguments[0] && node.arguments[0].name) { // store name of component const componentName = node.arguments[0].name; @@ -91,8 +92,15 @@ module.exports = { } } if (node.arguments[1].type === 'ObjectExpression') { - const style = node.arguments[1].properties.find((property) => property.key && property.key.name === 'style' && !property.computed); - if (style) { + const style = node.arguments[1].properties.find((property) => ( + 'key' in property + && property.key + && 'name' in property.key + && property.key.name === 'style' + && !property.computed + )); + + if (style && 'value' in style) { if (style.value.type === 'Identifier') { checkIdentifiers(style.value); } else if (isNonNullaryLiteral(style.value)) { diff --git a/lib/rules/void-dom-elements-no-children.js b/lib/rules/void-dom-elements-no-children.js index 66db2a8ad3..f8187b09ea 100644 --- a/lib/rules/void-dom-elements-no-children.js +++ b/lib/rules/void-dom-elements-no-children.js @@ -47,6 +47,7 @@ function isVoidDOMElement(elementName) { const noChildrenInVoidEl = 'Void DOM element <{{element}} /> cannot receive children.'; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -119,7 +120,7 @@ module.exports = { return; } - const elementName = args[0].value; + const elementName = 'value' in args[0] ? args[0].value : undefined; if (!isVoidDOMElement(elementName)) { // e.g. React.createElement('div'); @@ -144,7 +145,7 @@ module.exports = { const props = args[1].properties; const hasChildrenPropOrDanger = props.some((prop) => { - if (!prop.key) { + if (!('key' in prop) || !prop.key || !('name' in prop.key)) { return false; } From f435df9b8a53413a873aaa9379e556f9394fdd97 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Tue, 9 Apr 2024 11:36:44 +0900 Subject: [PATCH 2/2] [Tests] `button-has-type`: add test case with spread --- CHANGELOG.md | 1 + tests/lib/rules/button-has-type.js | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43e05c494a..7aa2c37164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange * [Tests] add @typescript-eslint/parser v7 and v8 ([#3629][] @hampustagerud) * [Docs] [`no-danger`]: update broken link ([#3817][] @lucasrmendonca) * [types] add jsdoc type annotations ([#3731][] @y-hsgw) +* [Tests] `button-has-type`: add test case with spread ([#3731][] @y-hsgw) [#3632]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3632 diff --git a/tests/lib/rules/button-has-type.js b/tests/lib/rules/button-has-type.js index 1f9819ada8..e5d6be6d5e 100644 --- a/tests/lib/rules/button-has-type.js +++ b/tests/lib/rules/button-has-type.js @@ -304,6 +304,12 @@ ruleTester.run('button-has-type', rule, { }, ], }, + { + code: 'React.createElement("button", {...extraProps})', + errors: [ + { messageId: 'missingType' }, + ], + }, { code: 'Foo.createElement("button")', errors: [