Skip to content

JS-1475 Fix false positives in S7763 for local exports flagged as re-export candidates#6644

Open
sonar-nigel[bot] wants to merge 7 commits intomasterfrom
fix/JS-1475-fix-fp-on-s7763-local-exports-incorrectly-flagged-as-re-export-candidates-sonnet
Open

JS-1475 Fix false positives in S7763 for local exports flagged as re-export candidates#6644
sonar-nigel[bot] wants to merge 7 commits intomasterfrom
fix/JS-1475-fix-fp-on-s7763-local-exports-incorrectly-flagged-as-re-export-candidates-sonnet

Conversation

@sonar-nigel
Copy link
Contributor

@sonar-nigel sonar-nigel bot commented Mar 19, 2026

Fix false positives in rule S7763 where locally-defined exports were incorrectly flagged as re-export candidates (JS-1475).

What changed

  • Extended the S7763 decorator to suppress two additional false positive cases:
    • ExportNamedDeclaration nodes with a declaration property (e.g. export const alias = importedThing), where the binding is locally defined rather than re-exported
    • ExportSpecifier nodes where the local identifier does not appear in any import specifier, meaning it is locally defined
    • Added a defensive suppression when the identifier name cannot be extracted

Why

The rule was raising on patterns like export const alias = importedThing even when alias is defined locally in the same file. Only genuine re-export patterns (import then export the same binding) should be flagged.

Validation

  • Added failing tests covering single and multiple alias patterns, adapter/facade patterns, and namespace import aliases (import * as _X from '...'; export const X = _X)
  • Ruling analysis confirmed all 73 currently-raising entries are genuine re-exports; the 4 suppressed entries are the correctly-fixed local export patterns
  • Updated expected ruling files to reflect the refined behavior

Vibe Bot added 4 commits March 19, 2026 15:01
Tests cover the scenario where locally-defined exports using the
`export const alias = importedThing` pattern are incorrectly flagged
as re-export candidates. The tests verify that ExportNamedDeclaration
nodes with a declaration (single and multiple aliases, adapter/facade
patterns) are not flagged, while genuine named re-exports still raise.
An upstream sentinel test confirms the unicorn rule still raises on
these patterns so the decorator fix remains valid.

Relates to JS-1475
Local exports are incorrectly flagged as re-export candidates when
identifiers are defined in the same file. The decorator now suppresses
two additional cases: (1) ExportNamedDeclaration with a declaration
property (e.g. `export const alias = importedThing`), where the binding
is a locally defined variable; and (2) ExportSpecifier nodes where the
local identifier name does not appear in any import specifier, meaning
the identifier is locally defined rather than imported. A defensive
suppression is also added when the identifier name cannot be extracted.

Relates to JS-1475
No implementation changes were needed: ruling analysis confirmed that all
73 entries currently raising are genuine re-export patterns (import then
export) and the 4 entries not raising are `export const alias = imported`
patterns correctly suppressed by the decorator.

Added one new test case covering the namespace import alias pattern found
in ruling data: `import * as _AllIcons from './svgs'; export const AllIcons
= _AllIcons` should not be flagged, consistent with the existing suppression
of ExportNamedDeclaration nodes that carry a declaration.
@github-actions
Copy link
Contributor

Ruling Report

Code no longer flagged (4 issues)

S7763

eigen/src/palette/index.tsx:15

    13 | 
    14 | /** Icons used as a map for Palette docs */
>   15 | export const AllIcons = _AllIcons
    16 | 

rxjs/src/observable/concat.ts:3

     1 | import {  concatStatic  } from '../operator/concat';
     2 | 
>    3 | export const concat = concatStatic;

rxjs/src/observable/merge.ts:3

     1 | import {  mergeStatic  } from '../operator/merge';
     2 | 
>    3 | export const merge = mergeStatic;

rxjs/src/observable/zip.ts:3

     1 | import {  zipStatic  } from '../operator/zip';
     2 | 
>    3 | export const zip = zipStatic;

Remove unnecessary type assertion in decorator.ts (typescript:S4325).
After the `node.type === 'ExportNamedDeclaration'` discriminated union
check, TypeScript already narrows `node` to `estree.ExportNamedDeclaration`,
making the explicit `(node as estree.ExportNamedDeclaration)` cast redundant.
@sonar-nigel sonar-nigel bot marked this pull request as ready for review March 19, 2026 17:03
@sonar-nigel sonar-nigel bot requested a review from a team March 19, 2026 17:03
@sonar-review-alpha
Copy link

sonar-review-alpha bot commented Mar 19, 2026

Summary

This PR eliminates false positives in rule S7763 (prefer-export-from) where locally-defined exports were incorrectly flagged as re-export candidates.

Key distinction: export const X = importedY creates a local binding X with imported value, not a re-export. The rule should only flag genuine re-export patterns like export { X } from './module' or import X from './m'; export { X }.

What changed:

  • New isFromImport() check: suppresses exports for identifiers not found in any import statement (clearly local)
  • Suppression for ExportNamedDeclaration with declaration property (the export const alias = ... pattern)
  • Reordered checks: local/non-import cases → identifier validation → default import special case
  • Validation: 4 false positives fixed across eigen and rxjs, ruling analysis confirms 73 genuine re-exports remain flagged

What reviewers should know

Start here: packages/jsts/src/rules/S7763/decorator.ts lines 47-68 show the three suppression conditions in execution order — understanding the order is key to why this works.

