Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions apps/oxlint/src-js/plugins/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,14 +407,53 @@ export function getScope(node: ESTree.Node): Scope {

/**
* Marks as used a variable with the given name in a scope indicated by the given reference node.
* This affects the `no-unused-vars` rule.
*
* IMPORTANT: At present marking variables as used only affects other JS plugins.
* It does *not* get communicated to Oxlint's rules which are implemented on Rust side e.g. `no-unused-vars`.
* This is a known shortcoming, and will be addressed in a future release.
* https://github.com/oxc-project/oxc/issues/20350
*
* @param name - Variable name
* @param refNode - Reference node
* @param refNode - Reference node. Defaults to `Program` node if not provided.
* @returns `true` if a variable with the given name was found and marked as used, otherwise `false`
*/
/* oxlint-disable no-unused-vars */
export function markVariableAsUsed(name: string, refNode: ESTree.Node): boolean {
// TODO: Implement
throw new Error("`sourceCode.markVariableAsUsed` not implemented yet");
export function markVariableAsUsed(name: string, refNode?: ESTree.Node): boolean {
// ref: https://github.com/eslint/eslint/blob/e7cda3bdf1bdd664e6033503a3315ad81736b200/lib/languages/js/source-code/source-code.js#L984-L1024
if (refNode === undefined) {
if (ast === null) initAst();
debugAssertIsNonNull(ast);
refNode = ast;
}

let currentScope = getScope(refNode);

// `getScope` calls `initTsScopeManager` which calls `initAst`, so `ast` must have been initialized
debugAssertIsNonNull(ast);

// When in the global scope, check if there's a module/function child scope whose `block` is the Program node.
// In ESM, top-level `var` declarations live in the module scope, not the global scope.
// In CommonJS, they live in the outer function scope. If we don't step down into that child scope,
// we'd miss those variables.
if (currentScope.type === "global") {
const { childScopes } = currentScope;
if (childScopes.length !== 0) {
// Top-level scopes refer to a `Program` node
const firstChild = childScopes[0];
if (firstChild.block === ast) currentScope = firstChild;
}
}

for (let scope: Scope | null = currentScope; scope !== null; scope = scope.upper) {
const { variables } = scope;
for (let i = 0, len = variables.length; i < len; i++) {
const variable = variables[i];
if (variable.name === name) {
// @ts-expect-error - `eslintUsed` is a dynamic property not in TS-ESLint's types
variable.eslintUsed = true;
return true;
}
}
}

return false;
}
/* oxlint-enable no-unused-vars */
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"jsPlugins": ["./plugin.ts"],
"categories": {
"correctness": "off"
},
"rules": {
"mark-used-plugin/mark-used": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// `unusedTopLevel` is declared but never referenced in code,
// so `eslintUsed` should start as `false`.
const unusedTopLevel = "top";
const unusedTopLevel2 = "top2";

// `shadowedName` exists at module scope AND inside `inner`.
// Used to test that omitting `refNode` finds the module-scope one,
// and that a name can be in scope or out of scope depending on `refNode`.
const shadowedName = "module-level";

function outer(param) {
// `nestedVar` is only in `outer`'s scope
const nestedVar = param;
const nestedVar2 = param + 1;

function inner() {
// Shadows module-level `shadowedName`
const shadowedName = "inner-level";
return shadowedName;
}

return nestedVar + inner();
}

module.exports = outer(unusedTopLevel);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// `unusedTopLevel` is declared but never referenced in code,
// so `eslintUsed` should start as `false`.
const unusedTopLevel = "top";
const unusedTopLevel2 = "top2";

// `shadowedName` exists at module scope AND inside `inner`.
// Used to test that omitting `refNode` finds the module-scope one,
// and that a name can be in scope or out of scope depending on `refNode`.
const shadowedName = "module-level";

function outer(param) {
// `nestedVar` is only in `outer`'s scope
const nestedVar = param;
const nestedVar2 = param + 1;

function inner() {
// Shadows module-level `shadowedName`
const shadowedName = "inner-level";
return shadowedName;
}

return nestedVar + inner();
}

export default outer(unusedTopLevel);
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Exit code
1

# stdout
```
x mark-used-plugin(mark-used): [1] mark `unusedTopLevel` from Program:
| before: false
| result: true
| after: true
,-[files/index.cjs:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [2] mark `nonExistent` from Program:
| result: false
,-[files/index.cjs:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [3] mark `shadowedName` (no refNode):
| before: false
| result: true
| after: true
,-[files/index.cjs:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [4] mark `nestedVar` from outer:
| before: false
| result: true
| after: true
,-[files/index.cjs:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [5] mark `unusedTopLevel2` from outer:
| before: false
| result: true
| after: true
,-[files/index.cjs:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [6] mark `nestedVar2` from inner:
| before: false
| result: true
| after: true
,-[files/index.cjs:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [7] mark `doesNotExist` from inner:
| result: false
,-[files/index.cjs:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [1] mark `unusedTopLevel` from Program:
| before: false
| result: true
| after: true
,-[files/index.js:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [2] mark `nonExistent` from Program:
| result: false
,-[files/index.js:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [3] mark `shadowedName` (no refNode):
| before: false
| result: true
| after: true
,-[files/index.js:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [4] mark `nestedVar` from outer:
| before: false
| result: true
| after: true
,-[files/index.js:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [5] mark `unusedTopLevel2` from outer:
| before: false
| result: true
| after: true
,-[files/index.js:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [6] mark `nestedVar2` from inner:
| before: false
| result: true
| after: true
,-[files/index.js:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

x mark-used-plugin(mark-used): [7] mark `doesNotExist` from inner:
| result: false
,-[files/index.js:1:1]
1 | // `unusedTopLevel` is declared but never referenced in code,
: ^
2 | // so `eslintUsed` should start as `false`.
`----

Found 0 warnings and 14 errors.
Finished in Xms on 2 files with 1 rules using X threads.
```

# stderr
```
```
Loading
Loading