Skip to content

Commit 10022b1

Browse files
authored
feat: Copy getScope() to SourceCode (#17004)
* feat: Copy getScope() to SourceCode Refs #16999 * Fix no-obj-calls * Add getScope() tests * Throw error if argument is missing * Update docs * Add caching * Clean up caching
1 parent 1665c02 commit 10022b1

62 files changed

Lines changed: 638 additions & 272 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/src/extend/custom-rules.md

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -131,62 +131,13 @@ Additionally, the `context` object has the following methods:
131131
* Otherwise, if the node does not declare any variables, an empty array is returned.
132132
* `getFilename()` - returns the filename associated with the source.
133133
* `getPhysicalFilename()` - when linting a file, it returns the full path of the file on disk without any code block information. When linting text, it returns the value passed to `—stdin-filename` or `<text>` if not specified.
134-
* `getScope()` - returns the [scope](./scope-manager-interface#scope-interface) of the currently-traversed node. This information can be used to track references to variables.
134+
* `getScope()` - (**Deprecated: Use `SourceCode.getScope(node)` instead.**) returns the [scope](./scope-manager-interface#scope-interface) of the currently-traversed node. This information can be used to track references to variables.
135135
* `getSourceCode()` - returns a [`SourceCode`](#contextgetsourcecode) object that you can use to work with the source that was passed to ESLint.
136136
* `markVariableAsUsed(name)` - marks a variable with the given name in the current scope as used. This affects the [no-unused-vars](../rules/no-unused-vars) rule. Returns `true` if a variable with the given name was found and marked as used, otherwise `false`.
137137
* `report(descriptor)` - reports a problem in the code (see the [dedicated section](#contextreport)).
138138

139139
**Note:** Earlier versions of ESLint supported additional methods on the `context` object. Those methods were removed in the new format and should not be relied upon.
140140

141-
### context.getScope()
142-
143-
This method returns the scope of the current node. It is a useful method for finding information about the variables in a given scope, and how they are used in other scopes.
144-
145-
#### Scope types
146-
147-
The following table contains a list of AST node types and the scope type that they correspond to. For more information about the scope types, refer to the [`Scope` object documentation](./scope-manager-interface#scope-interface).
148-
149-
| AST Node Type | Scope Type |
150-
|:--------------------------|:-----------|
151-
| `Program` | `global` |
152-
| `FunctionDeclaration` | `function` |
153-
| `FunctionExpression` | `function` |
154-
| `ArrowFunctionExpression` | `function` |
155-
| `ClassDeclaration` | `class` |
156-
| `ClassExpression` | `class` |
157-
| `BlockStatement` ※1 | `block` |
158-
| `SwitchStatement` ※1 | `switch` |
159-
| `ForStatement` ※2 | `for` |
160-
| `ForInStatement` ※2 | `for` |
161-
| `ForOfStatement` ※2 | `for` |
162-
| `WithStatement` | `with` |
163-
| `CatchClause` | `catch` |
164-
| others | ※3 |
165-
166-
**※1** Only if the configured parser provided the block-scope feature. The default parser provides the block-scope feature if `parserOptions.ecmaVersion` is not less than `6`.<br>
167-
**※2** Only if the `for` statement defines the iteration variable as a block-scoped variable (E.g., `for (let i = 0;;) {}`).<br>
168-
**※3** The scope of the closest ancestor node which has own scope. If the closest ancestor node has multiple scopes then it chooses the innermost scope (E.g., the `Program` node has a `global` scope and a `module` scope if `Program#sourceType` is `"module"`. The innermost scope is the `module` scope.).
169-
170-
#### Scope Variables
171-
172-
The `Scope#variables` property contains an array of [`Variable` objects](./scope-manager-interface#variable-interface). These are the variables declared in current scope. You can use these `Variable` objects to track references to a variable throughout the entire module.
173-
174-
Inside of each `Variable`, the `Variable#references` property contains an array of [`Reference` objects](./scope-manager-interface#reference-interface). The `Reference` array contains all the locations where the variable is referenced in the module's source code.
175-
176-
Also inside of each `Variable`, the `Variable#defs` property contains an array of [`Definition` objects](./scope-manager-interface#definition-interface). You can use the `Definitions` to find where the variable was defined.
177-
178-
Global variables have the following additional properties:
179-
180-
* `Variable#writeable` (`boolean | undefined`) ... If `true`, this global variable can be assigned arbitrary value. If `false`, this global variable is read-only.
181-
* `Variable#eslintExplicitGlobal` (`boolean | undefined`) ... If `true`, this global variable was defined by a `/* globals */` directive comment in the source code file.
182-
* `Variable#eslintExplicitGlobalComments` (`Comment[] | undefined`) ... The array of `/* globals */` directive comments which defined this global variable in the source code file. This property is `undefined` if there are no `/* globals */` directive comments.
183-
* `Variable#eslintImplicitGlobalSetting` (`"readonly" | "writable" | undefined`) ... The configured value in config files. This can be different from `variable.writeable` if there are `/* globals */` directive comments.
184-
185-
For examples of using `context.getScope()` to track variables, refer to the source code for the following built-in rules:
186-
187-
* [no-shadow](https://github.com/eslint/eslint/blob/main/lib/rules/no-shadow.js): Calls `context.getScopes()` at the global scope and parses all child scopes to make sure a variable name is not reused at a lower scope. ([no-shadow](../rules/no-shadow) documentation)
188-
* [no-redeclare](https://github.com/eslint/eslint/blob/main/lib/rules/no-redeclare.js): Calls `context.getScope()` at each scope to make sure that a variable is not declared twice at that scope. ([no-redeclare](../rules/no-redeclare) documentation)
189-
190141
### context.report()
191142

192143
The main method you'll use is `context.report()`, which publishes a warning or error (depending on the configuration being used). This method accepts a single argument, which is an object containing the following properties:
@@ -684,6 +635,57 @@ Finally, comments can be accessed through many of `sourceCode`'s methods using t
684635

685636
Shebangs are represented by tokens of type `"Shebang"`. They are treated as comments and can be accessed by the methods outlined above.
686637

638+
### Accessing Variable Scopes
639+
640+
The `SourceCode#getScope(node)` method returns the scope of the given node. It is a useful method for finding information about the variables in a given scope and how they are used in other scopes.
641+
642+
**Deprecated:** The `context.getScope()` is deprecated; make sure to use `SourceCode#getScope(node)` instead.
643+
644+
#### Scope types
645+
646+
The following table contains a list of AST node types and the scope type that they correspond to. For more information about the scope types, refer to the [`Scope` object documentation](./scope-manager-interface#scope-interface).
647+
648+
| AST Node Type | Scope Type |
649+
|:--------------------------|:-----------|
650+
| `Program` | `global` |
651+
| `FunctionDeclaration` | `function` |
652+
| `FunctionExpression` | `function` |
653+
| `ArrowFunctionExpression` | `function` |
654+
| `ClassDeclaration` | `class` |
655+
| `ClassExpression` | `class` |
656+
| `BlockStatement` ※1 | `block` |
657+
| `SwitchStatement` ※1 | `switch` |
658+
| `ForStatement` ※2 | `for` |
659+
| `ForInStatement` ※2 | `for` |
660+
| `ForOfStatement` ※2 | `for` |
661+
| `WithStatement` | `with` |
662+
| `CatchClause` | `catch` |
663+
| others | ※3 |
664+
665+
**※1** Only if the configured parser provided the block-scope feature. The default parser provides the block-scope feature if `parserOptions.ecmaVersion` is not less than `6`.<br>
666+
**※2** Only if the `for` statement defines the iteration variable as a block-scoped variable (E.g., `for (let i = 0;;) {}`).<br>
667+
**※3** The scope of the closest ancestor node which has own scope. If the closest ancestor node has multiple scopes then it chooses the innermost scope (E.g., the `Program` node has a `global` scope and a `module` scope if `Program#sourceType` is `"module"`. The innermost scope is the `module` scope.).
668+
669+
#### Scope Variables
670+
671+
The `Scope#variables` property contains an array of [`Variable` objects](./scope-manager-interface#variable-interface). These are the variables declared in current scope. You can use these `Variable` objects to track references to a variable throughout the entire module.
672+
673+
Inside of each `Variable`, the `Variable#references` property contains an array of [`Reference` objects](./scope-manager-interface#reference-interface). The `Reference` array contains all the locations where the variable is referenced in the module's source code.
674+
675+
Also inside of each `Variable`, the `Variable#defs` property contains an array of [`Definition` objects](./scope-manager-interface#definition-interface). You can use the `Definitions` to find where the variable was defined.
676+
677+
Global variables have the following additional properties:
678+
679+
* `Variable#writeable` (`boolean | undefined`) ... If `true`, this global variable can be assigned arbitrary value. If `false`, this global variable is read-only.
680+
* `Variable#eslintExplicitGlobal` (`boolean | undefined`) ... If `true`, this global variable was defined by a `/* globals */` directive comment in the source code file.
681+
* `Variable#eslintExplicitGlobalComments` (`Comment[] | undefined`) ... The array of `/* globals */` directive comments which defined this global variable in the source code file. This property is `undefined` if there are no `/* globals */` directive comments.
682+
* `Variable#eslintImplicitGlobalSetting` (`"readonly" | "writable" | undefined`) ... The configured value in config files. This can be different from `variable.writeable` if there are `/* globals */` directive comments.
683+
684+
For examples of using `SourceCode#getScope()` to track variables, refer to the source code for the following built-in rules:
685+
686+
* [no-shadow](https://github.com/eslint/eslint/blob/main/lib/rules/no-shadow.js): Calls `sourceCode.getScope()` at the `Program` node and inspects all child scopes to make sure a variable name is not reused at a lower scope. ([no-shadow](../rules/no-shadow) documentation)
687+
* [no-redeclare](https://github.com/eslint/eslint/blob/main/lib/rules/no-redeclare.js): Calls `sourceCode.getScope()` at each scope to make sure that a variable is not declared twice in the same scope. ([no-redeclare](../rules/no-redeclare) documentation)
688+
687689
### Accessing Code Paths
688690

689691
ESLint analyzes code paths while traversing AST.

lib/linter/linter.js

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -857,47 +857,22 @@ function parse(text, languageOptions, filePath) {
857857
}
858858
}
859859

860-
/**
861-
* Gets the scope for the current node
862-
* @param {ScopeManager} scopeManager The scope manager for this AST
863-
* @param {ASTNode} currentNode The node to get the scope of
864-
* @returns {eslint-scope.Scope} The scope information for this node
865-
*/
866-
function getScope(scopeManager, currentNode) {
867-
868-
// On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope.
869-
const inner = currentNode.type !== "Program";
870-
871-
for (let node = currentNode; node; node = node.parent) {
872-
const scope = scopeManager.acquire(node, inner);
873-
874-
if (scope) {
875-
if (scope.type === "function-expression-name") {
876-
return scope.childScopes[0];
877-
}
878-
return scope;
879-
}
880-
}
881-
882-
return scopeManager.scopes[0];
883-
}
884-
885860
/**
886861
* Marks a variable as used in the current scope
887-
* @param {ScopeManager} scopeManager The scope manager for this AST. The scope may be mutated by this function.
862+
* @param {SourceCode} sourceCode The source code for the currently linted file.
888863
* @param {ASTNode} currentNode The node currently being traversed
889864
* @param {LanguageOptions} languageOptions The options used to parse this text
890865
* @param {string} name The name of the variable that should be marked as used.
891866
* @returns {boolean} True if the variable was found and marked as used, false if not.
892867
*/
893-
function markVariableAsUsed(scopeManager, currentNode, languageOptions, name) {
868+
function markVariableAsUsed(sourceCode, currentNode, languageOptions, name) {
894869
const parserOptions = languageOptions.parserOptions;
895870
const sourceType = languageOptions.sourceType;
896871
const hasGlobalReturn =
897872
(parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn) ||
898873
sourceType === "commonjs";
899874
const specialScope = hasGlobalReturn || sourceType === "module";
900-
const currentScope = getScope(scopeManager, currentNode);
875+
const currentScope = sourceCode.getScope(currentNode);
901876

902877
// Special Node.js scope means we need to start one level deeper
903878
const initialScope = currentScope.type === "global" && specialScope ? currentScope.childScopes[0] : currentScope;
@@ -1026,9 +1001,9 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
10261001
getCwd: () => cwd,
10271002
getFilename: () => filename,
10281003
getPhysicalFilename: () => physicalFilename || filename,
1029-
getScope: () => getScope(sourceCode.scopeManager, currentNode),
1004+
getScope: () => sourceCode.getScope(currentNode),
10301005
getSourceCode: () => sourceCode,
1031-
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name),
1006+
markVariableAsUsed: name => markVariableAsUsed(sourceCode, currentNode, languageOptions, name),
10321007
parserOptions: {
10331008
...languageOptions.parserOptions
10341009
},

lib/rules/camelcase.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module.exports = {
7373
const ignoreImports = options.ignoreImports;
7474
const ignoreGlobals = options.ignoreGlobals;
7575
const allow = options.allow || [];
76+
const sourceCode = context.getSourceCode();
7677

7778
//--------------------------------------------------------------------------
7879
// Helpers
@@ -245,8 +246,8 @@ module.exports = {
245246
return {
246247

247248
// Report camelcase of global variable references ------------------
248-
Program() {
249-
const scope = context.getScope();
249+
Program(node) {
250+
const scope = sourceCode.getScope(node);
250251

251252
if (!ignoreGlobals) {
252253

lib/rules/consistent-this.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module.exports = {
3636

3737
create(context) {
3838
let aliases = [];
39+
const sourceCode = context.getSourceCode();
3940

4041
if (context.options.length === 0) {
4142
aliases.push("that");
@@ -115,10 +116,11 @@ module.exports = {
115116

116117
/**
117118
* Check each alias to ensure that is was assigned to the correct value.
119+
* @param {ASTNode} node The node that represents the scope to check.
118120
* @returns {void}
119121
*/
120-
function ensureWasAssigned() {
121-
const scope = context.getScope();
122+
function ensureWasAssigned(node) {
123+
const scope = sourceCode.getScope(node);
122124

123125
aliases.forEach(alias => {
124126
checkWasAssigned(alias, scope);

lib/rules/global-require.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ module.exports = {
7171
},
7272

7373
create(context) {
74+
const sourceCode = context.getSourceCode();
75+
7476
return {
7577
CallExpression(node) {
76-
const currentScope = context.getScope();
78+
const currentScope = sourceCode.getScope(node);
7779

7880
if (node.callee.name === "require" && !isShadowed(currentScope, node.callee)) {
7981
const isGoodRequire = context.getAncestors().every(parent => ACCEPTABLE_PARENTS.has(parent.type));

lib/rules/handle-callback-err.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ module.exports = {
3838
create(context) {
3939

4040
const errorArgument = context.options[0] || "err";
41+
const sourceCode = context.getSourceCode();
4142

4243
/**
4344
* Checks if the given argument should be interpreted as a regexp pattern.
@@ -79,7 +80,7 @@ module.exports = {
7980
* @returns {void}
8081
*/
8182
function checkForError(node) {
82-
const scope = context.getScope(),
83+
const scope = sourceCode.getScope(node),
8384
parameters = getParameters(scope),
8485
firstParameter = parameters[0];
8586

lib/rules/id-blacklist.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ module.exports = {
140140

141141
const denyList = new Set(context.options);
142142
const reportedNodes = new Set();
143+
const sourceCode = context.getSourceCode();
143144

144145
let globalScope;
145146

@@ -231,8 +232,8 @@ module.exports = {
231232

232233
return {
233234

234-
Program() {
235-
globalScope = context.getScope();
235+
Program(node) {
236+
globalScope = sourceCode.getScope(node);
236237
},
237238

238239
Identifier(node) {

lib/rules/id-denylist.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ module.exports = {
121121

122122
const denyList = new Set(context.options);
123123
const reportedNodes = new Set();
124+
const sourceCode = context.getSourceCode();
124125

125126
let globalScope;
126127

@@ -210,8 +211,8 @@ module.exports = {
210211

211212
return {
212213

213-
Program() {
214-
globalScope = context.getScope();
214+
Program(node) {
215+
globalScope = sourceCode.getScope(node);
215216
},
216217

217218
[[

lib/rules/id-match.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ module.exports = {
6767
onlyDeclarations = !!options.onlyDeclarations,
6868
ignoreDestructuring = !!options.ignoreDestructuring;
6969

70+
const sourceCode = context.getSourceCode();
7071
let globalScope;
7172

7273
//--------------------------------------------------------------------------
@@ -170,8 +171,8 @@ module.exports = {
170171

171172
return {
172173

173-
Program() {
174-
globalScope = context.getScope();
174+
Program(node) {
175+
globalScope = sourceCode.getScope(node);
175176
},
176177

177178
Identifier(node) {

lib/rules/logical-assignment-operators.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ module.exports = {
206206
const mode = context.options[0] === "never" ? "never" : "always";
207207
const checkIf = mode === "always" && context.options.length > 1 && context.options[1].enforceForIfStatements;
208208
const sourceCode = context.getSourceCode();
209-
const isStrict = context.getScope().isStrict;
209+
const isStrict = sourceCode.getScope(sourceCode.ast).isStrict;
210210

211211
/**
212212
* Returns false if the access could be a getter
@@ -409,7 +409,7 @@ module.exports = {
409409
}
410410

411411
const body = hasBody ? ifNode.consequent.body[0] : ifNode.consequent;
412-
const scope = context.getScope();
412+
const scope = sourceCode.getScope(ifNode);
413413
const existence = getExistence(ifNode.test, scope);
414414

415415
if (

0 commit comments

Comments
 (0)