Skip to content

fix: crash with plain this attribute. #316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/itchy-toes-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": patch
---

fix: crash with plain `this` attribute.
10 changes: 6 additions & 4 deletions src/parser/converts/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import type ESTree from "estree";
/** indexOf */
export function indexOf(
str: string,
search: (c: string) => boolean,
start: number
search: (c: string, index: number) => boolean,
start: number,
end?: number
): number {
for (let index = start; index < str.length; index++) {
const endIndex = end ?? str.length;
for (let index = start; index < endIndex; index++) {
const c = str[index];
if (search(c)) {
if (search(c, index)) {
return index;
}
}
Expand Down
184 changes: 154 additions & 30 deletions src/parser/converts/element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
SvelteAttribute,
SvelteAwaitBlock,
SvelteAwaitCatchBlock,
SvelteAwaitPendingBlock,
Expand Down Expand Up @@ -45,6 +46,7 @@ import { convertAttributes } from "./attr";
import { convertConstTag } from "./const";
import { sortNodes } from "../sort";
import type { ScriptLetBlockParam } from "../../context/script-let";
import { ParseError } from "../..";

/* eslint-disable complexity -- X */
/** Convert for Fragment or Element or ... */
Expand Down Expand Up @@ -397,59 +399,181 @@ function convertSpecialElement(
node.expression) ||
(node.type === "Element" && elementName === "svelte:element" && node.tag);
if (thisExpression) {
const eqIndex = ctx.code.lastIndexOf("=", getWithLoc(thisExpression).start);
processThisAttribute(node, thisExpression, element, ctx);
}

extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name: SvelteName = {
type: "SvelteName",
name: elementName,
parent: element,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});

return element;
}

/** process `this=` */
function processThisAttribute(
node: SvAST.SvelteElement | SvAST.InlineSvelteComponent,
thisValue: string | ESTree.Expression,
element: SvelteSpecialElement,
ctx: Context
) {
let thisNode: SvelteSpecialDirective | SvelteAttribute;
if (typeof thisValue === "string") {
// this="..."
const startIndex = findStartIndexOfThis(node, ctx);
const eqIndex = ctx.code.indexOf("=", startIndex + 4 /* t,h,i,s */);
const valueStartIndex = indexOf(
ctx.code,
(c) => Boolean(c.trim()),
eqIndex + 1
);
const quote = ctx.code.startsWith(thisValue, valueStartIndex)
? null
: ctx.code[valueStartIndex];
const literalStartIndex = quote
? valueStartIndex + quote.length
: valueStartIndex;
const literalEndIndex = literalStartIndex + thisValue.length;
const endIndex = quote ? literalEndIndex + quote.length : literalEndIndex;
const thisAttr: SvelteAttribute = {
type: "SvelteAttribute",
key: null as any,
boolean: false,
value: [],
parent: element.startTag,
...ctx.getConvertLocation({ start: startIndex, end: endIndex }),
};
thisAttr.key = {
type: "SvelteName",
name: "this",
parent: thisAttr,
...ctx.getConvertLocation({ start: startIndex, end: eqIndex }),
};
thisAttr.value.push({
type: "SvelteLiteral",
value: thisValue,
parent: thisAttr,
...ctx.getConvertLocation({
start: literalStartIndex,
end: literalEndIndex,
}),
});
// this
ctx.addToken("HTMLIdentifier", {
start: startIndex,
end: startIndex + 4,
});
// =
ctx.addToken("Punctuator", {
start: eqIndex,
end: eqIndex + 1,
});
if (quote) {
// "
ctx.addToken("Punctuator", {
start: valueStartIndex,
end: literalStartIndex,
});
}
ctx.addToken("HTMLText", {
start: literalStartIndex,
end: literalEndIndex,
});
if (quote) {
// "
ctx.addToken("Punctuator", {
start: literalEndIndex,
end: endIndex,
});
}
thisNode = thisAttr;
} else {
// this={...}
const eqIndex = ctx.code.lastIndexOf("=", getWithLoc(thisValue).start);
const startIndex = ctx.code.lastIndexOf("this", eqIndex);
const closeIndex = ctx.code.indexOf("}", getWithLoc(thisExpression).end);
const closeIndex = ctx.code.indexOf("}", getWithLoc(thisValue).end);
const endIndex = indexOf(
ctx.code,
(c) => c === ">" || !c.trim(),
closeIndex
);
const thisAttr: SvelteSpecialDirective = {
const thisDir: SvelteSpecialDirective = {
type: "SvelteSpecialDirective",
kind: "this",
key: null as any,
expression: null as any,
parent: element.startTag,
...ctx.getConvertLocation({ start: startIndex, end: endIndex }),
};
thisAttr.key = {
thisDir.key = {
type: "SvelteSpecialDirectiveKey",
parent: thisAttr,
parent: thisDir,
...ctx.getConvertLocation({ start: startIndex, end: eqIndex }),
};
// this
ctx.addToken("HTMLIdentifier", {
start: startIndex,
end: eqIndex,
end: startIndex + 4,
});
ctx.scriptLet.addExpression(thisExpression, thisAttr, null, (es) => {
thisAttr.expression = es;
// =
ctx.addToken("Punctuator", {
start: eqIndex,
end: eqIndex + 1,
});

const targetIndex = element.startTag.attributes.findIndex(
(attr) => thisAttr.range[1] <= attr.range[0]
);
if (targetIndex === -1) {
element.startTag.attributes.push(thisAttr);
} else {
element.startTag.attributes.splice(targetIndex, 0, thisAttr);
}
ctx.scriptLet.addExpression(thisValue, thisDir, null, (es) => {
thisDir.expression = es;
});
thisNode = thisDir;
}

extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name: SvelteName = {
type: "SvelteName",
name: elementName,
parent: element,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});
const targetIndex = element.startTag.attributes.findIndex(
(attr) => thisNode.range[1] <= attr.range[0]
);
if (targetIndex === -1) {
element.startTag.attributes.push(thisNode);
} else {
element.startTag.attributes.splice(targetIndex, 0, thisNode);
}
}

return element;
/** Find the start index of `this` */
function findStartIndexOfThis(
node: SvAST.SvelteElement | SvAST.InlineSvelteComponent,
ctx: Context
) {
// Get the end index of `svelte:element`
const startIndex = ctx.code.indexOf(node.name, node.start) + node.name.length;
const sortedAttrs = [...node.attributes].sort((a, b) => a.start - b.start);
// Find the start index of `this` from the end index of `svelte:element`.
// However, it only seeks to the start index of the first attribute (or the end index of element node).
let thisIndex = indexOf(
ctx.code,
(_c, index) => ctx.code.startsWith("this", index),
startIndex,
sortedAttrs[0]?.start ?? node.end
);
while (thisIndex < 0) {
if (sortedAttrs.length === 0)
throw new ParseError("Cannot resolved `this` attribute.", thisIndex, ctx);
// Step3: Find the start index of `this` from the end index of attribute.
// However, it only seeks to the start index of the first attribute (or the end index of element node).
const nextStartIndex = sortedAttrs.shift()!.end;
thisIndex = indexOf(
ctx.code,
(_c, index) => ctx.code.startsWith("this", index),
nextStartIndex,
sortedAttrs[0]?.start ?? node.end
);
}
return thisIndex;
}

/** Convert for ComponentElement */
Expand Down
6 changes: 3 additions & 3 deletions src/parser/svelte-ast-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,11 @@ export interface BaseElement extends BaseNode {
export interface BasicElement extends BaseElement {
tag?: undefined;
}
export interface SvelteComponent extends BaseElement {
export interface SvelteElement extends BaseElement {
name: "svelte:element";
tag: ESTree.Expression;
tag: ESTree.Expression | string;
}
export type Element = BasicElement | SvelteComponent;
export type Element = BasicElement | SvelteElement;

export interface BaseInlineComponent extends BaseNode {
type: "InlineComponent";
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/parser/ast/this-attr04-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<svelte:element class="foo" this="input" type="number"/>
<svelte:element class="foo" this={"input"} type="number"/>
Loading