diff --git a/.changeset/mean-eagles-deny.md b/.changeset/mean-eagles-deny.md new file mode 100644 index 00000000..4ef1fa1c --- /dev/null +++ b/.changeset/mean-eagles-deny.md @@ -0,0 +1,5 @@ +--- +"astro-eslint-parser": patch +--- + +fix: crash with unquoted attribute value diff --git a/src/context/restore.ts b/src/context/restore.ts index 2bf8daf3..e8bb3b63 100644 --- a/src/context/restore.ts +++ b/src/context/restore.ts @@ -40,6 +40,11 @@ class RestoreNodeProcessContext { public getParent(node: TSESTree.Node): TSESTree.Node | null { return this.nodeMap.get(node) || null; } + + public findToken(startIndex: number): TSESTree.Token | null { + const tokens = this.result.ast.tokens || []; + return tokens.find((t) => t.range[0] === startIndex) || null; + } } export class RestoreContext { diff --git a/src/parser/process-template.ts b/src/parser/process-template.ts index 194f4c5b..fc1afbcd 100644 --- a/src/parser/process-template.ts +++ b/src/parser/process-template.ts @@ -174,7 +174,42 @@ export function processTemplate( processAttributePunctuators(attr); } } - if (attr.kind === "shorthand") { + if (attr.kind === "quoted") { + if ( + attr.raw && + !attr.raw.startsWith('"') && + !attr.raw.startsWith("'") + ) { + const attrStart = attr.position!.start.offset; + const start = calcAttributeValueStartOffset(attr, ctx); + const end = calcAttributeEndOffset(attr, ctx); + script.appendOriginal(start); + script.appendVirtualScript('"'); + script.appendOriginal(end); + script.appendVirtualScript('"'); + + script.restoreContext.addRestoreNodeProcess( + (scriptNode, context) => { + if ( + scriptNode.type === AST_NODE_TYPES.JSXAttribute && + scriptNode.range[0] === attrStart + ) { + const attrNode = scriptNode; + if ( + attrNode.value?.type === "Literal" && + typeof attrNode.value.value === "string" + ) { + const raw = ctx.code.slice(start, end); + attrNode.value.raw = raw; + context.findToken(start)!.value = raw; + return true; + } + } + return false; + }, + ); + } + } else if (attr.kind === "shorthand") { const start = attr.position!.start.offset; script.appendOriginal(start); const jsxName = /[\s"'[\]{}]/u.test(attr.name) diff --git a/tests/fixtures/parser/ast/attr01-unquote-input.astro b/tests/fixtures/parser/ast/attr01-unquote-input.astro new file mode 100644 index 00000000..8459244e --- /dev/null +++ b/tests/fixtures/parser/ast/attr01-unquote-input.astro @@ -0,0 +1,4 @@ +--- +--- + +
diff --git a/tests/fixtures/parser/ast/attr01-unquote-output.json b/tests/fixtures/parser/ast/attr01-unquote-output.json new file mode 100644 index 00000000..98b457e6 --- /dev/null +++ b/tests/fixtures/parser/ast/attr01-unquote-output.json @@ -0,0 +1,440 @@ +{ + "type": "Program", + "body": [ + { + "type": "AstroFragment", + "children": [ + { + "type": "JSXElement", + "children": [], + "closingElement": { + "type": "JSXClosingElement", + "name": { + "type": "JSXIdentifier", + "name": "div", + "range": [ + 26, + 29 + ], + "loc": { + "start": { + "line": 4, + "column": 17 + }, + "end": { + "line": 4, + "column": 20 + } + } + }, + "range": [ + 24, + 30 + ], + "loc": { + "start": { + "line": 4, + "column": 15 + }, + "end": { + "line": 4, + "column": 21 + } + } + }, + "openingElement": { + "type": "JSXOpeningElement", + "name": { + "type": "JSXIdentifier", + "name": "div", + "range": [ + 10, + 13 + ], + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 4 + } + } + }, + "attributes": [ + { + "type": "JSXAttribute", + "name": { + "type": "JSXIdentifier", + "name": "title", + "range": [ + 14, + 19 + ], + "loc": { + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 4, + "column": 10 + } + } + }, + "value": { + "type": "Literal", + "raw": "foo", + "value": "foo", + "range": [ + 20, + 23 + ], + "loc": { + "start": { + "line": 4, + "column": 11 + }, + "end": { + "line": 4, + "column": 14 + } + } + }, + "range": [ + 14, + 23 + ], + "loc": { + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 4, + "column": 14 + } + } + } + ], + "selfClosing": false, + "range": [ + 9, + 24 + ], + "loc": { + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 4, + "column": 15 + } + } + }, + "range": [ + 9, + 30 + ], + "loc": { + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 4, + "column": 21 + } + } + }, + { + "type": "JSXText", + "raw": "\n", + "value": "\n", + "range": [ + 30, + 31 + ], + "loc": { + "start": { + "line": 4, + "column": 21 + }, + "end": { + "line": 5, + "column": 0 + } + } + } + ], + "range": [ + 9, + 31 + ], + "loc": { + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 5, + "column": 0 + } + } + } + ], + "sourceType": "module", + "comments": [], + "tokens": [ + { + "type": "Punctuator", + "value": "---", + "range": [ + 0, + 3 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 3 + } + } + }, + { + "type": "Punctuator", + "value": "---", + "range": [ + 4, + 7 + ], + "loc": { + "start": { + "line": 2, + "column": 0 + }, + "end": { + "line": 2, + "column": 3 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 9, + 10 + ], + "loc": { + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 4, + "column": 1 + } + } + }, + { + "type": "JSXIdentifier", + "value": "div", + "range": [ + 10, + 13 + ], + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 4 + } + } + }, + { + "type": "JSXIdentifier", + "value": "title", + "range": [ + 14, + 19 + ], + "loc": { + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 4, + "column": 10 + } + } + }, + { + "type": "Punctuator", + "value": "=", + "range": [ + 19, + 20 + ], + "loc": { + "start": { + "line": 4, + "column": 10 + }, + "end": { + "line": 4, + "column": 11 + } + } + }, + { + "type": "JSXText", + "value": "foo", + "range": [ + 20, + 23 + ], + "loc": { + "start": { + "line": 4, + "column": 11 + }, + "end": { + "line": 4, + "column": 14 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 23, + 24 + ], + "loc": { + "start": { + "line": 4, + "column": 14 + }, + "end": { + "line": 4, + "column": 15 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 24, + 25 + ], + "loc": { + "start": { + "line": 4, + "column": 15 + }, + "end": { + "line": 4, + "column": 16 + } + } + }, + { + "type": "Punctuator", + "value": "/", + "range": [ + 25, + 26 + ], + "loc": { + "start": { + "line": 4, + "column": 16 + }, + "end": { + "line": 4, + "column": 17 + } + } + }, + { + "type": "JSXIdentifier", + "value": "div", + "range": [ + 26, + 29 + ], + "loc": { + "start": { + "line": 4, + "column": 17 + }, + "end": { + "line": 4, + "column": 20 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 29, + 30 + ], + "loc": { + "start": { + "line": 4, + "column": 20 + }, + "end": { + "line": 4, + "column": 21 + } + } + }, + { + "type": "JSXText", + "value": "\n", + "range": [ + 30, + 31 + ], + "loc": { + "start": { + "line": 4, + "column": 21 + }, + "end": { + "line": 5, + "column": 0 + } + } + } + ], + "range": [ + 0, + 31 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 0 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/parser/ast/attr01-unquote-scope-output.json b/tests/fixtures/parser/ast/attr01-unquote-scope-output.json new file mode 100644 index 00000000..6e0646f9 --- /dev/null +++ b/tests/fixtures/parser/ast/attr01-unquote-scope-output.json @@ -0,0 +1,15 @@ +{ + "type": "global", + "variables": [], + "references": [], + "childScopes": [ + { + "type": "module", + "variables": [], + "references": [], + "childScopes": [], + "through": [] + } + ], + "through": [] +} \ No newline at end of file diff --git a/tests/fixtures/parser/ast/html-entity03-unquote-input.astro b/tests/fixtures/parser/ast/html-entity03-unquote-input.astro new file mode 100644 index 00000000..cee50ac2 --- /dev/null +++ b/tests/fixtures/parser/ast/html-entity03-unquote-input.astro @@ -0,0 +1,5 @@ +--- +--- + +© +☃ diff --git a/tests/fixtures/parser/ast/html-entity03-unquote-output.json b/tests/fixtures/parser/ast/html-entity03-unquote-output.json new file mode 100644 index 00000000..838e1ca6 --- /dev/null +++ b/tests/fixtures/parser/ast/html-entity03-unquote-output.json @@ -0,0 +1,878 @@ +{ + "type": "Program", + "body": [ + { + "type": "AstroFragment", + "children": [ + { + "type": "JSXElement", + "children": [ + { + "type": "JSXText", + "raw": "©", + "value": "©", + "range": [ + 30, + 36 + ], + "loc": { + "start": { + "line": 4, + "column": 21 + }, + "end": { + "line": 4, + "column": 27 + } + } + } + ], + "closingElement": { + "type": "JSXClosingElement", + "name": { + "type": "JSXIdentifier", + "name": "span", + "range": [ + 38, + 42 + ], + "loc": { + "start": { + "line": 4, + "column": 29 + }, + "end": { + "line": 4, + "column": 33 + } + } + }, + "range": [ + 36, + 43 + ], + "loc": { + "start": { + "line": 4, + "column": 27 + }, + "end": { + "line": 4, + "column": 34 + } + } + }, + "openingElement": { + "type": "JSXOpeningElement", + "name": { + "type": "JSXIdentifier", + "name": "span", + "range": [ + 10, + 14 + ], + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 5 + } + } + }, + "attributes": [ + { + "type": "JSXAttribute", + "name": { + "type": "JSXIdentifier", + "name": "title", + "range": [ + 15, + 20 + ], + "loc": { + "start": { + "line": 4, + "column": 6 + }, + "end": { + "line": 4, + "column": 11 + } + } + }, + "value": { + "type": "Literal", + "raw": "☃", + "value": "☃", + "range": [ + 21, + 29 + ], + "loc": { + "start": { + "line": 4, + "column": 12 + }, + "end": { + "line": 4, + "column": 20 + } + } + }, + "range": [ + 15, + 29 + ], + "loc": { + "start": { + "line": 4, + "column": 6 + }, + "end": { + "line": 4, + "column": 20 + } + } + } + ], + "selfClosing": false, + "range": [ + 9, + 30 + ], + "loc": { + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 4, + "column": 21 + } + } + }, + "range": [ + 9, + 43 + ], + "loc": { + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 4, + "column": 34 + } + } + }, + { + "type": "JSXText", + "raw": "\n", + "value": "\n", + "range": [ + 43, + 44 + ], + "loc": { + "start": { + "line": 4, + "column": 34 + }, + "end": { + "line": 5, + "column": 0 + } + } + }, + { + "type": "JSXElement", + "children": [ + { + "type": "JSXText", + "raw": "☃", + "value": "☃", + "range": [ + 63, + 71 + ], + "loc": { + "start": { + "line": 5, + "column": 19 + }, + "end": { + "line": 5, + "column": 27 + } + } + } + ], + "closingElement": { + "type": "JSXClosingElement", + "name": { + "type": "JSXIdentifier", + "name": "span", + "range": [ + 73, + 77 + ], + "loc": { + "start": { + "line": 5, + "column": 29 + }, + "end": { + "line": 5, + "column": 33 + } + } + }, + "range": [ + 71, + 78 + ], + "loc": { + "start": { + "line": 5, + "column": 27 + }, + "end": { + "line": 5, + "column": 34 + } + } + }, + "openingElement": { + "type": "JSXOpeningElement", + "name": { + "type": "JSXIdentifier", + "name": "span", + "range": [ + 45, + 49 + ], + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 5 + } + } + }, + "attributes": [ + { + "type": "JSXAttribute", + "name": { + "type": "JSXIdentifier", + "name": "title", + "range": [ + 50, + 55 + ], + "loc": { + "start": { + "line": 5, + "column": 6 + }, + "end": { + "line": 5, + "column": 11 + } + } + }, + "value": { + "type": "Literal", + "raw": "©", + "value": "©", + "range": [ + 56, + 62 + ], + "loc": { + "start": { + "line": 5, + "column": 12 + }, + "end": { + "line": 5, + "column": 18 + } + } + }, + "range": [ + 50, + 62 + ], + "loc": { + "start": { + "line": 5, + "column": 6 + }, + "end": { + "line": 5, + "column": 18 + } + } + } + ], + "selfClosing": false, + "range": [ + 44, + 63 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 19 + } + } + }, + "range": [ + 44, + 78 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 34 + } + } + }, + { + "type": "JSXText", + "raw": "\n", + "value": "\n", + "range": [ + 78, + 79 + ], + "loc": { + "start": { + "line": 5, + "column": 34 + }, + "end": { + "line": 6, + "column": 0 + } + } + } + ], + "range": [ + 9, + 79 + ], + "loc": { + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 6, + "column": 0 + } + } + } + ], + "sourceType": "module", + "comments": [], + "tokens": [ + { + "type": "Punctuator", + "value": "---", + "range": [ + 0, + 3 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 3 + } + } + }, + { + "type": "Punctuator", + "value": "---", + "range": [ + 4, + 7 + ], + "loc": { + "start": { + "line": 2, + "column": 0 + }, + "end": { + "line": 2, + "column": 3 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 9, + 10 + ], + "loc": { + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 4, + "column": 1 + } + } + }, + { + "type": "JSXIdentifier", + "value": "span", + "range": [ + 10, + 14 + ], + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 5 + } + } + }, + { + "type": "JSXIdentifier", + "value": "title", + "range": [ + 15, + 20 + ], + "loc": { + "start": { + "line": 4, + "column": 6 + }, + "end": { + "line": 4, + "column": 11 + } + } + }, + { + "type": "Punctuator", + "value": "=", + "range": [ + 20, + 21 + ], + "loc": { + "start": { + "line": 4, + "column": 11 + }, + "end": { + "line": 4, + "column": 12 + } + } + }, + { + "type": "JSXText", + "value": "☃", + "range": [ + 21, + 29 + ], + "loc": { + "start": { + "line": 4, + "column": 12 + }, + "end": { + "line": 4, + "column": 20 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 29, + 30 + ], + "loc": { + "start": { + "line": 4, + "column": 20 + }, + "end": { + "line": 4, + "column": 21 + } + } + }, + { + "type": "JSXText", + "value": "©", + "range": [ + 30, + 36 + ], + "loc": { + "start": { + "line": 4, + "column": 21 + }, + "end": { + "line": 4, + "column": 27 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 36, + 37 + ], + "loc": { + "start": { + "line": 4, + "column": 27 + }, + "end": { + "line": 4, + "column": 28 + } + } + }, + { + "type": "Punctuator", + "value": "/", + "range": [ + 37, + 38 + ], + "loc": { + "start": { + "line": 4, + "column": 28 + }, + "end": { + "line": 4, + "column": 29 + } + } + }, + { + "type": "JSXIdentifier", + "value": "span", + "range": [ + 38, + 42 + ], + "loc": { + "start": { + "line": 4, + "column": 29 + }, + "end": { + "line": 4, + "column": 33 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 42, + 43 + ], + "loc": { + "start": { + "line": 4, + "column": 33 + }, + "end": { + "line": 4, + "column": 34 + } + } + }, + { + "type": "JSXText", + "value": "\n", + "range": [ + 43, + 44 + ], + "loc": { + "start": { + "line": 4, + "column": 34 + }, + "end": { + "line": 5, + "column": 0 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 44, + 45 + ], + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 1 + } + } + }, + { + "type": "JSXIdentifier", + "value": "span", + "range": [ + 45, + 49 + ], + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 5 + } + } + }, + { + "type": "JSXIdentifier", + "value": "title", + "range": [ + 50, + 55 + ], + "loc": { + "start": { + "line": 5, + "column": 6 + }, + "end": { + "line": 5, + "column": 11 + } + } + }, + { + "type": "Punctuator", + "value": "=", + "range": [ + 55, + 56 + ], + "loc": { + "start": { + "line": 5, + "column": 11 + }, + "end": { + "line": 5, + "column": 12 + } + } + }, + { + "type": "JSXText", + "value": "©", + "range": [ + 56, + 62 + ], + "loc": { + "start": { + "line": 5, + "column": 12 + }, + "end": { + "line": 5, + "column": 18 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 62, + 63 + ], + "loc": { + "start": { + "line": 5, + "column": 18 + }, + "end": { + "line": 5, + "column": 19 + } + } + }, + { + "type": "JSXText", + "value": "☃", + "range": [ + 63, + 71 + ], + "loc": { + "start": { + "line": 5, + "column": 19 + }, + "end": { + "line": 5, + "column": 27 + } + } + }, + { + "type": "Punctuator", + "value": "<", + "range": [ + 71, + 72 + ], + "loc": { + "start": { + "line": 5, + "column": 27 + }, + "end": { + "line": 5, + "column": 28 + } + } + }, + { + "type": "Punctuator", + "value": "/", + "range": [ + 72, + 73 + ], + "loc": { + "start": { + "line": 5, + "column": 28 + }, + "end": { + "line": 5, + "column": 29 + } + } + }, + { + "type": "JSXIdentifier", + "value": "span", + "range": [ + 73, + 77 + ], + "loc": { + "start": { + "line": 5, + "column": 29 + }, + "end": { + "line": 5, + "column": 33 + } + } + }, + { + "type": "Punctuator", + "value": ">", + "range": [ + 77, + 78 + ], + "loc": { + "start": { + "line": 5, + "column": 33 + }, + "end": { + "line": 5, + "column": 34 + } + } + }, + { + "type": "JSXText", + "value": "\n", + "range": [ + 78, + 79 + ], + "loc": { + "start": { + "line": 5, + "column": 34 + }, + "end": { + "line": 6, + "column": 0 + } + } + } + ], + "range": [ + 0, + 79 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 6, + "column": 0 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/parser/ast/html-entity03-unquote-scope-output.json b/tests/fixtures/parser/ast/html-entity03-unquote-scope-output.json new file mode 100644 index 00000000..6e0646f9 --- /dev/null +++ b/tests/fixtures/parser/ast/html-entity03-unquote-scope-output.json @@ -0,0 +1,15 @@ +{ + "type": "global", + "variables": [], + "references": [], + "childScopes": [ + { + "type": "module", + "variables": [], + "references": [], + "childScopes": [], + "through": [] + } + ], + "through": [] +} \ No newline at end of file