Skip to content

Commit 85d5bc9

Browse files
sonar-nigel[bot]Vibe Botclaude
authored
JS-1388 Fix S6767 false positive: props accessed via dynamic bracket notation (#6577)
Co-authored-by: Vibe Bot <vibe-bot@sonarsource.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 34c1809 commit 85d5bc9

3 files changed

Lines changed: 98 additions & 8 deletions

File tree

its/ruling/src/test/expected/ant-design/typescript-S6767.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@
33
41,
44
41
55
],
6-
"ant-design:components/config-provider/index.tsx": [
7-
60,
8-
61,
9-
65,
10-
73,
11-
76,
12-
80
13-
],
146
"ant-design:components/input/ClearableLabeledInput.tsx": [
157
32,
168
40,

packages/jsts/src/rules/S6767/decorator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ function hasPropsCall(root: estree.Node, keys: SourceCode.VisitorKeys): boolean
7979
return true;
8080
}
8181

82+
// Check if this is a computed MemberExpression with props (for props[key] or this.props[key])
83+
if (
84+
root.type === 'MemberExpression' &&
85+
root.computed &&
86+
propsArgPatterns.some(p => p(root.object))
87+
) {
88+
return true;
89+
}
90+
8291
// Recursively check all children
8392
return childrenOf(root, keys).some(child => hasPropsCall(child, keys));
8493
}

packages/jsts/src/rules/S6767/unit.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,42 @@ MyComponent.propTypes = {
164164
});
165165
});
166166

167+
it('should not report props accessed via computed bracket notation', () => {
168+
const ruleTester = new NoTypeCheckingRuleTester();
169+
170+
ruleTester.run('no-unused-prop-types', rule, {
171+
valid: [
172+
{
173+
// FP: props[key] computed access
174+
code: `
175+
function AnimationComponent(props) {
176+
const key = 'offsetX';
177+
return <div value={props[key]} />;
178+
}
179+
AnimationComponent.propTypes = {
180+
offsetX: PropTypes.number,
181+
};
182+
`,
183+
},
184+
{
185+
// FP: this.props[key] computed access
186+
code: `
187+
class VictoryAxis extends React.Component {
188+
render() {
189+
const key = 'offsetX';
190+
return <div>{this.props[key]}</div>;
191+
}
192+
}
193+
VictoryAxis.propTypes = {
194+
offsetX: PropTypes.number,
195+
};
196+
`,
197+
},
198+
],
199+
invalid: [],
200+
});
201+
});
202+
167203
it('should exercise TypeScript type-checking paths (Strategy C in react helpers)', () => {
168204
const ruleTester = new RuleTester({
169205
parserOptions: {
@@ -220,6 +256,31 @@ interface MyComponentProps {
220256
function MyComponent(props: MyComponentProps) {
221257
return <SomeComponent {...props} />;
222258
}
259+
`,
260+
filename: fixtureFile,
261+
},
262+
{
263+
// FP: TypeScript class component with this.props[key] — Strategy C matches
264+
// VictoryAxisProps to VictoryAxis via matchesClassProps, hasPropsCall finds computed MemberExpression.
265+
code: `
266+
declare const React: any;
267+
interface VictoryAxisProps {
268+
offsetX?: number;
269+
offsetY?: number;
270+
}
271+
class VictoryAxis extends React.Component<VictoryAxisProps> {
272+
props: VictoryAxisProps;
273+
static animationWhitelist: Array<keyof VictoryAxisProps> = ['offsetX', 'offsetY'];
274+
animate() {
275+
return VictoryAxis.animationWhitelist.reduce((acc: Record<string, unknown>, key) => {
276+
acc[key] = this.props[key];
277+
return acc;
278+
}, {});
279+
}
280+
render() {
281+
return <div>{this.animate()}</div>;
282+
}
283+
}
223284
`,
224285
filename: fixtureFile,
225286
},
@@ -350,6 +411,34 @@ Wrapper.propTypes = { onClick: PropTypes.func };
350411
});
351412
});
352413

414+
it('upstream rule should report dynamic bracket notation FP pattern (sentinel: remove decorator check if this fails)', () => {
415+
// Confirms that the upstream eslint-plugin-react no-unused-prop-types rule DOES raise
416+
// issues when props are accessed via computed bracket notation (props[key], this.props[key]).
417+
// If this test starts failing, the upstream rule has been fixed and the computed
418+
// MemberExpression handling in the S6767 decorator can be removed.
419+
const upstreamRule = rules['no-unused-prop-types'];
420+
const ruleTester = new NoTypeCheckingRuleTester();
421+
422+
ruleTester.run('no-unused-prop-types (upstream, bracket notation)', upstreamRule, {
423+
valid: [],
424+
invalid: [
425+
{
426+
// upstream does not track props[key] computed bracket access
427+
code: `
428+
function AnimationComponent(props) {
429+
const key = 'offsetX';
430+
return <div value={props[key]} />;
431+
}
432+
AnimationComponent.propTypes = {
433+
offsetX: PropTypes.number,
434+
};
435+
`,
436+
errors: 1,
437+
},
438+
],
439+
});
440+
});
441+
353442
it('upstream rule should NOT report state interface properties when TypeScript type info is available', () => {
354443
// Confirms that the upstream rule uses TypeScript type information to distinguish
355444
// state types from props types. AnchorState (used as the second type parameter of

0 commit comments

Comments
 (0)