Skip to content

Commit a27697a

Browse files
authored
feat: change it to use modern AST, if svelte v5 is installed (#437)
1 parent 5f2b111 commit a27697a

25 files changed

+4756
-637
lines changed

.changeset/loud-geese-trade.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
feat: change it to use modern AST, if svelte v5 is installed

src/context/index.ts

+34-7
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import type {
1212
} from "../ast";
1313
import type ESTree from "estree";
1414
import type * as SvAST from "../parser/svelte-ast-types";
15+
import type * as Compiler from "svelte/compiler";
1516
import { ScriptLetContext } from "./script-let";
1617
import { LetDirectiveCollections } from "./let-directive-collection";
17-
import type { AttributeToken } from "../parser/html";
1818
import { parseAttributes } from "../parser/html";
1919
import { sortedLastIndex } from "../utils";
2020
import {
@@ -164,6 +164,19 @@ export class Context {
164164
| SvAST.SlotTemplate
165165
| SvAST.Slot
166166
| SvAST.Title
167+
| Compiler.RegularElement
168+
| Compiler.Component
169+
| Compiler.SvelteComponent
170+
| Compiler.SvelteElement
171+
| Compiler.SvelteWindow
172+
| Compiler.SvelteBody
173+
| Compiler.SvelteHead
174+
| Compiler.SvelteDocument
175+
| Compiler.SvelteFragment
176+
| Compiler.SvelteSelf
177+
| Compiler.SvelteOptionsRaw
178+
| Compiler.SlotElement
179+
| Compiler.TitleElement
167180
>();
168181

169182
public readonly snippets: SvelteSnippetBlock[] = [];
@@ -190,8 +203,16 @@ export class Context {
190203
if (block.selfClosing) {
191204
continue;
192205
}
193-
const lang = block.attrs.find((attr) => attr.key.name === "lang");
194-
if (!lang || !lang.value || lang.value.value === "html") {
206+
const lang = block.attrs.find((attr) => attr.name === "lang");
207+
if (!lang || !Array.isArray(lang.value)) {
208+
continue;
209+
}
210+
const langValue = lang.value[0];
211+
if (
212+
!langValue ||
213+
langValue.type !== "Text" ||
214+
langValue.data === "html"
215+
) {
195216
continue;
196217
}
197218
}
@@ -221,7 +242,13 @@ export class Context {
221242
spaces.slice(start, block.contentRange[0]) +
222243
code.slice(...block.contentRange);
223244
for (const attr of block.attrs) {
224-
scriptAttrs[attr.key.name] = attr.value?.value;
245+
if (Array.isArray(attr.value)) {
246+
const attrValue = attr.value[0];
247+
scriptAttrs[attr.name] =
248+
attrValue && attrValue.type === "Text"
249+
? attrValue.data
250+
: undefined;
251+
}
225252
}
226253
} else {
227254
scriptCode += spaces.slice(start, block.contentRange[1]);
@@ -347,7 +374,7 @@ type Block =
347374
| {
348375
tag: "script" | "style" | "template";
349376
originalTag: string;
350-
attrs: AttributeToken[];
377+
attrs: Compiler.Attribute[];
351378
selfClosing?: false;
352379
contentRange: [number, number];
353380
startTagRange: [number, number];
@@ -358,7 +385,7 @@ type Block =
358385
type SelfClosingBlock = {
359386
tag: "script" | "style" | "template";
360387
originalTag: string;
361-
attrs: AttributeToken[];
388+
attrs: Compiler.Attribute[];
362389
selfClosing: true;
363390
startTagRange: [number, number];
364391
};
@@ -380,7 +407,7 @@ function* extractBlocks(code: string): IterableIterator<Block> {
380407

381408
const lowerTag = tag.toLowerCase() as "script" | "style" | "template";
382409

383-
let attrs: AttributeToken[] = [];
410+
let attrs: Compiler.Attribute[] = [];
384411
if (!nextChar.trim()) {
385412
const attrsData = parseAttributes(code, startTagOpenRe.lastIndex);
386413
attrs = attrsData.attributes;

src/context/script-let.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,15 @@ export class ScriptLetContext {
246246
}
247247

248248
public addVariableDeclarator(
249-
expression: ESTree.AssignmentExpression,
249+
declarator: ESTree.VariableDeclarator | ESTree.AssignmentExpression,
250250
parent: SvelteNode,
251251
...callbacks: ScriptLetCallback<ESTree.VariableDeclarator>[]
252252
): ScriptLetCallback<ESTree.VariableDeclarator>[] {
253-
const range = getNodeRange(expression);
253+
const range =
254+
declarator.type === "VariableDeclarator"
255+
? // As of Svelte v5-next.65, VariableDeclarator nodes do not have location information.
256+
[getNodeRange(declarator.id)[0], getNodeRange(declarator.init!)[1]]
257+
: getNodeRange(declarator);
254258
const part = this.ctx.code.slice(...range);
255259
this.appendScript(
256260
`const ${part};`,

src/parser/compat.ts

+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/** Compatibility for Svelte v4 <-> v5 */
2+
import type ESTree from "estree";
3+
import type * as SvAST from "./svelte-ast-types";
4+
import type * as Compiler from "svelte/compiler";
5+
import { parseAttributes } from "./html";
6+
7+
export type Child =
8+
| Compiler.Text
9+
| Compiler.Tag
10+
| Compiler.ElementLike
11+
| Compiler.Block
12+
| Compiler.Comment;
13+
type HasChildren = { children?: SvAST.TemplateNode[] };
14+
// Root
15+
export function getFragmentFromRoot(
16+
svelteAst: Compiler.Root | SvAST.AstLegacy,
17+
): SvAST.Fragment | Compiler.Fragment | undefined {
18+
return (
19+
(svelteAst as Compiler.Root).fragment ?? (svelteAst as SvAST.AstLegacy).html
20+
);
21+
}
22+
export function getInstanceFromRoot(
23+
svelteAst: Compiler.Root | SvAST.AstLegacy,
24+
): SvAST.Script | Compiler.Script | null | undefined {
25+
return svelteAst.instance;
26+
}
27+
export function getModuleFromRoot(
28+
svelteAst: Compiler.Root | SvAST.AstLegacy,
29+
): SvAST.Script | Compiler.Script | null | undefined {
30+
return svelteAst.module;
31+
}
32+
export function getOptionsFromRoot(
33+
svelteAst: Compiler.Root | SvAST.AstLegacy,
34+
code: string,
35+
): Compiler.SvelteOptionsRaw | null {
36+
const root = svelteAst as Compiler.Root;
37+
if (root.options) {
38+
if ((root.options as any).__raw__) {
39+
return (root.options as any).__raw__;
40+
}
41+
// If there is no `__raw__` property in the `SvelteOptions` node,
42+
// we will parse `<svelte:options>` ourselves.
43+
return parseSvelteOptions(root.options, code);
44+
}
45+
return null;
46+
}
47+
48+
export function getChildren(
49+
fragment: Required<HasChildren> | { nodes: (Child | SvAST.TemplateNode)[] },
50+
): (SvAST.TemplateNode | Child)[];
51+
export function getChildren(
52+
fragment: HasChildren | { nodes: (Child | SvAST.TemplateNode)[] },
53+
): (SvAST.TemplateNode | Child)[] | undefined;
54+
export function getChildren(
55+
fragment: HasChildren | { nodes: (Child | SvAST.TemplateNode)[] },
56+
): (SvAST.TemplateNode | Child)[] | undefined {
57+
return (
58+
(fragment as { nodes: (Child | SvAST.TemplateNode)[] }).nodes ??
59+
(fragment as HasChildren).children
60+
);
61+
}
62+
export function trimChildren(
63+
children: (SvAST.TemplateNode | Child)[],
64+
): (SvAST.TemplateNode | Child)[] {
65+
if (
66+
!startsWithWhitespace(children[0]) &&
67+
!endsWithWhitespace(children[children.length - 1])
68+
) {
69+
return children;
70+
}
71+
72+
const nodes = [...children];
73+
while (isWhitespace(nodes[0])) {
74+
nodes.shift();
75+
}
76+
const first = nodes[0];
77+
if (startsWithWhitespace(first)) {
78+
nodes[0] = { ...first, data: first.data.trimStart() };
79+
}
80+
while (isWhitespace(nodes[nodes.length - 1])) {
81+
nodes.pop();
82+
}
83+
const last = nodes[nodes.length - 1];
84+
if (endsWithWhitespace(last)) {
85+
nodes[nodes.length - 1] = { ...last, data: last.data.trimEnd() };
86+
}
87+
return nodes;
88+
89+
function startsWithWhitespace(
90+
child: SvAST.TemplateNode | Child | undefined,
91+
): child is SvAST.Text | Compiler.Text {
92+
if (!child) {
93+
return false;
94+
}
95+
return child.type === "Text" && child.data.trimStart() !== child.data;
96+
}
97+
98+
function endsWithWhitespace(
99+
child: SvAST.TemplateNode | Child | undefined,
100+
): child is SvAST.Text | Compiler.Text {
101+
if (!child) {
102+
return false;
103+
}
104+
return child.type === "Text" && child.data.trimEnd() !== child.data;
105+
}
106+
107+
function isWhitespace(child: SvAST.TemplateNode | Child | undefined) {
108+
if (!child) {
109+
return false;
110+
}
111+
return child.type === "Text" && child.data.trim() === "";
112+
}
113+
}
114+
export function getFragment(
115+
element:
116+
| {
117+
fragment: Compiler.Fragment;
118+
}
119+
| Required<HasChildren>,
120+
): Compiler.Fragment | Required<HasChildren>;
121+
export function getFragment(
122+
element:
123+
| {
124+
fragment: Compiler.Fragment;
125+
}
126+
| HasChildren,
127+
): Compiler.Fragment | HasChildren;
128+
export function getFragment(
129+
element:
130+
| {
131+
fragment: Compiler.Fragment;
132+
}
133+
| HasChildren,
134+
): Compiler.Fragment | HasChildren {
135+
if (
136+
(
137+
element as {
138+
fragment: Compiler.Fragment;
139+
}
140+
).fragment
141+
) {
142+
return (
143+
element as {
144+
fragment: Compiler.Fragment;
145+
}
146+
).fragment;
147+
}
148+
return element as HasChildren;
149+
}
150+
export function getModifiers(
151+
node: SvAST.Directive | SvAST.StyleDirective | Compiler.Directive,
152+
): string[] {
153+
return (node as { modifiers?: string[] }).modifiers ?? [];
154+
}
155+
// IfBlock
156+
export function getTestFromIfBlock(
157+
block: SvAST.IfBlock | Compiler.IfBlock,
158+
): ESTree.Expression {
159+
return (
160+
(block as SvAST.IfBlock).expression ?? (block as Compiler.IfBlock).test
161+
);
162+
}
163+
export function getConsequentFromIfBlock(
164+
block: SvAST.IfBlock | Compiler.IfBlock,
165+
): Compiler.Fragment | SvAST.IfBlock {
166+
return (block as Compiler.IfBlock).consequent ?? (block as SvAST.IfBlock);
167+
}
168+
export function getAlternateFromIfBlock(
169+
block: SvAST.IfBlock | Compiler.IfBlock,
170+
): Compiler.Fragment | SvAST.ElseBlock | null {
171+
if ((block as Compiler.IfBlock).alternate) {
172+
return (block as Compiler.IfBlock).alternate;
173+
}
174+
return (block as SvAST.IfBlock).else ?? null;
175+
}
176+
// EachBlock
177+
export function getBodyFromEachBlock(
178+
block: SvAST.EachBlock | Compiler.EachBlock,
179+
): Compiler.Fragment | SvAST.EachBlock {
180+
if ((block as Compiler.EachBlock).body) {
181+
return (block as Compiler.EachBlock).body;
182+
}
183+
return block as SvAST.EachBlock;
184+
}
185+
export function getFallbackFromEachBlock(
186+
block: SvAST.EachBlock | Compiler.EachBlock,
187+
): Compiler.Fragment | SvAST.ElseBlock | null {
188+
if ((block as Compiler.EachBlock).fallback) {
189+
return (block as Compiler.EachBlock).fallback!;
190+
}
191+
return (block as SvAST.EachBlock).else ?? null;
192+
}
193+
// AwaitBlock
194+
export function getPendingFromAwaitBlock(
195+
block: SvAST.AwaitBlock | Compiler.AwaitBlock,
196+
): Compiler.Fragment | SvAST.PendingBlock | null {
197+
const pending = block.pending;
198+
if (!pending) {
199+
return null;
200+
}
201+
if (pending.type === "Fragment") {
202+
return pending;
203+
}
204+
return pending.skip ? null : pending;
205+
}
206+
export function getThenFromAwaitBlock(
207+
block: SvAST.AwaitBlock | Compiler.AwaitBlock,
208+
): Compiler.Fragment | SvAST.ThenBlock | null {
209+
const then = block.then;
210+
if (!then) {
211+
return null;
212+
}
213+
if (then.type === "Fragment") {
214+
return then;
215+
}
216+
return then.skip ? null : then;
217+
}
218+
export function getCatchFromAwaitBlock(
219+
block: SvAST.AwaitBlock | Compiler.AwaitBlock,
220+
): Compiler.Fragment | SvAST.CatchBlock | null {
221+
const catchFragment = block.catch;
222+
if (!catchFragment) {
223+
return null;
224+
}
225+
if (catchFragment.type === "Fragment") {
226+
return catchFragment;
227+
}
228+
return catchFragment.skip ? null : catchFragment;
229+
}
230+
231+
// ConstTag
232+
export function getDeclaratorFromConstTag(
233+
node: SvAST.ConstTag | Compiler.ConstTag,
234+
):
235+
| ESTree.AssignmentExpression
236+
| Compiler.ConstTag["declaration"]["declarations"][0] {
237+
return (
238+
(node as Compiler.ConstTag).declaration?.declarations?.[0] ??
239+
(node as SvAST.ConstTag).expression
240+
);
241+
}
242+
243+
function parseSvelteOptions(
244+
options: Compiler.SvelteOptions,
245+
code: string,
246+
): Compiler.SvelteOptionsRaw {
247+
const { start, end } = options;
248+
const nameEndName = start + "<svelte:options".length;
249+
const { attributes, index: tagEndIndex } = parseAttributes(
250+
code,
251+
nameEndName + 1,
252+
);
253+
const fragment: Compiler.Fragment = {
254+
type: "Fragment",
255+
nodes: [],
256+
transparent: true,
257+
};
258+
if (code.startsWith(">", tagEndIndex)) {
259+
const childEndIndex = code.indexOf("</svelte:options", tagEndIndex);
260+
fragment.nodes.push({
261+
type: "Text",
262+
data: code.slice(tagEndIndex + 1, childEndIndex),
263+
start: tagEndIndex + 1,
264+
end: childEndIndex,
265+
raw: code.slice(tagEndIndex + 1, childEndIndex),
266+
parent: fragment,
267+
});
268+
}
269+
return {
270+
type: "SvelteOptions",
271+
name: "svelte:options",
272+
attributes,
273+
fragment,
274+
start,
275+
end,
276+
parent: null as any,
277+
};
278+
}

0 commit comments

Comments
 (0)