From 02e4910156d797a772bce1720b6713b4e846d7af Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 16 Jun 2024 16:09:36 +0900 Subject: [PATCH 1/6] feat: makes it optional whether to parse runes. --- README.md | 14 +- src/parser/analyze-scope.ts | 38 +- src/parser/globals.ts | 30 +- src/parser/index.ts | 54 ++- src/parser/parser-options.ts | 4 + src/parser/svelte-parse-context.ts | 71 ++++ src/parser/svelte-version.ts | 7 +- src/parser/typescript/analyze/index.ts | 36 +- src/parser/typescript/index.ts | 9 +- src/scope/index.ts | 20 + src/svelte-config/index.ts | 68 +++ src/svelte-config/parser.ts | 186 +++++++++ tests/fixtures/parser/ast/svelte.config.js | 10 + .../ast/svelte5/svelte-options02-input.svelte | 1 + .../ast/svelte5/svelte-options02-output.json | 388 ++++++++++++++++++ .../svelte-options02-scope-output.json | 34 ++ tests/src/parser/typescript/index.ts | 6 + tests/src/svelte-config/parser.ts | 55 +++ 18 files changed, 982 insertions(+), 49 deletions(-) create mode 100644 src/parser/svelte-parse-context.ts create mode 100644 src/svelte-config/index.ts create mode 100644 src/svelte-config/parser.ts create mode 100644 tests/fixtures/parser/ast/svelte.config.js create mode 100644 tests/fixtures/parser/ast/svelte5/svelte-options02-input.svelte create mode 100644 tests/fixtures/parser/ast/svelte5/svelte-options02-output.json create mode 100644 tests/fixtures/parser/ast/svelte5/svelte-options02-scope-output.json create mode 100644 tests/src/svelte-config/parser.ts diff --git a/README.md b/README.md index b90865f3..98159189 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,12 @@ export default [ parser: svelteParser, parserOptions: { svelteFeatures: { + /* -- Experimental Svelte Features -- */ + /* It may be changed or removed in minor versions without notice. */ + // If true, it will analyze Runes. + // By default, it will try to read `compilerOptions.runes` from `svelte.config.js`. + // However, note that if it cannot be resolved due to static analysis, it will behave as false. + runes: false, /* -- Experimental Svelte Features -- */ /* It may be changed or removed in minor versions without notice. */ // Whether to parse the `generics` attribute. @@ -278,6 +284,12 @@ For example in `.eslintrc.*`: "parser": "svelte-eslint-parser", "parserOptions": { "svelteFeatures": { + /* -- Experimental Svelte Features -- */ + /* It may be changed or removed in minor versions without notice. */ + // If true, it will analyze Runes. + // By default, it will try to read `compilerOptions.runes` from `svelte.config.js`. + // However, note that if it cannot be resolved due to static analysis, it will behave as false. + "runes": false, /* -- Experimental Svelte Features -- */ /* It may be changed or removed in minor versions without notice. */ // Whether to parse the `generics` attribute. @@ -292,7 +304,7 @@ For example in `.eslintrc.*`: **_This is an experimental feature. It may be changed or removed in minor versions without notice._** -If you install Svelte v5 the parser will be able to parse runes, and will also be able to parse `*.js` and `*.ts` files. +If you install Svelte v5 and turn on runes (`compilerOptions.runes` in `svelte.config.js` or `parserOptions.svelteFeatures.runes` in ESLint config is `true`), the parser will be able to parse runes, and will also be able to parse `*.js` and `*.ts` files. When using this mode in an ESLint configuration, it is recommended to set it per file pattern as below. diff --git a/src/parser/analyze-scope.ts b/src/parser/analyze-scope.ts index 12bd2a4c..c0700dc2 100644 --- a/src/parser/analyze-scope.ts +++ b/src/parser/analyze-scope.ts @@ -10,6 +10,7 @@ import type { import { addReference, addVariable, getScopeFromNode } from "../scope"; import { addElementToSortedArray } from "../utils"; import type { NormalizedParserOptions } from "./parser-options"; +import type { SvelteParseContext } from "./svelte-parse-context"; /** * Analyze scope */ @@ -160,6 +161,7 @@ export function analyzeStoreScope(scopeManager: ScopeManager): void { export function analyzePropsScope( body: SvelteScriptElement, scopeManager: ScopeManager, + svelteParseContext: SvelteParseContext, ): void { const moduleScope = scopeManager.scopes.find( (scope) => scope.type === "module", @@ -187,23 +189,25 @@ export function analyzePropsScope( } } } else if (node.type === "VariableDeclaration") { - // Process for Svelte v5 Runes props. e.g. `let { x = $bindable() } = $props()`; - for (const decl of node.declarations) { - if ( - decl.init?.type === "CallExpression" && - decl.init.callee.type === "Identifier" && - decl.init.callee.name === "$props" && - decl.id.type === "ObjectPattern" - ) { - for (const pattern of extractPattern(decl.id)) { - if ( - pattern.type === "AssignmentPattern" && - pattern.left.type === "Identifier" && - pattern.right.type === "CallExpression" && - pattern.right.callee.type === "Identifier" && - pattern.right.callee.name === "$bindable" - ) { - addPropReference(pattern.left, moduleScope); + if (svelteParseContext.runes) { + // Process for Svelte v5 Runes props. e.g. `let { x = $bindable() } = $props()`; + for (const decl of node.declarations) { + if ( + decl.init?.type === "CallExpression" && + decl.init.callee.type === "Identifier" && + decl.init.callee.name === "$props" && + decl.id.type === "ObjectPattern" + ) { + for (const pattern of extractPattern(decl.id)) { + if ( + pattern.type === "AssignmentPattern" && + pattern.left.type === "Identifier" && + pattern.right.type === "CallExpression" && + pattern.right.callee.type === "Identifier" && + pattern.right.callee.name === "$bindable" + ) { + addPropReference(pattern.left, moduleScope); + } } } } diff --git a/src/parser/globals.ts b/src/parser/globals.ts index 9ae23b3a..29ac3c9c 100644 --- a/src/parser/globals.ts +++ b/src/parser/globals.ts @@ -1,6 +1,6 @@ -import { svelteVersion } from "./svelte-version"; +import type { SvelteParseContext } from "./svelte-parse-context"; -const globalsForSvelte4 = ["$$slots", "$$props", "$$restProps"] as const; +const globalsForSvelte = ["$$slots", "$$props", "$$restProps"] as const; export const globalsForRunes = [ "$state", "$derived", @@ -10,10 +10,22 @@ export const globalsForRunes = [ "$inspect", "$host", ] as const; -const globalsForSvelte5 = [...globalsForSvelte4, ...globalsForRunes]; -export const globals = svelteVersion.gte(5) - ? globalsForSvelte5 - : globalsForSvelte4; -export const globalsForSvelteScript = svelteVersion.gte(5) - ? globalsForRunes - : []; +type Global = + | (typeof globalsForSvelte)[number] + | (typeof globalsForRunes)[number]; +export function getGlobalsForSvelte( + svelteParseContext: SvelteParseContext, +): readonly Global[] { + if (svelteParseContext.runes) { + return [...globalsForSvelte, ...globalsForRunes]; + } + return globalsForSvelte; +} +export function getGlobalsForSvelteScript( + svelteParseContext: SvelteParseContext, +): readonly Global[] { + if (svelteParseContext.runes) { + return globalsForRunes; + } + return []; +} diff --git a/src/parser/index.ts b/src/parser/index.ts index 0a8cdf1c..0fcfb982 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -34,11 +34,18 @@ import { styleNodeLoc, styleNodeRange, } from "./style-context"; -import { globals, globalsForSvelteScript } from "./globals"; -import { svelteVersion } from "./svelte-version"; +import { getGlobalsForSvelte, getGlobalsForSvelteScript } from "./globals"; import type { NormalizedParserOptions } from "./parser-options"; import { isTypeScript, normalizeParserOptions } from "./parser-options"; import { getFragmentFromRoot } from "./compat"; +import { + isEnableRunes, + resolveSvelteParseContextForSvelte, + resolveSvelteParseContextForSvelteScript, + type SvelteParseContext, +} from "./svelte-parse-context"; +import type { StaticSvelteConfig } from "../svelte-config"; +import { resolveSvelteConfig } from "../svelte-config"; export { StyleContext, @@ -74,8 +81,13 @@ type ParseResult = { isSvelteScript: false; getSvelteHtmlAst: () => SvAST.Fragment | Compiler.Fragment; getStyleContext: () => StyleContext; + svelteParseContext: SvelteParseContext; + } + | { + isSvelte: false; + isSvelteScript: true; + svelteParseContext: SvelteParseContext; } - | { isSvelte: false; isSvelteScript: true } ); visitorKeys: { [type: string]: string[] }; scopeManager: ScopeManager; @@ -84,10 +96,11 @@ type ParseResult = { * Parse source code */ export function parseForESLint(code: string, options?: any): ParseResult { + const svelteConfig = resolveSvelteConfig(options?.filePath); const parserOptions = normalizeParserOptions(options); if ( - svelteVersion.hasRunes && + isEnableRunes(svelteConfig, parserOptions) && parserOptions.filePath && !parserOptions.filePath.endsWith(".svelte") && // If no `filePath` is set in ESLint, "" will be specified. @@ -95,11 +108,15 @@ export function parseForESLint(code: string, options?: any): ParseResult { ) { const trimmed = code.trim(); if (!trimmed.startsWith("<") && !trimmed.endsWith(">")) { - return parseAsScript(code, parserOptions); + const svelteParseContext = resolveSvelteParseContextForSvelteScript( + svelteConfig, + parserOptions, + ); + return parseAsScript(code, parserOptions, svelteParseContext); } } - return parseAsSvelte(code, parserOptions); + return parseAsSvelte(code, svelteConfig, parserOptions); } /** @@ -107,6 +124,7 @@ export function parseForESLint(code: string, options?: any): ParseResult { */ function parseAsSvelte( code: string, + svelteConfig: StaticSvelteConfig | null, parserOptions: NormalizedParserOptions, ): ParseResult { const ctx = new Context(code, parserOptions); @@ -115,6 +133,11 @@ function parseAsSvelte( ctx, parserOptions, ); + const svelteParseContext = resolveSvelteParseContextForSvelte( + svelteConfig, + parserOptions, + resultTemplate.svelteAst, + ); const scripts = ctx.sourceCode.scripts; const resultScript = ctx.isTypeScript() @@ -122,7 +145,7 @@ function parseAsSvelte( scripts.getCurrentVirtualCodeInfo(), scripts.attrs, parserOptions, - { slots: ctx.slots }, + { slots: ctx.slots, svelteParseContext }, ) : parseScriptInSvelte( scripts.getCurrentVirtualCode(), @@ -141,7 +164,10 @@ function parseAsSvelte( analyzeSnippetsScope(ctx.snippets, resultScript.scopeManager!); // Add $$xxx variable - addGlobalVariables(resultScript.scopeManager!, globals); + addGlobalVariables( + resultScript.scopeManager!, + getGlobalsForSvelte(svelteParseContext), + ); const ast = resultTemplate.ast; @@ -177,7 +203,7 @@ function parseAsSvelte( attr.value[0].value === "module", ) ) { - analyzePropsScope(body, resultScript.scopeManager!); + analyzePropsScope(body, resultScript.scopeManager!, svelteParseContext); } } if (statements.length) { @@ -208,6 +234,7 @@ function parseAsSvelte( }, styleNodeLoc, styleNodeRange, + svelteParseContext, }); resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys); @@ -220,18 +247,23 @@ function parseAsSvelte( function parseAsScript( code: string, parserOptions: NormalizedParserOptions, + svelteParseContext: SvelteParseContext, ): ParseResult { const lang = parserOptions.filePath?.split(".").pop(); const resultScript = isTypeScript(parserOptions, lang) - ? parseTypeScript(code, { lang }, parserOptions) + ? parseTypeScript(code, { lang }, parserOptions, svelteParseContext) : parseScript(code, { lang }, parserOptions); // Add $$xxx variable - addGlobalVariables(resultScript.scopeManager!, globalsForSvelteScript); + addGlobalVariables( + resultScript.scopeManager!, + getGlobalsForSvelteScript(svelteParseContext), + ); resultScript.services = Object.assign(resultScript.services || {}, { isSvelte: false, isSvelteScript: true, + svelteParseContext, }); resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys); return resultScript as any; diff --git a/src/parser/parser-options.ts b/src/parser/parser-options.ts index 7b10badd..3bfc818d 100644 --- a/src/parser/parser-options.ts +++ b/src/parser/parser-options.ts @@ -20,6 +20,10 @@ export type NormalizedParserOptions = { [key: string]: any; }; svelteFeatures?: { + // If true, it will analyze Runes. + // By default, it will try to read `compilerOptions.runes` from `svelte.config.js`. + // However, note that if it cannot be resolved due to static analysis, it will behave as false. + runes?: boolean; /* -- Experimental Svelte Features -- */ // Whether to parse the `generics` attribute. // See https://github.com/sveltejs/rfcs/pull/38 diff --git a/src/parser/svelte-parse-context.ts b/src/parser/svelte-parse-context.ts new file mode 100644 index 00000000..676bd1ba --- /dev/null +++ b/src/parser/svelte-parse-context.ts @@ -0,0 +1,71 @@ +import type * as Compiler from "svelte/compiler"; +import type * as SvAST from "./svelte-ast-types"; +import type { NormalizedParserOptions } from "./parser-options"; +import { compilerVersion, svelteVersion } from "./svelte-version"; +import type { StaticSvelteConfig } from "../svelte-config"; + +/** The context for parsing. */ +export type SvelteParseContext = { + /** + * Whether to use Runes mode. + * May be `true` if the user is using Svelte v5. + * Resolved from `svelte.config.js` or `parserOptions`, but may be overridden by ``. + */ + runes: boolean; + /** The version of "svelte/compiler". */ + compilerVersion: string; + /** The result of static analysis of `svelte.config.js`. */ + svelteConfig: StaticSvelteConfig | null; +}; + +export function isEnableRunes( + svelteConfig: StaticSvelteConfig | null, + parserOptions: NormalizedParserOptions, +): boolean { + if (!svelteVersion.gte(5)) return false; + if (parserOptions.svelteFeatures?.runes != null) { + return Boolean(parserOptions.svelteFeatures.runes); + } else if (svelteConfig?.compilerOptions?.runes != null) { + return Boolean(svelteConfig.compilerOptions.runes); + } + return false; +} + +export function resolveSvelteParseContextForSvelte( + svelteConfig: StaticSvelteConfig | 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), + compilerVersion, + svelteConfig, + }; +} + +export function resolveSvelteParseContextForSvelteScript( + svelteConfig: StaticSvelteConfig | null, + parserOptions: NormalizedParserOptions, +): SvelteParseContext { + return resolveSvelteParseContext(svelteConfig, parserOptions); +} + +function resolveSvelteParseContext( + svelteConfig: StaticSvelteConfig | null, + parserOptions: NormalizedParserOptions, +): SvelteParseContext { + return { + runes: isEnableRunes(svelteConfig, parserOptions), + compilerVersion, + svelteConfig, + }; +} diff --git a/src/parser/svelte-version.ts b/src/parser/svelte-version.ts index 9c859aee..3257aa72 100644 --- a/src/parser/svelte-version.ts +++ b/src/parser/svelte-version.ts @@ -1,10 +1,11 @@ -import { VERSION as SVELTE_VERSION } from "svelte/compiler"; +import { VERSION as compilerVersion } from "svelte/compiler"; -const verStrings = SVELTE_VERSION.split("."); +export { compilerVersion }; + +const verStrings = compilerVersion.split("."); export const svelteVersion = { gte(v: number): boolean { return Number(verStrings[0]) >= v; }, - hasRunes: Number(verStrings[0]) >= 5, }; diff --git a/src/parser/typescript/analyze/index.ts b/src/parser/typescript/analyze/index.ts index bb27f6f7..0e1d8bbe 100644 --- a/src/parser/typescript/analyze/index.ts +++ b/src/parser/typescript/analyze/index.ts @@ -16,12 +16,14 @@ import { VirtualTypeScriptContext } from "../context"; import type { TSESParseForESLintResult } from "../types"; import type ESTree from "estree"; import type { SvelteAttribute, SvelteHTMLElement } from "../../../ast"; -import { globals, globalsForRunes } from "../../../parser/globals"; import type { NormalizedParserOptions } from "../../parser-options"; import { setParent } from "../set-parent"; +import { getGlobalsForSvelte, globalsForRunes } from "../../globals"; +import type { SvelteParseContext } from "../../svelte-parse-context"; export type AnalyzeTypeScriptContext = { slots: Set; + svelteParseContext: SvelteParseContext; }; type TransformInfo = { @@ -57,14 +59,22 @@ export function analyzeTypeScriptInSvelte( ctx._beforeResult = result; - analyzeStoreReferenceNames(result, ctx); + analyzeStoreReferenceNames(result, context.svelteParseContext, ctx); - analyzeDollarDollarVariables(result, ctx, context.slots); + analyzeDollarDollarVariables( + result, + ctx, + context.svelteParseContext, + context.slots, + ); - analyzeRuneVariables(result, ctx); + analyzeRuneVariables(result, ctx, context.svelteParseContext); applyTransforms( - [...analyzeReactiveScopes(result), ...analyzeDollarDerivedScopes(result)], + [ + ...analyzeReactiveScopes(result), + ...analyzeDollarDerivedScopes(result, context.svelteParseContext), + ], ctx, ); @@ -83,6 +93,7 @@ export function analyzeTypeScript( code: string, attrs: Record, parserOptions: NormalizedParserOptions, + svelteParseContext: SvelteParseContext, ): VirtualTypeScriptContext { const ctx = new VirtualTypeScriptContext(code); ctx.appendOriginal(/^\s*/u.exec(code)![0].length); @@ -95,9 +106,12 @@ export function analyzeTypeScript( ctx._beforeResult = result; - analyzeRuneVariables(result, ctx); + analyzeRuneVariables(result, ctx, svelteParseContext); - applyTransforms([...analyzeDollarDerivedScopes(result)], ctx); + applyTransforms( + [...analyzeDollarDerivedScopes(result, svelteParseContext)], + ctx, + ); ctx.appendOriginalToEnd(); @@ -110,8 +124,10 @@ export function analyzeTypeScript( */ function analyzeStoreReferenceNames( result: TSESParseForESLintResult, + svelteParseContext: SvelteParseContext, ctx: VirtualTypeScriptContext, ) { + const globals = getGlobalsForSvelte(svelteParseContext); const scopeManager = result.scopeManager; const programScope = getProgramScope(scopeManager as ScopeManager); const maybeStoreRefNames = new Set(); @@ -198,8 +214,10 @@ function analyzeStoreReferenceNames( function analyzeDollarDollarVariables( result: TSESParseForESLintResult, ctx: VirtualTypeScriptContext, + svelteParseContext: SvelteParseContext, slots: Set, ) { + const globals = getGlobalsForSvelte(svelteParseContext); const scopeManager = result.scopeManager; for (const globalName of globals) { if ( @@ -308,7 +326,9 @@ function analyzeDollarDollarVariables( function analyzeRuneVariables( result: TSESParseForESLintResult, ctx: VirtualTypeScriptContext, + svelteParseContext: SvelteParseContext, ) { + if (!svelteParseContext.runes) return; const scopeManager = result.scopeManager; for (const globalName of globalsForRunes) { if ( @@ -511,7 +531,9 @@ function* analyzeReactiveScopes( */ function* analyzeDollarDerivedScopes( result: TSESParseForESLintResult, + svelteParseContext: SvelteParseContext, ): Iterable { + if (!svelteParseContext.runes) return; const scopeManager = result.scopeManager; const derivedReferences = scopeManager.globalScope!.through.filter( (reference) => reference.identifier.name === "$derived", diff --git a/src/parser/typescript/index.ts b/src/parser/typescript/index.ts index 4f2f2e3d..91abd973 100644 --- a/src/parser/typescript/index.ts +++ b/src/parser/typescript/index.ts @@ -1,6 +1,7 @@ import type { ESLintExtendedProgram } from ".."; import type { NormalizedParserOptions } from "../parser-options"; import { parseScript, parseScriptInSvelte } from "../script"; +import type { SvelteParseContext } from "../svelte-parse-context"; import type { AnalyzeTypeScriptContext } from "./analyze"; import { analyzeTypeScript, analyzeTypeScriptInSvelte } from "./analyze"; import { setParent } from "./set-parent"; @@ -30,8 +31,14 @@ export function parseTypeScript( code: string, attrs: Record, parserOptions: NormalizedParserOptions, + svelteParseContext: SvelteParseContext, ): ESLintExtendedProgram { - const tsCtx = analyzeTypeScript(code, attrs, parserOptions); + const tsCtx = analyzeTypeScript( + code, + attrs, + parserOptions, + svelteParseContext, + ); const result = parseScript(tsCtx.script, attrs, parserOptions); setParent(result); diff --git a/src/scope/index.ts b/src/scope/index.ts index d43de84f..a3c3cb13 100644 --- a/src/scope/index.ts +++ b/src/scope/index.ts @@ -80,6 +80,26 @@ export function getScopeFromNode( const global = scopeManager.globalScope; return global; } + +/** + * Find the variable of a given identifier. + */ +export function findVariable( + scopeManager: ScopeManager, + node: ESTree.Identifier, +): Variable | null { + let scope: Scope | null = getScopeFromNode(scopeManager, node); + + while (scope != null) { + const variable = scope.set.get(node.name); + if (variable != null) { + return variable; + } + scope = scope.upper; + } + + return null; +} /** * Gets the scope for the Program node */ diff --git a/src/svelte-config/index.ts b/src/svelte-config/index.ts new file mode 100644 index 00000000..c661e219 --- /dev/null +++ b/src/svelte-config/index.ts @@ -0,0 +1,68 @@ +import path from "path"; +import fs from "fs"; +import { parseConfig } from "./parser"; + +/** The result of static analysis of `svelte.config.js`. */ +export type StaticSvelteConfig = { + configFilePath: string; + compilerOptions?: { + runes?: boolean; + }; + kit?: { + files?: { + routes?: string; + }; + }; +}; + +const caches = new Map(); + +/** + * Resolves svelte.config.js. + * It searches the parent directories of the given file to find svelte.config.js, + * and returns the static analysis result for it. + */ +export function resolveSvelteConfig( + filePath: string | undefined, +): StaticSvelteConfig | null { + const cwd = + filePath && fs.existsSync(filePath) + ? path.dirname(filePath) + : process.cwd(); + const configFilePath = findConfigFilePath(cwd); + if (!configFilePath) return null; + + if (caches.has(configFilePath)) { + return caches.get(configFilePath) as StaticSvelteConfig | null; + } + + const code = fs.readFileSync(configFilePath, "utf8"); + const config = parseConfig(code); + const result = config ? { ...config, configFilePath } : null; + caches.set(configFilePath, result); + return result; +} + +/** + * Searches from the current working directory up until finding the config filename. + * @param {string} cwd The current working directory to search from. + * @returns {string|undefined} The file if found or `undefined` if not. + */ +function findConfigFilePath(cwd: string) { + let directory = path.resolve(cwd); + const { root } = path.parse(directory); + const stopAt = path.resolve(directory, root); + while (directory !== stopAt) { + const target = path.resolve(directory, "svelte.config.js"); + const stat = fs.existsSync(target) + ? fs.statSync(target, { + throwIfNoEntry: false, + }) + : null; + if (stat?.isFile()) { + return target; + } + directory = path.dirname(directory); + } + return null; +} diff --git a/src/svelte-config/parser.ts b/src/svelte-config/parser.ts new file mode 100644 index 00000000..033e94ee --- /dev/null +++ b/src/svelte-config/parser.ts @@ -0,0 +1,186 @@ +import type { StaticSvelteConfig as StaticSvelteConfigAll } from "."; +import { getEspree } from "../parser/espree"; +import type { + Program, + ExportDefaultDeclaration, + Expression, + Identifier, +} from "estree"; +import { getFallbackKeys, traverseNodes } from "../traverse"; +import type { ScopeManager } from "eslint-scope"; +import { analyze } from "eslint-scope"; +import { findVariable } from "../scope"; + +type StaticSvelteConfig = Omit; + +export function parseConfig(code: string): StaticSvelteConfig | null { + const espree = getEspree(); + const ast = espree.parse(code, { + range: true, + loc: true, + ecmaVersion: espree.latestEcmaVersion, + sourceType: "module", + }); + // Set parent nodes. + traverseNodes(ast, { + enterNode(node, parent) { + (node as any).parent = parent; + }, + leaveNode() { + /* do nothing */ + }, + }); + // Analyze scopes. + const scopeManager = analyze(ast, { + ignoreEval: true, + nodejsScope: false, + ecmaVersion: espree.latestEcmaVersion, + sourceType: "module", + fallback: getFallbackKeys, + }); + return parseAst(ast, scopeManager); +} + +const enum EvaluatedType { + literal, + object, +} + +type Evaluated = + | { + type: EvaluatedType.literal; + value: string | number | bigint | boolean | null | undefined | RegExp; + } + | { + type: EvaluatedType.object; + properties: EvaluatedProperties; + }; + +class EvaluatedProperties { + private readonly cached = new Map(); + + private readonly getter: (key: string) => Evaluated | null; + + public constructor(getter: (key: string) => Evaluated | null) { + this.getter = getter; + } + + public get(key: string): Evaluated | null { + if (this.cached.has(key)) return this.cached.get(key) || null; + const value = this.getter(key); + this.cached.set(key, value); + return value; + } +} + +function parseAst( + ast: Program, + scopeManager: ScopeManager, +): StaticSvelteConfig { + const edd = ast.body.find( + (node): node is ExportDefaultDeclaration => + node.type === "ExportDefaultDeclaration", + ); + if (!edd) return {}; + const decl = edd.declaration; + if (decl.type === "ClassDeclaration" || decl.type === "FunctionDeclaration") + return {}; + return parseSvelteConfigExpression(decl, scopeManager); +} + +function parseSvelteConfigExpression( + node: Expression, + scopeManager: ScopeManager, +): StaticSvelteConfig { + const tracked = new Map(); + const parsed = parseExpression(node); + if (parsed?.type !== EvaluatedType.object) return {}; + const properties = parsed.properties; + const result: StaticSvelteConfig = {}; + // Returns only known properties. + const compilerOptions = properties.get("compilerOptions"); + if (compilerOptions?.type === EvaluatedType.object) { + result.compilerOptions = {}; + const runes = compilerOptions.properties.get("runes"); + if ( + runes?.type === EvaluatedType.literal && + typeof runes.value === "boolean" + ) { + result.compilerOptions.runes = runes.value; + } + } + const kit = properties.get("kit"); + if (kit?.type === EvaluatedType.object) { + result.kit = {}; + const kitFiles = kit.properties.get("files"); + if (kitFiles?.type === EvaluatedType.object) { + result.kit.files = {}; + const kitFilesRoutes = kitFiles.properties.get("routes"); + if ( + kitFilesRoutes?.type === EvaluatedType.literal && + typeof kitFilesRoutes.value === "string" + ) { + result.kit.files.routes = kitFilesRoutes.value; + } + } + } + return result; + + function parseExpression(node: Expression): Evaluated | null { + if (node.type === "Literal") { + return { type: EvaluatedType.literal, value: node.value }; + } + if (node.type === "Identifier") { + const expr = trackIdentifier(node); + if (!expr) return null; + return parseExpression(expr); + } + if (node.type === "ObjectExpression") { + const reversedProperties = [...node.properties].reverse(); + return { + type: EvaluatedType.object, + properties: new EvaluatedProperties((key) => { + for (const prop of reversedProperties) { + if (prop.type === "Property") { + if ( + !prop.computed && + prop.key.type === "Identifier" && + prop.key.name === key + ) { + return parseExpression(prop.value as Expression); + } + const evaluatedKey = parseExpression(prop.key as Expression); + if ( + evaluatedKey?.type === EvaluatedType.literal && + String(evaluatedKey.value) === key + ) { + return parseExpression(prop.value as Expression); + } + } else if (prop.type === "SpreadElement") { + const nesting = parseExpression(prop.argument); + if (nesting?.type === EvaluatedType.object) { + const value = nesting.properties.get(key); + if (value) return value; + } + } + } + return null; + }), + }; + } + + return null; + } + + function trackIdentifier(node: Identifier): Expression | null { + if (tracked.has(node)) return tracked.get(node) || null; + tracked.set(node, null); + const variable = findVariable(scopeManager, node); + if (!variable || variable.defs.length !== 1) return null; + const def = variable.defs[0]; + if (def.type !== "Variable" || def.parent.kind !== "const") return null; + const init = def.node.init || null; + tracked.set(node, init); + return init; + } +} diff --git a/tests/fixtures/parser/ast/svelte.config.js b/tests/fixtures/parser/ast/svelte.config.js new file mode 100644 index 00000000..f24eca30 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte.config.js @@ -0,0 +1,10 @@ +/** Config for testing */ + +/** @type {import('svelte/compiler').CompileOptions} */ +const options = { + runes: true +}; + +export default { + compilerOptions: options +}; diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options02-input.svelte b/tests/fixtures/parser/ast/svelte5/svelte-options02-input.svelte new file mode 100644 index 00000000..abfb3298 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/svelte-options02-input.svelte @@ -0,0 +1 @@ + diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options02-output.json b/tests/fixtures/parser/ast/svelte5/svelte-options02-output.json new file mode 100644 index 00000000..2bd30457 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/svelte-options02-output.json @@ -0,0 +1,388 @@ +{ + "type": "Program", + "body": [ + { + "type": "SvelteElement", + "kind": "special", + "name": { + "type": "SvelteName", + "name": "svelte:options", + "range": [ + 1, + 15 + ], + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 15 + } + } + }, + "startTag": { + "type": "SvelteStartTag", + "attributes": [ + { + "type": "SvelteAttribute", + "key": { + "type": "SvelteName", + "name": "runes", + "range": [ + 16, + 21 + ], + "loc": { + "start": { + "line": 1, + "column": 16 + }, + "end": { + "line": 1, + "column": 21 + } + } + }, + "boolean": false, + "value": [ + { + "type": "SvelteMustacheTag", + "kind": "text", + "expression": { + "type": "Literal", + "raw": "false", + "value": false, + "range": [ + 23, + 28 + ], + "loc": { + "start": { + "line": 1, + "column": 23 + }, + "end": { + "line": 1, + "column": 28 + } + } + }, + "range": [ + 22, + 29 + ], + "loc": { + "start": { + "line": 1, + "column": 22 + }, + "end": { + "line": 1, + "column": 29 + } + } + } + ], + "range": [ + 16, + 29 + ], + "loc": { + "start": { + "line": 1, + "column": 16 + }, + "end": { + "line": 1, + "column": 29 + } + } + } + ], + "selfClosing": false, + "range": [ + 0, + 31 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 31 + } + } + }, + "children": [], + "endTag": { + "type": "SvelteEndTag", + "range": [ + 31, + 48 + ], + "loc": { + "start": { + "line": 1, + "column": 31 + }, + "end": { + "line": 1, + "column": 48 + } + } + }, + "range": [ + 0, + 48 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 48 + } + } + } + ], + "sourceType": "module", + "comments": [], + "tokens": [ + { + "type": "Punctuator", + "value": "<", + "range": [ + 0, + 1 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "svelte:options", + "range": [ + 1, + 15 + ], + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 15 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "runes", + "range": [ + 16, + 21 + ], + "loc": { + "start": { + "line": 1, + "column": 16 + }, + "end": { + "line": 1, + "column": 21 + } + } + }, + { + "type": "Punctuator", + "value": "=", + "range": [ + 21, + 22 + ], + "loc": { + "start": { + "line": 1, + "column": 21 + }, + "end": { + "line": 1, + "column": 22 + } + } + }, + { + "type": "Punctuator", + "value": "{", + "range": [ + 22, + 23 + ], + "loc": { + "start": { + "line": 1, + "column": 22 + }, + "end": { + "line": 1, + "column": 23 + } + } + }, + { + "type": "Boolean", + "value": "false", + "range": [ + 23, + 28 + ], + "loc": { + "start": { + "line": 1, + "column": 23 + }, + "end": { + "line": 1, + "column": 28 + } + } + }, + { + "type": "Punctuator", + "value": "}", + "range": [ + 28, + 29 + ], + "loc": { + "start": { + "line": 1, + "column": 28 + }, + "end": { + "line": 1, + "column": 29 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 30, + 31 + ], + "loc": { + "start": { + "line": 1, + "column": 30 + }, + "end": { + "line": 1, + "column": 31 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 31, + 32 + ], + "loc": { + "start": { + "line": 1, + "column": 31 + }, + "end": { + "line": 1, + "column": 32 + } + } + }, + { + "type": "Punctuator", + "value": "/", + "range": [ + 32, + 33 + ], + "loc": { + "start": { + "line": 1, + "column": 32 + }, + "end": { + "line": 1, + "column": 33 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "svelte:options", + "range": [ + 33, + 47 + ], + "loc": { + "start": { + "line": 1, + "column": 33 + }, + "end": { + "line": 1, + "column": 47 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 47, + 48 + ], + "loc": { + "start": { + "line": 1, + "column": 47 + }, + "end": { + "line": 1, + "column": 48 + } + } + } + ], + "range": [ + 0, + 49 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 0 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/parser/ast/svelte5/svelte-options02-scope-output.json b/tests/fixtures/parser/ast/svelte5/svelte-options02-scope-output.json new file mode 100644 index 00000000..d392ca4b --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/svelte-options02-scope-output.json @@ -0,0 +1,34 @@ +{ + "type": "global", + "variables": [ + { + "name": "$$slots", + "identifiers": [], + "defs": [], + "references": [] + }, + { + "name": "$$props", + "identifiers": [], + "defs": [], + "references": [] + }, + { + "name": "$$restProps", + "identifiers": [], + "defs": [], + "references": [] + } + ], + "references": [], + "childScopes": [ + { + "type": "module", + "variables": [], + "references": [], + "childScopes": [], + "through": [] + } + ], + "through": [] +} \ No newline at end of file diff --git a/tests/src/parser/typescript/index.ts b/tests/src/parser/typescript/index.ts index 6bfd733f..49051278 100644 --- a/tests/src/parser/typescript/index.ts +++ b/tests/src/parser/typescript/index.ts @@ -1,6 +1,7 @@ import { Context } from "../../../../src/context"; import type { NormalizedParserOptions } from "../../../../src/parser/parser-options"; import { parseScriptInSvelte } from "../../../../src/parser/script"; +import { compilerVersion } from "../../../../src/parser/svelte-version"; import { parseTemplate } from "../../../../src/parser/template"; import { parseTypeScriptInSvelte } from "../../../../src/parser/typescript"; import { generateParserOptions, listupFixtures } from "../test-utils"; @@ -49,6 +50,11 @@ describe("Check for typescript analyze result.", () => { parserOptions, { slots: new Set(), + svelteParseContext: { + runes: true, + compilerVersion, + svelteConfig: null, + }, }, ); const result = parseScriptInSvelte( diff --git a/tests/src/svelte-config/parser.ts b/tests/src/svelte-config/parser.ts new file mode 100644 index 00000000..9009b837 --- /dev/null +++ b/tests/src/svelte-config/parser.ts @@ -0,0 +1,55 @@ +import assert from "assert"; +import { parseConfig } from "../../../src/svelte-config/parser"; + +describe("parseConfig", () => { + const testCases = [ + { + code: `export default {compilerOptions:{runes:true}}`, + output: { compilerOptions: { runes: true } }, + }, + { + code: ` + const opt = {compilerOptions:{runes:true}} + export default opt + `, + output: { compilerOptions: { runes: true } }, + }, + { + code: ` + const compilerOptions = {runes:true} + export default {compilerOptions} + `, + output: { compilerOptions: { runes: true } }, + }, + { + code: ` + const kit = {files:{routes:"src/custom"}} + const compilerOptions = {runes:false} + export default {compilerOptions,kit} + `, + output: { + compilerOptions: { runes: false }, + kit: { files: { routes: "src/custom" } }, + }, + }, + { + code: ` + const opt = {compilerOptions:{runes:true}} + export default {...opt} + `, + output: { compilerOptions: { runes: true } }, + }, + { + code: ` + const key = "compilerOptions" + export default {[key]:{runes:false}} + `, + output: { compilerOptions: { runes: false } }, + }, + ]; + for (const { code, output } of testCases) { + it(code, () => { + assert.deepStrictEqual(parseConfig(code), output); + }); + } +}); From 32e224ab7d40056012fcab50c1c7271b533f44a6 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sun, 16 Jun 2024 16:23:52 +0900 Subject: [PATCH 2/6] Create shy-cups-visit.md --- .changeset/shy-cups-visit.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shy-cups-visit.md diff --git a/.changeset/shy-cups-visit.md b/.changeset/shy-cups-visit.md new file mode 100644 index 00000000..5e160db6 --- /dev/null +++ b/.changeset/shy-cups-visit.md @@ -0,0 +1,5 @@ +--- +"svelte-eslint-parser": minor +--- + +feat: makes it optional whether to parse runes. From 9d1fd8b266a7bce00cb6fd77fa0c63c6c2b69971 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 16 Jun 2024 18:07:06 +0900 Subject: [PATCH 3/6] refactor --- src/parser/index.ts | 4 ++-- src/parser/svelte-parse-context.ts | 16 ++++++++-------- src/svelte-config/index.ts | 15 ++++++++++----- src/svelte-config/parser.ts | 4 +--- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/parser/index.ts b/src/parser/index.ts index 0fcfb982..da752e97 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -44,7 +44,7 @@ import { resolveSvelteParseContextForSvelteScript, type SvelteParseContext, } from "./svelte-parse-context"; -import type { StaticSvelteConfig } from "../svelte-config"; +import type { StaticSvelteConfigFile } from "../svelte-config"; import { resolveSvelteConfig } from "../svelte-config"; export { @@ -124,7 +124,7 @@ export function parseForESLint(code: string, options?: any): ParseResult { */ function parseAsSvelte( code: string, - svelteConfig: StaticSvelteConfig | null, + svelteConfig: StaticSvelteConfigFile | null, parserOptions: NormalizedParserOptions, ): ParseResult { const ctx = new Context(code, parserOptions); diff --git a/src/parser/svelte-parse-context.ts b/src/parser/svelte-parse-context.ts index 676bd1ba..51ba25f9 100644 --- a/src/parser/svelte-parse-context.ts +++ b/src/parser/svelte-parse-context.ts @@ -2,7 +2,7 @@ import type * as Compiler from "svelte/compiler"; import type * as SvAST from "./svelte-ast-types"; import type { NormalizedParserOptions } from "./parser-options"; import { compilerVersion, svelteVersion } from "./svelte-version"; -import type { StaticSvelteConfig } from "../svelte-config"; +import type { StaticSvelteConfigFile } from "../svelte-config"; /** The context for parsing. */ export type SvelteParseContext = { @@ -15,24 +15,24 @@ export type SvelteParseContext = { /** The version of "svelte/compiler". */ compilerVersion: string; /** The result of static analysis of `svelte.config.js`. */ - svelteConfig: StaticSvelteConfig | null; + svelteConfig: StaticSvelteConfigFile | null; }; export function isEnableRunes( - svelteConfig: StaticSvelteConfig | null, + svelteConfig: StaticSvelteConfigFile | null, parserOptions: NormalizedParserOptions, ): boolean { if (!svelteVersion.gte(5)) return false; if (parserOptions.svelteFeatures?.runes != null) { return Boolean(parserOptions.svelteFeatures.runes); - } else if (svelteConfig?.compilerOptions?.runes != null) { - return Boolean(svelteConfig.compilerOptions.runes); + } else if (svelteConfig?.config.compilerOptions?.runes != null) { + return Boolean(svelteConfig.config.compilerOptions.runes); } return false; } export function resolveSvelteParseContextForSvelte( - svelteConfig: StaticSvelteConfig | null, + svelteConfig: StaticSvelteConfigFile | null, parserOptions: NormalizedParserOptions, svelteAst: Compiler.Root | SvAST.AstLegacy, ): SvelteParseContext { @@ -53,14 +53,14 @@ export function resolveSvelteParseContextForSvelte( } export function resolveSvelteParseContextForSvelteScript( - svelteConfig: StaticSvelteConfig | null, + svelteConfig: StaticSvelteConfigFile | null, parserOptions: NormalizedParserOptions, ): SvelteParseContext { return resolveSvelteParseContext(svelteConfig, parserOptions); } function resolveSvelteParseContext( - svelteConfig: StaticSvelteConfig | null, + svelteConfig: StaticSvelteConfigFile | null, parserOptions: NormalizedParserOptions, ): SvelteParseContext { return { diff --git a/src/svelte-config/index.ts b/src/svelte-config/index.ts index c661e219..377bcdf5 100644 --- a/src/svelte-config/index.ts +++ b/src/svelte-config/index.ts @@ -4,7 +4,6 @@ import { parseConfig } from "./parser"; /** The result of static analysis of `svelte.config.js`. */ export type StaticSvelteConfig = { - configFilePath: string; compilerOptions?: { runes?: boolean; }; @@ -14,8 +13,12 @@ export type StaticSvelteConfig = { }; }; }; +export type StaticSvelteConfigFile = { + filePath: string; + config: StaticSvelteConfig; +}; -const caches = new Map(); +const caches = new Map(); /** * Resolves svelte.config.js. @@ -24,7 +27,7 @@ const caches = new Map(); */ export function resolveSvelteConfig( filePath: string | undefined, -): StaticSvelteConfig | null { +): StaticSvelteConfigFile | null { const cwd = filePath && fs.existsSync(filePath) ? path.dirname(filePath) @@ -33,12 +36,14 @@ export function resolveSvelteConfig( if (!configFilePath) return null; if (caches.has(configFilePath)) { - return caches.get(configFilePath) as StaticSvelteConfig | null; + return caches.get(configFilePath) || null; } const code = fs.readFileSync(configFilePath, "utf8"); const config = parseConfig(code); - const result = config ? { ...config, configFilePath } : null; + const result: StaticSvelteConfigFile | null = config + ? { config, filePath: configFilePath } + : null; caches.set(configFilePath, result); return result; } diff --git a/src/svelte-config/parser.ts b/src/svelte-config/parser.ts index 033e94ee..47e2bc0a 100644 --- a/src/svelte-config/parser.ts +++ b/src/svelte-config/parser.ts @@ -1,4 +1,4 @@ -import type { StaticSvelteConfig as StaticSvelteConfigAll } from "."; +import type { StaticSvelteConfig } from "."; import { getEspree } from "../parser/espree"; import type { Program, @@ -11,8 +11,6 @@ import type { ScopeManager } from "eslint-scope"; import { analyze } from "eslint-scope"; import { findVariable } from "../scope"; -type StaticSvelteConfig = Omit; - export function parseConfig(code: string): StaticSvelteConfig | null { const espree = getEspree(); const ast = espree.parse(code, { From 7b76dee25c3a3d652df2b474855db048071d3ca1 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Mon, 17 Jun 2024 09:34:09 +0900 Subject: [PATCH 4/6] fix for destructuring --- src/svelte-config/parser.ts | 166 +++++++++++++++++++++++------- tests/src/svelte-config/parser.ts | 14 +++ 2 files changed, 145 insertions(+), 35 deletions(-) diff --git a/src/svelte-config/parser.ts b/src/svelte-config/parser.ts index 47e2bc0a..7c44dc20 100644 --- a/src/svelte-config/parser.ts +++ b/src/svelte-config/parser.ts @@ -1,13 +1,9 @@ import type { StaticSvelteConfig } from "."; -import { getEspree } from "../parser/espree"; -import type { - Program, - ExportDefaultDeclaration, - Expression, - Identifier, -} from "estree"; -import { getFallbackKeys, traverseNodes } from "../traverse"; +import type * as ESTree from "estree"; +import type { Scope } from "eslint"; import type { ScopeManager } from "eslint-scope"; +import { getFallbackKeys, traverseNodes } from "../traverse"; +import { getEspree } from "../parser/espree"; import { analyze } from "eslint-scope"; import { findVariable } from "../scope"; @@ -72,11 +68,11 @@ class EvaluatedProperties { } function parseAst( - ast: Program, + ast: ESTree.Program, scopeManager: ScopeManager, ): StaticSvelteConfig { const edd = ast.body.find( - (node): node is ExportDefaultDeclaration => + (node): node is ESTree.ExportDefaultDeclaration => node.type === "ExportDefaultDeclaration", ); if (!edd) return {}; @@ -87,10 +83,10 @@ function parseAst( } function parseSvelteConfigExpression( - node: Expression, + node: ESTree.Expression, scopeManager: ScopeManager, ): StaticSvelteConfig { - const tracked = new Map(); + const tracked = new Map(); const parsed = parseExpression(node); if (parsed?.type !== EvaluatedType.object) return {}; const properties = parsed.properties; @@ -124,37 +120,37 @@ function parseSvelteConfigExpression( } return result; - function parseExpression(node: Expression): Evaluated | null { + function parseExpression(node: ESTree.Expression): Evaluated | null { if (node.type === "Literal") { return { type: EvaluatedType.literal, value: node.value }; } if (node.type === "Identifier") { - const expr = trackIdentifier(node); - if (!expr) return null; - return parseExpression(expr); + return parseIdentifier(node); } if (node.type === "ObjectExpression") { const reversedProperties = [...node.properties].reverse(); return { type: EvaluatedType.object, properties: new EvaluatedProperties((key) => { + let hasUnknown = false; for (const prop of reversedProperties) { if (prop.type === "Property") { - if ( - !prop.computed && - prop.key.type === "Identifier" && - prop.key.name === key - ) { - return parseExpression(prop.value as Expression); - } - const evaluatedKey = parseExpression(prop.key as Expression); - if ( - evaluatedKey?.type === EvaluatedType.literal && - String(evaluatedKey.value) === key - ) { - return parseExpression(prop.value as Expression); + if (!prop.computed && prop.key.type === "Identifier") { + if (prop.key.name === key) + return parseExpression(prop.value as ESTree.Expression); + } else { + const evaluatedKey = parseExpression( + prop.key as ESTree.Expression, + ); + if (evaluatedKey?.type === EvaluatedType.literal) { + if (String(evaluatedKey.value) === key) + return parseExpression(prop.value as ESTree.Expression); + } else { + hasUnknown = true; + } } } else if (prop.type === "SpreadElement") { + hasUnknown = true; const nesting = parseExpression(prop.argument); if (nesting?.type === EvaluatedType.object) { const value = nesting.properties.get(key); @@ -162,7 +158,9 @@ function parseSvelteConfigExpression( } } } - return null; + return hasUnknown + ? null + : { type: EvaluatedType.literal, value: undefined }; }), }; } @@ -170,15 +168,113 @@ function parseSvelteConfigExpression( return null; } - function trackIdentifier(node: Identifier): Expression | null { + function parseIdentifier(node: ESTree.Identifier) { + const def = getIdentifierDefinition(node); + if (!def) return null; + if (def.type !== "Variable") return null; + if (def.parent.kind !== "const" || !def.node.init) return null; + const evaluated = parseExpression(def.node.init); + if (!evaluated) return null; + const assigns = parsePatternAssign(def.name, def.node.id); + let result = evaluated; + while (assigns.length) { + const assign = assigns.shift()!; + if (assign.type === "member") { + if (result.type !== EvaluatedType.object) return null; + const next = result.properties.get(assign.name); + if (!next) return null; + result = next; + } else if (assign.type === "assignment") { + if ( + result.type === EvaluatedType.literal && + result.value === undefined + ) { + const next = parseExpression(assign.node.right); + if (!next) return null; + result = next; + } + } + } + return result; + } + + function getIdentifierDefinition( + node: ESTree.Identifier, + ): Scope.Definition | null { if (tracked.has(node)) return tracked.get(node) || null; tracked.set(node, null); const variable = findVariable(scopeManager, node); if (!variable || variable.defs.length !== 1) return null; const def = variable.defs[0]; - if (def.type !== "Variable" || def.parent.kind !== "const") return null; - const init = def.node.init || null; - tracked.set(node, init); - return init; + tracked.set(node, def); + if ( + def.type !== "Variable" || + def.parent.kind !== "const" || + def.node.id.type !== "Identifier" || + def.node.init?.type !== "Identifier" + ) { + return def; + } + const newDef = getIdentifierDefinition(def.node.init); + tracked.set(node, newDef); + return newDef; + } +} + +function parsePatternAssign( + node: ESTree.Pattern, + root: ESTree.Pattern, +): ( + | { type: "member"; name: string } + | { type: "assignment"; node: ESTree.AssignmentPattern } +)[] { + return parse(root) || []; + + function parse( + target: ESTree.Pattern, + ): + | ( + | { type: "member"; name: string } + | { type: "assignment"; node: ESTree.AssignmentPattern } + )[] + | null { + if (node === target) { + return []; + } + if (target.type === "Identifier") { + return null; + } + if (target.type === "AssignmentPattern") { + const left = parse(target.left); + if (!left) return null; + return [{ type: "assignment", node: target }, ...left]; + } + if (target.type === "ObjectPattern") { + for (const prop of target.properties) { + if (prop.type === "Property") { + const name = + !prop.computed && prop.key.type === "Identifier" + ? prop.key.name + : prop.key.type === "Literal" + ? String(prop.key.value) + : null; + if (!name) continue; + const value = parse(prop.value); + if (!value) return null; + return [{ type: "member", name }, ...value]; + } + } + return null; + } + if (target.type === "ArrayPattern") { + for (const [index, element] of target.elements.entries()) { + if (!element) continue; + const value = parse(element); + if (!value) return null; + return [{ type: "member", name: String(index) }, ...value]; + } + return null; + } + return null; } } diff --git a/tests/src/svelte-config/parser.ts b/tests/src/svelte-config/parser.ts index 9009b837..7f97721d 100644 --- a/tests/src/svelte-config/parser.ts +++ b/tests/src/svelte-config/parser.ts @@ -46,6 +46,20 @@ describe("parseConfig", () => { `, output: { compilerOptions: { runes: false } }, }, + { + code: ` + const {compilerOptions} = {compilerOptions:{runes:true}} + export default {compilerOptions} + `, + output: { compilerOptions: { runes: true } }, + }, + { + code: ` + const {compilerOptions = {runes:true}} = {} + export default {compilerOptions} + `, + output: { compilerOptions: { runes: true } }, + }, ]; for (const { code, output } of testCases) { it(code, () => { From 548301bb914cb7bcb8b57b5d20a7148c61151b7d Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Mon, 17 Jun 2024 12:33:54 +0900 Subject: [PATCH 5/6] add parserOptions.svelteConfig --- README.md | 34 +- src/index.ts | 11 +- src/parser/index.ts | 8 +- src/parser/svelte-parse-context.ts | 16 +- src/svelte-config/index.ts | 116 ++- src/svelte-config/parser.ts | 284 +++--- .../$props-without-runes01-config.json | 5 + .../$props-without-runes01-input.svelte | 5 + ...props-without-runes01-no-undef-result.json | 8 + .../$props-without-runes01-output.json | 860 ++++++++++++++++++ .../$props-without-runes01-scope-output.json | 445 +++++++++ .../$props-without-runes02-config.json | 5 + .../$props-without-runes02-input.svelte | 5 + ...props-without-runes02-no-undef-result.json | 8 + .../$props-without-runes02-output.json | 860 ++++++++++++++++++ .../$props-without-runes02-scope-output.json | 445 +++++++++ 16 files changed, 2955 insertions(+), 160 deletions(-) create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes01-config.json create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes01-input.svelte create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes01-no-undef-result.json create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes01-output.json create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes01-scope-output.json create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes02-config.json create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes02-input.svelte create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes02-no-undef-result.json create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes02-output.json create mode 100644 tests/fixtures/parser/ast/svelte5/$props-without-runes02-scope-output.json diff --git a/README.md b/README.md index 98159189..26029588 100644 --- a/README.md +++ b/README.md @@ -245,9 +245,30 @@ For example in `.eslintrc.*`: } ``` +### parserOptions.svelteConfig + +If you are using `eslint.config.js`, you can provide a `svelte.config.js` in the `parserOptions.svelteConfig` property. + +For example: + +```js +import svelteConfig from "./svelte.config.js"; +export default [ + { + files: ["**/*.svelte", "*.svelte"], + languageOptions: { + parser: svelteParser, + parserOptions: { + svelteConfig: svelteConfig, + }, + }, + }, +]; +``` + ### parserOptions.svelteFeatures -You can use `parserOptions.svelteFeatures` property to specify how to parse related to Svelte features. For example: +You can use `parserOptions.svelteFeatures` property to specify how to parse related to Svelte features. For example in `eslint.config.js`: @@ -263,7 +284,7 @@ export default [ /* It may be changed or removed in minor versions without notice. */ // If true, it will analyze Runes. // By default, it will try to read `compilerOptions.runes` from `svelte.config.js`. - // However, note that if it cannot be resolved due to static analysis, it will behave as false. + // However, note that if `parserOptions.svelteConfig` is not specified and the file cannot be parsed by static analysis, it will behave as `false`. runes: false, /* -- Experimental Svelte Features -- */ /* It may be changed or removed in minor versions without notice. */ @@ -288,7 +309,7 @@ For example in `.eslintrc.*`: /* It may be changed or removed in minor versions without notice. */ // If true, it will analyze Runes. // By default, it will try to read `compilerOptions.runes` from `svelte.config.js`. - // However, note that if it cannot be resolved due to static analysis, it will behave as false. + // However, note that if the file cannot be parsed by static analysis, it will behave as false. "runes": false, /* -- Experimental Svelte Features -- */ /* It may be changed or removed in minor versions without notice. */ @@ -311,6 +332,7 @@ When using this mode in an ESLint configuration, it is recommended to set it per For example in `eslint.config.js`: ```js +import svelteConfig from "./svelte.config.js"; export default [ { files: ["**/*.svelte", "*.svelte"], @@ -318,6 +340,7 @@ export default [ parser: svelteParser, parserOptions: { parser: "...", + svelteConfig, /* ... */ }, }, @@ -327,6 +350,7 @@ export default [ languageOptions: { parser: svelteParser, parserOptions: { + svelteConfig, /* ... */ }, }, @@ -337,6 +361,7 @@ export default [ parser: svelteParser, parserOptions: { parser: "...(ts parser)...", + svelteConfig, /* ... */ }, }, @@ -354,6 +379,7 @@ For example in `.eslintrc.*`: "parser": "svelte-eslint-parser", "parserOptions": { "parser": "...", + "svelteFeatures": { "runes": true }, /* ... */ }, }, @@ -361,6 +387,7 @@ For example in `.eslintrc.*`: "files": ["*.svelte.js"], "parser": "svelte-eslint-parser", "parserOptions": { + "svelteFeatures": { "runes": true }, /* ... */ }, }, @@ -369,6 +396,7 @@ For example in `.eslintrc.*`: "parser": "svelte-eslint-parser", "parserOptions": { "parser": "...(ts parser)...", + "svelteFeatures": { "runes": true }, /* ... */ }, }, diff --git a/src/index.ts b/src/index.ts index 08290598..3cb39f11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,14 +4,15 @@ import { KEYS } from "./visitor-keys"; import { ParseError } from "./errors"; export { parseForESLint, - StyleContext, - StyleContextNoStyleElement, - StyleContextParseError, - StyleContextSuccess, - StyleContextUnknownLang, + type StyleContext, + type StyleContextNoStyleElement, + type StyleContextParseError, + type StyleContextSuccess, + type StyleContextUnknownLang, } from "./parser"; export * as meta from "./meta"; export { name } from "./meta"; +export type { SvelteConfig } from "./svelte-config"; export { AST, ParseError }; diff --git a/src/parser/index.ts b/src/parser/index.ts index da752e97..981f2f4f 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -44,8 +44,8 @@ import { resolveSvelteParseContextForSvelteScript, type SvelteParseContext, } from "./svelte-parse-context"; -import type { StaticSvelteConfigFile } from "../svelte-config"; -import { resolveSvelteConfig } from "../svelte-config"; +import type { SvelteConfig } from "../svelte-config"; +import { resolveSvelteConfigFromOption } from "../svelte-config"; export { StyleContext, @@ -96,7 +96,7 @@ type ParseResult = { * Parse source code */ export function parseForESLint(code: string, options?: any): ParseResult { - const svelteConfig = resolveSvelteConfig(options?.filePath); + const svelteConfig = resolveSvelteConfigFromOption(options); const parserOptions = normalizeParserOptions(options); if ( @@ -124,7 +124,7 @@ export function parseForESLint(code: string, options?: any): ParseResult { */ function parseAsSvelte( code: string, - svelteConfig: StaticSvelteConfigFile | null, + svelteConfig: SvelteConfig | null, parserOptions: NormalizedParserOptions, ): ParseResult { const ctx = new Context(code, parserOptions); diff --git a/src/parser/svelte-parse-context.ts b/src/parser/svelte-parse-context.ts index 51ba25f9..33ec8ee0 100644 --- a/src/parser/svelte-parse-context.ts +++ b/src/parser/svelte-parse-context.ts @@ -2,7 +2,7 @@ import type * as Compiler from "svelte/compiler"; import type * as SvAST from "./svelte-ast-types"; import type { NormalizedParserOptions } from "./parser-options"; import { compilerVersion, svelteVersion } from "./svelte-version"; -import type { StaticSvelteConfigFile } from "../svelte-config"; +import type { SvelteConfig } from "../svelte-config"; /** The context for parsing. */ export type SvelteParseContext = { @@ -15,24 +15,24 @@ export type SvelteParseContext = { /** The version of "svelte/compiler". */ compilerVersion: string; /** The result of static analysis of `svelte.config.js`. */ - svelteConfig: StaticSvelteConfigFile | null; + svelteConfig: SvelteConfig | null; }; export function isEnableRunes( - svelteConfig: StaticSvelteConfigFile | null, + svelteConfig: SvelteConfig | null, parserOptions: NormalizedParserOptions, ): boolean { if (!svelteVersion.gte(5)) return false; if (parserOptions.svelteFeatures?.runes != null) { return Boolean(parserOptions.svelteFeatures.runes); - } else if (svelteConfig?.config.compilerOptions?.runes != null) { - return Boolean(svelteConfig.config.compilerOptions.runes); + } else if (svelteConfig?.compilerOptions?.runes != null) { + return Boolean(svelteConfig.compilerOptions.runes); } return false; } export function resolveSvelteParseContextForSvelte( - svelteConfig: StaticSvelteConfigFile | null, + svelteConfig: SvelteConfig | null, parserOptions: NormalizedParserOptions, svelteAst: Compiler.Root | SvAST.AstLegacy, ): SvelteParseContext { @@ -53,14 +53,14 @@ export function resolveSvelteParseContextForSvelte( } export function resolveSvelteParseContextForSvelteScript( - svelteConfig: StaticSvelteConfigFile | null, + svelteConfig: SvelteConfig | null, parserOptions: NormalizedParserOptions, ): SvelteParseContext { return resolveSvelteParseContext(svelteConfig, parserOptions); } function resolveSvelteParseContext( - svelteConfig: StaticSvelteConfigFile | null, + svelteConfig: SvelteConfig | null, parserOptions: NormalizedParserOptions, ): SvelteParseContext { return { diff --git a/src/svelte-config/index.ts b/src/svelte-config/index.ts index 377bcdf5..ac35a8c8 100644 --- a/src/svelte-config/index.ts +++ b/src/svelte-config/index.ts @@ -1,33 +1,108 @@ import path from "path"; import fs from "fs"; import { parseConfig } from "./parser"; +import type * as Compiler from "svelte/compiler"; -/** The result of static analysis of `svelte.config.js`. */ -export type StaticSvelteConfig = { - compilerOptions?: { - runes?: boolean; +export type SvelteConfig = { + compilerOptions?: Compiler.CompileOptions; + extensions?: string[]; + kit?: KitConfig; + preprocess?: unknown; + vitePlugin?: unknown; + onwarn?: ( + warning: Compiler.Warning, + defaultHandler: (warning: Compiler.Warning) => void, + ) => void; + [key: string]: unknown; +}; + +interface KitConfig { + adapter?: unknown; + alias?: Record; + appDir?: string; + csp?: { + mode?: "hash" | "nonce" | "auto"; + directives?: unknown; + reportOnly?: unknown; + }; + csrf?: { + checkOrigin?: boolean; }; - kit?: { - files?: { - routes?: string; + embedded?: boolean; + env?: { + dir?: string; + publicPrefix?: string; + privatePrefix?: string; + }; + files?: { + assets?: string; + hooks?: { + client?: string; + server?: string; + universal?: string; }; + lib?: string; + params?: string; + routes?: string; + serviceWorker?: string; + appTemplate?: string; + errorTemplate?: string; }; -}; -export type StaticSvelteConfigFile = { - filePath: string; - config: StaticSvelteConfig; -}; + inlineStyleThreshold?: number; + moduleExtensions?: string[]; + outDir?: string; + output?: { + preloadStrategy?: "modulepreload" | "preload-js" | "preload-mjs"; + }; + paths?: { + assets?: "" | `http://${string}` | `https://${string}`; + base?: "" | `/${string}`; + relative?: boolean; + }; + prerender?: { + concurrency?: number; + crawl?: boolean; + entries?: ("*" | `/${string}`)[]; + handleHttpError?: unknown; + handleMissingId?: unknown; + handleEntryGeneratorMismatch?: unknown; + origin?: string; + }; + serviceWorker?: { + register?: boolean; + files?(filepath: string): boolean; + }; + typescript?: { + config?: (config: Record) => Record | void; + }; + version?: { + name?: string; + pollInterval?: number; + }; +} + +const caches = new Map(); -const caches = new Map(); +/** + * Resolves svelte.config. + */ +export function resolveSvelteConfigFromOption( + options: any, +): SvelteConfig | null { + if (options?.svelteConfig) { + return options.svelteConfig; + } + return resolveSvelteConfig(options?.filePath); +} /** - * Resolves svelte.config.js. - * It searches the parent directories of the given file to find svelte.config.js, + * Resolves `svelte.config.js`. + * It searches the parent directories of the given file to find `svelte.config.js`, * and returns the static analysis result for it. */ -export function resolveSvelteConfig( +function resolveSvelteConfig( filePath: string | undefined, -): StaticSvelteConfigFile | null { +): SvelteConfig | null { const cwd = filePath && fs.existsSync(filePath) ? path.dirname(filePath) @@ -41,11 +116,8 @@ export function resolveSvelteConfig( const code = fs.readFileSync(configFilePath, "utf8"); const config = parseConfig(code); - const result: StaticSvelteConfigFile | null = config - ? { config, filePath: configFilePath } - : null; - caches.set(configFilePath, result); - return result; + caches.set(configFilePath, config); + return config; } /** diff --git a/src/svelte-config/parser.ts b/src/svelte-config/parser.ts index 7c44dc20..451072d1 100644 --- a/src/svelte-config/parser.ts +++ b/src/svelte-config/parser.ts @@ -1,4 +1,4 @@ -import type { StaticSvelteConfig } from "."; +import type { SvelteConfig } from "."; import type * as ESTree from "estree"; import type { Scope } from "eslint"; import type { ScopeManager } from "eslint-scope"; @@ -7,7 +7,7 @@ import { getEspree } from "../parser/espree"; import { analyze } from "eslint-scope"; import { findVariable } from "../scope"; -export function parseConfig(code: string): StaticSvelteConfig | null { +export function parseConfig(code: string): SvelteConfig | null { const espree = getEspree(); const ast = espree.parse(code, { range: true, @@ -35,42 +35,10 @@ export function parseConfig(code: string): StaticSvelteConfig | null { return parseAst(ast, scopeManager); } -const enum EvaluatedType { - literal, - object, -} - -type Evaluated = - | { - type: EvaluatedType.literal; - value: string | number | bigint | boolean | null | undefined | RegExp; - } - | { - type: EvaluatedType.object; - properties: EvaluatedProperties; - }; - -class EvaluatedProperties { - private readonly cached = new Map(); - - private readonly getter: (key: string) => Evaluated | null; - - public constructor(getter: (key: string) => Evaluated | null) { - this.getter = getter; - } - - public get(key: string): Evaluated | null { - if (this.cached.has(key)) return this.cached.get(key) || null; - const value = this.getter(key); - this.cached.set(key, value); - return value; - } -} - function parseAst( ast: ESTree.Program, scopeManager: ScopeManager, -): StaticSvelteConfig { +): SvelteConfig { const edd = ast.body.find( (node): node is ESTree.ExportDefaultDeclaration => node.type === "ExportDefaultDeclaration", @@ -85,92 +53,162 @@ function parseAst( function parseSvelteConfigExpression( node: ESTree.Expression, scopeManager: ScopeManager, -): StaticSvelteConfig { - const tracked = new Map(); - const parsed = parseExpression(node); - if (parsed?.type !== EvaluatedType.object) return {}; - const properties = parsed.properties; - const result: StaticSvelteConfig = {}; +): SvelteConfig { + const evaluated = evaluateExpression(node, scopeManager); + if (evaluated?.type !== EvaluatedType.object) return {}; + const result: SvelteConfig = {}; // Returns only known properties. - const compilerOptions = properties.get("compilerOptions"); + const compilerOptions = evaluated.getProperty("compilerOptions"); if (compilerOptions?.type === EvaluatedType.object) { result.compilerOptions = {}; - const runes = compilerOptions.properties.get("runes"); - if ( - runes?.type === EvaluatedType.literal && - typeof runes.value === "boolean" - ) { - result.compilerOptions.runes = runes.value; + const runes = compilerOptions.getProperty("runes")?.getStatic(); + if (runes) { + result.compilerOptions.runes = Boolean(runes.value); } } - const kit = properties.get("kit"); + const kit = evaluated.getProperty("kit"); if (kit?.type === EvaluatedType.object) { result.kit = {}; - const kitFiles = kit.properties.get("files"); - if (kitFiles?.type === EvaluatedType.object) { - result.kit.files = {}; - const kitFilesRoutes = kitFiles.properties.get("routes"); - if ( - kitFilesRoutes?.type === EvaluatedType.literal && - typeof kitFilesRoutes.value === "string" - ) { - result.kit.files.routes = kitFilesRoutes.value; + const files = kit.getProperty("files")?.getStatic(); + if (files) result.kit.files = files.value as never; + } + return result; +} + +const enum EvaluatedType { + literal, + object, +} + +type Evaluated = EvaluatedLiteral | EvaluatedObject; + +class EvaluatedLiteral { + public readonly type = EvaluatedType.literal; + + public value: unknown; + + public constructor(value: unknown) { + this.value = value; + } + + public getStatic() { + return this; + } +} + +/** Evaluating an object expression. */ +class EvaluatedObject { + public readonly type = EvaluatedType.object; + + private readonly cached = new Map(); + + private readonly node: ESTree.ObjectExpression; + + private readonly parseExpression: ( + node: ESTree.Expression | ESTree.Pattern | ESTree.PrivateIdentifier, + ) => Evaluated | null; + + public constructor( + node: ESTree.ObjectExpression, + parseExpression: ( + node: ESTree.Expression | ESTree.Pattern | ESTree.PrivateIdentifier, + ) => Evaluated | null, + ) { + this.node = node; + this.parseExpression = parseExpression; + } + + /** Gets the evaluated value of the property with the given name. */ + public getProperty(key: string): Evaluated | null { + return this.withCache(key, () => { + let unknown = false; + for (const prop of [...this.node.properties].reverse()) { + if (prop.type === "Property") { + const name = this.getKey(prop); + if (name === key) return this.parseExpression(prop.value); + if (name == null) unknown = true; + } else if (prop.type === "SpreadElement") { + const evaluated = this.parseExpression(prop.argument); + if (evaluated?.type === EvaluatedType.object) { + const value = evaluated.getProperty(key); + if (value) return value; + } + unknown = true; + } + } + return unknown ? null : new EvaluatedLiteral(undefined); + }); + } + + public getStatic(): { value: Record } | null { + const object: Record = {}; + for (const prop of this.node.properties) { + if (prop.type === "Property") { + const name = this.getKey(prop); + if (name == null) return null; + const evaluated = this.withCache(name, () => + this.parseExpression(prop.value), + )?.getStatic(); + if (!evaluated) return null; + object[name] = evaluated.value; + } else if (prop.type === "SpreadElement") { + const evaluated = this.parseExpression(prop.argument)?.getStatic(); + if (!evaluated) return null; + Object.assign(object, evaluated.value); } } + return { value: object }; } - return result; - function parseExpression(node: ESTree.Expression): Evaluated | null { + private withCache( + key: string, + parse: () => Evaluated | null, + ): Evaluated | null { + if (this.cached.has(key)) return this.cached.get(key) || null; + const evaluated = parse(); + this.cached.set(key, evaluated); + return evaluated; + } + + private getKey(node: ESTree.Property): string | null { + if (!node.computed && node.key.type === "Identifier") return node.key.name; + const evaluatedKey = this.parseExpression(node.key)?.getStatic(); + if (evaluatedKey) return String(evaluatedKey.value); + return null; + } +} + +function evaluateExpression( + node: ESTree.Expression, + scopeManager: ScopeManager, +): Evaluated | null { + const tracked = new Map(); + return parseExpression(node); + + function parseExpression( + node: ESTree.Expression | ESTree.Pattern | ESTree.PrivateIdentifier, + ): Evaluated | null { if (node.type === "Literal") { - return { type: EvaluatedType.literal, value: node.value }; + return new EvaluatedLiteral(node.value); } if (node.type === "Identifier") { return parseIdentifier(node); } if (node.type === "ObjectExpression") { - const reversedProperties = [...node.properties].reverse(); - return { - type: EvaluatedType.object, - properties: new EvaluatedProperties((key) => { - let hasUnknown = false; - for (const prop of reversedProperties) { - if (prop.type === "Property") { - if (!prop.computed && prop.key.type === "Identifier") { - if (prop.key.name === key) - return parseExpression(prop.value as ESTree.Expression); - } else { - const evaluatedKey = parseExpression( - prop.key as ESTree.Expression, - ); - if (evaluatedKey?.type === EvaluatedType.literal) { - if (String(evaluatedKey.value) === key) - return parseExpression(prop.value as ESTree.Expression); - } else { - hasUnknown = true; - } - } - } else if (prop.type === "SpreadElement") { - hasUnknown = true; - const nesting = parseExpression(prop.argument); - if (nesting?.type === EvaluatedType.object) { - const value = nesting.properties.get(key); - if (value) return value; - } - } - } - return hasUnknown - ? null - : { type: EvaluatedType.literal, value: undefined }; - }), - }; + return new EvaluatedObject(node, parseExpression); } return null; } - function parseIdentifier(node: ESTree.Identifier) { - const def = getIdentifierDefinition(node); - if (!def) return null; + function parseIdentifier(node: ESTree.Identifier): Evaluated | null { + const defs = getIdentifierDefinitions(node); + if (defs.length !== 1) { + if (defs.length === 0 && node.name === "undefined") + return new EvaluatedLiteral(undefined); + return null; + } + const def = defs[0]; if (def.type !== "Variable") return null; if (def.parent.kind !== "const" || !def.node.init) return null; const evaluated = parseExpression(def.node.init); @@ -181,7 +219,7 @@ function parseSvelteConfigExpression( const assign = assigns.shift()!; if (assign.type === "member") { if (result.type !== EvaluatedType.object) return null; - const next = result.properties.get(assign.name); + const next = result.getProperty(assign.name); if (!next) return null; result = next; } else if (assign.type === "assignment") { @@ -198,29 +236,39 @@ function parseSvelteConfigExpression( return result; } - function getIdentifierDefinition( + function getIdentifierDefinitions( node: ESTree.Identifier, - ): Scope.Definition | null { - if (tracked.has(node)) return tracked.get(node) || null; - tracked.set(node, null); - const variable = findVariable(scopeManager, node); - if (!variable || variable.defs.length !== 1) return null; - const def = variable.defs[0]; - tracked.set(node, def); - if ( - def.type !== "Variable" || - def.parent.kind !== "const" || - def.node.id.type !== "Identifier" || - def.node.init?.type !== "Identifier" - ) { - return def; + ): Scope.Definition[] { + if (tracked.has(node)) return tracked.get(node)!; + tracked.set(node, []); + const defs = findVariable(scopeManager, node)?.defs; + if (!defs) return []; + tracked.set(node, defs); + if (defs.length !== 1) { + const def = defs[0]; + if ( + def.type === "Variable" && + def.parent.kind === "const" && + def.node.id.type === "Identifier" && + def.node.init?.type === "Identifier" + ) { + const newDef = getIdentifierDefinitions(def.node.init); + tracked.set(node, newDef); + return newDef; + } } - const newDef = getIdentifierDefinition(def.node.init); - tracked.set(node, newDef); - return newDef; + return defs; } } +/** + * Returns the assignment path. + * For example, + * `let {a: {target}} = {}` + * -> `[{type: "member", name: 'a'}, {type: "member", name: 'target'}]`. + * `let {a: {target} = foo} = {}` + * -> `[{type: "member", name: 'a'}, {type: "assignment"}, {type: "member", name: 'target'}]`. + */ function parsePatternAssign( node: ESTree.Pattern, root: ESTree.Pattern, diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes01-config.json b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-config.json new file mode 100644 index 00000000..64e36863 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-config.json @@ -0,0 +1,5 @@ +{ + "svelteFeatures": { + "runes": false + } +} diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes01-input.svelte b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-input.svelte new file mode 100644 index 00000000..60106664 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-input.svelte @@ -0,0 +1,5 @@ + + +{p} diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes01-no-undef-result.json b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-no-undef-result.json new file mode 100644 index 00000000..5cb69474 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-no-undef-result.json @@ -0,0 +1,8 @@ +[ + { + "ruleId": "no-undef", + "code": "$props", + "line": 2, + "column": 16 + } +] \ No newline at end of file diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes01-output.json b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-output.json new file mode 100644 index 00000000..0ccd50d3 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-output.json @@ -0,0 +1,860 @@ +{ + "type": "Program", + "body": [ + { + "type": "SvelteScriptElement", + "name": { + "type": "SvelteName", + "name": "script", + "range": [ + 1, + 7 + ], + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 7 + } + } + }, + "startTag": { + "type": "SvelteStartTag", + "attributes": [], + "selfClosing": false, + "range": [ + 0, + 8 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 8 + } + } + }, + "body": [ + { + "type": "VariableDeclaration", + "kind": "const", + "declarations": [ + { + "type": "VariableDeclarator", + "id": { + "type": "ObjectPattern", + "properties": [ + { + "type": "Property", + "kind": "init", + "computed": false, + "key": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "method": false, + "shorthand": true, + "value": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + ], + "range": [ + 16, + 21 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 12 + } + } + }, + "init": { + "type": "CallExpression", + "arguments": [], + "callee": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "optional": false, + "range": [ + 24, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 23 + } + } + }, + "range": [ + 16, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 23 + } + } + } + ], + "range": [ + 10, + 33 + ], + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 24 + } + } + } + ], + "endTag": { + "type": "SvelteEndTag", + "range": [ + 34, + 43 + ], + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 9 + } + } + }, + "range": [ + 0, + 43 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 9 + } + } + }, + { + "type": "SvelteText", + "value": "\n\n", + "range": [ + 43, + 45 + ], + "loc": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 5, + "column": 0 + } + } + }, + { + "type": "SvelteElement", + "kind": "html", + "name": { + "type": "SvelteName", + "name": "span", + "range": [ + 46, + 50 + ], + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 5 + } + } + }, + "startTag": { + "type": "SvelteStartTag", + "attributes": [], + "selfClosing": false, + "range": [ + 45, + 51 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 6 + } + } + }, + "children": [ + { + "type": "SvelteMustacheTag", + "kind": "text", + "expression": { + "type": "Identifier", + "name": "p", + "range": [ + 52, + 53 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + } + }, + "range": [ + 51, + 54 + ], + "loc": { + "start": { + "line": 5, + "column": 6 + }, + "end": { + "line": 5, + "column": 9 + } + } + } + ], + "endTag": { + "type": "SvelteEndTag", + "range": [ + 54, + 61 + ], + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 16 + } + } + }, + "range": [ + 45, + 61 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 16 + } + } + } + ], + "sourceType": "module", + "comments": [], + "tokens": [ + { + "type": "Punctuator", + "value": "<", + "range": [ + 0, + 1 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "script", + "range": [ + 1, + 7 + ], + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 7 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 7, + 8 + ], + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 8 + } + } + }, + { + "type": "Keyword", + "value": "const", + "range": [ + 10, + 15 + ], + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 6 + } + } + }, + { + "type": "Punctuator", + "value": "{", + "range": [ + 16, + 17 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 8 + } + } + }, + { + "type": "Identifier", + "value": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + { + "type": "Punctuator", + "value": "}", + "range": [ + 20, + 21 + ], + "loc": { + "start": { + "line": 2, + "column": 11 + }, + "end": { + "line": 2, + "column": 12 + } + } + }, + { + "type": "Punctuator", + "value": "=", + "range": [ + 22, + 23 + ], + "loc": { + "start": { + "line": 2, + "column": 13 + }, + "end": { + "line": 2, + "column": 14 + } + } + }, + { + "type": "Identifier", + "value": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + { + "type": "Punctuator", + "value": "(", + "range": [ + 30, + 31 + ], + "loc": { + "start": { + "line": 2, + "column": 21 + }, + "end": { + "line": 2, + "column": 22 + } + } + }, + { + "type": "Punctuator", + "value": ")", + "range": [ + 31, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 22 + }, + "end": { + "line": 2, + "column": 23 + } + } + }, + { + "type": "Punctuator", + "value": ";", + "range": [ + 32, + 33 + ], + "loc": { + "start": { + "line": 2, + "column": 23 + }, + "end": { + "line": 2, + "column": 24 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 34, + 35 + ], + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + } + }, + { + "type": "Punctuator", + "value": "/", + "range": [ + 35, + 36 + ], + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 2 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "script", + "range": [ + 36, + 42 + ], + "loc": { + "start": { + "line": 3, + "column": 2 + }, + "end": { + "line": 3, + "column": 8 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 42, + 43 + ], + "loc": { + "start": { + "line": 3, + "column": 8 + }, + "end": { + "line": 3, + "column": 9 + } + } + }, + { + "type": "HTMLText", + "value": "\n\n", + "range": [ + 43, + 45 + ], + "loc": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 5, + "column": 0 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 45, + 46 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 1 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "span", + "range": [ + 46, + 50 + ], + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 5 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 50, + 51 + ], + "loc": { + "start": { + "line": 5, + "column": 5 + }, + "end": { + "line": 5, + "column": 6 + } + } + }, + { + "type": "Punctuator", + "value": "{", + "range": [ + 51, + 52 + ], + "loc": { + "start": { + "line": 5, + "column": 6 + }, + "end": { + "line": 5, + "column": 7 + } + } + }, + { + "type": "Identifier", + "value": "p", + "range": [ + 52, + 53 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + } + }, + { + "type": "Punctuator", + "value": "}", + "range": [ + 53, + 54 + ], + "loc": { + "start": { + "line": 5, + "column": 8 + }, + "end": { + "line": 5, + "column": 9 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 54, + 55 + ], + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 10 + } + } + }, + { + "type": "Punctuator", + "value": "/", + "range": [ + 55, + 56 + ], + "loc": { + "start": { + "line": 5, + "column": 10 + }, + "end": { + "line": 5, + "column": 11 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "span", + "range": [ + 56, + 60 + ], + "loc": { + "start": { + "line": 5, + "column": 11 + }, + "end": { + "line": 5, + "column": 15 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 60, + 61 + ], + "loc": { + "start": { + "line": 5, + "column": 15 + }, + "end": { + "line": 5, + "column": 16 + } + } + } + ], + "range": [ + 0, + 62 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 6, + "column": 0 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes01-scope-output.json b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-scope-output.json new file mode 100644 index 00000000..3adbd737 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes01-scope-output.json @@ -0,0 +1,445 @@ +{ + "type": "global", + "variables": [ + { + "name": "$$slots", + "identifiers": [], + "defs": [], + "references": [] + }, + { + "name": "$$props", + "identifiers": [], + "defs": [], + "references": [] + }, + { + "name": "$$restProps", + "identifiers": [], + "defs": [], + "references": [] + } + ], + "references": [], + "childScopes": [ + { + "type": "module", + "variables": [ + { + "name": "p", + "identifiers": [ + { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + ], + "defs": [ + { + "type": "Variable", + "name": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "node": { + "type": "VariableDeclarator", + "id": { + "type": "ObjectPattern", + "properties": [ + { + "type": "Property", + "kind": "init", + "computed": false, + "key": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "method": false, + "shorthand": true, + "value": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + ], + "range": [ + 16, + 21 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 12 + } + } + }, + "init": { + "type": "CallExpression", + "arguments": [], + "callee": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "optional": false, + "range": [ + 24, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 23 + } + } + }, + "range": [ + 16, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 23 + } + } + } + } + ], + "references": [ + { + "identifier": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "from": "module", + "init": true, + "resolved": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + }, + { + "identifier": { + "type": "Identifier", + "name": "p", + "range": [ + 52, + 53 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + } + }, + "from": "module", + "init": null, + "resolved": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + } + ] + } + ], + "references": [ + { + "identifier": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "from": "module", + "init": true, + "resolved": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + }, + { + "identifier": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "from": "module", + "init": null, + "resolved": null + }, + { + "identifier": { + "type": "Identifier", + "name": "p", + "range": [ + 52, + 53 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + } + }, + "from": "module", + "init": null, + "resolved": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + } + ], + "childScopes": [], + "through": [ + { + "identifier": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "from": "module", + "init": null, + "resolved": null + } + ] + } + ], + "through": [ + { + "identifier": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "from": "module", + "init": null, + "resolved": null + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes02-config.json b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-config.json new file mode 100644 index 00000000..2a0441e5 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-config.json @@ -0,0 +1,5 @@ +{ + "svelteConfig": { + "runes": false + } +} diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes02-input.svelte b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-input.svelte new file mode 100644 index 00000000..60106664 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-input.svelte @@ -0,0 +1,5 @@ + + +{p} diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes02-no-undef-result.json b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-no-undef-result.json new file mode 100644 index 00000000..5cb69474 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-no-undef-result.json @@ -0,0 +1,8 @@ +[ + { + "ruleId": "no-undef", + "code": "$props", + "line": 2, + "column": 16 + } +] \ No newline at end of file diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes02-output.json b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-output.json new file mode 100644 index 00000000..0ccd50d3 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-output.json @@ -0,0 +1,860 @@ +{ + "type": "Program", + "body": [ + { + "type": "SvelteScriptElement", + "name": { + "type": "SvelteName", + "name": "script", + "range": [ + 1, + 7 + ], + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 7 + } + } + }, + "startTag": { + "type": "SvelteStartTag", + "attributes": [], + "selfClosing": false, + "range": [ + 0, + 8 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 8 + } + } + }, + "body": [ + { + "type": "VariableDeclaration", + "kind": "const", + "declarations": [ + { + "type": "VariableDeclarator", + "id": { + "type": "ObjectPattern", + "properties": [ + { + "type": "Property", + "kind": "init", + "computed": false, + "key": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "method": false, + "shorthand": true, + "value": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + ], + "range": [ + 16, + 21 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 12 + } + } + }, + "init": { + "type": "CallExpression", + "arguments": [], + "callee": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "optional": false, + "range": [ + 24, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 23 + } + } + }, + "range": [ + 16, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 23 + } + } + } + ], + "range": [ + 10, + 33 + ], + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 24 + } + } + } + ], + "endTag": { + "type": "SvelteEndTag", + "range": [ + 34, + 43 + ], + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 9 + } + } + }, + "range": [ + 0, + 43 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 9 + } + } + }, + { + "type": "SvelteText", + "value": "\n\n", + "range": [ + 43, + 45 + ], + "loc": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 5, + "column": 0 + } + } + }, + { + "type": "SvelteElement", + "kind": "html", + "name": { + "type": "SvelteName", + "name": "span", + "range": [ + 46, + 50 + ], + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 5 + } + } + }, + "startTag": { + "type": "SvelteStartTag", + "attributes": [], + "selfClosing": false, + "range": [ + 45, + 51 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 6 + } + } + }, + "children": [ + { + "type": "SvelteMustacheTag", + "kind": "text", + "expression": { + "type": "Identifier", + "name": "p", + "range": [ + 52, + 53 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + } + }, + "range": [ + 51, + 54 + ], + "loc": { + "start": { + "line": 5, + "column": 6 + }, + "end": { + "line": 5, + "column": 9 + } + } + } + ], + "endTag": { + "type": "SvelteEndTag", + "range": [ + 54, + 61 + ], + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 16 + } + } + }, + "range": [ + 45, + 61 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 16 + } + } + } + ], + "sourceType": "module", + "comments": [], + "tokens": [ + { + "type": "Punctuator", + "value": "<", + "range": [ + 0, + 1 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "script", + "range": [ + 1, + 7 + ], + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 7 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 7, + 8 + ], + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 8 + } + } + }, + { + "type": "Keyword", + "value": "const", + "range": [ + 10, + 15 + ], + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 6 + } + } + }, + { + "type": "Punctuator", + "value": "{", + "range": [ + 16, + 17 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 8 + } + } + }, + { + "type": "Identifier", + "value": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + { + "type": "Punctuator", + "value": "}", + "range": [ + 20, + 21 + ], + "loc": { + "start": { + "line": 2, + "column": 11 + }, + "end": { + "line": 2, + "column": 12 + } + } + }, + { + "type": "Punctuator", + "value": "=", + "range": [ + 22, + 23 + ], + "loc": { + "start": { + "line": 2, + "column": 13 + }, + "end": { + "line": 2, + "column": 14 + } + } + }, + { + "type": "Identifier", + "value": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + { + "type": "Punctuator", + "value": "(", + "range": [ + 30, + 31 + ], + "loc": { + "start": { + "line": 2, + "column": 21 + }, + "end": { + "line": 2, + "column": 22 + } + } + }, + { + "type": "Punctuator", + "value": ")", + "range": [ + 31, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 22 + }, + "end": { + "line": 2, + "column": 23 + } + } + }, + { + "type": "Punctuator", + "value": ";", + "range": [ + 32, + 33 + ], + "loc": { + "start": { + "line": 2, + "column": 23 + }, + "end": { + "line": 2, + "column": 24 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 34, + 35 + ], + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + } + }, + { + "type": "Punctuator", + "value": "/", + "range": [ + 35, + 36 + ], + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 2 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "script", + "range": [ + 36, + 42 + ], + "loc": { + "start": { + "line": 3, + "column": 2 + }, + "end": { + "line": 3, + "column": 8 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 42, + 43 + ], + "loc": { + "start": { + "line": 3, + "column": 8 + }, + "end": { + "line": 3, + "column": 9 + } + } + }, + { + "type": "HTMLText", + "value": "\n\n", + "range": [ + 43, + 45 + ], + "loc": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 5, + "column": 0 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 45, + 46 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 1 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "span", + "range": [ + 46, + 50 + ], + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 5 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 50, + 51 + ], + "loc": { + "start": { + "line": 5, + "column": 5 + }, + "end": { + "line": 5, + "column": 6 + } + } + }, + { + "type": "Punctuator", + "value": "{", + "range": [ + 51, + 52 + ], + "loc": { + "start": { + "line": 5, + "column": 6 + }, + "end": { + "line": 5, + "column": 7 + } + } + }, + { + "type": "Identifier", + "value": "p", + "range": [ + 52, + 53 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + } + }, + { + "type": "Punctuator", + "value": "}", + "range": [ + 53, + 54 + ], + "loc": { + "start": { + "line": 5, + "column": 8 + }, + "end": { + "line": 5, + "column": 9 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 54, + 55 + ], + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 10 + } + } + }, + { + "type": "Punctuator", + "value": "/", + "range": [ + 55, + 56 + ], + "loc": { + "start": { + "line": 5, + "column": 10 + }, + "end": { + "line": 5, + "column": 11 + } + } + }, + { + "type": "HTMLIdentifier", + "value": "span", + "range": [ + 56, + 60 + ], + "loc": { + "start": { + "line": 5, + "column": 11 + }, + "end": { + "line": 5, + "column": 15 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 60, + 61 + ], + "loc": { + "start": { + "line": 5, + "column": 15 + }, + "end": { + "line": 5, + "column": 16 + } + } + } + ], + "range": [ + 0, + 62 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 6, + "column": 0 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/parser/ast/svelte5/$props-without-runes02-scope-output.json b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-scope-output.json new file mode 100644 index 00000000..3adbd737 --- /dev/null +++ b/tests/fixtures/parser/ast/svelte5/$props-without-runes02-scope-output.json @@ -0,0 +1,445 @@ +{ + "type": "global", + "variables": [ + { + "name": "$$slots", + "identifiers": [], + "defs": [], + "references": [] + }, + { + "name": "$$props", + "identifiers": [], + "defs": [], + "references": [] + }, + { + "name": "$$restProps", + "identifiers": [], + "defs": [], + "references": [] + } + ], + "references": [], + "childScopes": [ + { + "type": "module", + "variables": [ + { + "name": "p", + "identifiers": [ + { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + ], + "defs": [ + { + "type": "Variable", + "name": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "node": { + "type": "VariableDeclarator", + "id": { + "type": "ObjectPattern", + "properties": [ + { + "type": "Property", + "kind": "init", + "computed": false, + "key": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "method": false, + "shorthand": true, + "value": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + ], + "range": [ + 16, + 21 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 12 + } + } + }, + "init": { + "type": "CallExpression", + "arguments": [], + "callee": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "optional": false, + "range": [ + 24, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 23 + } + } + }, + "range": [ + 16, + 32 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 23 + } + } + } + } + ], + "references": [ + { + "identifier": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "from": "module", + "init": true, + "resolved": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + }, + { + "identifier": { + "type": "Identifier", + "name": "p", + "range": [ + 52, + 53 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + } + }, + "from": "module", + "init": null, + "resolved": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + } + ] + } + ], + "references": [ + { + "identifier": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + }, + "from": "module", + "init": true, + "resolved": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + }, + { + "identifier": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "from": "module", + "init": null, + "resolved": null + }, + { + "identifier": { + "type": "Identifier", + "name": "p", + "range": [ + 52, + 53 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + } + }, + "from": "module", + "init": null, + "resolved": { + "type": "Identifier", + "name": "p", + "range": [ + 18, + 19 + ], + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 10 + } + } + } + } + ], + "childScopes": [], + "through": [ + { + "identifier": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "from": "module", + "init": null, + "resolved": null + } + ] + } + ], + "through": [ + { + "identifier": { + "type": "Identifier", + "name": "$props", + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + "from": "module", + "init": null, + "resolved": null + } + ] +} \ No newline at end of file From baf724add98bd1672d44d88eb4c7c500bf0f285a Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Mon, 17 Jun 2024 12:52:22 +0900 Subject: [PATCH 6/6] update readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 26029588..d27cabc2 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,10 @@ export default [ ]; ``` +If `parserOptions.svelteConfig` is not specified, some config will be statically parsed from the `svelte.config.js` file. + +The `.eslintrc.*` style configuration cannot load `svelte.config.js` because it cannot use ESM. We recommend using the `eslint.config.js` style configuration. + ### parserOptions.svelteFeatures You can use `parserOptions.svelteFeatures` property to specify how to parse related to Svelte features.