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 {};