JS-1394 Fix S6767 false positive for decorator-consumed props#6947
Conversation
Tests cover props consumed through decorator-factory callbacks, including direct member reads and whole typed props forwarding to helper logic. They also verify unrelated decorator callbacks still allow unused props to be reported. Relates to JS-1394
Suppress unused prop reports when a React component is wrapped by a decorator factory whose callback consumes the reported prop directly or forwards the callback's whole props object to helper logic. The implementation follows the approved JS-1394 direction by keeping the suppression component-local, using composable pattern checks, and moving component variable lookup into shared React helpers. Relates to JS-1394
Remove the export from getComponentIdentifier because it is only used internally by the React rule helper. This keeps the helper available for getComponentVariable while resolving the unused export reported by knip.
Rule Profile
interface CardProps {
title: string;
subtitle: string;
}
function Card(props: CardProps) {
return <div>{props.title}</div>;
}False Positive PatternThe false positives now covered by this branch fall into two narrow families. The first is decorator-based indirect prop usage: a React prop is not read inside the component body, but is consumed by typed decorator machinery through a callback whose first parameter is the component props object. The second is an upstream typing mismatch: in function DecoratorFactoryComponent(props: DecoratorFactoryProps) {
return <div />;
}
track((props: DecoratorFactoryProps) => ({
context_module: props.contextModule,
user_id: props.userId,
}))(DecoratorFactoryComponent);interface AnchorState {
activeLink: null | string;
}
interface AnchorProps {
href?: string;
}
interface AnchorSnapshot {
scrollTop: number;
}
class Anchor extends React.Component<AnchorProps, AnchorState, AnchorSnapshot> {
render() {
const { activeLink } = this.state;
return <a href={this.props.href}>{activeLink}</a>;
}
}False Negative RiskThe risk is bounded by reporting by default unless TypeScript proves that the decorator callback's first parameter has the same declared props type as the component, and by suppressing non-props generic reports only when the reported declaration is proven to belong to a React class state/snapshot slot rather than a real props contract. This avoids suppressing unrelated metadata callbacks and avoids hiding legitimate reports when the same declaration is also used as props elsewhere. interface SharedType {
unused: string;
}
interface Snapshot {
scrollTop: number;
}
class PropsOwner extends React.Component<SharedType> {
render() {
return <div />;
}
}
class StateOwner extends React.Component<{}, SharedType, Snapshot> {
render() {
return <div>{this.state.unused}</div>;
}
}Fix SummaryThe fix adds two focused S6767 suppressions: one for indirect prop usage through typed decorator callbacks, and one for upstream reports on React class non-props generics. It resolves the component binding used at the decorator site, matches callback parameter types against the component props type, and separately classifies reported type declarations as props vs state/snapshot before suppressing.
|
Ruling ReportCode no longer flagged (21 issues)S6767ant-design/components/input/ClearableLabeledInput.tsx:32 30 | disabled?: boolean;
31 | direction?: DirectionType;
> 32 | focused?: boolean;
33 | readOnly?: boolean;
34 | bordered: boolean;ant-design/components/input/ClearableLabeledInput.tsx:40 38 | /** This props only for input. */
39 | export interface ClearableInputProps extends BasicProps {
> 40 | size?: SizeType;
41 | suffix?: React.ReactNode;
42 | prefix?: React.ReactNode;ant-design/components/input/ClearableLabeledInput.tsx:42 40 | size?: SizeType;
41 | suffix?: React.ReactNode;
> 42 | prefix?: React.ReactNode;
43 | addonBefore?: React.ReactNode;
44 | addonAfter?: React.ReactNode;ant-design/components/input/ClearableLabeledInput.tsx:43 41 | suffix?: React.ReactNode;
42 | prefix?: React.ReactNode;
> 43 | addonBefore?: React.ReactNode;
44 | addonAfter?: React.ReactNode;
45 | triggerFocus?: () => void;ant-design/components/input/ClearableLabeledInput.tsx:44 42 | prefix?: React.ReactNode;
43 | addonBefore?: React.ReactNode;
> 44 | addonAfter?: React.ReactNode;
45 | triggerFocus?: () => void;
46 | status?: InputStatus;ant-design/components/input/ClearableLabeledInput.tsx:45 43 | addonBefore?: React.ReactNode;
44 | addonAfter?: React.ReactNode;
> 45 | triggerFocus?: () => void;
46 | status?: InputStatus;
47 | }desktop/app/src/ui/app-menu/app-menu.tsx:41 39 | * menu will receive focus.
40 | */
> 41 | readonly openedWithAccessKey: boolean
42 |
43 | /**desktop/app/src/ui/branches/branch-list.tsx:30 28 | * See IBranchesState.defaultBranch
29 | */
> 30 | readonly defaultBranch: Branch | null
31 |
32 | /**desktop/app/src/ui/branches/branch-list.tsx:35 33 | * The currently checked out branch or null if HEAD is detached
34 | */
> 35 | readonly currentBranch: Branch | null
36 |
37 | /**desktop/app/src/ui/branches/branch-list.tsx:45 43 | * See IBranchesState.recentBranches
44 | */
> 45 | readonly recentBranches: ReadonlyArray<Branch>
46 |
47 | /**...and 11 more New issues flagged (2 issues)S6767eigen/src/app/Scenes/Artwork/Components/CommercialInformation.tsx:34 32 | me: CommercialInformation_me$data
33 | hasStarted?: boolean
> 34 | tracking?: TrackingProp
35 | biddingEndAt?: string
36 | hasBeenExtended?: booleaneigen/src/app/Scenes/Artwork/Components/CommercialInformation.tsx:37 35 | biddingEndAt?: string
36 | hasBeenExtended?: boolean
> 37 | refetchArtwork: () => void
38 | setAuctionTimerState?: (auctionTimerState: string) => void
39 | }📋 View full reportCode no longer flagged (21)
New issues flagged (2) |
s6767-props-reported-unused-when-consumed-through-a-typescriptes-decorator-gpt-5.5
Add unit tests to cover S6767 decorator and React helper fallback paths. This improves code coverage to meet the quality gate threshold.
Make unused exported type aliases module-local in the analysis and gRPC packages. The affected types are still available internally where they shape exported APIs, but they are no longer reported by knip as unused exports.
SummaryThis PR adds five targeted false-positive suppressions to rule S6767 (unused prop types) to handle indirect prop consumption patterns that the upstream The new escape hatches:
Scope: The fixes are narrowly scoped — decorator callbacks must have TypeScript-verified props types matching the component, and React class non-props reports are only suppressed when a third generic parameter is present. This keeps legitimate unused-prop reports visible while eliminating false positives observed in real projects (ant-design, desktop, eigen, etc.). Impact: Test expectations across multiple projects show dozens of false-positive suppressions, confirming these patterns are widely used in production TypeScript React code. What reviewers should knowStart here: Read Key implementation details:
Non-obvious decisions:
Testing: Unit tests show edge cases like generic aliases, named function expressions, method signatures, and literal-keyed properties — all now correctly handled.
|
…rc/jsts/rules/helpers/react.ts
Comment: **Logic duplication:** The two `if` blocks in `getComponentIdentifier` are structurally identical — both check `componentNode.id` and return it. The only difference is the set of node types in the condition. They can be merged into one:
```typescript
if (
(componentNode.type === 'ClassDeclaration' ||
componentNode.type === 'FunctionDeclaration' ||
componentNode.type === 'ClassExpression' ||
componentNode.type === 'FunctionExpression') &&
componentNode.id
) {
return componentNode.id;
}
```
As written, adding a new node type that has an `id` would require updating two separate blocks.
```suggestion
if (
(componentNode.type === 'ClassExpression' ||
componentNode.type === 'FunctionExpression' ||
componentNode.type === 'ClassDeclaration' ||
componentNode.type === 'FunctionDeclaration') &&
componentNode.id
) {
return componentNode.id;
}
```
- [ ] Mark as noise
…rc/jsts/rules/S6767/decorator.ts Comment: **Logic duplication:** `isWholePropsUsage` encodes the same four "whole props usage" patterns as `hasPropsCall` (lines 276–316): a non-`Super` call with props as argument, `SpreadElement`, `JSXSpreadAttribute`, and computed `MemberExpression`. The difference is that `hasPropsCall` uses name-based pattern matching and recurses over the whole subtree, while this function uses reference-identity and checks a single parent node. If the taxonomy of "whole props usage" ever grows (e.g. object rest/spread assignment), both functions would need to be updated independently. Consider extracting the shared set of patterns into a shared predicate or a documented constant so the two sites stay in sync. - [ ] Mark as noise
|
There is still a gap in the decorator-factory suppression for named function expressions.
const WrappedComponent = function InnerWrappedComponent(props: Props) {
return <div>{props.label}</div>;
};But track((props: Props) => ({
context_module: props.contextModule,
}))(WrappedComponent);In this shape, the relevant reference is I reproduced this locally after Please adjust the component-variable lookup to follow the outer binding for named expressions here, and add a regression test for that case. A good fit would be a new valid case in declare const React: any;
declare function track<P>(
mapper: (props: P) => Record<string, unknown>,
): <TComponent>(target: TComponent) => TComponent;
interface NamedExpressionProps {
label: string;
contextModule: string;
}
const WrappedComponent = function InnerWrappedComponent(props: NamedExpressionProps) {
return <div>{props.label}</div>;
};
track((props: NamedExpressionProps) => ({
context_module: props.contextModule,
}))(WrappedComponent); |
Comment: There is still a gap in the decorator-factory suppression for **named function expressions**.
`getComponentIdentifier()` currently prefers the inner self-name of a `FunctionExpression` / `ClassExpression` over the outer variable binding:
```ts
const WrappedComponent = function InnerWrappedComponent(props: Props) {
return <div>{props.label}</div>;
};
```
But `hasDecoratorFactoryCallPropUsage()` needs to follow the binding that is actually used at the decorator application site:
```ts
track((props: Props) => ({
context_module: props.contextModule,
}))(WrappedComponent);
```
In this shape, the relevant reference is `WrappedComponent`, not `InnerWrappedComponent`, so the current implementation misses the decorator application and still reports `contextModule` as unused.
I reproduced this locally after `npm ci` + `npm run bbf` with a small RuleTester case, and it still fails with one `unusedPropType` error.
Please adjust the component-variable lookup to follow the outer binding for named expressions here, and add a regression test for that case. A good fit would be a new **valid** case in `packages/analysis/src/jsts/rules/S6767/unit.typescript.test.ts` like:
```ts
declare const React: any;
declare function track<P>(
mapper: (props: P) => Record<string, unknown>,
): <TComponent>(target: TComponent) => TComponent;
interface NamedExpressionProps {
label: string;
contextModule: string;
}
const WrappedComponent = function InnerWrappedComponent(props: NamedExpressionProps) {
return <div>{props.label}</div>;
};
track((props: NamedExpressionProps) => ({
context_module: props.contextModule,
}))(WrappedComponent);
```
|
I think there is still a scope gap against JS-1394. The original ticket is explicitly about props consumed through decorator annotations ( Could you extend the remediation to also cover the annotation form, for example: @track((props: Props) => ({
context_module: props.contextModule,
user_id: props.userId,
}))
class MyTrackedComponent extends React.Component<Props> {
render() {
return <div />;
}
}As it stands, the branch fixes only one syntactic manifestation of the ticket's FP pattern. |
Comment: I think there is still a scope gap against JS-1394. The original ticket is explicitly about props consumed through decorator annotations (`@track(...)` / `@screenTrack(...)` on the component), and the Jira discussion later treated decorator and decorator-factory forms as the same FP family. The current branch only implements the curried wrapper shape in `hasDecoratorFactoryCallPropUsage()`; I don't see corresponding support for class decorator annotations via `node.decorators`. Could you extend the remediation to also cover the annotation form, for example: ```ts @track((props: Props) => ({ context_module: props.contextModule, user_id: props.userId, })) class MyTrackedComponent extends React.Component<Props> { render() { return <div />; } } ``` As it stands, the branch fixes only one syntactic manifestation of the ticket's FP pattern.
The upstream react/no-unused-prop-types rule now accepts typed class-decorator callback member reads, so the sentinel should treat that sample as valid. Keep the factory-call member-read and whole-props forwarding samples invalid because upstream still reports them.
…rc/jsts/rules/S6767/unit.typescript.test.ts Comment: The upstream sentinel test (`unit.upstream.test.ts`, the new `valid` entry) confirms that the upstream `no-unused-prop-types` rule already suppresses this exact `@track(callback)` class decorator pattern when TypeScript type information is available — it never calls `context.report()` for it. Because `interceptReportForReact` only fires when the upstream rule calls `report()`, `hasDecoratorAnnotationPropUsage()` is never invoked for this test case. The test would pass even if that function were deleted. Two questions: 1. Is there a scenario where the upstream rule *does* report a class decorator case (e.g. without TypeScript type info, or a whole-props-forwarding variant like `@track(props => buildPayload(props))`), where our new function provides the actual suppression? 2. If yes, can a test be added for that scenario so `hasDecoratorAnnotationPropUsage()` is verified to change the outcome? If `hasDecoratorAnnotationPropUsage()` is intentional defense-in-depth against future upstream changes, a comment explaining that intent would prevent confusion. - [ ] Mark as noise
|
The new decorator callback suppression still looks too broad.
function decorate(metadataMapper) {
return function applyDecorator(target) {
return target;
};
}
function DecoratedComponent(props) {
return <div />;
}
DecoratedComponent.propTypes = {
contextModule: PropTypes.string,
};
decorate(function (metadata) {
return {
context_module: metadata.contextModule,
};
})(DecoratedComponent);I reproduced that locally with So the current remediation is still suppression-first: unknown decorator callback APIs are assumed to be props mappers based only on a property-name coincidence. Because Can we tighten this back to a report-first shape and add an invalid regression test for this boundary? If we cannot prove that the callback parameter is actually the component props, we should report by default rather than suppress. |
Comment: The new decorator callback suppression still looks too broad.
`hasDecoratorFactoryCallPropUsage()` / `isPropsCallbackUsingProp()` currently treat any first-parameter callback passed through `decorate(...)(Component)` as if that parameter were the component props. That means a shape like this is now suppressed even though the prop is still genuinely unused:
```ts
function decorate(metadataMapper) {
return function applyDecorator(target) {
return target;
};
}
function DecoratedComponent(props) {
return <div />;
}
DecoratedComponent.propTypes = {
contextModule: PropTypes.string,
};
decorate(function (metadata) {
return {
context_module: metadata.contextModule,
};
})(DecoratedComponent);
```
I reproduced that locally with `NoTypeCheckingRuleTester`: expected `1` `unusedPropType` error, got `0`.
So the current remediation is still suppression-first: unknown decorator callback APIs are assumed to be props mappers based only on a property-name coincidence. Because `isPropsCallbackUsingProp()` is shared, the same risk also applies to the annotation path when upstream still reports.
Can we tighten this back to a report-first shape and add an invalid regression test for this boundary? If we cannot prove that the callback parameter is actually the component props, we should report by default rather than suppress.
s6767-props-reported-unused-when-consumed-through-a-typescriptes-decorator-gpt-5.5
s6767-props-reported-unused-when-consumed-through-a-typescriptes-decorator-gpt-5.5
francois-mora-sonarsource
left a comment
There was a problem hiding this comment.
Addressed the review feedback already reflected in the branch. Re-verified the S6767 TypeScript coverage on current HEAD.
|
While validating the non-props escape, I found a false negative on wrapped props contracts. A class such as Fixed in |
|
There was a problem hiding this comment.
This is a large, well-structured PR that tackles a real problem (false positives in S6767 for decorator-consumed props) with careful type-system reasoning. The test coverage is extensive and the five escape hatches are narrowly scoped. However there are two real bugs that need fixing before merge.
Bug 1 — areSameTypeArguments / Box<any> comparison: The recursive path in areSameTypeArguments calls areSameTypeDeclarations when both sides share a declared type symbol, but areSameTypeDeclarations immediately returns false for any/unknown at the top. This means Box<any> vs Box<any> fails the recursive check and the test cases in unit.typescript.test.ts that expect identical any/unknown generic instantiations to be recognised as the same type will fail at runtime (even if they pass in the current test suite due to a different code path). See the inline comment on type.ts.
Bug 2 — getComponentIdentifier returns the variable declarator id for ordinary VariableDeclarator nodes: The final return in getComponentIdentifier fires for any VariableDeclarator with an identifier id, regardless of whether its init is a function/class expression. This means a variable like const label = "hello" that happens to appear as a parent of a component node could produce a wrong identifier. See the inline comment on react.ts.
|
Clarification on the latest Sonar Review summary:
So there is no remaining blocker from that review body on the current branch. |




Relates to JS-1394
This updates S6767 false-positive handling in two narrowly-scoped cases: typed decorator callbacks that the upstream
react/no-unused-prop-typesrule does not fully credit, and React class non-props generics such asStateorSnapshotthat the upstream rule can misclassify as props inReact.Component<Props, State, Snapshot>declarations. The remediation suppresses reports only when the callback's first parameter is proven to be the component props type and the callback reads the reported prop or forwards the whole props object, or when the reported declaration is proven to be a non-props generic rather than a props contract.track(callback)(Component)and@track(callback)shapes, including whole-props forwarding from typed callbacks.