Skip to content

fix: resolve to module scope for top level statements #291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
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
5 changes: 5 additions & 0 deletions .changeset/dull-bananas-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": minor
---

fix: resolve to module scope for top level statements
15 changes: 13 additions & 2 deletions src/parser/analyze-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
/**
Expand All @@ -25,14 +25,25 @@ export function analyzeScope(
sourceType,
};

return analyze(root, {
let scopeManager = analyze(root, {
ignoreEval: true,
nodejsScope: false,
impliedStrict: ecmaFeatures.impliedStrict,
ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 2022,
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 */
Expand Down
69 changes: 69 additions & 0 deletions tests/src/scope/scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import assert from "assert";
import * as svelte from "../../../src";
import { FlatESLint } from 'eslint/use-at-your-own-risk';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you change it to use the Linter class instead of FlatESLint? I believe that using the Linter class is sufficient for this test.

https://eslint.org/docs/latest/integrate/nodejs-api#linter


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');
});
});