Skip to content

Commit

Permalink
fix #1216: friendly error for missing "async"
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Apr 28, 2021
1 parent b5aed81 commit d86a3b9
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 32 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,30 @@

The names of symbols imported from other chunks were previously not considered for renaming during minified name assignment. This could cause a syntax error due to a name collision when two symbols have the same original name. This was just an oversight and has been fixed, so symbols imported from other chunks should now be renamed when minification is enabled.

* Provide a friendly error message when you forget `async` ([#1216](https://github.com/evanw/esbuild/issues/1216))

If the parser hits a parse error inside a non-asynchronous function or arrow expression and the previous token is `await`, esbuild will now report a friendly error about a missing `async` keyword instead of reporting the parse error. This behavior matches other JavaScript parsers including TypeScript, Babel, and V8.

The previous error looked like this:

```
> test.ts:2:8: error: Expected ";" but found "f"
2 │ await f();
╵ ^
```

The error now looks like this:

```
> example.js:2:2: error: "await" can only be used inside an "async" function
2 │ await f();
╵ ~~~~~
example.js:1:0: note: Consider adding the "async" keyword here
1 │ function f() {
│ ^
╵ async
```

## 0.11.15

* Provide options for how to handle legal comments ([#919](https://github.com/evanw/esbuild/issues/919))
Expand Down
54 changes: 44 additions & 10 deletions internal/js_lexer/js_lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,15 @@ type Lexer struct {
end int
ApproximateNewlineCount int
LegacyOctalLoc logger.Loc
AwaitKeywordLoc logger.Loc
FnOrArrowStartLoc logger.Loc
PreviousBackslashQuoteInJSX logger.Range
Token T
HasNewlineBefore bool
HasPureCommentBefore bool
PreserveAllCommentsBefore bool
IsLegacyOctalLiteral bool
PrevTokenWasAwaitKeyword bool
CommentsToPreserveBefore []js_ast.Comment
AllOriginalComments []js_ast.Comment
codePoint rune
Expand All @@ -251,9 +254,10 @@ type LexerPanic struct{}

func NewLexer(log logger.Log, source logger.Source) Lexer {
lexer := Lexer{
log: log,
source: source,
prevErrorLoc: logger.Loc{Start: -1},
log: log,
source: source,
prevErrorLoc: logger.Loc{Start: -1},
FnOrArrowStartLoc: logger.Loc{Start: -1},
}
lexer.step()
lexer.Next()
Expand All @@ -262,10 +266,11 @@ func NewLexer(log logger.Log, source logger.Source) Lexer {

func NewLexerGlobalName(log logger.Log, source logger.Source) Lexer {
lexer := Lexer{
log: log,
source: source,
prevErrorLoc: logger.Loc{Start: -1},
forGlobalName: true,
log: log,
source: source,
prevErrorLoc: logger.Loc{Start: -1},
FnOrArrowStartLoc: logger.Loc{Start: -1},
forGlobalName: true,
}
lexer.step()
lexer.Next()
Expand All @@ -274,9 +279,10 @@ func NewLexerGlobalName(log logger.Log, source logger.Source) Lexer {

func NewLexerJSON(log logger.Log, source logger.Source, allowComments bool) Lexer {
lexer := Lexer{
log: log,
source: source,
prevErrorLoc: logger.Loc{Start: -1},
log: log,
source: source,
prevErrorLoc: logger.Loc{Start: -1},
FnOrArrowStartLoc: logger.Loc{Start: -1},
json: json{
parse: true,
allowComments: allowComments,
Expand Down Expand Up @@ -384,6 +390,21 @@ func (lexer *Lexer) SyntaxError() {
}

func (lexer *Lexer) ExpectedString(text string) {
// Provide a friendly error message about "await" without "async"
if lexer.PrevTokenWasAwaitKeyword {
var notes []logger.MsgData
if lexer.FnOrArrowStartLoc.Start != -1 {
note := logger.RangeData(&lexer.source, logger.Range{Loc: lexer.FnOrArrowStartLoc},
"Consider adding the \"async\" keyword here")
note.Location.Suggestion = "async"
notes = []logger.MsgData{note}
}
lexer.addRangeErrorWithNotes(RangeOfIdentifier(lexer.source, lexer.AwaitKeywordLoc),
"\"await\" can only be used inside an \"async\" function",
notes)
panic(LexerPanic{})
}

found := fmt.Sprintf("%q", lexer.Raw())
if lexer.start == len(lexer.source.Contents) {
found = "end of file"
Expand Down Expand Up @@ -992,6 +1013,7 @@ func (lexer *Lexer) NextInsideJSXElement() {
func (lexer *Lexer) Next() {
lexer.HasNewlineBefore = lexer.end == 0
lexer.HasPureCommentBefore = false
lexer.PrevTokenWasAwaitKeyword = false
lexer.CommentsToPreserveBefore = nil

for {
Expand Down Expand Up @@ -2440,6 +2462,18 @@ func (lexer *Lexer) addRangeError(r logger.Range, text string) {
}
}

func (lexer *Lexer) addRangeErrorWithNotes(r logger.Range, text string, notes []logger.MsgData) {
// Don't report multiple errors in the same spot
if r.Loc == lexer.prevErrorLoc {
return
}
lexer.prevErrorLoc = r.Loc

if !lexer.IsLogDisabled {
lexer.log.AddRangeErrorWithNotes(&lexer.source, r, text, notes)
}
}

func hasPrefixWithWordBoundary(text string, prefix string) bool {
t := len(text)
p := len(prefix)
Expand Down
33 changes: 26 additions & 7 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ const (
// restored on the call stack around code that parses nested functions and
// arrow expressions.
type fnOrArrowDataParse struct {
needsAsyncLoc logger.Loc
asyncRange logger.Range
arrowArgErrors *deferredArrowArgErrors
await awaitOrYield
Expand Down Expand Up @@ -1993,6 +1994,7 @@ func (p *parser) parseProperty(kind js_ast.PropertyKind, opts propertyOpts, erro
}

fn, hadBody := p.parseFn(nil, fnOrArrowDataParse{
needsAsyncLoc: key.Loc,
asyncRange: opts.asyncRange,
await: await,
yield: yield,
Expand Down Expand Up @@ -2235,7 +2237,9 @@ func (p *parser) parseAsyncPrefixExpr(asyncRange logger.Range, level js_ast.L) j
p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, asyncRange.Loc)
defer p.popScope()

return js_ast.Expr{Loc: asyncRange.Loc, Data: p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{})}
return js_ast.Expr{Loc: asyncRange.Loc, Data: p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{
needsAsyncLoc: asyncRange.Loc,
})}
}

// "async x => {}"
Expand All @@ -2249,7 +2253,10 @@ func (p *parser) parseAsyncPrefixExpr(asyncRange logger.Range, level js_ast.L) j
p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, asyncRange.Loc)
defer p.popScope()

arrow := p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{await: allowExpr})
arrow := p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{
needsAsyncLoc: arg.Binding.Loc,
await: allowExpr,
})
arrow.IsAsync = true
return js_ast.Expr{Loc: asyncRange.Loc, Data: arrow}
}
Expand Down Expand Up @@ -2316,9 +2323,10 @@ func (p *parser) parseFnExpr(loc logger.Loc, isAsync bool, asyncRange logger.Ran
}

fn, _ := p.parseFn(name, fnOrArrowDataParse{
asyncRange: asyncRange,
await: await,
yield: yield,
needsAsyncLoc: loc,
asyncRange: asyncRange,
await: await,
yield: yield,
})
p.validateFunctionName(fn, fnExpr)
return js_ast.Expr{Loc: loc, Data: &js_ast.EFunction{Fn: fn}}
Expand Down Expand Up @@ -2471,7 +2479,10 @@ func (p *parser) parseParenExpr(loc logger.Loc, level js_ast.L, opts parenExprOp
await = allowExpr
}

arrow := p.parseArrowBody(args, fnOrArrowDataParse{await: await})
arrow := p.parseArrowBody(args, fnOrArrowDataParse{
needsAsyncLoc: loc,
await: await,
})
arrow.IsAsync = opts.isAsync
arrow.HasRestArg = spreadRange.Len > 0
p.popScope()
Expand Down Expand Up @@ -2747,6 +2758,11 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EAwait{Value: value}}
}

case allowIdent:
p.lexer.PrevTokenWasAwaitKeyword = true
p.lexer.AwaitKeywordLoc = loc
p.lexer.FnOrArrowStartLoc = p.fnOrArrowDataParse.needsAsyncLoc
}

case "yield":
Expand Down Expand Up @@ -2788,7 +2804,9 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF
p.pushScopeForParsePass(js_ast.ScopeFunctionArgs, loc)
defer p.popScope()

return js_ast.Expr{Loc: loc, Data: p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{})}
return js_ast.Expr{Loc: loc, Data: p.parseArrowBody([]js_ast.Arg{arg}, fnOrArrowDataParse{
needsAsyncLoc: loc,
})}
}

ref := p.storeNameInRef(name)
Expand Down Expand Up @@ -5125,6 +5143,7 @@ func (p *parser) parseFnStmt(loc logger.Loc, opts parseStmtOpts, isAsync bool, a
}

fn, hadBody := p.parseFn(name, fnOrArrowDataParse{
needsAsyncLoc: loc,
asyncRange: asyncRange,
await: await,
yield: yield,
Expand Down
33 changes: 18 additions & 15 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,9 @@ func TestAsync(t *testing.T) {
expectPrinted(t, "new async function() { await 0 }", "new async function() {\n await 0;\n}();\n")
expectPrinted(t, "new async function() { await 0 }.x", "new async function() {\n await 0;\n}.x();\n")

friendlyAwaitError := "<stdin>: error: \"await\" can only be used inside an \"async\" function\n" +
"<stdin>: note: Consider adding the \"async\" keyword here\n"

expectPrinted(t, "async", "async;\n")
expectPrinted(t, "async + 1", "async + 1;\n")
expectPrinted(t, "async => {}", "(async) => {\n};\n")
Expand All @@ -1525,7 +1528,7 @@ func TestAsync(t *testing.T) {
expectPrinted(t, "new (async().x)", "new (async()).x();\n")
expectParseError(t, "async x;", "<stdin>: error: Expected \"=>\" but found \";\"\n")
expectParseError(t, "async (...x,) => {}", "<stdin>: error: Unexpected \",\" after rest pattern\n")
expectParseError(t, "async => await 0", "<stdin>: error: Expected \";\" but found \"0\"\n")
expectParseError(t, "async => await 0", friendlyAwaitError)
expectParseError(t, "new async => {}", "<stdin>: error: Expected \";\" but found \"=>\"\n")
expectParseError(t, "new async () => {}", "<stdin>: error: Expected \";\" but found \"=>\"\n")

Expand All @@ -1546,19 +1549,19 @@ func TestAsync(t *testing.T) {

noAwait := "<stdin>: error: The keyword \"await\" cannot be used here\n"
expectParseError(t, "async function bar(x = await y) {}", noAwait+"<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async (function(x = await y) {})", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async ({ foo(x = await y) {} })", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async (function(x = await y) {})", friendlyAwaitError)
expectParseError(t, "async ({ foo(x = await y) {} })", friendlyAwaitError)
expectParseError(t, "class Foo { async foo(x = await y) {} }", noAwait+"<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "(class { async foo(x = await y) {} })", noAwait+"<stdin>: error: Expected \")\" but found \"y\"\n")

expectParseError(t, "async function foo() { function bar(x = await y) {} }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { (function(x = await y) {}) }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { ({ foo(x = await y) {} }) }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { class Foo { foo(x = await y) {} } }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { (class { foo(x = await y) {} }) }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { function bar(x = await y) {} }", friendlyAwaitError)
expectParseError(t, "async function foo() { (function(x = await y) {}) }", friendlyAwaitError)
expectParseError(t, "async function foo() { ({ foo(x = await y) {} }) }", friendlyAwaitError)
expectParseError(t, "async function foo() { class Foo { foo(x = await y) {} } }", friendlyAwaitError)
expectParseError(t, "async function foo() { (class { foo(x = await y) {} }) }", friendlyAwaitError)
expectParseError(t, "async function foo() { (x = await y) => {} }", "<stdin>: error: Cannot use an \"await\" expression here\n")
expectPrinted(t, "async function foo() { (x = await y) }", "async function foo() {\n x = await y;\n}\n")
expectParseError(t, "function foo() { (x = await y) }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "function foo() { (x = await y) }", friendlyAwaitError)

// Newlines
expectPrinted(t, "(class { async \n foo() {} })", "(class {\n async;\n foo() {\n }\n});\n")
Expand All @@ -1569,18 +1572,18 @@ func TestAsync(t *testing.T) {
// Top-level await
expectPrinted(t, "await foo;", "await foo;\n")
expectPrinted(t, "for await(foo of bar);", "for await (foo of bar)\n ;\n")
expectParseError(t, "function foo() { await foo }", "<stdin>: error: Expected \";\" but found \"foo\"\n")
expectParseError(t, "function foo() { await foo }", friendlyAwaitError)
expectParseError(t, "function foo() { for await(foo of bar); }", "<stdin>: error: Cannot use \"await\" outside an async function\n")
expectPrinted(t, "function foo(x = await) {}", "function foo(x = await) {\n}\n")
expectParseError(t, "function foo(x = await y) {}", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "function foo(x = await y) {}", friendlyAwaitError)
expectPrinted(t, "(function(x = await) {})", "(function(x = await) {\n});\n")
expectParseError(t, "(function(x = await y) {})", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "(function(x = await y) {})", friendlyAwaitError)
expectPrinted(t, "({ foo(x = await) {} })", "({foo(x = await) {\n}});\n")
expectParseError(t, "({ foo(x = await y) {} })", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "({ foo(x = await y) {} })", friendlyAwaitError)
expectPrinted(t, "class Foo { foo(x = await) {} }", "class Foo {\n foo(x = await) {\n }\n}\n")
expectParseError(t, "class Foo { foo(x = await y) {} }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "class Foo { foo(x = await y) {} }", friendlyAwaitError)
expectPrinted(t, "(class { foo(x = await) {} })", "(class {\n foo(x = await) {\n }\n});\n")
expectParseError(t, "(class { foo(x = await y) {} })", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "(class { foo(x = await y) {} })", friendlyAwaitError)
expectParseError(t, "(x = await) => {}", "<stdin>: error: Unexpected \")\"\n")
expectParseError(t, "(x = await y) => {}", "<stdin>: error: Cannot use an \"await\" expression here\n")
expectParseError(t, "(x = await)", "<stdin>: error: Unexpected \")\"\n")
Expand Down

0 comments on commit d86a3b9

Please sign in to comment.