diff --git a/.changeset/big-ligers-turn.md b/.changeset/big-ligers-turn.md new file mode 100644 index 00000000..48573e67 --- /dev/null +++ b/.changeset/big-ligers-turn.md @@ -0,0 +1,5 @@ +--- +"svelte-eslint-parser": patch +--- + +fix: assign actual `runes` value to `SvelteParseContext` diff --git a/src/parser/index.ts b/src/parser/index.ts index f7ad5aa8..877b1a64 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -50,7 +50,6 @@ import type { NormalizedParserOptions } from "./parser-options.js"; import { isTypeScript, normalizeParserOptions } from "./parser-options.js"; import { getFragmentFromRoot } from "./compat.js"; import { - isEnableRunes, resolveSvelteParseContextForSvelte, resolveSvelteParseContextForSvelteScript, type SvelteParseContext, @@ -117,20 +116,13 @@ export function parseForESLint(code: string, options?: any): ParseResult { const parserOptions = normalizeParserOptions(options); if ( - isEnableRunes(svelteConfig, parserOptions) && parserOptions.filePath && - !parserOptions.filePath.endsWith(".svelte") && - // If no `filePath` is set in ESLint, "" will be specified. - parserOptions.filePath !== "" + (parserOptions.filePath.endsWith(".svelte.js") || + parserOptions.filePath.endsWith(".svelte.ts")) ) { - const trimmed = code.trim(); - if (!trimmed.startsWith("<") && !trimmed.endsWith(">")) { - const svelteParseContext = resolveSvelteParseContextForSvelteScript( - svelteConfig, - parserOptions, - ); - return parseAsScript(code, parserOptions, svelteParseContext); - } + const svelteParseContext = + resolveSvelteParseContextForSvelteScript(svelteConfig); + return parseAsScript(code, parserOptions, svelteParseContext); } return parseAsSvelte(code, svelteConfig, parserOptions); diff --git a/src/parser/svelte-parse-context.ts b/src/parser/svelte-parse-context.ts index d28799eb..19871f00 100644 --- a/src/parser/svelte-parse-context.ts +++ b/src/parser/svelte-parse-context.ts @@ -1,8 +1,20 @@ import type * as Compiler from "./svelte-ast-types-for-v5.js"; import type * as SvAST from "./svelte-ast-types.js"; +import type * as ESTree from "estree"; import type { NormalizedParserOptions } from "./parser-options.js"; import { compilerVersion, svelteVersion } from "./svelte-version.js"; import type { SvelteConfig } from "../svelte-config/index.js"; +import { traverseNodes } from "../traverse.js"; + +const runeSymbols: string[] = [ + "$state", + "$derived", + "$effect", + "$props", + "$bindable", + "$inspect", + "$host", +] as const; /** The context for parsing. */ export type SvelteParseContext = { @@ -18,36 +30,13 @@ export type SvelteParseContext = { svelteConfig: SvelteConfig | null; }; -export function isEnableRunes( - svelteConfig: SvelteConfig | null, - parserOptions: NormalizedParserOptions, -): boolean { - if (!svelteVersion.gte(5)) return false; - if (parserOptions.svelteFeatures?.runes != null) { - return Boolean(parserOptions.svelteFeatures.runes); - } - if (svelteConfig?.compilerOptions?.runes != null) { - return Boolean(svelteConfig.compilerOptions.runes); - } - return true; -} - export function resolveSvelteParseContextForSvelte( svelteConfig: SvelteConfig | null, parserOptions: NormalizedParserOptions, svelteAst: Compiler.Root | SvAST.AstLegacy, ): SvelteParseContext { - const svelteOptions = (svelteAst as Compiler.Root).options; - if (svelteOptions?.runes != null) { - return { - runes: svelteOptions.runes, - compilerVersion, - svelteConfig, - }; - } - return { - runes: isEnableRunes(svelteConfig, parserOptions), + runes: isRunes(svelteConfig, parserOptions, svelteAst), compilerVersion, svelteConfig, }; @@ -55,18 +44,62 @@ export function resolveSvelteParseContextForSvelte( export function resolveSvelteParseContextForSvelteScript( svelteConfig: SvelteConfig | null, - parserOptions: NormalizedParserOptions, -): SvelteParseContext { - return resolveSvelteParseContext(svelteConfig, parserOptions); -} - -function resolveSvelteParseContext( - svelteConfig: SvelteConfig | null, - parserOptions: NormalizedParserOptions, ): SvelteParseContext { return { - runes: isEnableRunes(svelteConfig, parserOptions), + // .svelte.js files are always in Runes mode for Svelte 5. + runes: svelteVersion.gte(5), compilerVersion, svelteConfig, }; } + +function isRunes( + svelteConfig: SvelteConfig | null, + parserOptions: NormalizedParserOptions, + svelteAst: Compiler.Root | SvAST.AstLegacy, +): boolean { + // Svelte 3/4 does not support Runes mode. + if (!svelteVersion.gte(5)) { + return false; + } + + // Compiler option. + if (parserOptions.svelteFeatures?.runes != null) { + return parserOptions.svelteFeatures?.runes; + } + if (svelteConfig?.compilerOptions?.runes != null) { + return svelteConfig?.compilerOptions?.runes; + } + + // ``. + const svelteOptions = (svelteAst as Compiler.Root).options; + if (svelteOptions?.runes != null) { + return svelteOptions?.runes; + } + + // Static analysis. + const { module, instance } = svelteAst; + return ( + (module != null && hasRuneSymbol(module)) || + (instance != null && hasRuneSymbol(instance)) + ); +} + +function hasRuneSymbol(ast: Compiler.Script | SvAST.Script): boolean { + let hasRuneSymbol = false; + traverseNodes(ast as unknown as ESTree.Node, { + enterNode(node) { + if (hasRuneSymbol) { + return; + } + if (node.type === "Identifier" && runeSymbols.includes(node.name)) { + hasRuneSymbol = true; + } + }, + leaveNode() { + // do nothing + }, + }); + + return hasRuneSymbol; +} diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options01-input.svelte b/tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options01-input.svelte similarity index 100% rename from tests/fixtures/parser/ast/svelte5/svelte-options01-input.svelte rename to tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options01-input.svelte diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options01-output.json b/tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options01-output.json similarity index 100% rename from tests/fixtures/parser/ast/svelte5/svelte-options01-output.json rename to tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options01-output.json diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options01-scope-output.json b/tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options01-scope-output.json similarity index 100% rename from tests/fixtures/parser/ast/svelte5/svelte-options01-scope-output.json rename to tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options01-scope-output.json diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options02-input.svelte b/tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options02-input.svelte similarity index 100% rename from tests/fixtures/parser/ast/svelte5/svelte-options02-input.svelte rename to tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options02-input.svelte diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options02-output.json b/tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options02-output.json similarity index 100% rename from tests/fixtures/parser/ast/svelte5/svelte-options02-output.json rename to tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options02-output.json diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options02-scope-output.json b/tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options02-scope-output.json similarity index 100% rename from tests/fixtures/parser/ast/svelte5/svelte-options02-scope-output.json rename to tests/fixtures/parser/ast/svelte5/svelte-options/svelte-options02-scope-output.json diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options/svelte.config.js b/tests/fixtures/parser/ast/svelte5/svelte-options/svelte.config.js new file mode 100644 index 00000000..cbc7a6c8 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/svelte-options/svelte.config.js @@ -0,0 +1,2 @@ +/** Config for testing */ +export default {};