diff --git a/.changeset/modern-spiders-retire.md b/.changeset/modern-spiders-retire.md new file mode 100644 index 00000000..9c8ffa98 --- /dev/null +++ b/.changeset/modern-spiders-retire.md @@ -0,0 +1,7 @@ +--- +"svelte-eslint-parser": minor +--- + +BREAKING: fix resolve to module scope for top level statements + +This change corrects the result of `context.getScope()`, but it is a breaking change. diff --git a/src/context/script-let.ts b/src/context/script-let.ts index 439d81aa..463aa5cc 100644 --- a/src/context/script-let.ts +++ b/src/context/script-let.ts @@ -58,6 +58,7 @@ type ScriptLetRestoreCallbackOption = { registerNodeToScope: (node: any, scope: Scope) => void; scopeManager: ScopeManager; visitorKeys?: { [type: string]: string[] }; + addPostProcess: (callback: () => void) => void; }; /** @@ -130,6 +131,8 @@ export class ScriptLetContext { private readonly restoreCallbacks: RestoreCallback[] = []; + private readonly programRestoreCallbacks: ScriptLetRestoreCallback[] = []; + private readonly closeScopeCallbacks: (() => void)[] = []; private readonly unique = new UniqueIdGenerator(); @@ -574,6 +577,10 @@ export class ScriptLetContext { this.closeScopeCallbacks.pop()!(); } + public addProgramRestore(callback: ScriptLetRestoreCallback): void { + this.programRestoreCallbacks.push(callback); + } + private appendScript( text: string, offset: number, @@ -631,6 +638,57 @@ export class ScriptLetContext { * Restore AST nodes */ public restore(result: ESLintExtendedProgram): void { + const nodeToScope = getNodeToScope(result.scopeManager!); + const postprocessList: (() => void)[] = []; + + const callbackOption: ScriptLetRestoreCallbackOption = { + getScope, + getInnermostScope, + registerNodeToScope, + scopeManager: result.scopeManager!, + visitorKeys: result.visitorKeys, + addPostProcess: (cb) => postprocessList.push(cb), + }; + + this.restoreNodes(result, callbackOption); + this.restoreProgram(result, callbackOption); + postprocessList.forEach((p) => p()); + + // Helpers + /** Get scope */ + function getScope(node: ESTree.Node) { + return getScopeFromNode(result.scopeManager!, node); + } + + /** Get innermost scope */ + function getInnermostScope(node: ESTree.Node) { + return getInnermostScopeFromNode(result.scopeManager!, node); + } + + /** Register node to scope */ + function registerNodeToScope(node: any, scope: Scope): void { + // If we replace the `scope.block` at this time, + // the scope restore calculation will not work, so we will replace the `scope.block` later. + postprocessList.push(() => { + scope.block = node; + }); + + const scopes = nodeToScope.get(node); + if (scopes) { + scopes.push(scope); + } else { + nodeToScope.set(node, [scope]); + } + } + } + + /** + * Restore AST nodes + */ + private restoreNodes( + result: ESLintExtendedProgram, + callbackOption: ScriptLetRestoreCallbackOption + ): void { let orderedRestoreCallback = this.restoreCallbacks.shift(); if (!orderedRestoreCallback) { return; @@ -640,8 +698,6 @@ export class ScriptLetContext { const processedTokens = []; const comments = result.ast.comments; const processedComments = []; - const nodeToScope = getNodeToScope(result.scopeManager!); - const postprocessList: (() => void)[] = []; let tok; while ((tok = tokens.shift())) { @@ -731,13 +787,12 @@ export class ScriptLetContext { startIndex.comment, endIndex.comment - startIndex.comment ); - restoreCallback.callback(node, targetTokens, targetComments, { - getScope, - getInnermostScope, - registerNodeToScope, - scopeManager: result.scopeManager!, - visitorKeys: result.visitorKeys, - }); + restoreCallback.callback( + node, + targetTokens, + targetComments, + callbackOption + ); processedTokens.push(...targetTokens); processedComments.push(...targetComments); @@ -750,33 +805,22 @@ export class ScriptLetContext { result.ast.tokens = processedTokens; result.ast.comments = processedComments; - postprocessList.forEach((p) => p()); - - // Helpers - /** Get scope */ - function getScope(node: ESTree.Node) { - return getScopeFromNode(result.scopeManager!, node); - } - - /** Get innermost scope */ - function getInnermostScope(node: ESTree.Node) { - return getInnermostScopeFromNode(result.scopeManager!, node); - } - - /** Register node to scope */ - function registerNodeToScope(node: any, scope: Scope): void { - // If we replace the `scope.block` at this time, - // the scope restore calculation will not work, so we will replace the `scope.block` later. - postprocessList.push(() => { - scope.block = node; - }); + } - const scopes = nodeToScope.get(node); - if (scopes) { - scopes.push(scope); - } else { - nodeToScope.set(node, [scope]); - } + /** + * Restore program node + */ + private restoreProgram( + result: ESLintExtendedProgram, + callbackOption: ScriptLetRestoreCallbackOption + ): void { + for (const callback of this.programRestoreCallbacks) { + callback( + result.ast, + result.ast.tokens, + result.ast.comments, + callbackOption + ); } } diff --git a/src/parser/converts/root.ts b/src/parser/converts/root.ts index b08a72c7..500abba6 100644 --- a/src/parser/converts/root.ts +++ b/src/parser/converts/root.ts @@ -9,6 +9,7 @@ import {} from "./common"; import type { Context } from "../../context"; import { convertChildren, extractElementTags } from "./element"; import { convertAttributeTokens } from "./attr"; +import type { Scope } from "eslint-scope"; /** * Convert root @@ -127,6 +128,30 @@ export function convertSvelteRoot( body.push(style); } + // Set the scope of the Program node. + ctx.scriptLet.addProgramRestore( + ( + node, + _tokens, + _comments, + { scopeManager, registerNodeToScope, addPostProcess } + ) => { + const scopes: Scope[] = []; + for (const scope of scopeManager.scopes) { + if (scope.block === node) { + registerNodeToScope(ast, scope); + scopes.push(scope); + } + } + addPostProcess(() => { + // Reverts the node indicated by `block` to the original Program node. + // This state is incorrect, but `eslint-utils`'s `referenceTracker.iterateEsmReferences()` tracks import statements + // from Program nodes set to `block` in global scope. This can only be handled by the original Program node. + scopeManager.globalScope.block = node; + }); + } + ); + return ast; } diff --git a/tests/src/scope/scope.ts b/tests/src/scope/scope.ts new file mode 100644 index 00000000..89ae82fb --- /dev/null +++ b/tests/src/scope/scope.ts @@ -0,0 +1,77 @@ +import { Linter } from "eslint"; +import assert from "assert"; +import * as parser from "../../../src/index"; +import type { Scope } from "eslint-scope"; + +function generateScopeTestCase(code: string, selector: string, type: string) { + const linter = new Linter(); + let scope: Scope; + linter.defineParser("svelte-eslint-parser", parser as any); + linter.defineRule("test", { + create(context) { + return { + [selector]() { + scope = context.getScope(); + }, + }; + }, + }); + linter.verify(code, { + parser: "svelte-eslint-parser", + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + rules: { + test: "error", + }, + }); + assert.strictEqual(scope!.type, type); +} + +describe("context.getScope", () => { + it("returns the global scope for the root node", () => { + generateScopeTestCase("", "Program", "global"); + }); + + it("returns the global scope for the script element", () => { + generateScopeTestCase("", "SvelteScriptElement", "module"); + }); + + it("returns the module scope for nodes for top level nodes of script", () => { + generateScopeTestCase( + '', + "ImportDeclaration", + "module" + ); + }); + + it("returns the module scope for nested nodes without their own scope", () => { + generateScopeTestCase( + "", + "LogicalExpression", + "module" + ); + }); + + it("returns the the child scope of top level nodes with their own scope", () => { + generateScopeTestCase( + "", + "FunctionDeclaration", + "function" + ); + }); + + it("returns the own scope for nested nodes", () => { + generateScopeTestCase( + "", + "ArrowFunctionExpression", + "function" + ); + }); + + it("returns the the nearest child scope for statements inside non-global scopes", () => { + generateScopeTestCase( + "", + "ExpressionStatement", + "function" + ); + }); +});