Skip to content

Commit

Permalink
feat: change it to use modern AST, if svelte v5 is installed (#437)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Mar 3, 2024
1 parent 5f2b111 commit a27697a
Show file tree
Hide file tree
Showing 25 changed files with 4,756 additions and 637 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-geese-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": minor
---

feat: change it to use modern AST, if svelte v5 is installed
41 changes: 34 additions & 7 deletions src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import type {
} from "../ast";
import type ESTree from "estree";
import type * as SvAST from "../parser/svelte-ast-types";
import type * as Compiler from "svelte/compiler";
import { ScriptLetContext } from "./script-let";
import { LetDirectiveCollections } from "./let-directive-collection";
import type { AttributeToken } from "../parser/html";
import { parseAttributes } from "../parser/html";
import { sortedLastIndex } from "../utils";
import {
Expand Down Expand Up @@ -164,6 +164,19 @@ export class Context {
| SvAST.SlotTemplate
| SvAST.Slot
| SvAST.Title
| Compiler.RegularElement
| Compiler.Component
| Compiler.SvelteComponent
| Compiler.SvelteElement
| Compiler.SvelteWindow
| Compiler.SvelteBody
| Compiler.SvelteHead
| Compiler.SvelteDocument
| Compiler.SvelteFragment
| Compiler.SvelteSelf
| Compiler.SvelteOptionsRaw
| Compiler.SlotElement
| Compiler.TitleElement
>();

public readonly snippets: SvelteSnippetBlock[] = [];
Expand All @@ -190,8 +203,16 @@ export class Context {
if (block.selfClosing) {
continue;
}
const lang = block.attrs.find((attr) => attr.key.name === "lang");
if (!lang || !lang.value || lang.value.value === "html") {
const lang = block.attrs.find((attr) => attr.name === "lang");
if (!lang || !Array.isArray(lang.value)) {
continue;
}
const langValue = lang.value[0];
if (
!langValue ||
langValue.type !== "Text" ||
langValue.data === "html"
) {
continue;
}
}
Expand Down Expand Up @@ -221,7 +242,13 @@ export class Context {
spaces.slice(start, block.contentRange[0]) +
code.slice(...block.contentRange);
for (const attr of block.attrs) {
scriptAttrs[attr.key.name] = attr.value?.value;
if (Array.isArray(attr.value)) {
const attrValue = attr.value[0];
scriptAttrs[attr.name] =
attrValue && attrValue.type === "Text"
? attrValue.data
: undefined;
}
}
} else {
scriptCode += spaces.slice(start, block.contentRange[1]);
Expand Down Expand Up @@ -347,7 +374,7 @@ type Block =
| {
tag: "script" | "style" | "template";
originalTag: string;
attrs: AttributeToken[];
attrs: Compiler.Attribute[];
selfClosing?: false;
contentRange: [number, number];
startTagRange: [number, number];
Expand All @@ -358,7 +385,7 @@ type Block =
type SelfClosingBlock = {
tag: "script" | "style" | "template";
originalTag: string;
attrs: AttributeToken[];
attrs: Compiler.Attribute[];
selfClosing: true;
startTagRange: [number, number];
};
Expand All @@ -380,7 +407,7 @@ function* extractBlocks(code: string): IterableIterator<Block> {

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

let attrs: AttributeToken[] = [];
let attrs: Compiler.Attribute[] = [];
if (!nextChar.trim()) {
const attrsData = parseAttributes(code, startTagOpenRe.lastIndex);
attrs = attrsData.attributes;
Expand Down
8 changes: 6 additions & 2 deletions src/context/script-let.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,15 @@ export class ScriptLetContext {
}

public addVariableDeclarator(
expression: ESTree.AssignmentExpression,
declarator: ESTree.VariableDeclarator | ESTree.AssignmentExpression,
parent: SvelteNode,
...callbacks: ScriptLetCallback<ESTree.VariableDeclarator>[]
): ScriptLetCallback<ESTree.VariableDeclarator>[] {
const range = getNodeRange(expression);
const range =
declarator.type === "VariableDeclarator"
? // As of Svelte v5-next.65, VariableDeclarator nodes do not have location information.
[getNodeRange(declarator.id)[0], getNodeRange(declarator.init!)[1]]
: getNodeRange(declarator);
const part = this.ctx.code.slice(...range);
this.appendScript(
`const ${part};`,
Expand Down
278 changes: 278 additions & 0 deletions src/parser/compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/** Compatibility for Svelte v4 <-> v5 */
import type ESTree from "estree";
import type * as SvAST from "./svelte-ast-types";
import type * as Compiler from "svelte/compiler";
import { parseAttributes } from "./html";

export type Child =
| Compiler.Text
| Compiler.Tag
| Compiler.ElementLike
| Compiler.Block
| Compiler.Comment;
type HasChildren = { children?: SvAST.TemplateNode[] };
// Root
export function getFragmentFromRoot(
svelteAst: Compiler.Root | SvAST.AstLegacy,
): SvAST.Fragment | Compiler.Fragment | undefined {
return (
(svelteAst as Compiler.Root).fragment ?? (svelteAst as SvAST.AstLegacy).html
);
}
export function getInstanceFromRoot(
svelteAst: Compiler.Root | SvAST.AstLegacy,
): SvAST.Script | Compiler.Script | null | undefined {
return svelteAst.instance;
}
export function getModuleFromRoot(
svelteAst: Compiler.Root | SvAST.AstLegacy,
): SvAST.Script | Compiler.Script | null | undefined {
return svelteAst.module;
}
export function getOptionsFromRoot(
svelteAst: Compiler.Root | SvAST.AstLegacy,
code: string,
): Compiler.SvelteOptionsRaw | null {
const root = svelteAst as Compiler.Root;
if (root.options) {
if ((root.options as any).__raw__) {
return (root.options as any).__raw__;
}
// If there is no `__raw__` property in the `SvelteOptions` node,
// we will parse `<svelte:options>` ourselves.
return parseSvelteOptions(root.options, code);
}
return null;
}

export function getChildren(
fragment: Required<HasChildren> | { nodes: (Child | SvAST.TemplateNode)[] },
): (SvAST.TemplateNode | Child)[];
export function getChildren(
fragment: HasChildren | { nodes: (Child | SvAST.TemplateNode)[] },
): (SvAST.TemplateNode | Child)[] | undefined;
export function getChildren(
fragment: HasChildren | { nodes: (Child | SvAST.TemplateNode)[] },
): (SvAST.TemplateNode | Child)[] | undefined {
return (
(fragment as { nodes: (Child | SvAST.TemplateNode)[] }).nodes ??
(fragment as HasChildren).children
);
}
export function trimChildren(
children: (SvAST.TemplateNode | Child)[],
): (SvAST.TemplateNode | Child)[] {
if (
!startsWithWhitespace(children[0]) &&
!endsWithWhitespace(children[children.length - 1])
) {
return children;
}

const nodes = [...children];
while (isWhitespace(nodes[0])) {
nodes.shift();
}
const first = nodes[0];
if (startsWithWhitespace(first)) {
nodes[0] = { ...first, data: first.data.trimStart() };
}
while (isWhitespace(nodes[nodes.length - 1])) {
nodes.pop();
}
const last = nodes[nodes.length - 1];
if (endsWithWhitespace(last)) {
nodes[nodes.length - 1] = { ...last, data: last.data.trimEnd() };
}
return nodes;

function startsWithWhitespace(
child: SvAST.TemplateNode | Child | undefined,
): child is SvAST.Text | Compiler.Text {
if (!child) {
return false;
}
return child.type === "Text" && child.data.trimStart() !== child.data;
}

function endsWithWhitespace(
child: SvAST.TemplateNode | Child | undefined,
): child is SvAST.Text | Compiler.Text {
if (!child) {
return false;
}
return child.type === "Text" && child.data.trimEnd() !== child.data;
}

function isWhitespace(child: SvAST.TemplateNode | Child | undefined) {
if (!child) {
return false;
}
return child.type === "Text" && child.data.trim() === "";
}
}
export function getFragment(
element:
| {
fragment: Compiler.Fragment;
}
| Required<HasChildren>,
): Compiler.Fragment | Required<HasChildren>;
export function getFragment(
element:
| {
fragment: Compiler.Fragment;
}
| HasChildren,
): Compiler.Fragment | HasChildren;
export function getFragment(
element:
| {
fragment: Compiler.Fragment;
}
| HasChildren,
): Compiler.Fragment | HasChildren {
if (
(
element as {
fragment: Compiler.Fragment;
}
).fragment
) {
return (
element as {
fragment: Compiler.Fragment;
}
).fragment;
}
return element as HasChildren;
}
export function getModifiers(
node: SvAST.Directive | SvAST.StyleDirective | Compiler.Directive,
): string[] {
return (node as { modifiers?: string[] }).modifiers ?? [];
}
// IfBlock
export function getTestFromIfBlock(
block: SvAST.IfBlock | Compiler.IfBlock,
): ESTree.Expression {
return (
(block as SvAST.IfBlock).expression ?? (block as Compiler.IfBlock).test
);
}
export function getConsequentFromIfBlock(
block: SvAST.IfBlock | Compiler.IfBlock,
): Compiler.Fragment | SvAST.IfBlock {
return (block as Compiler.IfBlock).consequent ?? (block as SvAST.IfBlock);
}
export function getAlternateFromIfBlock(
block: SvAST.IfBlock | Compiler.IfBlock,
): Compiler.Fragment | SvAST.ElseBlock | null {
if ((block as Compiler.IfBlock).alternate) {
return (block as Compiler.IfBlock).alternate;
}
return (block as SvAST.IfBlock).else ?? null;
}
// EachBlock
export function getBodyFromEachBlock(
block: SvAST.EachBlock | Compiler.EachBlock,
): Compiler.Fragment | SvAST.EachBlock {
if ((block as Compiler.EachBlock).body) {
return (block as Compiler.EachBlock).body;
}
return block as SvAST.EachBlock;
}
export function getFallbackFromEachBlock(
block: SvAST.EachBlock | Compiler.EachBlock,
): Compiler.Fragment | SvAST.ElseBlock | null {
if ((block as Compiler.EachBlock).fallback) {
return (block as Compiler.EachBlock).fallback!;
}
return (block as SvAST.EachBlock).else ?? null;
}
// AwaitBlock
export function getPendingFromAwaitBlock(
block: SvAST.AwaitBlock | Compiler.AwaitBlock,
): Compiler.Fragment | SvAST.PendingBlock | null {
const pending = block.pending;
if (!pending) {
return null;
}
if (pending.type === "Fragment") {
return pending;
}
return pending.skip ? null : pending;
}
export function getThenFromAwaitBlock(
block: SvAST.AwaitBlock | Compiler.AwaitBlock,
): Compiler.Fragment | SvAST.ThenBlock | null {
const then = block.then;
if (!then) {
return null;
}
if (then.type === "Fragment") {
return then;
}
return then.skip ? null : then;
}
export function getCatchFromAwaitBlock(
block: SvAST.AwaitBlock | Compiler.AwaitBlock,
): Compiler.Fragment | SvAST.CatchBlock | null {
const catchFragment = block.catch;
if (!catchFragment) {
return null;
}
if (catchFragment.type === "Fragment") {
return catchFragment;
}
return catchFragment.skip ? null : catchFragment;
}

// ConstTag
export function getDeclaratorFromConstTag(
node: SvAST.ConstTag | Compiler.ConstTag,
):
| ESTree.AssignmentExpression
| Compiler.ConstTag["declaration"]["declarations"][0] {
return (
(node as Compiler.ConstTag).declaration?.declarations?.[0] ??
(node as SvAST.ConstTag).expression
);
}

function parseSvelteOptions(
options: Compiler.SvelteOptions,
code: string,
): Compiler.SvelteOptionsRaw {
const { start, end } = options;
const nameEndName = start + "<svelte:options".length;
const { attributes, index: tagEndIndex } = parseAttributes(
code,
nameEndName + 1,
);
const fragment: Compiler.Fragment = {
type: "Fragment",
nodes: [],
transparent: true,
};
if (code.startsWith(">", tagEndIndex)) {
const childEndIndex = code.indexOf("</svelte:options", tagEndIndex);
fragment.nodes.push({
type: "Text",
data: code.slice(tagEndIndex + 1, childEndIndex),
start: tagEndIndex + 1,
end: childEndIndex,
raw: code.slice(tagEndIndex + 1, childEndIndex),
parent: fragment,
});
}
return {
type: "SvelteOptions",
name: "svelte:options",
attributes,
fragment,
start,
end,
parent: null as any,
};
}

0 comments on commit a27697a

Please sign in to comment.