Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ 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)

[#3835]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3835

## [7.37.1] - 2024.10.01

### Fixed
Expand Down
46 changes: 40 additions & 6 deletions lib/rules/destructuring-assignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -196,12 +215,7 @@ module.exports = {
'FunctionExpression:exit': handleStatelessComponentExit,

MemberExpression(node) {
let scope = getScope(context, node);
let SFCComponent = components.get(scope.block);
while (!SFCComponent && scope.upper && scope.upper !== scope) {
SFCComponent = components.get(scope.upper.block);
scope = scope.upper;
}
const SFCComponent = utils.getParentStatelessComponent(node);
if (SFCComponent) {
handleSFCUsage(node);
}
Expand All @@ -212,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);
Expand Down Expand Up @@ -257,6 +290,7 @@ module.exports = {
if (!propsRefs) {
return;
}

// Skip if props is used elsewhere
if (propsRefs.length > 1) {
return;
Expand Down
65 changes: 64 additions & 1 deletion tests/lib/rules/destructuring-assignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,69 @@ ${' '}
`,
features: ['ts', 'no-babel'],
},
] : []
] : [],
{
code: `
type Props = { text: string };
export const MyComponent: React.FC<Props> = (props) => {
type MyType = typeof props.text;
return <div>{props.text as MyType}</div>;
};
`,
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> = (props) => {
const { text } = props;
type MyType = typeof props.text;
return <div>{text as MyType}</div>;
};
`,
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 <div />
}
`,
options: ['always'],
features: ['types'],
errors: [
{
messageId: 'useDestructAssignment',
data: { type: 'props' },
},
{
messageId: 'useDestructAssignment',
data: { type: 'props' },
},
],
}
)),
});