diff --git a/apps/oxlint/package.json b/apps/oxlint/package.json index fff090507425b..68a4e97d91331 100644 --- a/apps/oxlint/package.json +++ b/apps/oxlint/package.json @@ -59,5 +59,9 @@ "darwin-x64", "darwin-arm64" ] + }, + "dependencies": { + "@typescript-eslint/scope-manager": "^8.46.2", + "@typescript-eslint/types": "^8.46.2" } } diff --git a/apps/oxlint/src-js/plugins/scope.ts b/apps/oxlint/src-js/plugins/scope.ts index dce6183e4e246..f588dfc46d331 100644 --- a/apps/oxlint/src-js/plugins/scope.ts +++ b/apps/oxlint/src-js/plugins/scope.ts @@ -4,7 +4,12 @@ import type * as ESTree from '../generated/types.d.ts'; -import type { Node } from './types.ts'; +import { + analyze, + type AnalyzeOptions, + type ScopeManager as TSESLintScopeManager, +} from '@typescript-eslint/scope-manager'; +import { SOURCE_CODE } from './source_code.js'; type Identifier = | ESTree.IdentifierName @@ -14,8 +19,65 @@ type Identifier = | ESTree.TSThisParameter | ESTree.TSIndexSignatureName; +/** + * @see https://eslint.org/docs/latest/developer-guide/scope-manager-interface#scopemanager-interface + */ +// This is a wrapper class around the @typescript-eslint/scope-manager package. +// We want to control what APIs are exposed to the user to limit breaking changes when we switch our implementation. export class ScopeManager { - // TODO + #scopeManager: TSESLintScopeManager; + + constructor(ast: ESTree.Program) { + const defaultOptions: AnalyzeOptions = { + globalReturn: false, + jsxFragmentName: null, + jsxPragma: 'React', + lib: ['esnext'], + sourceType: ast.sourceType, + }; + // The effectiveness of this assertion depends on our alignment with ESTree. + // It could eventually be removed as we align the remaining corner cases and the typegen. + // @ts-expect-error // TODO: our types don't quite align yet + this.#scopeManager = analyze(ast, defaultOptions); + } + + /** + * All scopes + */ + get scopes(): Scope[] { + // @ts-expect-error // TODO: our types don't quite align yet + return this.#scopeManager.scopes; + } + + /** + * The root scope + */ + get globalScope(): Scope | null { + return this.#scopeManager.globalScope as any; + } + + /** + * Get the variables that a given AST node defines. The gotten variables' `def[].node`/`def[].parent` property is the node. + * If the node does not define any variable, this returns an empty array. + * @param node An AST node to get their variables. + */ + getDeclaredVariables(node: ESTree.Node): Variable[] { + // @ts-expect-error // TODO: our types don't quite align yet + return this.#scopeManager.getDeclaredVariables(node); + } + + /** + * 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`. + * + * @param node An AST node to get their scope. + * @param inner If the node has multiple scopes, this returns the outermost scope normally. + * If `inner` is `true` then this returns the innermost scope. + */ + acquire(node: ESTree.Node, inner?: boolean): Scope | null { + // @ts-expect-error // TODO: our types don't quite align yet + return this.#scopeManager.acquire(node, inner); + } } export interface Scope { @@ -24,7 +86,7 @@ export interface Scope { upper: Scope | null; childScopes: Scope[]; variableScope: Scope; - block: Node; + block: ESTree.Node; variables: Variable[]; set: Map; references: Reference[]; @@ -74,8 +136,8 @@ export interface Reference { export interface Definition { type: DefinitionType; name: Identifier; - node: Node; - parent: Node | null; + node: ESTree.Node; + parent: ESTree.Node | null; } export type DefinitionType = @@ -92,9 +154,43 @@ export type DefinitionType = * @param node - `Identifier` node to check. * @returns `true` if the identifier is a reference to a global variable. */ -// oxlint-disable-next-line no-unused-vars -export function isGlobalReference(node: Node): boolean { - throw new Error('`sourceCode.isGlobalReference` not implemented yet'); // TODO +export function isGlobalReference(node: ESTree.Node): boolean { + // ref: https://github.com/eslint/eslint/blob/e7cda3bdf1bdd664e6033503a3315ad81736b200/lib/languages/js/source-code/source-code.js#L934-L962 + if (!node) { + throw new TypeError('Missing required argument: node.'); + } + + if (node.type !== 'Identifier') { + return false; + } + + const { name } = node; + if (typeof name !== 'string') { + return false; + } + + const globalScope = SOURCE_CODE.scopeManager.scopes[0]; + if (!globalScope) return false; + + // If the identifier is a reference to a global variable, the global scope should have a variable with the name. + const variable = globalScope.set.get(name); + + // Global variables are not defined by any node, so they should have no definitions. + if (!variable || variable.defs.length > 0) { + return false; + } + + // If there is a variable by the same name exists in the global scope, we need to check our node is one of its references. + const { references } = variable; + + for (let i = 0; i < references.length; i++) { + const reference = references[i]; + if (reference.identifier === node) { + return true; + } + } + + return false; } /** @@ -103,9 +199,9 @@ export function isGlobalReference(node: Node): boolean { * @param node - The node for which the variables are obtained. * @returns An array of variable nodes representing the variables that `node` defines. */ -// oxlint-disable-next-line no-unused-vars -export function getDeclaredVariables(node: Node): Variable[] { - throw new Error('`sourceCode.getDeclaredVariables` not implemented yet'); // TODO +export function getDeclaredVariables(node: ESTree.Node): Variable[] { + // ref: https://github.com/eslint/eslint/blob/e7cda3bdf1bdd664e6033503a3315ad81736b200/lib/languages/js/source-code/source-code.js#L904 + return SOURCE_CODE.scopeManager.getDeclaredVariables(node); } /** @@ -113,18 +209,27 @@ export function getDeclaredVariables(node: Node): Variable[] { * @param node - The node to get the scope of. * @returns The scope information for this node. */ -// oxlint-disable-next-line no-unused-vars -export function getScope(node: Node): Scope { - throw new Error('`sourceCode.getScope` not implemented yet'); // TODO -} +export function getScope(node: ESTree.Node): Scope { + // ref: https://github.com/eslint/eslint/blob/e7cda3bdf1bdd664e6033503a3315ad81736b200/lib/languages/js/source-code/source-code.js#L862-L892 + if (!node) { + throw new TypeError('Missing required argument: node.'); + } -/** - * Mark a variable as used in the current scope - * @param name - The name of the variable to mark as used. - * @param refNode? - The closest node to the variable reference. - * @returns `true` if the variable was found and marked as used, `false` if not. - */ -// oxlint-disable-next-line no-unused-vars -export function markVariableAsUsed(name: string, refNode: Node): boolean { - throw new Error('`sourceCode.markVariableAsUsed` not implemented yet'); // TODO + const { scopeManager } = SOURCE_CODE; + const inner = node.type !== 'Program'; + + // Traverse up the AST to find a `Node` whose scope can be acquired. + for (let current: any = node; current; current = current.parent) { + const scope = scopeManager.acquire(current, inner); + + if (scope) { + if (scope.type === 'function-expression-name') { + return scope.childScopes[0]; + } + + return scope; + } + } + + return scopeManager.scopes[0]; } diff --git a/apps/oxlint/src-js/plugins/source_code.ts b/apps/oxlint/src-js/plugins/source_code.ts index d98ac670d6f6d..699fd0c0e46a4 100644 --- a/apps/oxlint/src-js/plugins/source_code.ts +++ b/apps/oxlint/src-js/plugins/source_code.ts @@ -15,11 +15,11 @@ import { lines, resetLines, } from './location.js'; +import { ScopeManager } from './scope.js'; import * as scopeMethods from './scope.js'; import * as tokenMethods from './tokens.js'; import type { Program } from '../generated/types.d.ts'; -import type { ScopeManager } from './scope.ts'; import type { BufferWithArrays, Node, NodeOrToken, Ranged } from './types.ts'; const { max } = Math; @@ -81,6 +81,7 @@ export function resetSourceAndAst(): void { buffer = null; sourceText = null; ast = null; + scopeManagerInstance = null; resetBuffer(); resetLines(); } @@ -94,6 +95,10 @@ export function resetSourceAndAst(): void { // 2. No need for private properties, which are somewhat expensive to access - use top-level variables instead. // // Freeze the object to prevent user mutating it. + +// ScopeManager instance for current file (reset between files) +let scopeManagerInstance: ScopeManager | null = null; + export const SOURCE_CODE = Object.freeze({ // Get source text. get text(): string { @@ -114,7 +119,8 @@ export const SOURCE_CODE = Object.freeze({ // Get `ScopeManager` for the file. get scopeManager(): ScopeManager { - throw new Error('`sourceCode.scopeManager` not implemented yet'); // TODO + if (ast === null) initAst(); + return (scopeManagerInstance ??= new ScopeManager(ast)); }, // Get visitor keys to traverse this AST. @@ -216,7 +222,6 @@ export const SOURCE_CODE = Object.freeze({ isGlobalReference: scopeMethods.isGlobalReference, getDeclaredVariables: scopeMethods.getDeclaredVariables, getScope: scopeMethods.getScope, - markVariableAsUsed: scopeMethods.markVariableAsUsed, // Token methods getTokens: tokenMethods.getTokens, diff --git a/apps/oxlint/test/e2e.test.ts b/apps/oxlint/test/e2e.test.ts index b4bf6a71e2d38..ca1c1a5079491 100644 --- a/apps/oxlint/test/e2e.test.ts +++ b/apps/oxlint/test/e2e.test.ts @@ -177,6 +177,14 @@ describe('oxlint CLI', () => { await testFixture('sourceCode_late_access_after_only'); }); + it('should support scopeManager', async () => { + await testFixture('scope_manager'); + }); + + it('should support scope helper methods in `context.sourceCode`', async () => { + await testFixture('sourceCode_scope_methods'); + }); + it('should support selectors', async () => { await testFixture('selector'); }); diff --git a/apps/oxlint/test/fixtures/scope_manager/.oxlintrc.json b/apps/oxlint/test/fixtures/scope_manager/.oxlintrc.json new file mode 100644 index 0000000000000..b186b9be187c4 --- /dev/null +++ b/apps/oxlint/test/fixtures/scope_manager/.oxlintrc.json @@ -0,0 +1,9 @@ +{ + "jsPlugins": ["./plugin.ts"], + "categories": { + "correctness": "off" + }, + "rules": { + "scope-manager-plugin/scope": "error" + } +} diff --git a/apps/oxlint/test/fixtures/scope_manager/files/index.ts b/apps/oxlint/test/fixtures/scope_manager/files/index.ts new file mode 100644 index 0000000000000..9f3140bb98e67 --- /dev/null +++ b/apps/oxlint/test/fixtures/scope_manager/files/index.ts @@ -0,0 +1,59 @@ +const { a, b, c } = {}; + +let x = 1; +var y = 'hello'; +z = 'world'; + +function topLevelFunction(param: number) { + const localVar = param + x; + { + const deepestVar = y + localVar; + return deepestVar; + } + return localVar; +} + +export module TopLevelModule { + interface ConcreteInterface { + concreteVar: number; + } + export interface GenericInterface extends ConcreteInterface { + genericVar: T; + } + export const x: GenericInterface = { + concreteVar: 42, + genericVar: 'string', + }; +} + +const concreteValue: TopLevelModule.GenericInterface = { + concreteVar: TopLevelModule.x.concreteVar, + genericVar: 'string', +}; + +class TestClass { + instanceVar: string; + #privateVar: string; + static { + const privateVar = 'private'; + this.prototype.#privateVar = arrowFunc(privateVar); + + const arrowFunc = (param: string) => { + const arrowVar = param; + return arrowVar + y; + }; + } + + constructor(x: string) { + if (x) { + this.instanceVar = x; + } + } +} + +label: { + const blockVar = 'block'; + console.log(blockVar); +} + +const unusedVar = 'should be detected'; diff --git a/apps/oxlint/test/fixtures/scope_manager/output.snap.md b/apps/oxlint/test/fixtures/scope_manager/output.snap.md new file mode 100644 index 0000000000000..9823352f1e4ec --- /dev/null +++ b/apps/oxlint/test/fixtures/scope_manager/output.snap.md @@ -0,0 +1,86 @@ +# Exit code +1 + +# stdout +``` + x scope-manager-plugin(scope): File has 12 scopes: , , topLevelFunction, , TopLevelModule, GenericInterface, TestClass, , + | , , , + ,-[files/index.ts:1:1] + 1 | const { a, b, c } = {}; + : ^ + 2 | + `---- + + x scope-manager-plugin(scope): VariableDeclaration declares 3 variables: a, b, c. + ,-[files/index.ts:1:1] + 1 | const { a, b, c } = {}; + : ^^^^^^^^^^^^^^^^^^^^^^^ + 2 | + `---- + + x scope-manager-plugin(scope): topLevelFunction has 3 local variables: arguments, param, localVar. Child scopes: 1. + ,-[files/index.ts:7:1] + 6 | + 7 | ,-> function topLevelFunction(param: number) { + 8 | | const localVar = param + x; + 9 | | { + 10 | | const deepestVar = y + localVar; + 11 | | return deepestVar; + 12 | | } + 13 | | return localVar; + 14 | `-> } + 15 | + `---- + + x scope-manager-plugin(scope): TopLevelModule has 3 local variables: ConcreteInterface, GenericInterface, x. Child scopes: 1. + ,-[files/index.ts:16:8] + 15 | + 16 | ,-> export module TopLevelModule { + 17 | | interface ConcreteInterface { + 18 | | concreteVar: number; + 19 | | } + 20 | | export interface GenericInterface extends ConcreteInterface { + 21 | | genericVar: T; + 22 | | } + 23 | | export const x: GenericInterface = { + 24 | | concreteVar: 42, + 25 | | genericVar: 'string', + 26 | | }; + 27 | `-> } + 28 | + `---- + + x scope-manager-plugin(scope): TestClass static block has 2 local variables: privateVar, arrowFunc. Child scopes: 1. + ,-[files/index.ts:37:3] + 36 | #privateVar: string; + 37 | ,-> static { + 38 | | const privateVar = 'private'; + 39 | | this.prototype.#privateVar = arrowFunc(privateVar); + 40 | | + 41 | | const arrowFunc = (param: string) => { + 42 | | const arrowVar = param; + 43 | | return arrowVar + y; + 44 | | }; + 45 | `-> } + 46 | + `---- + + x scope-manager-plugin(scope): LabeledStatement's block has 1 local variables: blockVar. Child scopes: 0. + ,-[files/index.ts:54:1] + 53 | + 54 | ,-> label: { + 55 | | const blockVar = 'block'; + 56 | | console.log(blockVar); + 57 | `-> } + 58 | + `---- + +Found 0 warnings and 6 errors. +Finished in Xms on 1 file using X threads. +``` + +# stderr +``` +WARNING: JS plugins are experimental and not subject to semver. +Breaking changes are possible while JS plugins support is under development. +``` diff --git a/apps/oxlint/test/fixtures/scope_manager/plugin.ts b/apps/oxlint/test/fixtures/scope_manager/plugin.ts new file mode 100644 index 0000000000000..02e366a0400f9 --- /dev/null +++ b/apps/oxlint/test/fixtures/scope_manager/plugin.ts @@ -0,0 +1,107 @@ +import assert from 'node:assert'; +import type { Context, Node, Plugin, Scope } from '../../../dist/index.js'; + +const SPAN: Node = { + start: 0, + end: 0, + range: [0, 0], + loc: { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }, +}; + +const plugin: Plugin = { + meta: { + name: 'scope-manager-plugin', + }, + rules: { + scope: { + createOnce(context: Context) { + let moduleScope: Scope | null = null; + + return { + Program(program) { + const { scopeManager } = context.sourceCode; + + moduleScope = scopeManager.scopes.at(1) as unknown as Scope; + assert.equal(moduleScope.upper, scopeManager.globalScope); + + context.report({ + message: `File has ${scopeManager.scopes.length} scopes: ${ + scopeManager.scopes.map((s: any) => s.block?.id?.name ?? '<' + s.constructor.name + '>').join(', ') + }`, + node: SPAN, + }); + + const acquiredScope = scopeManager.acquire(program); + assert.equal(acquiredScope, scopeManager.globalScope); + }, + VariableDeclaration(node) { + if (node.declarations[0].id.type === 'ObjectPattern') { + const variables = context.sourceCode.scopeManager.getDeclaredVariables(node); + context.report({ + message: `VariableDeclaration declares ${variables.length} variables: ${ + variables.map(v => v.name).join(', ') + }.`, + node: node, + }); + } + }, + FunctionDeclaration(node) { + if (node.id && node.id.name === 'topLevelFunction') { + const topLevelFunctionScope = context.sourceCode.scopeManager.acquire(node)!; + assert.equal(topLevelFunctionScope.upper, moduleScope); + context.report({ + message: `topLevelFunction has ${topLevelFunctionScope.variables.length} local variables: ${ + topLevelFunctionScope?.variables.map(v => v.name).join(', ') + }. Child scopes: ${topLevelFunctionScope.childScopes.length}.`, + node: topLevelFunctionScope.block, + }); + } + }, + TSModuleDeclaration(node) { + if (node.id.type === 'Identifier' && node.id.name === 'TopLevelModule') { + const topLevelModuleScope = context.sourceCode.scopeManager.acquire(node)!; + assert.equal(topLevelModuleScope.upper, moduleScope); + context.report({ + message: `TopLevelModule has ${topLevelModuleScope.variables.length} local variables: ${ + topLevelModuleScope?.variables.map(v => v.name).join(', ') + }. Child scopes: ${topLevelModuleScope.childScopes.length}.`, + node: topLevelModuleScope.block, + }); + } + }, + StaticBlock(node) { + const staticBlockScope = context.sourceCode.scopeManager.acquire(node)!; + const upperBlock = staticBlockScope.upper!.block; + assert('type' in upperBlock); + assert(upperBlock.type === 'ClassDeclaration'); + assert('id' in upperBlock); + assert(typeof upperBlock.id === 'object' && upperBlock.id !== null); + assert('name' in upperBlock.id); + assert.equal(upperBlock.id.name, 'TestClass'); + context.report({ + message: `TestClass static block has ${staticBlockScope.variables.length} local variables: ${ + staticBlockScope?.variables.map(v => v.name).join(', ') + }. Child scopes: ${staticBlockScope.childScopes.length}.`, + node: node, + }); + }, + LabeledStatement(node) { + const labeledStatementScope = context.sourceCode.scopeManager.acquire(node.body)!; + assert.equal(labeledStatementScope.upper, moduleScope); + context.report({ + message: `LabeledStatement's block has ${labeledStatementScope.variables.length} local variables: ${ + labeledStatementScope?.variables.map(v => v.name).join(', ') + }. Child scopes: ${labeledStatementScope.childScopes.length}.`, + node: node, + }); + }, + }; + }, + }, + }, +}; + +export default plugin; diff --git a/apps/oxlint/test/fixtures/sourceCode_scope_methods/.oxlintrc.json b/apps/oxlint/test/fixtures/sourceCode_scope_methods/.oxlintrc.json new file mode 100644 index 0000000000000..acfec97c86d7d --- /dev/null +++ b/apps/oxlint/test/fixtures/sourceCode_scope_methods/.oxlintrc.json @@ -0,0 +1,9 @@ +{ + "jsPlugins": ["./plugin.ts"], + "categories": { + "correctness": "off" + }, + "rules": { + "scope-plugin/scope": "error" + } +} diff --git a/apps/oxlint/test/fixtures/sourceCode_scope_methods/files/index.js b/apps/oxlint/test/fixtures/sourceCode_scope_methods/files/index.js new file mode 100644 index 0000000000000..65c2e95448686 --- /dev/null +++ b/apps/oxlint/test/fixtures/sourceCode_scope_methods/files/index.js @@ -0,0 +1,11 @@ +const topLevelConstant = 1, + secondTopLevelConstant = 2; + +function topLevelFunction(param) { + const localConstant = topLevelConstant + param; + return function innerFunction() { + return localConstant + Math.PI; + }; +} + +export const topLevelExport = topLevelFunction(2); diff --git a/apps/oxlint/test/fixtures/sourceCode_scope_methods/output.snap.md b/apps/oxlint/test/fixtures/sourceCode_scope_methods/output.snap.md new file mode 100644 index 0000000000000..60d61164649bc --- /dev/null +++ b/apps/oxlint/test/fixtures/sourceCode_scope_methods/output.snap.md @@ -0,0 +1,154 @@ +# Exit code +1 + +# stdout +``` + x scope-plugin(scope): getDeclaredVariables(): topLevelConstant, secondTopLevelConstant + ,-[files/index.js:1:1] + 1 | ,-> const topLevelConstant = 1, + 2 | `-> secondTopLevelConstant = 2; + 3 | + `---- + + x scope-plugin(scope): isGlobalReference(topLevelConstant): false + ,-[files/index.js:1:7] + 1 | const topLevelConstant = 1, + : ^^^^^^^^^^^^^^^^ + 2 | secondTopLevelConstant = 2; + `---- + + x scope-plugin(scope): isGlobalReference(secondTopLevelConstant): false + ,-[files/index.js:2:3] + 1 | const topLevelConstant = 1, + 2 | secondTopLevelConstant = 2; + : ^^^^^^^^^^^^^^^^^^^^^^ + 3 | + `---- + + x scope-plugin(scope): getScope(topLevelFunction): type: function + | isStrict: true + | vars: [arguments, param, localConstant] + | through: [topLevelConstant, Math] + | upper: module + | + ,-[files/index.js:4:1] + 3 | + 4 | ,-> function topLevelFunction(param) { + 5 | | const localConstant = topLevelConstant + param; + 6 | | return function innerFunction() { + 7 | | return localConstant + Math.PI; + 8 | | }; + 9 | `-> } + 10 | + `---- + + x scope-plugin(scope): isGlobalReference(topLevelFunction): false + ,-[files/index.js:4:10] + 3 | + 4 | function topLevelFunction(param) { + : ^^^^^^^^^^^^^^^^ + 5 | const localConstant = topLevelConstant + param; + `---- + + x scope-plugin(scope): isGlobalReference(param): false + ,-[files/index.js:4:27] + 3 | + 4 | function topLevelFunction(param) { + : ^^^^^ + 5 | const localConstant = topLevelConstant + param; + `---- + + x scope-plugin(scope): getDeclaredVariables(): localConstant + ,-[files/index.js:5:3] + 4 | function topLevelFunction(param) { + 5 | const localConstant = topLevelConstant + param; + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 | return function innerFunction() { + `---- + + x scope-plugin(scope): isGlobalReference(localConstant): false + ,-[files/index.js:5:9] + 4 | function topLevelFunction(param) { + 5 | const localConstant = topLevelConstant + param; + : ^^^^^^^^^^^^^ + 6 | return function innerFunction() { + `---- + + x scope-plugin(scope): isGlobalReference(topLevelConstant): false + ,-[files/index.js:5:25] + 4 | function topLevelFunction(param) { + 5 | const localConstant = topLevelConstant + param; + : ^^^^^^^^^^^^^^^^ + 6 | return function innerFunction() { + `---- + + x scope-plugin(scope): isGlobalReference(param): false + ,-[files/index.js:5:44] + 4 | function topLevelFunction(param) { + 5 | const localConstant = topLevelConstant + param; + : ^^^^^ + 6 | return function innerFunction() { + `---- + + x scope-plugin(scope): isGlobalReference(innerFunction): false + ,-[files/index.js:6:19] + 5 | const localConstant = topLevelConstant + param; + 6 | return function innerFunction() { + : ^^^^^^^^^^^^^ + 7 | return localConstant + Math.PI; + `---- + + x scope-plugin(scope): isGlobalReference(localConstant): false + ,-[files/index.js:7:12] + 6 | return function innerFunction() { + 7 | return localConstant + Math.PI; + : ^^^^^^^^^^^^^ + 8 | }; + `---- + + x scope-plugin(scope): isGlobalReference(Math): false + ,-[files/index.js:7:28] + 6 | return function innerFunction() { + 7 | return localConstant + Math.PI; + : ^^^^ + 8 | }; + `---- + + x scope-plugin(scope): isGlobalReference(PI): false + ,-[files/index.js:7:33] + 6 | return function innerFunction() { + 7 | return localConstant + Math.PI; + : ^^ + 8 | }; + `---- + + x scope-plugin(scope): getDeclaredVariables(): topLevelExport + ,-[files/index.js:11:8] + 10 | + 11 | export const topLevelExport = topLevelFunction(2); + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + + x scope-plugin(scope): isGlobalReference(topLevelExport): false + ,-[files/index.js:11:14] + 10 | + 11 | export const topLevelExport = topLevelFunction(2); + : ^^^^^^^^^^^^^^ + `---- + + x scope-plugin(scope): isGlobalReference(topLevelFunction): false + ,-[files/index.js:11:31] + 10 | + 11 | export const topLevelExport = topLevelFunction(2); + : ^^^^^^^^^^^^^^^^ + `---- + +Found 0 warnings and 17 errors. +Finished in Xms on 1 file using X threads. +``` + +# stderr +``` +WARNING: JS plugins are experimental and not subject to semver. +Breaking changes are possible while JS plugins support is under development. +``` diff --git a/apps/oxlint/test/fixtures/sourceCode_scope_methods/plugin.ts b/apps/oxlint/test/fixtures/sourceCode_scope_methods/plugin.ts new file mode 100644 index 0000000000000..2345f85ea8ec3 --- /dev/null +++ b/apps/oxlint/test/fixtures/sourceCode_scope_methods/plugin.ts @@ -0,0 +1,66 @@ +import assert from 'node:assert'; + +import type { ESTree, Node, Plugin, Rule, Scope, Variable } from '../../../dist/index.js'; + +type Program = ESTree.Program; + +type Identifier = + | ESTree.IdentifierName + | ESTree.IdentifierReference + | ESTree.BindingIdentifier + | ESTree.LabelIdentifier; + +const SPAN: Node = { + start: 0, + end: 0, + range: [0, 0], + loc: { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }, +}; + +const rule: Rule = { + create(context) { + const { sourceCode } = context; + + return { + VariableDeclaration(node) { + const variables = sourceCode.getDeclaredVariables(node); + context.report({ + message: `getDeclaredVariables(): ${variables.map(v => v.name).join(', ')}`, + node, + }); + }, + Identifier(node) { + const { name } = node; + const isGlobal = sourceCode.isGlobalReference(node); + context.report({ + message: `isGlobalReference(${name}): ${isGlobal}`, + node, + }); + }, + FunctionDeclaration(node) { + const scope = sourceCode.getScope(node); + let text = ''; + text += `type: ${scope.type}\n`; + text += `isStrict: ${scope.isStrict}\n`; + text += `vars: [${scope.variables.map(v => v.name).join(', ')}]\n`; + text += `through: [${scope.through.map(r => (r.identifier as any).name).join(', ')}]\n`; + if (scope.upper) text += `upper: ${scope.upper.type}\n`; + + context.report({ + message: `getScope(${node.id.name}): ${text}`, + node, + }); + }, + }; + }, +}; + +const plugin: Plugin = { + meta: { name: 'scope-plugin' }, + rules: { scope: rule }, +}; + +export default plugin; diff --git a/apps/oxlint/tsconfig.json b/apps/oxlint/tsconfig.json index f1a7450d93b5b..eab5604f8a947 100644 --- a/apps/oxlint/tsconfig.json +++ b/apps/oxlint/tsconfig.json @@ -9,6 +9,7 @@ }, "exclude": [ "node_modules", - "fixtures" + "fixtures", + "test/fixtures/*/files" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf3229b6cbb37..3f7d8bc3a9942 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,13 @@ importers: version: 4.0.3(@types/node@24.9.1)(@vitest/browser-playwright@4.0.3)(jiti@2.6.1) apps/oxlint: + dependencies: + '@typescript-eslint/scope-manager': + specifier: ^8.46.2 + version: 8.46.2 + '@typescript-eslint/types': + specifier: ^8.46.2 + version: 8.46.2 devDependencies: '@types/esquery': specifier: ^1.5.4 @@ -1904,6 +1911,10 @@ packages: '@types/vscode@1.93.0': resolution: {integrity: sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==} + '@typescript-eslint/scope-manager@8.46.2': + resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.46.2': resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5528,6 +5539,11 @@ snapshots: '@types/vscode@1.93.0': {} + '@typescript-eslint/scope-manager@8.46.2': + dependencies: + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/types@8.46.2': {} '@typescript-eslint/visitor-keys@8.46.2':