Skip to content

Commit

Permalink
feat: Implement SourceCode#markVariableAsUsed()
Browse files Browse the repository at this point in the history
Implements `SourceCode#markVariableAsUsed()` while leaving
`context.markVariableAsUsed()` alone.

Refs #16999
  • Loading branch information
nzakas committed Apr 13, 2023
1 parent 1fea279 commit 6a24d06
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 2 deletions.
2 changes: 1 addition & 1 deletion docs/src/extend/custom-rules.md
Expand Up @@ -133,7 +133,7 @@ Additionally, the `context` object has the following methods:
* `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()` - (**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`.
* `markVariableAsUsed(name)` - (**Deprecated:** Use `SourceCode#markVariableAsUsed(node)` instead.) 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.
Expand Down
30 changes: 30 additions & 0 deletions lib/source-code/source-code.js
Expand Up @@ -681,6 +681,36 @@ class SourceCode extends TokenStore {
}
/* eslint-enable class-methods-use-this -- node is owned by SourceCode */

/**
* Marks a variable as used in the current scope
* @param {ASTNode} identifier The identifier node representing the variable.
* @returns {boolean} True if the variable was found and marked as used, false if not.
*/
markVariableAsUsed(identifier) {

const currentScope = this.getScope(identifier);
const name = identifier.name;
const hasSpecialScope = this.scopeManager.isGlobalReturn() ||
this.scopeManager.isModule();

// Special Node.js scope means we need to start one level deeper
const initialScope = currentScope.type === "global" && hasSpecialScope
? currentScope.childScopes[0]
: currentScope;

for (let scope = initialScope; scope; scope = scope.upper) {
const variable = scope.variables.find(scopeVar => scopeVar.name === name);

if (variable) {
variable.eslintUsed = true;
return true;
}
}

return false;
}


}

module.exports = SourceCode;
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -73,7 +73,7 @@
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.1.1",
"eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.0",
"espree": "^9.5.1",
"esquery": "^1.4.2",
Expand Down
213 changes: 213 additions & 0 deletions tests/lib/source-code/source-code.js
Expand Up @@ -34,6 +34,18 @@ const AST = espree.parse("let foo = bar;", DEFAULT_CONFIG),
TEST_CODE = "var answer = 6 * 7;",
SHEBANG_TEST_CODE = `#!/usr/bin/env node\n${TEST_CODE}`;

/**
* Get variables in the current scope
* @param {Object} scope current scope
* @param {string} name name of the variable to look for
* @returns {ASTNode|null} The variable object
* @private
*/
function getVariable(scope, name) {
return scope.variables.find(v => v.name === name) || null;
}


//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -3623,4 +3635,205 @@ describe("SourceCode", () => {
});
});

describe("markVariableAsUsed()", () => {

it("should mark variables in current scope as used", () => {
const code = "var a = 1, b = 2;";
let spy, idSpy;

const config = {
plugins: {
test: {
rules: {
checker: {
create(context) {
const sourceCode = context.getSourceCode();

spy = sinon.spy(node => {
const scope = sourceCode.getScope(node);

assert.isTrue(getVariable(scope, "a").eslintUsed);
assert.notOk(getVariable(scope, "b").eslintUsed);
});

idSpy = sinon.spy(node => {
if (node.name === "a") {
sourceCode.markVariableAsUsed(node);
}
});

return { Identifier: idSpy, "Program:exit": spy };
}
}
}
}
},
languageOptions: {
sourceType: "script"
},
rules: { "test/checker": "error" }
};

flatLinter.verify(code, config);
assert(spy && spy.calledOnce, "Program:exit wasn't called.");
assert(idSpy && idSpy.calledTwice, "Identifier wasn't called twice.");
});

it("should mark variables in function args as used", () => {
const code = "function abc(a, b) { return 1; }";
let spy, idSpy;

const config = {
plugins: {
test: {
rules: {
checker: {
create(context) {
const sourceCode = context.getSourceCode();

spy = sinon.spy(node => {
const scope = sourceCode.getScope(node);

assert.isTrue(getVariable(scope, "a").eslintUsed);
assert.notOk(getVariable(scope, "b").eslintUsed);
});

idSpy = sinon.spy(node => {
if (node.name === "a") {
sourceCode.markVariableAsUsed(node);
}
});

return { Identifier: idSpy, ReturnStatement: spy };
}
}
}
}
},
rules: { "test/checker": "error" }
};

flatLinter.verify(code, config);
assert(spy && spy.calledOnce, "ReturnStatement wasn't called");
assert(idSpy && idSpy.called, "Identifier wasn't called");
});

it("should mark variables as used when sourceType is commonjs", () => {
const code = "var a = 1, b = 2;";
let spy, idSpy;

const config = {
plugins: {
test: {
rules: {
checker: {
create(context) {
const sourceCode = context.getSourceCode();

spy = sinon.spy(node => {
const globalScope = sourceCode.getScope(node),
childScope = globalScope.childScopes[0];

assert.isTrue(getVariable(childScope, "a").eslintUsed, "'a' should be marked as used.");
assert.isUndefined(getVariable(childScope, "b").eslintUsed, "'b' should be marked as used.");
});

idSpy = sinon.spy(node => {
if (node.name === "a") {
sourceCode.markVariableAsUsed(node);
}
});


return { Identifier: idSpy, "Program:exit": spy };
}
}
}
}
},
languageOptions: {
sourceType: "commonjs"
},
rules: { "test/checker": "error" }
};

flatLinter.verify(code, config);
assert(spy && spy.calledOnce, "Program:exit wasn't called.");
assert(idSpy && idSpy.called, "Identifier wasn't called.");
});

it("should mark variables in modules as used", () => {
const code = "var a = 1, b = 2;";
let spy, idSpy;

const config = {
plugins: {
test: {
rules: {
checker: {
create(context) {

const sourceCode = context.getSourceCode();

spy = sinon.spy(node => {
const globalScope = sourceCode.getScope(node),
childScope = globalScope.childScopes[0];

assert.isTrue(getVariable(childScope, "a").eslintUsed);
assert.isUndefined(getVariable(childScope, "b").eslintUsed);
});

idSpy = sinon.spy(node => {
if (node.name === "a") {
sourceCode.markVariableAsUsed(node);
}
});

return { Identifier: idSpy, "Program:exit": spy };
}
}
}
}
},
languageOptions: {
ecmaVersion: 6,
sourceType: "module"
},
rules: { "test/checker": "error" }
};

flatLinter.verify(code, config);
assert(spy && spy.calledOnce, "Program:exit wasn't called");
assert(idSpy && idSpy.called, "Identifier wasn't called");
});

it("should return false if the given variable is not found", () => {
const code = "var a = 1, b = 2;";
let spy;

const config = {
plugins: {
test: {
rules: {
checker: {
create(context) {
const sourceCode = context.getSourceCode();

spy = sinon.spy(() => {
assert.isFalse(sourceCode.markVariableAsUsed({ name: "c" }));
});

return { "Program:exit": spy };
}
}
}
}
},
rules: { "test/checker": "error" }
};

flatLinter.verify(code, config);
assert(spy && spy.calledOnce, "Program:exit wasn't called");
});
});
});

0 comments on commit 6a24d06

Please sign in to comment.