diff --git a/.changeset/dull-bananas-trade.md b/.changeset/dull-bananas-trade.md new file mode 100644 index 00000000..e943548c --- /dev/null +++ b/.changeset/dull-bananas-trade.md @@ -0,0 +1,5 @@ +--- +"svelte-eslint-parser": minor +--- + +fix: resolve to module scope for top level statements diff --git a/src/parser/analyze-scope.ts b/src/parser/analyze-scope.ts index 9cec85bc..c29244d6 100644 --- a/src/parser/analyze-scope.ts +++ b/src/parser/analyze-scope.ts @@ -2,7 +2,7 @@ import type ESTree from "estree"; import type { Scope, ScopeManager } from "eslint-scope"; import { Variable, Reference, analyze } from "eslint-scope"; import { getFallbackKeys } from "../traverse"; -import type { SvelteReactiveStatement, SvelteScriptElement } from "../ast"; +import type { SvelteHTMLNode, SvelteReactiveStatement, SvelteScriptElement } from "../ast"; import { addReference, addVariable } from "../scope"; import { addElementToSortedArray } from "../utils"; /** @@ -25,7 +25,7 @@ export function analyzeScope( sourceType, }; - return analyze(root, { + let scopeManager = analyze(root, { ignoreEval: true, nodejsScope: false, impliedStrict: ecmaFeatures.impliedStrict, @@ -33,6 +33,17 @@ export function analyzeScope( sourceType, fallback: getFallbackKeys, }); + let originalAcquire = scopeManager.acquire; + scopeManager.acquire = function(node: ESTree.Node | SvelteHTMLNode, inner: boolean) { + if (scopeManager.__get(node) === undefined && node.type !== 'Program' && node.parent.type === 'SvelteScriptElement') { + // No deeper scope matched --> use module for nodes besides Program or SvelteScriptElement + return scopeManager.globalScope.childScopes.find((s) => s.type === 'module') || scopeManager.globalScope; + } + + return originalAcquire.call(scopeManager, node, inner); + }; + + return scopeManager; } /** Analyze reactive scope */ diff --git a/tests/src/scope/scope.ts b/tests/src/scope/scope.ts new file mode 100644 index 00000000..bcabc9e9 --- /dev/null +++ b/tests/src/scope/scope.ts @@ -0,0 +1,69 @@ +import assert from "assert"; +import * as svelte from "../../../src"; +import { FlatESLint } from 'eslint/use-at-your-own-risk'; + +async function generateScopeTestCase(code, selector, type) { + const eslint = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + languageOptions: { + parser: svelte, + }, + plugins: { + local: { + rules: { + rule: generateScopeRule(selector, type), + } + } + }, + rules: { + 'local/rule': 'error', + } + } + }); + await eslint.lintText(code); +} + +function generateScopeRule(selector, type) { + return { + create(context) { + return { + [selector]() { + const scope = context.getScope(); + + assert.strictEqual(scope.type, type); + } + }; + } + } +} + +describe('context.getScope', () => { + it('returns the global scope for the root node', async () => { + await generateScopeTestCase('', 'Program', 'global'); + }); + + it('returns the global scope for the script element', async () => { + await generateScopeTestCase('<script></script>', 'SvelteScriptElement', 'global'); + }); + + it.only('returns the module scope for nodes for top level nodes of script', async () => { + await generateScopeTestCase('<script>import mod from "mod";</script>', 'ImportDeclaration', 'module'); + }); + + it('returns the module scope for nested nodes without their own scope', async () => { + await generateScopeTestCase('<script>a || b</script>', 'LogicalExpression', 'module'); + }); + + it('returns the the child scope of top level nodes with their own scope', async () => { + await generateScopeTestCase('<script>function fn() {}</script>', 'FunctionDeclaration', 'function'); + }); + + it('returns the own scope for nested nodes', async () => { + await generateScopeTestCase('<script>a || (() => {})</script>', 'ArrowFunctionExpression', 'function'); + }); + + it('returns the the nearest child scope for statements inside non-global scopes', async () => { + await generateScopeTestCase('<script>function fn() { nested; }</script>', 'ExpressionStatement', 'function'); + }); +});