Skip to content

Commit 208b5cc

Browse files
feat!: Use ScopeManager#addGlobals() (#20132)
* feat!: Use `ScopeManager#addGlobals()` * patch `@typescript-eslint/parser` * update tests * fix knip errors * fix behavior with `--no-inline-config` * add more tests * add more tests * cleanup tests for predefined globals * add more test assertions * update types and docs * disable `eslint-webpack-plugin` types integration test in CI * update scope-manager-interface.md * fix tests * Update lib/languages/js/source-code/source-code.js Co-authored-by: Francesco Trotta <github@fasttime.org> * use main branch of eslint/js * update migration guide * update dependencies --------- Co-authored-by: Francesco Trotta <github@fasttime.org>
1 parent a2ee188 commit 208b5cc

14 files changed

Lines changed: 1403 additions & 175 deletions

File tree

.github/workflows/types-integration.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ permissions:
1010

1111
jobs:
1212
webpack_plugin:
13+
if: false # Temporarily disabled until @typescript-eslint/parser is updated
1314
name: Types (eslint-webpack-plugin)
1415
runs-on: ubuntu-latest
1516
steps:

docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
3232
"@munter/tap-render": "^0.2.0",
3333
"@types/markdown-it": "^12.2.3",
34-
"@typescript-eslint/parser": "^8.27.0",
34+
"@typescript-eslint/parser": "file:../tools/typescript-eslint-parser",
3535
"algoliasearch": "^4.12.1",
3636
"autoprefixer": "^10.4.13",
3737
"cross-env": "^7.0.3",

