From 63aceff37b11c03c9d8f1fb772422456c53ae59d Mon Sep 17 00:00:00 2001 From: Chiawen Chen Date: Sun, 29 Sep 2024 22:32:57 +0800 Subject: [PATCH] [Fix] `destructuring-assignment`: fix false negative when using `typeof props.a` Fixes #3828 Co-authored-by: Chiawen Chen Co-authored-by: Jordan Harband --- CHANGELOG.md | 3 + lib/rules/destructuring-assignment.js | 39 +++++++++++++ tests/lib/rules/destructuring-assignment.js | 65 ++++++++++++++++++++- 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e5188f9e7..f62ad8605e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +### Fixed +* [`destructuring-assignment`]: fix false negative when using `typeof props.a` ([#3835][] @golopot) + ### Changed * [Refactor] [`destructuring-assignment`]: use `getParentStatelessComponent` ([#3835][] @golopot) diff --git a/lib/rules/destructuring-assignment.js b/lib/rules/destructuring-assignment.js index 1956e29ca2..6e254f772f 100644 --- a/lib/rules/destructuring-assignment.js +++ b/lib/rules/destructuring-assignment.js @@ -181,6 +181,25 @@ module.exports = { } } + // valid-jsdoc cannot read function types + // eslint-disable-next-line valid-jsdoc + /** + * Find a parent that satisfy the given predicate + * @param {ASTNode} node + * @param {(node: ASTNode) => boolean} predicate + * @returns {ASTNode | undefined} + */ + function findParent(node, predicate) { + let n = node; + while (n) { + if (predicate(n)) { + return n; + } + n = n.parent; + } + return undefined; + } + return { FunctionDeclaration: handleStatelessComponent, @@ -207,6 +226,25 @@ module.exports = { } }, + TSQualifiedName(node) { + if (configuration !== 'always') { + return; + } + // handle `typeof props.a.b` + if (node.left.type === 'Identifier' + && node.left.name === sfcParams.propsName() + && findParent(node, (n) => n.type === 'TSTypeQuery') + && utils.getParentStatelessComponent(node) + ) { + report(context, messages.useDestructAssignment, 'useDestructAssignment', { + node, + data: { + type: 'props', + }, + }); + } + }, + VariableDeclarator(node) { const classComponent = utils.getParentComponent(node); const SFCComponent = components.get(getScope(context, node).block); @@ -252,6 +290,7 @@ module.exports = { if (!propsRefs) { return; } + // Skip if props is used elsewhere if (propsRefs.length > 1) { return; diff --git a/tests/lib/rules/destructuring-assignment.js b/tests/lib/rules/destructuring-assignment.js index e1f8e85b08..13d0c10ac7 100644 --- a/tests/lib/rules/destructuring-assignment.js +++ b/tests/lib/rules/destructuring-assignment.js @@ -872,6 +872,69 @@ ${' '} `, features: ['ts', 'no-babel'], }, - ] : [] + ] : [], + { + code: ` + type Props = { text: string }; + export const MyComponent: React.FC = (props) => { + type MyType = typeof props.text; + return
{props.text as MyType}
; + }; + `, + options: ['always', { destructureInSignature: 'always' }], + features: ['types', 'no-babel'], + errors: [ + { + messageId: 'useDestructAssignment', + type: 'TSQualifiedName', + data: { type: 'props' }, + }, + { + messageId: 'useDestructAssignment', + type: 'MemberExpression', + data: { type: 'props' }, + }, + ], + }, + { + code: ` + type Props = { text: string }; + export const MyOtherComponent: React.FC = (props) => { + const { text } = props; + type MyType = typeof props.text; + return
{text as MyType}
; + }; + `, + options: ['always', { destructureInSignature: 'always' }], + features: ['types', 'no-babel'], + errors: [ + { + messageId: 'useDestructAssignment', + type: 'TSQualifiedName', + data: { type: 'props' }, + }, + ], + }, + { + code: ` + function C(props: Props) { + void props.a + typeof props.b + return
+ } + `, + options: ['always'], + features: ['types'], + errors: [ + { + messageId: 'useDestructAssignment', + data: { type: 'props' }, + }, + { + messageId: 'useDestructAssignment', + data: { type: 'props' }, + }, + ], + } )), });