Skip to content

Commit ec061f5

Browse files
authored
fix: resolve to module scope for top level statements (#292)
* fix: resolve to module scope for top level statements * Create .changeset/modern-spiders-retire.md
1 parent 97a1a68 commit ec061f5

File tree

4 files changed

+188
-35
lines changed

4 files changed

+188
-35
lines changed

.changeset/modern-spiders-retire.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
BREAKING: fix resolve to module scope for top level statements
6+
7+
This change corrects the result of `context.getScope()`, but it is a breaking change.

src/context/script-let.ts

+79-35
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type ScriptLetRestoreCallbackOption = {
5858
registerNodeToScope: (node: any, scope: Scope) => void;
5959
scopeManager: ScopeManager;
6060
visitorKeys?: { [type: string]: string[] };
61+
addPostProcess: (callback: () => void) => void;
6162
};
6263

6364
/**
@@ -130,6 +131,8 @@ export class ScriptLetContext {
130131

131132
private readonly restoreCallbacks: RestoreCallback[] = [];
132133

134+
private readonly programRestoreCallbacks: ScriptLetRestoreCallback[] = [];
135+
133136
private readonly closeScopeCallbacks: (() => void)[] = [];
134137

135138
private readonly unique = new UniqueIdGenerator();
@@ -574,6 +577,10 @@ export class ScriptLetContext {
574577
this.closeScopeCallbacks.pop()!();
575578
}
576579

580+
public addProgramRestore(callback: ScriptLetRestoreCallback): void {
581+
this.programRestoreCallbacks.push(callback);
582+
}
583+
577584
private appendScript(
578585
text: string,
579586
offset: number,
@@ -631,6 +638,57 @@ export class ScriptLetContext {
631638
* Restore AST nodes
632639
*/
633640
public restore(result: ESLintExtendedProgram): void {
641+
const nodeToScope = getNodeToScope(result.scopeManager!);
642+
const postprocessList: (() => void)[] = [];
643+
644+
const callbackOption: ScriptLetRestoreCallbackOption = {
645+
getScope,
646+
getInnermostScope,
647+
registerNodeToScope,
648+
scopeManager: result.scopeManager!,
649+
visitorKeys: result.visitorKeys,
650+
addPostProcess: (cb) => postprocessList.push(cb),
651+
};
652+
653+
this.restoreNodes(result, callbackOption);
654+
this.restoreProgram(result, callbackOption);
655+
postprocessList.forEach((p) => p());
656+
657+
// Helpers
658+
/** Get scope */
659+
function getScope(node: ESTree.Node) {
660+
return getScopeFromNode(result.scopeManager!, node);
661+
}
662+
663+
/** Get innermost scope */
664+
function getInnermostScope(node: ESTree.Node) {
665+
return getInnermostScopeFromNode(result.scopeManager!, node);
666+
}
667+
668+
/** Register node to scope */
669+
function registerNodeToScope(node: any, scope: Scope): void {
670+
// If we replace the `scope.block` at this time,
671+
// the scope restore calculation will not work, so we will replace the `scope.block` later.
672+
postprocessList.push(() => {
673+
scope.block = node;
674+
});
675+
676+
const scopes = nodeToScope.get(node);
677+
if (scopes) {
678+
scopes.push(scope);
679+
} else {
680+
nodeToScope.set(node, [scope]);
681+
}
682+
}
683+
}
684+
685+
/**
686+
* Restore AST nodes
687+
*/
688+
private restoreNodes(
689+
result: ESLintExtendedProgram,
690+
callbackOption: ScriptLetRestoreCallbackOption
691+
): void {
634692
let orderedRestoreCallback = this.restoreCallbacks.shift();
635693
if (!orderedRestoreCallback) {
636694
return;
@@ -640,8 +698,6 @@ export class ScriptLetContext {
640698
const processedTokens = [];
641699
const comments = result.ast.comments;
642700
const processedComments = [];
643-
const nodeToScope = getNodeToScope(result.scopeManager!);
644-
const postprocessList: (() => void)[] = [];
645701

646702
let tok;
647703
while ((tok = tokens.shift())) {
@@ -731,13 +787,12 @@ export class ScriptLetContext {
731787
startIndex.comment,
732788
endIndex.comment - startIndex.comment
733789
);
734-
restoreCallback.callback(node, targetTokens, targetComments, {
735-
getScope,
736-
getInnermostScope,
737-
registerNodeToScope,
738-
scopeManager: result.scopeManager!,
739-
visitorKeys: result.visitorKeys,
740-
});
790+
restoreCallback.callback(
791+
node,
792+
targetTokens,
793+
targetComments,
794+
callbackOption
795+
);
741796

742797
processedTokens.push(...targetTokens);
743798
processedComments.push(...targetComments);
@@ -750,33 +805,22 @@ export class ScriptLetContext {
750805

751806
result.ast.tokens = processedTokens;
752807
result.ast.comments = processedComments;
753-
postprocessList.forEach((p) => p());
754-
755-
// Helpers
756-
/** Get scope */
757-
function getScope(node: ESTree.Node) {
758-
return getScopeFromNode(result.scopeManager!, node);
759-
}
760-
761-
/** Get innermost scope */
762-
function getInnermostScope(node: ESTree.Node) {
763-
return getInnermostScopeFromNode(result.scopeManager!, node);
764-
}
765-
766-
/** Register node to scope */
767-
function registerNodeToScope(node: any, scope: Scope): void {
768-
// If we replace the `scope.block` at this time,
769-
// the scope restore calculation will not work, so we will replace the `scope.block` later.
770-
postprocessList.push(() => {
771-
scope.block = node;
772-
});
808+
}
773809

774-
const scopes = nodeToScope.get(node);
775-
if (scopes) {
776-
scopes.push(scope);
777-
} else {
778-
nodeToScope.set(node, [scope]);
779-
}
810+
/**
811+
* Restore program node
812+
*/
813+
private restoreProgram(
814+
result: ESLintExtendedProgram,
815+
callbackOption: ScriptLetRestoreCallbackOption
816+
): void {
817+
for (const callback of this.programRestoreCallbacks) {
818+
callback(
819+
result.ast,
820+
result.ast.tokens,
821+
result.ast.comments,
822+
callbackOption
823+
);
780824
}
781825
}
782826

src/parser/converts/root.ts

+25
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {} from "./common";
99
import type { Context } from "../../context";
1010
import { convertChildren, extractElementTags } from "./element";
1111
import { convertAttributeTokens } from "./attr";
12+
import type { Scope } from "eslint-scope";
1213

1314
/**
1415
* Convert root
@@ -127,6 +128,30 @@ export function convertSvelteRoot(
127128
body.push(style);
128129
}
129130

131+
// Set the scope of the Program node.
132+
ctx.scriptLet.addProgramRestore(
133+
(
134+
node,
135+
_tokens,
136+
_comments,
137+
{ scopeManager, registerNodeToScope, addPostProcess }
138+
) => {
139+
const scopes: Scope[] = [];
140+
for (const scope of scopeManager.scopes) {
141+
if (scope.block === node) {
142+
registerNodeToScope(ast, scope);
143+
scopes.push(scope);
144+
}
145+
}
146+
addPostProcess(() => {
147+
// Reverts the node indicated by `block` to the original Program node.
148+
// This state is incorrect, but `eslint-utils`'s `referenceTracker.iterateEsmReferences()` tracks import statements
149+
// from Program nodes set to `block` in global scope. This can only be handled by the original Program node.
150+
scopeManager.globalScope.block = node;
151+
});
152+
}
153+
);
154+
130155
return ast;
131156
}
132157

tests/src/scope/scope.ts

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Linter } from "eslint";
2+
import assert from "assert";
3+
import * as parser from "../../../src/index";
4+
import type { Scope } from "eslint-scope";
5+
6+
function generateScopeTestCase(code: string, selector: string, type: string) {
7+
const linter = new Linter();
8+
let scope: Scope;
9+
linter.defineParser("svelte-eslint-parser", parser as any);
10+
linter.defineRule("test", {
11+
create(context) {
12+
return {
13+
[selector]() {
14+
scope = context.getScope();
15+
},
16+
};
17+
},
18+
});
19+
linter.verify(code, {
20+
parser: "svelte-eslint-parser",
21+
parserOptions: { ecmaVersion: 2020, sourceType: "module" },
22+
rules: {
23+
test: "error",
24+
},
25+
});
26+
assert.strictEqual(scope!.type, type);
27+
}
28+
29+
describe("context.getScope", () => {
30+
it("returns the global scope for the root node", () => {
31+
generateScopeTestCase("", "Program", "global");
32+
});
33+
34+
it("returns the global scope for the script element", () => {
35+
generateScopeTestCase("<script></script>", "SvelteScriptElement", "module");
36+
});
37+
38+
it("returns the module scope for nodes for top level nodes of script", () => {
39+
generateScopeTestCase(
40+
'<script>import mod from "mod";</script>',
41+
"ImportDeclaration",
42+
"module"
43+
);
44+
});
45+
46+
it("returns the module scope for nested nodes without their own scope", () => {
47+
generateScopeTestCase(
48+
"<script>a || b</script>",
49+
"LogicalExpression",
50+
"module"
51+
);
52+
});
53+
54+
it("returns the the child scope of top level nodes with their own scope", () => {
55+
generateScopeTestCase(
56+
"<script>function fn() {}</script>",
57+
"FunctionDeclaration",
58+
"function"
59+
);
60+
});
61+
62+
it("returns the own scope for nested nodes", () => {
63+
generateScopeTestCase(
64+
"<script>a || (() => {})</script>",
65+
"ArrowFunctionExpression",
66+
"function"
67+
);
68+
});
69+
70+
it("returns the the nearest child scope for statements inside non-global scopes", () => {
71+
generateScopeTestCase(
72+
"<script>function fn() { nested; }</script>",
73+
"ExpressionStatement",
74+
"function"
75+
);
76+
});
77+
});

0 commit comments

Comments
 (0)