Skip to content

Commit

Permalink
fix(tokenizer): handle unterminated quoted attributes gracefully (#923)
Browse files Browse the repository at this point in the history
* add test

* fix

* chore: changeset

* test: add single quoted attribute test

* add (seemingly incorrect?) test for feedback

* apply feedback to tests

* simple heuristic to rescue some chars off attrs

* update TS tests to refect changes

* simplify whitespace check

* Update internal/token.go

* Update internal/token.go

---------

Co-authored-by: Nate Moore <[email protected]>
  • Loading branch information
MoustaphaDev and natemoo-re authored Jan 4, 2024
1 parent 24e2886 commit b52f7d1
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilled-starfishes-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/compiler': patch
---

Fixes an issue where unterminated quoted attributes caused the compiler to crash
2 changes: 1 addition & 1 deletion internal/printer/print-to-json.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func renderNode(p *printer, parent *ASTNode, n *Node, opts t.ParseOptions) {
}
position := attrPositionAt(p, &attr, opts)
raw := ""
if attr.Type == QuotedAttribute {
if attr.Type == QuotedAttribute || attr.Type == TemplateLiteralAttribute {
start := attr.ValLoc.Start - 1
end := attr.ValLoc.Start + len(attr.Val)

Expand Down
15 changes: 15 additions & 0 deletions internal/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3342,6 +3342,21 @@ const c = '\''
source: `<html><body><h1>Hello world!</h1><style></style></body></html>`,
want: []ASTNode{{Type: "element", Name: "html", Children: []ASTNode{{Type: "element", Name: "body", Children: []ASTNode{{Type: "element", Name: "h1", Children: []ASTNode{{Type: "text", Value: "Hello world!"}}}, {Type: "element", Name: "style"}}}}}},
},
{
name: "element with unterminated double quote attribute",
source: `<main id="gotcha />`,
want: []ASTNode{{Type: "element", Name: "main", Attributes: []ASTNode{{Type: "attribute", Kind: "quoted", Name: "id", Value: "gotcha", Raw: "\"gotcha"}}}},
},
{
name: "element with unterminated single quote attribute",
source: `<main id='gotcha />`,
want: []ASTNode{{Type: "element", Name: "main", Attributes: []ASTNode{{Type: "attribute", Kind: "quoted", Name: "id", Value: "gotcha", Raw: "'gotcha"}}}},
},
{
name: "element with unterminated template literal attribute",
source: `<main id=` + BACKTICK + `gotcha />`,
want: []ASTNode{{Type: "element", Name: "main", Attributes: []ASTNode{{Type: "attribute", Kind: "template-literal", Name: "id", Value: "gotcha", Raw: "`gotcha"}}}},
},
}

for _, tt := range tests {
Expand Down
36 changes: 35 additions & 1 deletion internal/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,29 @@ func (z *Tokenizer) readTagAttrVal() {
for {
c := z.readByte()
if z.err != nil {
if z.err == io.EOF {
// rescan, closing any potentially unterminated quoted attribute values
for i := z.pendingAttr[1].Start; i < z.raw.End; i++ {
c := z.buf[i]
if unicode.IsSpace(rune(c)) || c == '/' || c == '>' {
z.pendingAttr[1].End = i
break
}
if i == z.raw.End-1 {
z.pendingAttr[1].End = i
break
}
}
z.handler.AppendError(&loc.ErrorWithRange{
Code: loc.ERROR_UNTERMINATED_STRING,
Text: `Unterminated quoted attribute`,
Range: loc.Range{
Loc: loc.Loc{Start: z.data.Start},
Len: z.raw.End,
},
})
return
}
z.pendingAttr[1].End = z.raw.End
return
}
Expand All @@ -1463,7 +1486,18 @@ func (z *Tokenizer) readTagAttrVal() {
c := z.readByte()
if z.err != nil {
if z.err == io.EOF {
z.pendingAttr[1].End = z.pendingAttr[1].Start
// rescan, closing any potentially unterminated attribute values
for i := z.pendingAttr[1].Start; i < z.raw.End; i++ {
c := z.buf[i]
if unicode.IsSpace(rune(c)) || c == '/' || c == '>' {
z.pendingAttr[1].End = i
break
}
if i == z.raw.End-1 {
z.pendingAttr[1].End = i
break
}
}
z.handler.AppendError(&loc.ErrorWithRange{
Code: loc.ERROR_UNTERMINATED_STRING,
Text: `Unterminated template literal attribute`,
Expand Down
31 changes: 28 additions & 3 deletions packages/compiler/test/tsx-sourcemaps/unfinished-literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { convertToTSX } from '@astrojs/compiler';

const input = `<div class=\`></div>
`;

test('does not panic on unfinished template literal attribute', async () => {
const input = `<div class=\`></div>
`;
let error = 0;
try {
const output = await convertToTSX(input, { filename: 'index.astro', sourcemap: 'inline' });
Expand All @@ -16,3 +15,29 @@ test('does not panic on unfinished template literal attribute', async () => {

assert.equal(error, 0, `compiler should not have panicked`);
});

test('does not panic on unfinished double quoted attribute', async () => {
const input = `<main id="gotcha />`;
let error = 0;
try {
const output = await convertToTSX(input, { filename: 'index.astro', sourcemap: 'inline' });
assert.match(output.code, `id="gotcha"`);
} catch (e) {
error = 1;
}

assert.equal(error, 0, `compiler should not have panicked`);
});

test('does not panic on unfinished single quoted attribute', async () => {
const input = `<main id='gotcha/>`;
let error = 0;
try {
const output = await convertToTSX(input, { filename: 'index.astro', sourcemap: 'inline' });
assert.match(output.code, `id="gotcha"`);
} catch (e) {
error = 1;
}

assert.equal(error, 0, `compiler should not have panicked`);
});

0 comments on commit b52f7d1

Please sign in to comment.