Skip to content

Commit

Permalink
fix #1959: parser recovery on invalid "@Keyframes"
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 25, 2022
1 parent 4e98e5f commit cccc6c6
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 29 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## Unreleased

* Make parsing of invalid `@keyframes` rules more robust ([#1959](https://github.com/evanw/esbuild/issues/1959))

This improves esbuild's parsing of certain malformed `@keyframes` rules to avoid them affecting the following rule. This fix only affects invalid CSS files, and does not change any behavior for files containing valid CSS. Here's an example of the fix:

```css
/* Original code */
@keyframes x { . }
@keyframes y { 1% { a: b; } }

/* Old output (with --minify) */
@keyframes x{y{1% {a: b;}}}

/* New output (with --minify) */
@keyframes x{.}@keyframes y{1%{a:b}}
```

## 0.14.13

* Be more consistent about external paths ([#619](https://github.com/evanw/esbuild/issues/619))
Expand Down
75 changes: 49 additions & 26 deletions internal/css_parser/css_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,22 +739,32 @@ func (p *parser) parseAtRule(context atRuleContext) css_ast.Rule {
}

p.eat(css_lexer.TWhitespace)
blockStart := p.index

if p.expect(css_lexer.TOpenBrace) {
var blocks []css_ast.KeyframeBlock

blocks:
badSyntax:
for {
switch p.current().Kind {
case css_lexer.TWhitespace:
p.advance()
continue

case css_lexer.TCloseBrace, css_lexer.TEndOfFile:
break blocks
case css_lexer.TCloseBrace:
p.advance()
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtKeyframes{
AtToken: atToken,
Name: name,
Blocks: blocks,
}}

case css_lexer.TEndOfFile:
break badSyntax

case css_lexer.TOpenBrace:
p.expect(css_lexer.TPercentage)
p.parseComponentValue()
break badSyntax

default:
var selectors []string
Expand All @@ -767,9 +777,14 @@ func (p *parser) parseAtRule(context atRuleContext) css_ast.Rule {
p.advance()
continue

case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
case css_lexer.TOpenBrace:
p.advance()
break selectors

case css_lexer.TCloseBrace, css_lexer.TEndOfFile:
p.expect(css_lexer.TOpenBrace)
break badSyntax

case css_lexer.TIdent, css_lexer.TPercentage:
text := p.decoded()
if t.Kind == css_lexer.TIdent {
Expand All @@ -786,38 +801,46 @@ func (p *parser) parseAtRule(context atRuleContext) css_ast.Rule {
selectors = append(selectors, text)
p.advance()

// Keyframe selectors are comma-separated
p.eat(css_lexer.TWhitespace)
if p.eat(css_lexer.TComma) {
p.eat(css_lexer.TWhitespace)
if k := p.current().Kind; k != css_lexer.TIdent && k != css_lexer.TPercentage {
p.expect(css_lexer.TPercentage)
break badSyntax
}
} else if k := p.current().Kind; k != css_lexer.TOpenBrace && k != css_lexer.TCloseBrace && k != css_lexer.TEndOfFile {
p.expect(css_lexer.TComma)
break badSyntax
}

default:
p.expect(css_lexer.TPercentage)
p.parseComponentValue()
}

p.eat(css_lexer.TWhitespace)
if t.Kind != css_lexer.TComma && !p.peek(css_lexer.TOpenBrace) {
p.expect(css_lexer.TComma)
break badSyntax
}
}

if p.expect(css_lexer.TOpenBrace) {
rules := p.parseListOfDeclarations()
p.expect(css_lexer.TCloseBrace)
rules := p.parseListOfDeclarations()
p.expect(css_lexer.TCloseBrace)

// "@keyframes { from {} to { color: red } }" => "@keyframes { to { color: red } }"
if !p.options.MangleSyntax || len(rules) > 0 {
blocks = append(blocks, css_ast.KeyframeBlock{
Selectors: selectors,
Rules: rules,
})
}
// "@keyframes { from {} to { color: red } }" => "@keyframes { to { color: red } }"
if !p.options.MangleSyntax || len(rules) > 0 {
blocks = append(blocks, css_ast.KeyframeBlock{
Selectors: selectors,
Rules: rules,
})
}
}
}

// Otherwise, finish parsing the body and return an unknown rule
for !p.peek(css_lexer.TCloseBrace) && !p.peek(css_lexer.TEndOfFile) {
p.parseComponentValue()
}
p.expect(css_lexer.TCloseBrace)
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtKeyframes{
AtToken: atToken,
Name: name,
Blocks: blocks,
}}
prelude := p.convertTokens(p.tokens[preludeStart:blockStart])
block, _ := p.convertTokensHelper(p.tokens[blockStart:p.index], css_lexer.TEndOfFile, convertTokensOpts{allowImports: true})
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude, Block: block}}
}

case "nest":
Expand Down
23 changes: 20 additions & 3 deletions internal/css_parser/css_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,10 +939,27 @@ func TestAtKeyframes(t *testing.T) {
expectParseError(t, "@keyframes name { {} 0% {} }", "<stdin>: WARNING: Expected percentage but found \"{\"\n")
expectParseError(t, "@keyframes name { 100 {} }", "<stdin>: WARNING: Expected percentage but found \"100\"\n")
expectParseError(t, "@keyframes name { into {} }", "<stdin>: WARNING: Expected percentage but found \"into\"\n")
expectParseError(t, "@keyframes name { 1,2 {} }", "<stdin>: WARNING: Expected percentage but found \"1\"\n<stdin>: WARNING: Expected percentage but found \"2\"\n")
expectParseError(t, "@keyframes name { 1, 2 {} }", "<stdin>: WARNING: Expected percentage but found \"1\"\n<stdin>: WARNING: Expected percentage but found \"2\"\n")
expectParseError(t, "@keyframes name { 1 ,2 {} }", "<stdin>: WARNING: Expected percentage but found \"1\"\n<stdin>: WARNING: Expected percentage but found \"2\"\n")
expectParseError(t, "@keyframes name { 1,2 {} }", "<stdin>: WARNING: Expected percentage but found \"1\"\n")
expectParseError(t, "@keyframes name { 1, 2 {} }", "<stdin>: WARNING: Expected percentage but found \"1\"\n")
expectParseError(t, "@keyframes name { 1 ,2 {} }", "<stdin>: WARNING: Expected percentage but found \"1\"\n")
expectParseError(t, "@keyframes name { 1%, {} }", "<stdin>: WARNING: Expected percentage but found \"{\"\n")
expectParseError(t, "@keyframes name { 1%, x {} }", "<stdin>: WARNING: Expected percentage but found \"x\"\n")
expectParseError(t, "@keyframes name { 1%, ! {} }", "<stdin>: WARNING: Expected percentage but found \"!\"\n")
expectParseError(t, "@keyframes name { .x {} }", "<stdin>: WARNING: Expected percentage but found \".\"\n")
expectParseError(t, "@keyframes name { {} }", "<stdin>: WARNING: Expected percentage but found \"{\"\n")
expectParseError(t, "@keyframes name { 1% }", "<stdin>: WARNING: Expected \"{\" but found \"}\"\n")
expectParseError(t, "@keyframes name { 1%", "<stdin>: WARNING: Expected \"{\" but found end of file\n")
expectParseError(t, "@keyframes name { 1%,,2% {} }", "<stdin>: WARNING: Expected percentage but found \",\"\n")
expectParseError(t, "@keyframes name {", "<stdin>: WARNING: Expected \"}\" but found end of file\n")

expectPrinted(t, "@keyframes x { 1%, {} } @keyframes z { 1% {} }", "@keyframes x { 1%, {} }\n@keyframes z {\n 1% {\n }\n}\n")
expectPrinted(t, "@keyframes x { .y {} } @keyframes z { 1% {} }", "@keyframes x { .y {} }\n@keyframes z {\n 1% {\n }\n}\n")
expectPrinted(t, "@keyframes x { x {} } @keyframes z { 1% {} }", "@keyframes x {\n x {\n }\n}\n@keyframes z {\n 1% {\n }\n}\n")
expectPrinted(t, "@keyframes x { {} } @keyframes z { 1% {} }", "@keyframes x { {} }\n@keyframes z {\n 1% {\n }\n}\n")
expectPrinted(t, "@keyframes x { 1% {}", "@keyframes x { 1% {} }\n")
expectPrinted(t, "@keyframes x { 1% {", "@keyframes x { 1% {} }\n")
expectPrinted(t, "@keyframes x { 1%", "@keyframes x { 1% }\n")
expectPrinted(t, "@keyframes x {", "@keyframes x {}\n")
}

func TestAtRuleValidation(t *testing.T) {
Expand Down

0 comments on commit cccc6c6

Please sign in to comment.