Skip to content

JS-1394 Handle S6767 React class non-props generics#7050

Draft
francois-mora-sonarsource wants to merge 34 commits into
masterfrom
fix/JS-1394-s6767-non-props-escape
Draft

JS-1394 Handle S6767 React class non-props generics#7050
francois-mora-sonarsource wants to merge 34 commits into
masterfrom
fix/JS-1394-s6767-non-props-escape

Conversation

@francois-mora-sonarsource
Copy link
Copy Markdown
Contributor

@francois-mora-sonarsource francois-mora-sonarsource commented May 18, 2026

Summary

This stacked PR updates the S6767 false-positive remediation for React class non-props generics.

It does four things:

  • teaches the React helper layer to classify reported types as props, non-props, mixed, or other, so state/snapshot-only declarations can be filtered without hiding real props-side owners
  • adds a narrow non-props escape for React.Component<Props, State, Snapshot> cases where upstream still reports the second generic as an unused prop
  • extends the S6767 decorator path to recognize typed decorator callbacks and class decorator annotations while still requiring every owning component to match a known false-positive pattern
  • expands S6767 and React helper coverage around mixed owners, wrapped components, generic identity, and upstream sentinels, and syncs the ant-design ruling by removing components/anchor/Anchor.tsx:70

Why

The upstream eslint-plugin-react rule still misreports some TypeScript React class declarations when the reported type is only used through non-props generic slots. The motivating case is a class declared as React.Component<Props, State, Snapshot>, where a State member can still be treated as an unused prop. The SonarJS decorator also needs to keep the report when the same declaration is still used as props elsewhere in the file.

Verification

  • node --import=tsx --test --test-reporter=spec packages/analysis/src/jsts/rules/S6767/unit.test.ts packages/analysis/src/jsts/rules/S6767/unit.upstream.test.ts packages/analysis/src/jsts/rules/S6767/unit.typescript.test.ts
  • node --import=tsx --test --test-reporter=spec packages/analysis/tests/jsts/rules/helpers/react.test.ts packages/analysis/tests/jsts/rules/helpers/react/component-analysis.test.ts packages/analysis/tests/jsts/rules/helpers/react/type-ownership.test.ts packages/analysis/tests/jsts/rules/helpers/reported-type.test.ts

The focused S6767 suites and the changed React helper suites pass locally. The branch also removes the existing ant-design components/anchor/Anchor.tsx:70 ruling entry, which matches the targeted state-only pattern.

Manual validation

  • components/anchor/Anchor.tsx declares class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigConsumerProps> and uses AnchorState.activeLink through this.state, so the removed ruling entry is in scope
  • mixed-owner cases remain reportable when the same declaration is used as props elsewhere; the new TypeScript and helper tests cover those boundaries explicitly

Summary by Gitar

  • Build infrastructure:
    • Added a fallback empty object to rspec-secrets vault access in .github/workflows/build.yml to prevent Windows build failures.

This will update automatically on new commits.

@hashicorp-vault-sonar-prod
Copy link
Copy Markdown

hashicorp-vault-sonar-prod Bot commented May 18, 2026

JS-1394

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

Ruling Report

Code no longer flagged (2 issues)

S6767

ant-design/components/anchor/Anchor.tsx:70

    68 | 
    69 | export interface AnchorState {
>   70 |   activeLink: null | string;
    71 | }
    72 | 

S2094

custom-jsts/S3854.js:350

   348 | 
   349 | 
