From 4cf1446ec24fb2f2d9888edf09d261b41691acbf Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Wed, 17 Feb 2021 21:26:34 -0800 Subject: [PATCH] forbid "let" starting a for-of loop initializer --- CHANGELOG.md | 2 ++ internal/js_parser/js_parser.go | 8 ++++++++ internal/js_parser/js_parser_test.go | 9 +++++++++ internal/js_printer/js_printer.go | 16 +++++++++++++++- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2140a5d2147..262481d6237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ * Code of the form `new async () => {}` must not be allowed. Previously this was incorrectly allowed since the speculative parsing of asynchronous arrow functions did not check the precedence level. + * It's not valid to start an initializer expression in a for-of loop with the token `let` such as `for (let.foo of bar) {}`. This is now forbidden. In addition, the code generator now respects this rule so `for ((let.foo) of bar) {}` is now printed as `for ((let).foo of bar) {}`. + ## 0.8.47 * Release native binaries for the Apple M1 chip ([#550](https://github.com/evanw/esbuild/issues/550)) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 7857287cf8a..651bec2717b 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -5502,6 +5502,10 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt { // "in" expressions aren't allowed here p.allowIn = false + var badLetRange logger.Range + if p.lexer.IsContextualKeyword("let") { + badLetRange = p.lexer.Range() + } decls := []js_ast.Decl{} initLoc := p.lexer.Loc() isVar := false @@ -5525,6 +5529,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt { var stmt js_ast.Stmt expr, stmt, decls = p.parseExprOrLetStmt(parseStmtOpts{lexicalDecl: lexicalDeclAllowAll}) if stmt.Data != nil { + badLetRange = logger.Range{} init = &stmt } else { init = &js_ast.Stmt{Loc: initLoc, Data: &js_ast.SExpr{Value: expr}} @@ -5536,6 +5541,9 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt { // Detect for-of loops if p.lexer.IsContextualKeyword("of") || isForAwait { + if badLetRange.Len > 0 { + p.log.AddRangeError(&p.source, badLetRange, "\"let\" must be wrapped in parentheses to be used as an expression here") + } if isForAwait && !p.lexer.IsContextualKeyword("of") { if init != nil { p.lexer.ExpectedString("\"of\"") diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index b5dc1bc68f1..142354bfacd 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -536,6 +536,15 @@ func TestFor(t *testing.T) { expectParseError(t, "for (let a, b of b) ;", ": error: for-of loops must have a single declaration\n") expectParseError(t, "for (const a, b of b) ;", ": error: for-of loops must have a single declaration\n") + // Avoid the initializer starting with "let" token + expectPrinted(t, "for ((let) of bar);", "for ((let) of bar)\n ;\n") + expectPrinted(t, "for ((let).foo of bar);", "for ((let).foo of bar)\n ;\n") + expectPrinted(t, "for ((let.foo) of bar);", "for ((let).foo of bar)\n ;\n") + expectPrinted(t, "for ((let``.foo) of bar);", "for ((let)``.foo of bar)\n ;\n") + expectParseError(t, "for (let.foo of bar);", ": error: \"let\" must be wrapped in parentheses to be used as an expression here\n") + expectParseError(t, "for (let().foo of bar);", ": error: \"let\" must be wrapped in parentheses to be used as an expression here\n") + expectParseError(t, "for (let``.foo of bar);", ": error: \"let\" must be wrapped in parentheses to be used as an expression here\n") + expectPrinted(t, "for (var x = 0 in y) ;", "x = 0;\nfor (var x in y)\n ;\n") // This is a weird special-case expectParseError(t, "for (let x = 0 in y) ;", ": error: for-in loop variables cannot have an initializer\n") expectParseError(t, "for (const x = 0 in y) ;", ": error: for-in loop variables cannot have an initializer\n") diff --git a/internal/js_printer/js_printer.go b/internal/js_printer/js_printer.go index e7c67ef8bde..83e25f0704c 100644 --- a/internal/js_printer/js_printer.go +++ b/internal/js_printer/js_printer.go @@ -464,6 +464,7 @@ type printer struct { stmtStart int exportDefaultStart int arrowExprStart int + forOfInitStart int prevOp js_ast.OpCode prevOpEnd int prevNumEnd int @@ -1904,8 +1905,19 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags int) { } case *js_ast.EIdentifier: + name := p.renamer.NameForSymbol(e.Ref) + wrap := len(p.js) == p.forOfInitStart && name == "let" + + if wrap { + p.print("(") + } + p.printSpaceBeforeIdentifier() - p.printSymbol(e.Ref) + p.printIdentifier(name) + + if wrap { + p.print(")") + } case *js_ast.EImportIdentifier: // Potentially use a property access instead of an identifier @@ -2711,6 +2723,7 @@ func (p *printer) printStmt(stmt js_ast.Stmt) { } p.printSpace() p.print("(") + p.forOfInitStart = len(p.js) p.printForLoopInit(s.Init) p.printSpace() p.printSpaceBeforeIdentifier() @@ -3057,6 +3070,7 @@ func Print(tree js_ast.AST, symbols js_ast.SymbolMap, r renamer.Renamer, options stmtStart: -1, exportDefaultStart: -1, arrowExprStart: -1, + forOfInitStart: -1, prevOpEnd: -1, prevNumEnd: -1, prevRegExpEnd: -1,