Core logic: The isFromImport() helper (lines 92-107) is the linchpin — it catches locally-defined exports by checking if an identifier actually appears in any import statement. If not imported, it's not a re-export candidate.

Watch for: The sentinel test at lines 23-44 of the unit file. It verifies that the upstream ESLint rule (unicorn/prefer-export-from) still raises on these patterns. If that starts failing in the future, it means upstream changed and the decorator may be obsolete — this is intentional guard-rail testing.

False positives fixed: The ruling file deletions (rxjs-S7763.json completely removed, one entry removed from eigen) represent real codebases where this pattern legitimately appears. The patterns (adapter/facade exports, namespace aliases) are documented in the test cases.


  • Generate Walkthrough
  • Generate Diagram

🗣️ Give feedback

@sonar-review-alpha
Copy link

sonar-review-alpha bot commented Mar 19, 2026

Summary

Fixes false positives in rule S7763 where locally-defined exports (like export const alias = importedThing) were incorrectly flagged as re-export candidates.

The core issue: the rule was treating all export statements as potential re-exports, when only genuine re-exports (import X, then export X) should be flagged. The fix adds two layers of suppression in the decorator:

  1. Suppress ExportNamedDeclaration nodes that have a declaration property (e.g., export const/function/class) — these are definitionally locally defined, not re-exports
  2. Suppress ExportSpecifier nodes where the exported identifier is not found in any import (meaning it's locally defined)

The helper function was refactored from isDefaultImport() to getImportKind() to precisely detect whether an identifier is imported (and what kind) or not imported at all. Includes a sentinel test that verifies the upstream ESLint rule still raises on these patterns — if that test ever breaks, the decorator can be safely removed.

What reviewers should know

Start with decorator.ts: the new ExportNamedDeclaration check (line ~51) and the refactored getImportKind() logic are the key changes. The comment at the top of the file clearly explains the distinction between patterns to suppress.

The unit tests are instructive: the "upstream sentinel" verifies that export const alias = importedThing still gets flagged by the raw unicorn rule — the decorator is suppressing that, not ignoring it upstream. The new valid test cases show the patterns that should no longer trigger (including adapter/facade patterns and namespace imports).

Ruling files show 4 false positives removed (from eigen and rxjs) and all 73 remaining entries are genuine re-exports. The refactored helper function is cleaner and more reliable than the original — it catches the case where an identifier is simply not imported, which was the missing piece.


  • Generate Walkthrough
  • Generate Diagram

🗣️ Give feedback

Copy link

@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.

Conclusion: The fix is correct and the sentinel test is a nice touch. One question to resolve before approving.

🗣️ Give feedback

@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 19, 2026

Thanks for the summary — the description accurately captures the intent and approach of this fix.

@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 19, 2026

Thanks for the summary — the description accurately captures the three suppression conditions added to the decorator and the net effect on ruling results.

Vibe Bot added 2 commits March 19, 2026 18:45
…ules/S7763/decorator.ts

Comment: **Logic duplication:** `isFromImport` and `isDefaultImport` both walk `sourceCode.ast.body`, enter each `ImportDeclaration`, and scan specifiers by `specifier.local.name`. Since `isDefaultImport` is only ever called when `isFromImport` already confirmed the identifier exists in an import, the AST is traversed twice for that name.

A single helper avoids the double pass and keeps the detection logic in one place:

```ts
function getImportKind(
  sourceCode: SourceCode,
  identifierName: string,
): 'default' | 'named' | null {
  for (const node of sourceCode.ast.body) {
    if (node.type !== 'ImportDeclaration') continue;
    for (const specifier of node.specifiers) {
      if (specifier.local.name !== identifierName) continue;
      if (
        specifier.type === 'ImportDefaultSpecifier' ||
        (specifier.type === 'ImportSpecifier' &&
          (specifier.imported.type === 'Identifier'
            ? specifier.imported.name
            : String(specifier.imported.value)) === 'default')
      ) {
        return 'default';
      }
      return 'named';
    }
  }
  return null;
}
```

The call site becomes:
```ts
const importKind = getImportKind(context.sourceCode, identifierName);
if (!importKind || importKind === 'default') return;
context.report(reportDescriptor);
```

If traversal logic ever needs to change (e.g. handling `export … from` re-declarations), there is only one place to update.

- [ ] Mark as noise
…ules/S7763/decorator.ts

Comment: Is suppressing all `ExportNamedDeclaration` nodes with a `declaration` intentional for *every* possible declared type, or only `VariableDeclaration`?

The unicorn rule can also report on `export function foo() {}` and `export class Foo {}` where the function/class body references an imported value. Those nodes also have `declaration !== null`. Suppressing them all is conservative and avoids FPs, but it means the rule goes silent on cases it might legitimately catch — worth a comment confirming the intended scope.

- [ ] Mark as noise
@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 19, 2026

Thanks for the review! Both questions have been addressed in the latest commits:

  1. The scope of ExportNamedDeclaration suppression is now documented with a comment explaining that all declared types (VariableDeclaration, FunctionDeclaration, ClassDeclaration) are intentionally suppressed — none of them can be simplified to export…from.
  2. The logic duplication between isFromImport/isDefaultImport has been eliminated by the getImportKind helper.

@sonarqube-next
Copy link

Copy link

@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.

LGTM! ✅

🗣️ Give feedback

@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 19, 2026

Thank you for the review!

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.

0 participants