Skip to content

Commit

Permalink
feat: Copy getScope() to SourceCode (#17004)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nzakas committed Mar 24, 2023
1 parent 1665c02 commit 10022b1
Show file tree
Hide file tree
Showing 62 changed files with 638 additions and 272 deletions.
102 changes: 52 additions & 50 deletions docs/src/extend/custom-rules.md
Expand Up @@ -131,62 +131,13 @@ Additionally, the `context` object has the following methods:
* Otherwise, if the node does not declare any variables, an empty array is returned.
* `getFilename()` - returns the filename associated with the source.
* `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.
* `getScope()` - returns the [scope](./scope-manager-interface#scope-interface) of the currently-traversed node. This information can be used to track references to variables.
* `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.
* `getSourceCode()` - returns a [`SourceCode`](#contextgetsourcecode) object that you can use to work with the source that was passed to ESLint.
* `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`.
* `report(descriptor)` - reports a problem in the code (see the [dedicated section](#contextreport)).

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

### context.getScope()

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.

#### Scope types

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

| AST Node Type | Scope Type |
|:--------------------------|:-----------|
| `Program` | `global` |
| `FunctionDeclaration` | `function` |
| `FunctionExpression` | `function` |
| `ArrowFunctionExpression` | `function` |
| `ClassDeclaration` | `class` |
| `ClassExpression` | `class` |
| `BlockStatement` ※1 | `block` |
| `SwitchStatement` ※1 | `switch` |
| `ForStatement` ※2 | `for` |
| `ForInStatement` ※2 | `for` |
| `ForOfStatement` ※2 | `for` |
| `WithStatement` | `with` |
| `CatchClause` | `catch` |
| others | ※3 |

**※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>
**※2** Only if the `for` statement defines the iteration variable as a block-scoped variable (E.g., `for (let i = 0;;) {}`).<br>
**※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.).

#### Scope Variables

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.

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.

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.

Global variables have the following additional properties:

* `Variable#writeable` (`boolean | undefined`) ... If `true`, this global variable can be assigned arbitrary value. If `false`, this global variable is read-only.
* `Variable#eslintExplicitGlobal` (`boolean | undefined`) ... If `true`, this global variable was defined by a `/* globals */` directive comment in the source code file.
* `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.
* `Variable#eslintImplicitGlobalSetting` (`"readonly" | "writable" | undefined`) ... The configured value in config files. This can be different from `variable.writeable` if there are `/* globals */` directive comments.

For examples of using `context.getScope()` to track variables, refer to the source code for the following built-in rules:

* [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)
* [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)

### context.report()

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:
Expand Down Expand Up @@ -684,6 +635,57 @@ Finally, comments can be accessed through many of `sourceCode`'s methods using t

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

### Accessing Variable Scopes

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.

**Deprecated:** The `context.getScope()` is deprecated; make sure to use `SourceCode#getScope(node)` instead.

#### Scope types

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

| AST Node Type | Scope Type |
|:--------------------------|:-----------|
| `Program` | `global` |
| `FunctionDeclaration` | `function` |
| `FunctionExpression` | `function` |
| `ArrowFunctionExpression` | `function` |
| `ClassDeclaration` | `class` |
| `ClassExpression` | `class` |
| `BlockStatement` ※1 | `block` |
| `SwitchStatement` ※1 | `switch` |
| `ForStatement` ※2 | `for` |
| `ForInStatement` ※2 | `for` |
| `ForOfStatement` ※2 | `for` |
| `WithStatement` | `with` |
| `CatchClause` | `catch` |
| others | ※3 |

**※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>
**※2** Only if the `for` statement defines the iteration variable as a block-scoped variable (E.g., `for (let i = 0;;) {}`).<br>
**※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.).

#### Scope Variables

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.

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.

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.

Global variables have the following additional properties:

* `Variable#writeable` (`boolean | undefined`) ... If `true`, this global variable can be assigned arbitrary value. If `false`, this global variable is read-only.
* `Variable#eslintExplicitGlobal` (`boolean | undefined`) ... If `true`, this global variable was defined by a `/* globals */` directive comment in the source code file.
* `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.
* `Variable#eslintImplicitGlobalSetting` (`"readonly" | "writable" | undefined`) ... The configured value in config files. This can be different from `variable.writeable` if there are `/* globals */` directive comments.

For examples of using `SourceCode#getScope()` to track variables, refer to the source code for the following built-in rules:

* [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)
* [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)

### Accessing Code Paths

ESLint analyzes code paths while traversing AST.
Expand Down
35 changes: 5 additions & 30 deletions lib/linter/linter.js
Expand Up @@ -857,47 +857,22 @@ function parse(text, languageOptions, filePath) {
}
}

/**
* Gets the scope for the current node
* @param {ScopeManager} scopeManager The scope manager for this AST
* @param {ASTNode} currentNode The node to get the scope of
* @returns {eslint-scope.Scope} The scope information for this node
*/
function getScope(scopeManager, currentNode) {

// On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope.
const inner = currentNode.type !== "Program";

for (let node = currentNode; node; node = node.parent) {
const scope = scopeManager.acquire(node, inner);

if (scope) {
if (scope.type === "function-expression-name") {
return scope.childScopes[0];
}
return scope;
}
}

return scopeManager.scopes[0];
}

/**
* Marks a variable as used in the current scope
* @param {ScopeManager} scopeManager The scope manager for this AST. The scope may be mutated by this function.
* @param {SourceCode} sourceCode The source code for the currently linted file.
* @param {ASTNode} currentNode The node currently being traversed
* @param {LanguageOptions} languageOptions The options used to parse this text
* @param {string} name The name of the variable that should be marked as used.
* @returns {boolean} True if the variable was found and marked as used, false if not.
*/
function markVariableAsUsed(scopeManager, currentNode, languageOptions, name) {
function markVariableAsUsed(sourceCode, currentNode, languageOptions, name) {
const parserOptions = languageOptions.parserOptions;
const sourceType = languageOptions.sourceType;
const hasGlobalReturn =
(parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn) ||
sourceType === "commonjs";
const specialScope = hasGlobalReturn || sourceType === "module";
const currentScope = getScope(scopeManager, currentNode);
const currentScope = sourceCode.getScope(currentNode);

// Special Node.js scope means we need to start one level deeper
const initialScope = currentScope.type === "global" && specialScope ? currentScope.childScopes[0] : currentScope;
Expand Down Expand Up @@ -1026,9 +1001,9 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
getCwd: () => cwd,
getFilename: () => filename,
getPhysicalFilename: () => physicalFilename || filename,
getScope: () => getScope(sourceCode.scopeManager, currentNode),
getScope: () => sourceCode.getScope(currentNode),
getSourceCode: () => sourceCode,
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name),
markVariableAsUsed: name => markVariableAsUsed(sourceCode, currentNode, languageOptions, name),
parserOptions: {
...languageOptions.parserOptions
},
Expand Down
5 changes: 3 additions & 2 deletions lib/rules/camelcase.js
Expand Up @@ -73,6 +73,7 @@ module.exports = {
const ignoreImports = options.ignoreImports;
const ignoreGlobals = options.ignoreGlobals;
const allow = options.allow || [];
const sourceCode = context.getSourceCode();

//--------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -245,8 +246,8 @@ module.exports = {
return {

// Report camelcase of global variable references ------------------
Program() {
const scope = context.getScope();
Program(node) {
const scope = sourceCode.getScope(node);

if (!ignoreGlobals) {

Expand Down
6 changes: 4 additions & 2 deletions lib/rules/consistent-this.js
Expand Up @@ -36,6 +36,7 @@ module.exports = {

create(context) {
let aliases = [];
const sourceCode = context.getSourceCode();

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

/**
* Check each alias to ensure that is was assigned to the correct value.
* @param {ASTNode} node The node that represents the scope to check.
* @returns {void}
*/
function ensureWasAssigned() {
const scope = context.getScope();
function ensureWasAssigned(node) {
const scope = sourceCode.getScope(node);

aliases.forEach(alias => {
checkWasAssigned(alias, scope);
Expand Down
4 changes: 3 additions & 1 deletion lib/rules/global-require.js
Expand Up @@ -71,9 +71,11 @@ module.exports = {
},

create(context) {
const sourceCode = context.getSourceCode();

return {
CallExpression(node) {
const currentScope = context.getScope();
const currentScope = sourceCode.getScope(node);

if (node.callee.name === "require" && !isShadowed(currentScope, node.callee)) {
const isGoodRequire = context.getAncestors().every(parent => ACCEPTABLE_PARENTS.has(parent.type));
Expand Down
3 changes: 2 additions & 1 deletion lib/rules/handle-callback-err.js
Expand Up @@ -38,6 +38,7 @@ module.exports = {
create(context) {

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

/**
* Checks if the given argument should be interpreted as a regexp pattern.
Expand Down Expand Up @@ -79,7 +80,7 @@ module.exports = {
* @returns {void}
*/
function checkForError(node) {
const scope = context.getScope(),
const scope = sourceCode.getScope(node),
parameters = getParameters(scope),
firstParameter = parameters[0];

Expand Down
5 changes: 3 additions & 2 deletions lib/rules/id-blacklist.js
Expand Up @@ -140,6 +140,7 @@ module.exports = {

const denyList = new Set(context.options);
const reportedNodes = new Set();
const sourceCode = context.getSourceCode();

let globalScope;

Expand Down Expand Up @@ -231,8 +232,8 @@ module.exports = {

return {

Program() {
globalScope = context.getScope();
Program(node) {
globalScope = sourceCode.getScope(node);
},

Identifier(node) {
Expand Down
5 changes: 3 additions & 2 deletions lib/rules/id-denylist.js
Expand Up @@ -121,6 +121,7 @@ module.exports = {

const denyList = new Set(context.options);
const reportedNodes = new Set();
const sourceCode = context.getSourceCode();

let globalScope;

Expand Down Expand Up @@ -210,8 +211,8 @@ module.exports = {

return {

Program() {
globalScope = context.getScope();
Program(node) {
globalScope = sourceCode.getScope(node);
},

[[
Expand Down
5 changes: 3 additions & 2 deletions lib/rules/id-match.js
Expand Up @@ -67,6 +67,7 @@ module.exports = {
onlyDeclarations = !!options.onlyDeclarations,
ignoreDestructuring = !!options.ignoreDestructuring;

const sourceCode = context.getSourceCode();
let globalScope;

//--------------------------------------------------------------------------
Expand Down Expand Up @@ -170,8 +171,8 @@ module.exports = {

return {

Program() {
globalScope = context.getScope();
Program(node) {
globalScope = sourceCode.getScope(node);
},

Identifier(node) {
Expand Down
4 changes: 2 additions & 2 deletions lib/rules/logical-assignment-operators.js
Expand Up @@ -206,7 +206,7 @@ module.exports = {
const mode = context.options[0] === "never" ? "never" : "always";
const checkIf = mode === "always" && context.options.length > 1 && context.options[1].enforceForIfStatements;
const sourceCode = context.getSourceCode();
const isStrict = context.getScope().isStrict;
const isStrict = sourceCode.getScope(sourceCode.ast).isStrict;

/**
* Returns false if the access could be a getter
Expand Down Expand Up @@ -409,7 +409,7 @@ module.exports = {
}

const body = hasBody ? ifNode.consequent.body[0] : ifNode.consequent;
const scope = context.getScope();
const scope = sourceCode.getScope(ifNode);
const existence = getExistence(ifNode.test, scope);

if (
Expand Down
4 changes: 3 additions & 1 deletion lib/rules/no-alert.js
Expand Up @@ -101,10 +101,12 @@ module.exports = {
},

create(context) {
const sourceCode = context.getSourceCode();

return {
CallExpression(node) {
const callee = skipChainExpression(node.callee),
currentScope = context.getScope();
currentScope = sourceCode.getScope(node);

// without window.
if (callee.type === "Identifier") {
Expand Down
4 changes: 3 additions & 1 deletion lib/rules/no-catch-shadow.js
Expand Up @@ -39,6 +39,8 @@ module.exports = {

create(context) {

const sourceCode = context.getSourceCode();

//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
Expand All @@ -60,7 +62,7 @@ module.exports = {
return {

"CatchClause[param!=null]"(node) {
let scope = context.getScope();
let scope = sourceCode.getScope(node);

/*
* When ecmaVersion >= 6, CatchClause creates its own scope
Expand Down
5 changes: 3 additions & 2 deletions lib/rules/no-console.js
Expand Up @@ -51,6 +51,7 @@ module.exports = {
create(context) {
const options = context.options[0] || {};
const allowed = options.allow || [];
const sourceCode = context.getSourceCode();

/**
* Checks whether the given reference is 'console' or not.
Expand Down Expand Up @@ -109,8 +110,8 @@ module.exports = {
}

return {
"Program:exit"() {
const scope = context.getScope();
"Program:exit"(node) {
const scope = sourceCode.getScope(node);
const consoleVar = astUtils.getVariableByName(scope, "console");
const shadowed = consoleVar && consoleVar.defs.length > 0;

Expand Down

0 comments on commit 10022b1

Please sign in to comment.