Skip to content

Commit c7046e6

Browse files
authored
feat!: enable JSX reference tracking (#20152)
* feat!: enable JSX reference tracking * apply suggestions and update migration guide * update Reference.identifier type to include JSXIdentifier * apply suggestions * apply suggestions * add tests for no-unused-vars rule * add guidance to remove workaround rules * simplify selector * apply suggestion
1 parent f148a5e commit c7046e6

File tree

11 files changed

+771
-18
lines changed

11 files changed

+771
-18
lines changed

docs/src/extend/scope-manager-interface.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ Those members are defined but not used in ESLint.
274274
#### identifier
275275

276276
- **Type:** `ASTNode`
277-
- **Description:** The `Identifier` node of this reference.
277+
- **Description:** The `Identifier` or `JSXIdentifier` node of this reference.
278278

279279
#### from
280280

docs/src/use/migrate-to-10.0.0.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The lists below are ordered roughly by the number of users each change is expect
1919
- [`eslint:recommended` has been updated](#eslint-recommended)
2020
- [New configuration file lookup algorithm](#config-lookup-from-file)
2121
- [Old config format no longer supported](#remove-eslintrc)
22+
- [JSX references are now tracked](#jsx-reference-tracking)
2223
- [`eslint-env` comments are reported as errors](#eslint-env-comments)
2324
- [Jiti < v2.2.0 are no longer supported](#drop-old-jiti)
2425
- [POSIX character classes in glob patterns](#posix-character-classes)
@@ -32,6 +33,7 @@ The lists below are ordered roughly by the number of users each change is expect
3233

3334
- [Node.js < v20.19, v21, v23 are no longer supported](#drop-old-node)
3435
- [Old config format no longer supported](#remove-eslintrc)
36+
- [JSX references are now tracked](#jsx-reference-tracking)
3537
- [Removal of `type` property in errors of invalid `RuleTester` cases](#ruletester-type-removed)
3638
- [`Program` AST node range spans entire source text](#program-node-range)
3739
- [Fixer methods now require string `text` arguments](#fixer-text-must-be-string)
@@ -103,6 +105,31 @@ Starting with ESLint v10.0.0, the old configuration format is no longer supporte
103105

104106
**Related issue(s):** [#13481](https://github.com/eslint/eslint/issues/13481)
105107

108+
## <a name="jsx-reference-tracking"></a> JSX references are now tracked
109+
110+
ESLint v10.0.0 now tracks JSX references, enabling correct scope analysis of JSX elements.
111+
112+
Previously, ESLint did not track references created by JSX identifiers, which could lead to incorrect results from rules that rely on scope information. For example:
113+
114+
```jsx
115+
import { Card } from "./card.jsx";
116+
117+
export function createCard(name) {
118+
return <Card name={name} />;
119+
}
120+
```
121+
122+
Prior to v10.0.0, ESLint did not recognize that `<Card>` is a reference to the imported `Card`, which could result in false positives such as reporting `Card` as "defined but never used" ([`no-unused-vars`](../rules/no-unused-vars)) or false negatives such as failing to report `Card` as undefined ([`no-undef`](../rules/no-undef)) if the import is removed. Starting with v10.0.0, `<Card>` is treated as a normal reference to the variable in scope. This brings JSX handling in line with developer expectations and improves the linting experience for modern JavaScript applications using JSX.
123+
124+
**To address:**
125+
126+
- For users:
127+
- New linting reports may appear in files with JSX. Update your code accordingly or adjust rule configurations if needed.
128+
- Rules previously used to work around ESLint’s lack of JSX reference tracking (for example, [`@eslint-react/jsx-uses-vars`](https://www.eslint-react.xyz/docs/rules/jsx-uses-vars)) are no longer needed. Remove or disable them in your configuration.
129+
- For plugin developers: Custom rules relying on scope analysis may now encounter `JSXIdentifier` references. Update rules to handle these correctly.
130+
131+
**Related issue(s):** [#19495](https://github.com/eslint/eslint/issues/19495)
132+
106133
## <a name="eslint-env-comments"></a> `eslint-env` comments are reported as errors
107134

108135
In the now obsolete ESLint v8 configuration system, `/* eslint-env */` comments could be used to define globals for a file. The current configuration system does not support such comments, and starting with ESLint v10.0.0, they are reported as errors during linting.

lib/languages/js/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ function analyzeScope(ast, languageOptions, visitorKeys) {
5555
sourceType: languageOptions.sourceType || "script",
5656
childVisitorKeys: visitorKeys || evk.KEYS,
5757
fallback: evk.getKeys,
58+
jsx: ecmaFeatures.jsx,
5859
});
5960
}
6061

lib/linter/linter.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ function analyzeScope(ast, languageOptions, visitorKeys) {
473473
sourceType: languageOptions.sourceType || "script",
474474
childVisitorKeys: visitorKeys || evk.KEYS,
475475
fallback: Traverser.getKeys,
476+
jsx: ecmaFeatures.jsx,
476477
});
477478
}
478479

lib/rules/no-useless-assignment.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,12 @@ module.exports = {
336336
}
337337

338338
if (
339-
targetAssignment.variable.references.some(
340-
ref => ref.identifier.type !== "Identifier",
341-
)
339+
targetAssignment.variable.references.some(ref => {
340+
const type = ref.identifier.type;
341+
return (
342+
type !== "Identifier" && type !== "JSXIdentifier"
343+
);
344+
})
342345
) {
343346
/**
344347
* Skip checking for a variable that has at least one non-identifier reference.
@@ -529,7 +532,7 @@ module.exports = {
529532
TryStatement(node) {
530533
scopeStack.tryStatementBlocks.push(node.block);
531534
},
532-
Identifier(node) {
535+
"Identifier, JSXIdentifier"(node) {
533536
for (const segment of scopeStack.currentSegments) {
534537
const segmentInfo = scopeStack.segments[segment.id];
535538

@@ -539,7 +542,7 @@ module.exports = {
539542
segmentInfo.last = node;
540543
}
541544
},
542-
":matches(VariableDeclarator[init!=null], AssignmentExpression, UpdateExpression):exit"(
545+
"VariableDeclarator[init!=null], AssignmentExpression, UpdateExpression:exit"(
543546
node,
544547
) {
545548
if (scopeStack.currentSegments.size === 0) {

lib/types/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ export namespace AST {
125125
}
126126
}
127127

128+
interface JSXIdentifier extends ESTree.BaseNode {
129+
type: "JSXIdentifier";
130+
name: string;
131+
}
132+
128133
export namespace Scope {
129134
interface ScopeManager {
130135
scopes: Scope[];
@@ -177,7 +182,7 @@ export namespace Scope {
177182
}
178183

179184
interface Reference {
180-
identifier: ESTree.Identifier;
185+
identifier: ESTree.Identifier | JSXIdentifier;
181186
from: Scope;
182187
resolved: Variable | null;
183188
writeExpr: ESTree.Node | null;

tests/lib/rules/no-undef.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,42 @@ ruleTester.run("no-undef", rule, {
225225
code: "AsyncDisposableStack; DisposableStack; SuppressedError",
226226
languageOptions: { ecmaVersion: 2026 },
227227
},
228+
{
229+
code: "/*global App*/ <App />;",
230+
languageOptions: {
231+
ecmaVersion: 6,
232+
parserOptions: { ecmaFeatures: { jsx: true } },
233+
},
234+
},
235+
{
236+
code: "const App = () => <div/>; <App />;",
237+
languageOptions: {
238+
ecmaVersion: 6,
239+
parserOptions: { ecmaFeatures: { jsx: true } },
240+
},
241+
},
242+
{
243+
code: "let Foo, Bar; <Foo><Bar /></Foo>;",
244+
languageOptions: {
245+
ecmaVersion: 6,
246+
parserOptions: { ecmaFeatures: { jsx: true } },
247+
},
248+
},
249+
{
250+
code: "import App from './App.jsx'; <App />;",
251+
languageOptions: {
252+
ecmaVersion: 6,
253+
sourceType: "module",
254+
parserOptions: { ecmaFeatures: { jsx: true } },
255+
},
256+
},
257+
{
258+
code: "function App() { return <div/> } <App />;",
259+
languageOptions: {
260+
ecmaVersion: 6,
261+
parserOptions: { ecmaFeatures: { jsx: true } },
262+
},
263+
},
228264
],
229265
invalid: [
230266
{
@@ -432,5 +468,73 @@ ruleTester.run("no-undef", rule, {
432468
},
433469
errors: [{ messageId: "undef", data: { name: "a" }, column: 31 }],
434470
},
471+
{
472+
code: "<App />;",
473+
languageOptions: {
474+
ecmaVersion: 6,
475+
parserOptions: { ecmaFeatures: { jsx: true } },
476+
},
477+
errors: [
478+
{
479+
messageId: "undef",
480+
data: { name: "App" },
481+
line: 1,
482+
column: 2,
483+
endLine: 1,
484+
endColumn: 5,
485+
},
486+
],
487+
},
488+
{
489+
code: "let React; React.render(<App />);",
490+
languageOptions: {
491+
ecmaVersion: 6,
492+
parserOptions: { ecmaFeatures: { jsx: true } },
493+
},
494+
errors: [
495+
{
496+
messageId: "undef",
497+
data: { name: "App" },
498+
line: 1,
499+
column: 26,
500+
endLine: 1,
501+
endColumn: 29,
502+
},
503+
],
504+
},
505+
{
506+
code: "function f() { return <Button/> }",
507+
languageOptions: {
508+
ecmaVersion: 6,
509+
parserOptions: { ecmaFeatures: { jsx: true } },
510+
},
511+
errors: [
512+
{
513+
messageId: "undef",
514+
data: { name: "Button" },
515+
line: 1,
516+
column: 24,
517+
endLine: 1,
518+
endColumn: 30,
519+
},
520+
],
521+
},
522+
{
523+
code: "<Foo.Bar />",
524+
languageOptions: {
525+
ecmaVersion: 6,
526+
parserOptions: { ecmaFeatures: { jsx: true } },
527+
},
528+
errors: [
529+
{
530+
messageId: "undef",
531+
data: { name: "Foo" },
532+
line: 1,
533+
column: 2,
534+
endLine: 1,
535+
endColumn: 5,
536+
},
537+
],
538+
},
435539
],
436540
});

0 commit comments

Comments
 (0)