docs/src/extend/custom-parsers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ The `parseForESLint` method should return an object that contains the required p
4848
- `services` can contain any parser-dependent services (such as type checkers for nodes). The value of the `services` property is available to rules as `context.sourceCode.parserServices`. Default is an empty object.
4949
- `scopeManager` can be a [ScopeManager](./scope-manager-interface) object. Custom parsers can use customized scope analysis for experimental/enhancement syntaxes. The default is the `ScopeManager` object which is created by [eslint-scope](https://github.com/eslint/js/tree/main/packages/eslint-scope).
5050
- Support for `scopeManager` was added in ESLint v4.14.0. ESLint versions that support `scopeManager` will provide an `eslintScopeManager: true` property in `parserOptions`, which can be used for feature detection.
51+
- As of ESLint v10.0.0, `ScopeManager` must automatically resolve references to global variables declared in the code, and provide an instance method `addGlobals(names: string[])` that creates variables with the given names in the global scope and resolves references to them.
5152
- `visitorKeys` can be an object to customize AST traversal. The keys of the object are the type of AST nodes. Each value is an array of the property names which should be traversed. The default is [KEYS of `eslint-visitor-keys`](https://github.com/eslint/js/tree/main/packages/eslint-visitor-keys#evkkeys).
5253
- Support for `visitorKeys` was added in ESLint v4.14.0. ESLint versions that support `visitorKeys` will provide an `eslintVisitorKeys: true` property in `parserOptions`, which can be used for feature detection.
5354

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ This document was written based on the implementation of [eslint-scope](https://
3232
- **Return type:** `Scope | null`
3333
- **Description:** Get the scope of a given AST node. The gotten scope's `block` property is the node. This method never returns `function-expression-name` scope. If the node does not have their scope, this returns `null`.
3434

35+
#### addGlobals(names)
36+
37+
- **Parameters:**
38+
- `names` (`string[]`) ... Names of variables to add to the global scope.
39+
- **Return type:** `undefined`
40+
- **Description:** Adds variables to the global scope and resolves references to them. This method is used by the ESLint core and should never be used in rules.
41+
3542
#### getDeclaredVariables(node)
3643

3744
- **Parameters:**

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ The lists below are ordered roughly by the number of users each change is expect
3131
- [Removal of `type` property in errors of invalid `RuleTester` cases](#ruletester-type-removed)
3232
- [Fixer methods now require string `text` arguments](#fixer-text-must-be-string)
3333
- [`Program` AST node range spans entire source text](#program-node-range)
34+
- [New requirements for `ScopeManager` implementations](#scope-manager)
3435

3536
### Breaking changes for integration developers
3637

@@ -176,6 +177,18 @@ Starting with ESLint v10, `Program.range` covers the entire source text, includi
176177

177178
**Related issue(s):** [eslint/js#648](https://github.com/eslint/js/issues/648)
178179

180+
## <a name="scope-manager"></a> New requirements for `ScopeManager` implementations
181+
182+
As of ESLint v10.0.0, custom `ScopeManager` implementations must automatically resolve references to global variables declared in the code, including `var` and `function` declarations, and provide an instance method `addGlobals(names: string[])` that creates variables with the given names in the global scope and resolves references to them.
183+
184+
The default `ScopeManager` implementation [`eslint-scope`](https://www.npmjs.com/package/eslint-scope) has already been updated.
185+
186+
This change does not affect custom rules.
187+
188+
**To address:** If you maintain a custom parser that provides a custom `ScopeManager` implementation, update your custom `ScopeManager` implementation.
189+
190+
**Related issue(s):** [eslint/js#665](https://github.com/eslint/js/issues/665)
191+
179192
## <a name="eslint-env-comments"></a> `eslint-env` comments are reported as errors
180193

181194
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, they are reported as errors during linting.

knip.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@
4141
"ignore": ["tests/types/**"],
4242
"ignoreDependencies": ["eslint"],
4343
},
44-
"tools/internal-rules": {},
44+
"tools/typescript-eslint-parser": {},
4545
},
4646
}

lib/languages/js/source-code/source-code.js

Lines changed: 30 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ const { isCommentToken } = require("@eslint-community/eslint-utils"),
2020
VisitNodeStep,
2121
CallMethodStep,
2222
Directive,
23-
} = require("@eslint/plugin-kit"),
24-
eslintScope = require("eslint-scope");
23+
} = require("@eslint/plugin-kit");
2524

2625
//------------------------------------------------------------------------------
2726
// Type Definitions
2827
//------------------------------------------------------------------------------
2928

3029
/** @typedef {import("eslint-scope").Variable} Variable */
3130
/** @typedef {import("eslint-scope").Scope} Scope */
31+
/** @typedef {import("eslint-scope").ScopeManager} ScopeManager */
3232
/** @typedef {import("@eslint/core").SourceCode} ISourceCode */
3333
/** @typedef {import("@eslint/core").Directive} IDirective */
3434
/** @typedef {import("@eslint/core").TraversalStep} ITraversalStep */
@@ -264,103 +264,37 @@ function findLineNumberBinarySearch(lineStartIndices, target) {
264264
* Ensures that variables representing built-in properties of the Global Object,
265265
* and any globals declared by special block comments, are present in the global
266266
* scope.
267-
* @param {Scope} globalScope The global scope.
267+
* @param {ScopeManager} scopeManager Scope manager.
268268
* @param {Object|undefined} configGlobals The globals declared in configuration
269269
* @param {Object|undefined} inlineGlobals The globals declared in the source code
270270
* @returns {void}
271271
*/
272272
function addDeclaredGlobals(
273-
globalScope,
274-
configGlobals = {},
275-
inlineGlobals = {},
273+
scopeManager,
274+
configGlobals = Object.create(null),
275+
inlineGlobals = Object.create(null),
276276
) {
277-
// Define configured global variables.
278-
for (const id of new Set([
279-
...Object.keys(configGlobals),
280-
...Object.keys(inlineGlobals),
281-
])) {
282-
/*
283-
* `normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would
284-
* typically be caught when validating a config anyway (validity for inline global comments is checked separately).
285-
*/
286-
const configValue =
287-
configGlobals[id] === void 0
288-
? void 0
289-
: normalizeConfigGlobal(configGlobals[id]);
290-
const commentValue = inlineGlobals[id] && inlineGlobals[id].value;
291-
const value = commentValue || configValue;
292-
const sourceComments = inlineGlobals[id] && inlineGlobals[id].comments;
293-
294-
if (value === "off") {
295-
continue;
296-
}
297-
298-
let variable = globalScope.set.get(id);
299-
300-
if (!variable) {
301-
variable = new eslintScope.Variable(id, globalScope);
302-
303-
globalScope.variables.push(variable);
304-
globalScope.set.set(id, variable);
305-
}
277+
const finalGlobals = { __proto__: null, ...configGlobals };
306278

307-
variable.eslintImplicitGlobalSetting = configValue;
308-
variable.eslintExplicitGlobal = sourceComments !== void 0;
309-
variable.eslintExplicitGlobalComments = sourceComments;
310-
variable.writeable = value === "writable";
279+
for (const [name, data] of Object.entries(inlineGlobals)) {
280+
finalGlobals[name] = data.value;
311281
}
312282

313-
/*
314-
* "through" contains all references which definitions cannot be found.
315-
* Since we augment the global scope using configuration, we need to update
316-
* references and remove the ones that were added by configuration.
317-
*/
318-
globalScope.through = globalScope.through.filter(reference => {
319-
const name = reference.identifier.name;
320-
const variable = globalScope.set.get(name);
321-
322-
if (variable) {
323-
/*
324-
* Links the variable and the reference.
325-
* And this reference is removed from `Scope#through`.
326-
*/
327-
reference.resolved = variable;
328-
variable.references.push(reference);
283+
const names = Object.keys(finalGlobals).filter(
284+
name => finalGlobals[name] !== "off",
285+
);
329286

330-
return false;
331-
}
287+
scopeManager.addGlobals(names);
332288

333-
return true;
334-
});
289+
const globalScope = scopeManager.scopes[0];
335290

336-
/*
337-
* "implicit" contains information about implicit global variables (those created
338-
* implicitly by assigning values to undeclared variables in non-strict code).
339-
* Since we augment the global scope using configuration, we need to remove
340-
* the ones that were added by configuration, as they are either built-in
341-
* or declared elsewhere, therefore not implicit.
342-
* Since the "implicit" property was not documented, first we'll check if it exists
343-
* because it's possible that not all custom scope managers create this property.
344-
* If it exists, we assume it has properties `variables` and `set`. Property
345-
* `left` is considered optional (for example, typescript-eslint's scope manage
346-
* has this property named `leftToBeResolved`).
347-
*/
348-
const { implicit } = globalScope;
349-
if (typeof implicit === "object" && implicit !== null) {
350-
implicit.variables = implicit.variables.filter(variable => {
351-
const name = variable.name;
352-
if (globalScope.set.has(name)) {
353-
implicit.set.delete(name);
354-
return false;
355-
}
356-
return true;
357-
});
291+
for (const name of names) {
292+
const variable = globalScope.set.get(name);
358293

359-
if (implicit.left) {
360-
implicit.left = implicit.left.filter(
361-
reference => !globalScope.set.has(reference.identifier.name),
362-
);
363-
}
294+
variable.eslintImplicitGlobalSetting = configGlobals[name];
295+
variable.eslintExplicitGlobal = !!inlineGlobals[name];
296+
variable.eslintExplicitGlobalComments = inlineGlobals[name]?.comments;
297+
variable.writeable = finalGlobals[name] === "writable";
364298
}
365299
}
366300

@@ -1162,6 +1096,15 @@ class SourceCode extends TokenStore {
11621096
: void 0,
11631097
languageOptions.globals,
11641098
);
1099+
1100+
/*
1101+
* `normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would
1102+
* typically be caught when validating a config anyway (validity for inline global comments is checked separately).
1103+
*/
1104+
for (const [name, value] of Object.entries(configGlobals)) {
1105+
configGlobals[name] = normalizeConfigGlobal(value);
1106+
}
1107+
11651108
const varsCache = this[caches].get("vars");
11661109

11671110
varsCache.set("configGlobals", configGlobals);
@@ -1282,7 +1225,7 @@ class SourceCode extends TokenStore {
12821225
const exportedVariables = varsCache.get("exportedVariables");
12831226
const globalScope = this.scopeManager.scopes[0];
12841227

1285-
addDeclaredGlobals(globalScope, configGlobals, inlineGlobals);
1228+
addDeclaredGlobals(this.scopeManager, configGlobals, inlineGlobals);
12861229

12871230
if (exportedVariables) {
12881231
markExportedVariables(globalScope, exportedVariables);

lib/types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ export namespace Scope {
134134
acquire(node: ESTree.Node, inner?: boolean): Scope | null;
135135

136136
getDeclaredVariables(node: ESTree.Node): Variable[];
137+
138+
addGlobals(names: string[]): void;
137139
}
138140

139141
interface Scope {

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@
123123
"cross-spawn": "^7.0.6",
124124
"debug": "^4.3.2",
125125
"escape-string-regexp": "^4.0.0",
126-
"eslint-scope": "^8.4.0",
127-
"eslint-visitor-keys": "^4.2.1",
126+
"eslint-scope": "^9.0.0",
127+
"eslint-visitor-keys": "^5.0.0",
128128
"espree": "^11.0.0",
129129
"esquery": "^1.5.0",
130130
"esutils": "^2.0.2",
@@ -150,7 +150,7 @@
150150
"@trunkio/launcher": "^1.3.4",
151151
"@types/esquery": "^1.5.4",
152152
"@types/node": "^22.13.14",
153-
"@typescript-eslint/parser": "^8.4.0",
153+
"@typescript-eslint/parser": "file:tools/typescript-eslint-parser",
154154
"babel-loader": "^8.0.5",
155155
"c8": "^7.12.0",
156156
"chai": "^4.0.1",

tests/fixtures/parsers/non-js-parser.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,17 @@ exports.parseForESLint = () => ({
225225
"range": {}
226226
},
227227
services: {},
228-
scopeManager: { variables: [], scopes: [{ set: new Map(), variables: [], through: [] }], getDeclaredVariables: () => {} },
228+
scopeManager: {
229+
variables: [],
230+
scopes: [{ set: new Map(), variables: [], through: [] }],
231+
getDeclaredVariables() {},
232+
addGlobals(names) {
233+
const globalScope = this.scopes[0];
234+
for (const name of names) {
235+
globalScope.set.set(name, {});
236+
}
237+
}
238+
},
229239
visitorKeys: {
230240
Document: ['definitions'],
231241
ObjectTypeDefinition: ['interfaces', 'directives', 'fields'],

0 commit comments

Comments
 (0)