>  350 | class Foo {
   351 |     constructor(name, options = {}) {
   352 |         this.name = name;

New issues flagged (4 issues)

S5845

rxjs/spec/Subject-spec.ts:268

   266 |         function (x) { results3.push(x); },
   267 |         function (err) {
>  268 |           expect(false).to.equal('should not throw error: ' + err.toString());
   269 |         }
   270 |       );

rxjs/spec/observables/IteratorObservable-spec.ts:152

   150 |     IteratorObservable.create('foo')
   151 |       .subscribe(
>  152 |         (x: number) => { expect(x).to.equal(expected.shift()); },
   153 |         (x) => {
   154 |           done(new Error('should not be called'));

vitest/test/coverage-test/fixtures/test/mocking-in-js-file.test.js:9

     7 | 
     8 | test('mocking in Javascript test should not break sourcemaps', () => {
>    9 |   expect(sum(1, 2)).toBe('mocked')
    10 | })
    11 | 

vitest/test/browser/fixtures/mocking/mocked-nested.test.ts:14

    12 | test('adds', () => {
    13 |   child.mockReturnValue(42)
>   14 |   expect(parent()).toBe(42)
    15 | })
    16 | 

@francois-mora-sonarsource francois-mora-sonarsource marked this pull request as ready for review May 18, 2026 12:15
@sonar-review-alpha
Copy link
Copy Markdown
Contributor

sonar-review-alpha Bot commented May 18, 2026

Summary

What changed: This PR fixes false-positive S6767 reports on React class non-props generics (state/snapshot) by introducing per-component suppression logic and a new TypeScript-based check.

When React classes declare all three type parameters (React.Component<Props, State, Snapshot>), the upstream eslint-plugin-react can misreport State members as unused props. This PR:

  • New file react-non-props-usage.ts: Detects when a type is used as a React class's 2nd+ generic argument (non-props), not props
  • Refactored decorator.ts: Changed from global report interception to per-component validation at Program:exit, enabling suppression on a component-by-component basis
  • Enhanced decorator usage escapes: Extended decorator-indirect-prop-usage.ts to recognize both factory patterns (track((props) => ...)(Component)) and decorator annotations (@track((props) => ...) class Component {})
  • Updated React helpers: Exported new analysis functions and added isReactComponentSuperclass() to handle imported aliases like import { Component as Base }

Why: The per-component approach makes suppression logic context-aware—a prop can be a false positive for one component (where it's state) but real for another (where it's props). This isolates the new non-props check from unrelated escapes and prevents incorrect suppressions.

What reviewers should know

Where to start:

  1. Review react-non-props-usage.ts first—it's the new core logic that distinguishes state/snapshot from props
  2. Then check decorator.ts to see how per-component validation replaces the previous interception approach
  3. Review the test changes in unit.typescript.test.ts for the main scenario: React.Component<Props, State, Snapshot> with state accessed via this.state

Key decisions:

  • Per-component reporting: Each component is validated independently at the end, so suppressions can be granular. This contrasts with the previous approach that intercepted reports after the fact.
  • New TypeScript-backed check: Uses the type checker to identify non-props generics, avoiding syntax-only heuristics and handling imports/aliases correctly
  • Decorator annotation support: Decorator suppression now covers both track(...)(Component) factory style and @track(...) class Component {} annotation style

Watch for:

  • The refactored decorator path now explicitly iterates components and applies suppressions during reporting, not during interception—verify all existing escapes (hasOwnCustomSuperclassPropsForwarding, hasForwardRefCallbackPropUsage, hasSupportedWholePropsUsage) are still checked
  • Cache behavior in react-non-props-usage.ts uses WeakMap for per-source memoization—verify this doesn't cause unexpected cache retention
  • One test was removed (wrapped callbacks in forwardRef). Confirm this behavior change is intentional

  • Generate Walkthrough
  • Generate Diagram

🗣️ Give feedback

sonar-review-alpha[bot]

This comment was marked as resolved.

@francois-mora-sonarsource francois-mora-sonarsource force-pushed the fix/JS-1394-s6767-decorator-callback-escape branch from 26d646e to 69ffa29 Compare May 19, 2026 09:03
sonar-review-alpha[bot]

This comment was marked as outdated.

@francois-mora-sonarsource francois-mora-sonarsource force-pushed the fix/JS-1394-s6767-decorator-callback-escape branch from 69ffa29 to 629141e Compare May 20, 2026 10:05
Copy link
Copy Markdown
Contributor

@sonar-review-alpha sonar-review-alpha Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commits are a pure rename refactoring — ReportedEnclosingTypeReportedType in reported-type.ts, propagated to all consumers (react.ts, react/member-ownership.ts, react/type-ownership.ts, react-non-props-usage.ts). No functional logic was changed.

The previously flagged duplication (REACT_COMPONENT_WRAPPER_CALLEES, isReactComponentWrapperCallee, isWrappedInReactComponentCall, getOutermostReactWrapperParent, isAnonymousDefaultExportComponent duplicated between react.ts:50 and react/component-analysis.ts:36) is still present and was not addressed by these commits.

🗣️ Give feedback

Comment thread packages/analysis/src/jsts/rules/S6767/decorator.ts Outdated
Comment thread packages/analysis/src/jsts/rules/helpers/react/type-ownership.ts Outdated
@sonarqube-next
Copy link
Copy Markdown

Co-authored-by: Gitar <noreply@gitar.ai>
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented May 27, 2026

Code Review ✅ Approved 2 resolved / 2 findings

Refactors React component analysis to better handle S6767 non-props generics and resolves misleading prop-independent conditions in isPropUsed and unsafe type casting in getReportedTypeUsageCacheKey. No issues found.

✅ 2 resolved
Quality: isPropUsed has misleading prop-independent conditions inside .some()

📄 packages/analysis/src/jsts/rules/S6767/decorator.ts:91-100
In isPropUsed, the conditions prop.type === 'shape', prop.type === 'exact', and prop.name === '__ANY_KEY__' are placed inside the usedPropTypes.some(usedProp => ...) callback but do not depend on usedProp. This makes it look like they always return true, but they're actually gated by .some() requiring at least one array element. If usedPropTypes is empty, these conditions are never evaluated, so shape/exact props are not considered "used".

This is functionally acceptable because skipShapeProps (default true) catches shape/exact before isPropUsed is reached, and the empty-usedPropTypes case is rare. However, extracting the prop-only conditions makes the intent clearer.

Quality: getReportedTypeUsageCacheKey uses unsafe type cast for cache key

📄 packages/analysis/src/jsts/rules/helpers/react/type-ownership.ts:243-248
The cache key function casts a TSESTree.Node to estree.Node via as unknown as estree.Node to use it as a WeakMap key. While this works because it's the same underlying object (identity-based WeakMap keying doesn't care about the type), the double cast obscures intent. A comment or using a more generic key type (e.g., object) for the WeakMap would make this clearer.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@francois-mora-sonarsource francois-mora-sonarsource marked this pull request as draft May 28, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants