diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e606326c0b..8e0e1ee68d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +* Transform object rest properties + + This release transforms object rest property bindings such as `let {...x} = y` when the language target is set to `--target=es2017` or earlier. + + If you're using Babel to transform your source code to ES6 for older browsers, this probably means esbuild's JavaScript API could now be a suitable replacement for Babel in your case. The only remaining features that esbuild can't yet transform to ES6 are a few very rarely used features that don't matter for the vast majority of real-world code (`for async` loops and `async` generators). + ## 0.5.9 * Add the `--strict:nullish-coalescing` option diff --git a/README.md b/README.md index b6d2bd9c571..f4afbd12397 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,9 @@ These syntax features are conditionally transformed for older browsers depending | Syntax transform | Transformed when `--target` is below | Example | |-------------------------------------------------------------------------------------|--------------------------------------|----------------------------| | [Exponentiation operator](https://github.com/tc39/proposal-exponentiation-operator) | `es2016` | `a ** b` | -| [Async functions](https://github.com/tc39/ecmascript-asyncawait) | `es2017` | `async () => {}` | +| [Async functions](https://github.com/tc39/ecmascript-asyncawait) | `es2017` | `async () => {}` | | [Spread properties](https://github.com/tc39/proposal-object-rest-spread) | `es2018` | `let x = {...y}` | +| [Rest properties](https://github.com/tc39/proposal-object-rest-spread) | `es2018` | `let {...x} = y` | | [Optional catch binding](https://github.com/tc39/proposal-optional-catch-binding) | `es2019` | `try {} catch {}` | | [Optional chaining](https://github.com/tc39/proposal-optional-chaining) | `es2020` | `a?.b` | | [Nullish coalescing](https://github.com/tc39/proposal-nullish-coalescing) | `es2020` | `a ?? b` | @@ -168,7 +169,6 @@ These syntax features are currently always passed through un-transformed: | Syntax transform | Unsupported when `--target` is below | Example | |-------------------------------------------------------------------------------------|--------------------------------------|-----------------------------| -| [Rest properties](https://github.com/tc39/proposal-object-rest-spread) | `es2018` | `let {...x} = y` | | [Asynchronous iteration](https://github.com/tc39/proposal-async-iteration) | `es2018` | `for await (let x of y) {}` | | [Async generators](https://github.com/tc39/proposal-async-iteration) | `es2018` | `async function* foo() {}` | | [BigInt](https://github.com/tc39/proposal-bigint) | `es2020` | `123n` | diff --git a/internal/bundler/bundler_lower_test.go b/internal/bundler/bundler_lower_test.go index 87c65b90a83..3d4fc5534d1 100644 --- a/internal/bundler/bundler_lower_test.go +++ b/internal/bundler/bundler_lower_test.go @@ -2181,3 +2181,222 @@ console.log(loose_default, strict_default); }, }) } + +func TestTSLowerObjectRest2017NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.ts": ` + const { ...local_const } = {}; + let { ...local_let } = {}; + var { ...local_var } = {}; + let arrow_fn = ({ ...x }) => { }; + let fn_expr = function ({ ...x } = default_value) {}; + let class_expr = class { method(x, ...[y, { ...z }]) {} }; + + function fn_stmt({ a = b(), ...x }, { c = d(), ...y }) {} + class class_stmt { method({ ...x }) {} } + namespace ns { export let { ...x } = {} } + try { } catch ({ ...catch_clause }) {} + + for (const { ...for_in_const } in { abc }) {} + for (let { ...for_in_let } in { abc }) {} + for (var { ...for_in_var } in { abc }) ; + for (const { ...for_of_const } of [{}]) ; + for (let { ...for_of_let } of [{}]) x() + for (var { ...for_of_var } of [{}]) x() + for (const { ...for_const } = {}; x; x = null) {} + for (let { ...for_let } = {}; x; x = null) {} + for (var { ...for_var } = {}; x; x = null) {} + for ({ ...x } in { abc }) {} + for ({ ...x } of [{}]) {} + for ({ ...x } = {}; x; x = null) {} + + ({ ...assign } = {}); + ({ obj_method({ ...x }) {} }); + `, + }, + entryPaths: []string{"/entry.ts"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2017, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _o, _p; +const local_const = __rest({}, []); +let local_let = __rest({}, []); +var local_var = __rest({}, []); +let arrow_fn = (_a) => { + var x2 = __rest(_a, []); +}; +let fn_expr = function(_b = default_value) { + var x2 = __rest(_b, []); +}; +let class_expr = class { + method(x2, ..._c) { + var [y, _d] = _c, z = __rest(_d, []); + } +}; +function fn_stmt(_e, _f) { + var {a = b()} = _e, x2 = __rest(_e, ["a"]); + var {c = d()} = _f, y = __rest(_f, ["c"]); +} +class class_stmt { + method(_g) { + var x2 = __rest(_g, []); + } +} +var ns; +(function(ns2) { + ns2.x = __rest({}, []); +})(ns || (ns = {})); +try { +} catch (_h) { + let catch_clause = __rest(_h, []); +} +for (const _i in {abc}) { + const for_in_const = __rest(_i, []); +} +for (let _j in {abc}) { + let for_in_let = __rest(_j, []); +} +for (var _k in {abc}) { + var for_in_var = __rest(_k, []); + ; +} +for (const _l of [{}]) { + const for_of_const = __rest(_l, []); + ; +} +for (let _m of [{}]) { + let for_of_let = __rest(_m, []); + x(); +} +for (var _n of [{}]) { + var for_of_var = __rest(_n, []); + x(); +} +for (const for_const = __rest({}, []); x; x = null) { +} +for (let for_let = __rest({}, []); x; x = null) { +} +for (var for_var = __rest({}, []); x; x = null) { +} +for (_o in {abc}) { + x = __rest(_o, []); +} +for (_p of [{}]) { + x = __rest(_p, []); +} +for (x = __rest({}, []); x; x = null) { +} +assign = __rest({}, []); +({obj_method(_q) { + var x2 = __rest(_q, []); +}}); +`, + }, + }) +} + +func TestTSLowerObjectRest2018NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.ts": ` + const { ...local_const } = {}; + let { ...local_let } = {}; + var { ...local_var } = {}; + let arrow_fn = ({ ...x }) => { }; + let fn_expr = function ({ ...x } = default_value) {}; + let class_expr = class { method(x, ...[y, { ...z }]) {} }; + + function fn_stmt({ a = b(), ...x }, { c = d(), ...y }) {} + class class_stmt { method({ ...x }) {} } + namespace ns { export let { ...x } = {} } + try { } catch ({ ...catch_clause }) {} + + for (const { ...for_in_const } in { abc }) {} + for (let { ...for_in_let } in { abc }) {} + for (var { ...for_in_var } in { abc }) ; + for (const { ...for_of_const } of [{}]) ; + for (let { ...for_of_let } of [{}]) x() + for (var { ...for_of_var } of [{}]) x() + for (const { ...for_const } = {}; x; x = null) {} + for (let { ...for_let } = {}; x; x = null) {} + for (var { ...for_var } = {}; x; x = null) {} + for ({ ...x } in { abc }) {} + for ({ ...x } of [{}]) {} + for ({ ...x } = {}; x; x = null) {} + + ({ ...assign } = {}); + ({ obj_method({ ...x }) {} }); + `, + }, + entryPaths: []string{"/entry.ts"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2018, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `const {...local_const} = {}; +let {...local_let} = {}; +var {...local_var} = {}; +let arrow_fn = ({...x2}) => { +}; +let fn_expr = function({...x2} = default_value) { +}; +let class_expr = class { + method(x2, ...[y, {...z}]) { + } +}; +function fn_stmt({a = b(), ...x2}, {c = d(), ...y}) { +} +class class_stmt { + method({...x2}) { + } +} +var ns; +(function(ns2) { + ({...ns2.x} = {}); +})(ns || (ns = {})); +try { +} catch ({...catch_clause}) { +} +for (const {...for_in_const} in {abc}) { +} +for (let {...for_in_let} in {abc}) { +} +for (var {...for_in_var} in {abc}) + ; +for (const {...for_of_const} of [{}]) + ; +for (let {...for_of_let} of [{}]) + x(); +for (var {...for_of_var} of [{}]) + x(); +for (const {...for_const} = {}; x; x = null) { +} +for (let {...for_let} = {}; x; x = null) { +} +for (var {...for_var} = {}; x; x = null) { +} +for ({...x} in {abc}) { +} +for ({...x} of [{}]) { +} +for ({...x} = {}; x; x = null) { +} +({...assign} = {}); +({obj_method({...x2}) { +}}); +`, + }, + }) +} diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 22b2c8edda4..962a5bbdb26 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -1762,7 +1762,6 @@ func (p *parser) parsePropertyBinding() ast.PropertyBinding { switch p.lexer.Token { case lexer.TDotDotDot: - p.markFutureSyntax(futureSyntaxRestProperty, p.lexer.Range()) p.lexer.Next() value := ast.Binding{p.lexer.Loc(), &ast.BIdentifier{p.storeNameInRef(p.lexer.Identifier)}} p.lexer.Expect(lexer.TIdentifier) @@ -2199,7 +2198,10 @@ func (p *parser) convertBindingToExpr(binding ast.Binding, wrapIdentifier func(a return ast.Expr{loc, &ast.EMissing{}} case *ast.BIdentifier: - return wrapIdentifier(loc, b.Ref) + if wrapIdentifier != nil { + return wrapIdentifier(loc, b.Ref) + } + return ast.Expr{loc, &ast.EIdentifier{b.Ref}} case *ast.BArray: exprs := make([]ast.Expr, len(b.Items)) @@ -4950,11 +4952,11 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { p.lexer.Next() // "for await (let x of y) {}" - isAwait := p.lexer.IsContextualKeyword("await") - if isAwait { + isForAwait := p.lexer.IsContextualKeyword("await") + if isForAwait { if !p.currentFnOpts.allowAwait { p.log.AddRangeError(&p.source, p.lexer.Range(), "Cannot use \"await\" outside an async function") - isAwait = false + isForAwait = false } else { p.markFutureSyntax(futureSyntaxForAwait, p.lexer.Range()) } @@ -5000,8 +5002,8 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { p.allowIn = true // Detect for-of loops - if p.lexer.IsContextualKeyword("of") || isAwait { - if isAwait && !p.lexer.IsContextualKeyword("of") { + if p.lexer.IsContextualKeyword("of") || isForAwait { + if isForAwait && !p.lexer.IsContextualKeyword("of") { if init != nil { p.lexer.ExpectedString("\"of\"") } else { @@ -5013,7 +5015,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { value := p.parseExpr(ast.LLowest) p.lexer.Expect(lexer.TCloseParen) body := p.parseStmt(parseStmtOpts{}) - return ast.Stmt{loc, &ast.SForOf{isAwait, *init, value, body}} + return ast.Stmt{loc, &ast.SForOf{isForAwait, *init, value, body}} } // Detect for-in loops @@ -6093,6 +6095,31 @@ func (p *parser) visitSingleStmt(stmt ast.Stmt) ast.Stmt { } } +func (p *parser) visitForLoopInit(stmt ast.Stmt, isInOrOf bool) ast.Stmt { + switch s := stmt.Data.(type) { + case *ast.SExpr: + assignTarget := ast.AssignTargetNone + if isInOrOf { + assignTarget = ast.AssignTargetReplace + } + s.Value, _ = p.visitExprInOut(s.Value, exprIn{assignTarget: assignTarget}) + + case *ast.SLocal: + for _, d := range s.Decls { + p.visitBinding(d.Binding) + if d.Value != nil { + *d.Value = p.visitExpr(*d.Value) + } + } + s.Decls = p.lowerObjectRestInDecls(s.Decls) + + default: + panic("Internal error") + } + + return stmt +} + func (p *parser) recordDeclaredSymbol(ref ast.Ref) { p.declaredSymbols = append(p.declaredSymbols, ast.DeclaredSymbol{ Ref: ref, @@ -6505,7 +6532,6 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt // Handle being exported inside a namespace if s.IsExport && p.enclosingNamespaceRef != nil { - var expr ast.Expr wrapIdentifier := func(loc ast.Loc, ref ast.Ref) ast.Expr { return ast.Expr{loc, &ast.EDot{ Target: ast.Expr{loc, &ast.EIdentifier{*p.enclosingNamespaceRef}}, @@ -6515,19 +6541,19 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt } for _, decl := range s.Decls { if decl.Value != nil { - expr = maybeJoinWithComma(expr, ast.Assign( - p.convertBindingToExpr(decl.Binding, wrapIdentifier), - *decl.Value, - )) + target := p.convertBindingToExpr(decl.Binding, wrapIdentifier) + if result, ok := p.lowerObjectRestInAssign(target, *decl.Value); ok { + target = result + } else { + target = ast.Assign(target, *decl.Value) + } + stmts = append(stmts, ast.Stmt{stmt.Loc, &ast.SExpr{target}}) } } - if expr.Data != nil { - stmts = append(stmts, ast.Stmt{stmt.Loc, &ast.SExpr{expr}}) - } return stmts } - s.Decls = p.lowerBindingsInDecls(s.Decls) + s.Decls = p.lowerObjectRestInDecls(s.Decls) case *ast.SExpr: s.Value = p.visitExpr(s.Value) @@ -6633,7 +6659,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt case *ast.SFor: p.pushScopeForVisitPass(ast.ScopeBlock, stmt.Loc) if s.Init != nil { - p.visitSingleStmt(*s.Init) + p.visitForLoopInit(*s.Init, false) } if s.Test != nil { @@ -6655,17 +6681,19 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt case *ast.SForIn: p.pushScopeForVisitPass(ast.ScopeBlock, stmt.Loc) - p.visitSingleStmt(s.Init) + p.visitForLoopInit(s.Init, true) s.Value = p.visitExpr(s.Value) s.Body = p.visitSingleStmt(s.Body) p.popScope() + p.lowerObjectRestInForLoopInit(s.Init, &s.Body) case *ast.SForOf: p.pushScopeForVisitPass(ast.ScopeBlock, stmt.Loc) - p.visitSingleStmt(s.Init) + p.visitForLoopInit(s.Init, true) s.Value = p.visitExpr(s.Value) s.Body = p.visitSingleStmt(s.Body) p.popScope() + p.lowerObjectRestInForLoopInit(s.Init, &s.Body) case *ast.STry: p.pushScopeForVisitPass(ast.ScopeBlock, stmt.Loc) @@ -6680,6 +6708,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt p.visitBinding(*s.Catch.Binding) } s.Catch.Body = p.visitStmts(s.Catch.Body) + p.lowerObjectRestInCatchBinding(s.Catch) p.popScope() } @@ -7747,6 +7776,16 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { return p.lowerPrivateSet(target, loc, private, e.Right), exprOut{} } + // Lower object rest patterns for browsers that don't support them. Note + // that assignment expressions are used to represent initializers in + // binding patterns, so only do this if we're not ourselves the target of + // an assignment. Example: "[a = b] = c" + if in.assignTarget == ast.AssignTargetNone { + if result, ok := p.lowerObjectRestInAssign(e.Left, e.Right); ok { + return result, exprOut{} + } + } + case ast.BinOpAddAssign: if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpAdd, e.Right), exprOut{} @@ -8064,7 +8103,12 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { *property.Initializer = p.visitExpr(*property.Initializer) } } - return p.lowerObjectSpread(expr.Loc, e), exprOut{} + + // Object expressions represent both object literals and binding patterns. + // Only lower object spread if we're an object literal, not a binding pattern. + if in.assignTarget == ast.AssignTargetNone { + return p.lowerObjectSpread(expr.Loc, e), exprOut{} + } case *ast.EImport: e.Expr = p.visitExpr(e.Expr) @@ -8224,8 +8268,8 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { p.pushScopeForVisitPass(ast.ScopeFunctionBody, e.Body.Loc) e.Body.Stmts = p.visitStmtsAndPrependTempRefs(e.Body.Stmts) p.popScope() + p.lowerFunction(&e.IsAsync, &e.Args, e.Body.Loc, &e.Body.Stmts, &e.PreferExpr) p.popScope() - p.lowerAsyncFunction(e.Body.Loc, &e.IsAsync, &e.Body.Stmts, &e.PreferExpr) if p.MangleSyntax && len(e.Body.Stmts) == 1 { if s, ok := e.Body.Stmts[0].Data.(*ast.SReturn); ok { @@ -8337,8 +8381,8 @@ func (p *parser) visitFn(fn *ast.Fn, scopeLoc ast.Loc) { p.pushScopeForVisitPass(ast.ScopeFunctionBody, fn.Body.Loc) fn.Body.Stmts = p.visitStmtsAndPrependTempRefs(fn.Body.Stmts) p.popScope() + p.lowerFunction(&fn.IsAsync, &fn.Args, fn.Body.Loc, &fn.Body.Stmts, nil) p.popScope() - p.lowerAsyncFunction(fn.Body.Loc, &fn.IsAsync, &fn.Body.Stmts, nil) p.tryBodyCount = oldTryBodyCount p.isThisCaptured = oldIsThisCaptured diff --git a/internal/parser/parser_lower.go b/internal/parser/parser_lower.go index 522acafb7f4..efc6d84e13b 100644 --- a/internal/parser/parser_lower.go +++ b/internal/parser/parser_lower.go @@ -20,7 +20,6 @@ type futureSyntax uint8 const ( futureSyntaxAsyncGenerator futureSyntax = iota - futureSyntaxRestProperty futureSyntaxForAwait futureSyntaxBigInteger futureSyntaxNonIdentifierArrayRest @@ -32,8 +31,6 @@ func (p *parser) markFutureSyntax(syntax futureSyntax, r ast.Range) { switch syntax { case futureSyntaxAsyncGenerator: target = ES2018 - case futureSyntaxRestProperty: - target = ES2018 case futureSyntaxForAwait: target = ES2018 case futureSyntaxBigInteger: @@ -49,8 +46,6 @@ func (p *parser) markFutureSyntax(syntax futureSyntax, r ast.Range) { switch syntax { case futureSyntaxAsyncGenerator: name = "Async generator functions" - case futureSyntaxRestProperty: - name = "Rest properties" case futureSyntaxForAwait: name = "For-await loops" case futureSyntaxBigInteger: @@ -66,77 +61,102 @@ func (p *parser) markFutureSyntax(syntax futureSyntax, r ast.Range) { } } -func (p *parser) lowerAsyncFunction(loc ast.Loc, isAsync *bool, stmts *[]ast.Stmt, preferExpr *bool) { - // Only lower this function if necessary - if p.Target >= asyncAwaitTarget || !*isAsync { - return - } - - // Use the shortened form if we're an arrow function - if preferExpr != nil { - *preferExpr = true - } - - // Determine the value for "this" - thisValue, hasThisValue := p.valueForThis(loc) - if !hasThisValue { - thisValue = ast.Expr{loc, &ast.EThis{}} - } - - // Only reference the "arguments" variable if it's actually used - var arguments ast.Expr - if p.argumentsRef != nil && p.useCountEstimates[*p.argumentsRef] > 0 { - arguments = ast.Expr{loc, &ast.EIdentifier{*p.argumentsRef}} - } else { - arguments = ast.Expr{loc, &ast.EArray{}} - } - - // "async function foo() { stmts }" => "function foo() { return __async(this, arguments, function* () { stmts }) }" - *isAsync = false - callAsync := p.callRuntime(loc, "__async", []ast.Expr{ - thisValue, - arguments, - ast.Expr{loc, &ast.EFunction{Fn: ast.Fn{ - IsGenerator: true, - Body: ast.FnBody{loc, *stmts}, - }}}, - }) - *stmts = []ast.Stmt{ast.Stmt{loc, &ast.SReturn{&callAsync}}} -} +func (p *parser) lowerFunction( + isAsync *bool, + args *[]ast.Arg, + bodyLoc ast.Loc, + bodyStmts *[]ast.Stmt, + preferExpr *bool, +) { + // Lower object rest binding patterns in function arguments + if p.Target < objectPropertyBindingTarget { + var prefixStmts []ast.Stmt + + // Lower each argument individually instead of lowering all arguments + // together. There is a correctness tradeoff here around default values + // for function arguments, with no right answer. + // + // Lowering all arguments together will preserve the order of side effects + // for default values, but will mess up their scope: + // + // // Side effect order: a(), b(), c() + // function foo([{[a()]: w, ...x}, y = b()], z = c()) {} + // + // // Side effect order is correct but scope is wrong + // function foo(_a, _b) { + // var [[{[a()]: w, ...x}, y = b()], z = c()] = [_a, _b] + // } + // + // Lowering each argument individually will preserve the scope for default + // values that don't contain object rest binding patterns, but will mess up + // the side effect order: + // + // // Side effect order: a(), b(), c() + // function foo([{[a()]: w, ...x}, y = b()], z = c()) {} + // + // // Side effect order is wrong but scope for c() is correct + // function foo(_a, z = c()) { + // var [{[a()]: w, ...x}, y = b()] = _a + // } + // + // This transform chooses to lower each argument individually with the + // thinking that perhaps scope matters more in real-world code than side + // effect order. + for i, arg := range *args { + if bindingHasObjectRest(arg.Binding) { + ref := p.generateTempRef(tempRefNoDeclare, "") + target := p.convertBindingToExpr(arg.Binding, nil) + init := ast.Expr{arg.Binding.Loc, &ast.EIdentifier{ref}} + + if decls, ok := p.lowerObjectRestToDecls(target, init, nil); ok { + // Replace the binding but leave the default value intact + (*args)[i].Binding.Data = &ast.BIdentifier{ref} + + // Append a variable declaration to the function body + prefixStmts = append(prefixStmts, ast.Stmt{arg.Binding.Loc, + &ast.SLocal{Kind: ast.LocalVar, Decls: decls}}) + } + } + } -func (p *parser) lowerBindingsInDecls(decls []ast.Decl) []ast.Decl { - if p.Target >= objectPropertyBindingTarget { - return decls + if len(prefixStmts) > 0 { + *bodyStmts = append(prefixStmts, *bodyStmts...) + } } - var visit func(ast.Binding) - visit = func(binding ast.Binding) { - switch b := binding.Data.(type) { - case *ast.BMissing: - - case *ast.BIdentifier: - - case *ast.BArray: - for _, item := range b.Items { - visit(item.Binding) - } + // Lower async functions + if p.Target < asyncAwaitTarget && *isAsync { + // Use the shortened form if we're an arrow function + if preferExpr != nil { + *preferExpr = true + } - case *ast.BObject: - for _, property := range b.Properties { - // property.IsSpread - visit(property.Value) - } + // Determine the value for "this" + thisValue, hasThisValue := p.valueForThis(bodyLoc) + if !hasThisValue { + thisValue = ast.Expr{bodyLoc, &ast.EThis{}} + } - default: - panic("Internal error") + // Only reference the "arguments" variable if it's actually used + var arguments ast.Expr + if p.argumentsRef != nil && p.useCountEstimates[*p.argumentsRef] > 0 { + arguments = ast.Expr{bodyLoc, &ast.EIdentifier{*p.argumentsRef}} + } else { + arguments = ast.Expr{bodyLoc, &ast.EArray{}} } - } - for _, decl := range decls { - visit(decl.Binding) + // "async function foo() { stmts }" => "function foo() { return __async(this, arguments, function* () { stmts }) }" + *isAsync = false + callAsync := p.callRuntime(bodyLoc, "__async", []ast.Expr{ + thisValue, + arguments, + ast.Expr{bodyLoc, &ast.EFunction{Fn: ast.Fn{ + IsGenerator: true, + Body: ast.FnBody{bodyLoc, *bodyStmts}, + }}}, + }) + *bodyStmts = []ast.Stmt{ast.Stmt{bodyLoc, &ast.SReturn{&callAsync}}} } - - return decls } func (p *parser) lowerOptionalChain(expr ast.Expr, in exprIn, out exprOut, thisArgFunc func() ast.Expr) (ast.Expr, exprOut) { @@ -741,6 +761,471 @@ func (p *parser) extractPrivateIndex(target ast.Expr) (ast.Expr, ast.Loc, *ast.E return ast.Expr{}, ast.Loc{}, nil } +func bindingHasObjectRest(binding ast.Binding) bool { + switch b := binding.Data.(type) { + case *ast.BArray: + for _, item := range b.Items { + if bindingHasObjectRest(item.Binding) { + return true + } + } + case *ast.BObject: + for _, property := range b.Properties { + if property.IsSpread || bindingHasObjectRest(property.Value) { + return true + } + } + } + return false +} + +func exprHasObjectRest(expr ast.Expr) bool { + switch e := expr.Data.(type) { + case *ast.EBinary: + if e.Op == ast.BinOpAssign && exprHasObjectRest(e.Left) { + return true + } + case *ast.EArray: + for _, item := range e.Items { + if exprHasObjectRest(item) { + return true + } + } + case *ast.EObject: + for _, property := range e.Properties { + if property.Kind == ast.PropertySpread || exprHasObjectRest(*property.Value) { + return true + } + } + } + return false +} + +func (p *parser) lowerObjectRestInDecls(decls []ast.Decl) []ast.Decl { + if p.Target >= objectPropertyBindingTarget { + return decls + } + + // Don't do any allocations if there are no object rest patterns. We want as + // little overhead as possible in the common case. + for i, decl := range decls { + if decl.Value != nil && bindingHasObjectRest(decl.Binding) { + clone := append([]ast.Decl{}, decls[:i]...) + for _, decl := range decls[i:] { + if decl.Value != nil { + target := p.convertBindingToExpr(decl.Binding, nil) + if result, ok := p.lowerObjectRestToDecls(target, *decl.Value, clone); ok { + clone = result + continue + } + } + clone = append(clone, decl) + } + + return clone + } + } + + return decls +} + +func (p *parser) lowerObjectRestInForLoopInit(init ast.Stmt, body *ast.Stmt) { + if p.Target >= objectPropertyBindingTarget { + return + } + + var bodyPrefixStmt ast.Stmt + + switch s := init.Data.(type) { + case *ast.SExpr: + // "for ({...x} in y) {}" + // "for ({...x} of y) {}" + if exprHasObjectRest(s.Value) { + ref := p.generateTempRef(tempRefNeedsDeclare, "") + if expr, ok := p.lowerObjectRestInAssign(s.Value, ast.Expr{init.Loc, &ast.EIdentifier{ref}}); ok { + s.Value.Data = &ast.EIdentifier{ref} + bodyPrefixStmt = ast.Stmt{expr.Loc, &ast.SExpr{expr}} + } + } + + case *ast.SLocal: + // "for (let {...x} in y) {}" + // "for (let {...x} of y) {}" + if len(s.Decls) == 1 && bindingHasObjectRest(s.Decls[0].Binding) { + ref := p.generateTempRef(tempRefNoDeclare, "") + decl := ast.Decl{s.Decls[0].Binding, &ast.Expr{init.Loc, &ast.EIdentifier{ref}}} + decls := p.lowerObjectRestInDecls([]ast.Decl{decl}) + s.Decls[0].Binding.Data = &ast.BIdentifier{ref} + bodyPrefixStmt = ast.Stmt{init.Loc, &ast.SLocal{Kind: s.Kind, Decls: decls}} + } + } + + if bodyPrefixStmt.Data != nil { + if block, ok := body.Data.(*ast.SBlock); ok { + // If there's already a block, insert at the front + stmts := make([]ast.Stmt, 0, 1+len(block.Stmts)) + block.Stmts = append(append(stmts, bodyPrefixStmt), block.Stmts...) + } else { + // Otherwise, make a block and insert at the front + body.Data = &ast.SBlock{[]ast.Stmt{bodyPrefixStmt, *body}} + } + } +} + +func (p *parser) lowerObjectRestInCatchBinding(catch *ast.Catch) { + if p.Target >= objectPropertyBindingTarget { + return + } + + if catch.Binding != nil && bindingHasObjectRest(*catch.Binding) { + ref := p.generateTempRef(tempRefNoDeclare, "") + decl := ast.Decl{*catch.Binding, &ast.Expr{catch.Binding.Loc, &ast.EIdentifier{ref}}} + decls := p.lowerObjectRestInDecls([]ast.Decl{decl}) + catch.Binding.Data = &ast.BIdentifier{ref} + stmts := make([]ast.Stmt, 0, 1+len(catch.Body)) + stmts = append(stmts, ast.Stmt{catch.Binding.Loc, &ast.SLocal{Kind: ast.LocalLet, Decls: decls}}) + catch.Body = append(stmts, catch.Body...) + } +} + +func (p *parser) lowerObjectRestInAssign(rootExpr ast.Expr, rootInit ast.Expr) (ast.Expr, bool) { + var expr ast.Expr + + assign := func(left ast.Expr, right ast.Expr) { + expr = maybeJoinWithComma(expr, ast.Assign(left, right)) + } + + if p.lowerObjectRestHelper(rootExpr, rootInit, assign, tempRefNeedsDeclare) { + return expr, true + } + + return ast.Expr{}, false +} + +func (p *parser) lowerObjectRestToDecls(rootExpr ast.Expr, rootInit ast.Expr, decls []ast.Decl) ([]ast.Decl, bool) { + assign := func(left ast.Expr, right ast.Expr) { + binding, log := p.convertExprToBinding(left, nil) + if len(log) > 0 { + panic("Internal error") + } + decls = append(decls, ast.Decl{binding, &right}) + } + + if p.lowerObjectRestHelper(rootExpr, rootInit, assign, tempRefNoDeclare) { + return decls, true + } + + return nil, false +} + +func (p *parser) lowerObjectRestHelper( + rootExpr ast.Expr, + rootInit ast.Expr, + assign func(ast.Expr, ast.Expr), + declare generateTempRefArg, +) bool { + if p.Target >= objectPropertyBindingTarget { + return false + } + + // Check if this could possibly contain an object rest binding + switch rootExpr.Data.(type) { + case *ast.EArray, *ast.EObject: + default: + return false + } + + // Scan for object rest bindings and initalize rest binding containment + containsRestBinding := make(map[ast.E]bool) + var findRestBindings func(ast.Expr) bool + findRestBindings = func(expr ast.Expr) bool { + found := false + switch e := expr.Data.(type) { + case *ast.EBinary: + if e.Op == ast.BinOpAssign && findRestBindings(e.Left) { + found = true + } + case *ast.EArray: + for _, item := range e.Items { + if findRestBindings(item) { + found = true + } + } + case *ast.EObject: + for _, property := range e.Properties { + if property.Kind == ast.PropertySpread || findRestBindings(*property.Value) { + found = true + } + } + } + if found { + containsRestBinding[expr.Data] = true + } + return found + } + findRestBindings(rootExpr) + if len(containsRestBinding) == 0 { + return false + } + + // If there is at least one rest binding, lower the whole expression + var visit func(ast.Expr, ast.Expr, []func() ast.Expr) + + captureIntoRef := func(expr ast.Expr) ast.Ref { + if id, ok := expr.Data.(*ast.EIdentifier); ok { + return id.Ref + } + + // If the initializer isn't already a bare identifier that we can + // reference, store the initializer first so we can reference it later. + // The initializer may have side effects so we must evaluate it once. + ref := p.generateTempRef(declare, "") + assign(ast.Expr{expr.Loc, &ast.EIdentifier{ref}}, expr) + return ref + } + + lowerObjectRestPattern := func( + before []ast.Property, + binding ast.Expr, + init ast.Expr, + capturedKeys []func() ast.Expr, + isSingleLine bool, + ) { + // If there are properties before this one, store the initializer in a + // temporary so we can reference it multiple times, then create a new + // destructuring assignment for these properties + if len(before) > 0 { + // "let {a, ...b} = c" + ref := captureIntoRef(init) + assign(ast.Expr{before[0].Key.Loc, &ast.EObject{Properties: before, IsSingleLine: isSingleLine}}, + ast.Expr{init.Loc, &ast.EIdentifier{ref}}) + init = ast.Expr{init.Loc, &ast.EIdentifier{ref}} + } + + // Call "__rest" to clone the initializer without the keys for previous + // properties, then assign the result to the binding for the rest pattern + keysToExclude := make([]ast.Expr, len(capturedKeys)) + for i, capturedKey := range capturedKeys { + keysToExclude[i] = capturedKey() + } + assign(binding, p.callRuntime(binding.Loc, "__rest", []ast.Expr{init, + ast.Expr{binding.Loc, &ast.EArray{Items: keysToExclude, IsSingleLine: isSingleLine}}})) + } + + splitArrayPattern := func( + before []ast.Expr, + split ast.Expr, + after []ast.Expr, + init ast.Expr, + isSingleLine bool, + ) { + // If this has a default value, skip the value to target the binding + binding := &split + if binary, ok := binding.Data.(*ast.EBinary); ok && binary.Op == ast.BinOpAssign { + binding = &binary.Left + } + + // Swap the binding with a temporary + splitRef := p.generateTempRef(declare, "") + deferredBinding := *binding + binding.Data = &ast.EIdentifier{splitRef} + items := append(before, split) + + // If there are any items left over, defer them until later too + var tailExpr ast.Expr + var tailInit ast.Expr + if len(after) > 0 { + tailRef := p.generateTempRef(declare, "") + loc := after[0].Loc + tailExpr = ast.Expr{loc, &ast.EArray{Items: after, IsSingleLine: isSingleLine}} + tailInit = ast.Expr{loc, &ast.EIdentifier{tailRef}} + items = append(items, ast.Expr{loc, &ast.ESpread{ast.Expr{loc, &ast.EIdentifier{tailRef}}}}) + } + + // The original destructuring assignment must come first + assign(ast.Expr{split.Loc, &ast.EArray{Items: items, IsSingleLine: isSingleLine}}, init) + + // Then the deferred split is evaluated + visit(deferredBinding, ast.Expr{split.Loc, &ast.EIdentifier{splitRef}}, nil) + + // Then anything after the split + if len(after) > 0 { + visit(tailExpr, tailInit, nil) + } + } + + splitObjectPattern := func( + upToSplit []ast.Property, + afterSplit []ast.Property, + init ast.Expr, + capturedKeys []func() ast.Expr, + isSingleLine bool, + ) { + // If there are properties after the split, store the initializer in a + // temporary so we can reference it multiple times + var afterSplitInit ast.Expr + if len(afterSplit) > 0 { + ref := captureIntoRef(init) + init = ast.Expr{init.Loc, &ast.EIdentifier{ref}} + afterSplitInit = ast.Expr{init.Loc, &ast.EIdentifier{ref}} + } + + split := &upToSplit[len(upToSplit)-1] + binding := split.Value + + // If this has a default value, skip the value to target the binding + if binary, ok := binding.Data.(*ast.EBinary); ok && binary.Op == ast.BinOpAssign { + binding = &binary.Left + } + + // Swap the binding with a temporary + splitRef := p.generateTempRef(declare, "") + deferredBinding := *binding + binding.Data = &ast.EIdentifier{splitRef} + + // Use a destructuring assignment to unpack everything up to and including + // the split point + assign(ast.Expr{binding.Loc, &ast.EObject{Properties: upToSplit, IsSingleLine: isSingleLine}}, init) + + // Handle any nested rest binding patterns inside the split point + visit(deferredBinding, ast.Expr{binding.Loc, &ast.EIdentifier{splitRef}}, nil) + + // Then continue on to any properties after the split + if len(afterSplit) > 0 { + visit(ast.Expr{binding.Loc, &ast.EObject{ + Properties: afterSplit, + IsSingleLine: isSingleLine, + }}, afterSplitInit, capturedKeys) + } + } + + // This takes an expression representing a binding pattern as input and + // returns that binding pattern with any object rest patterns stripped out. + // The object rest patterns are lowered and appended to "exprChain" along + // with any child binding patterns that came after the binding pattern + // containing the object rest pattern. + // + // This transform must be very careful to preserve the exact evaluation + // order of all assignments, default values, and computed property keys. + // + // Unlike the Babel and TypeScript compilers, this transform does not + // lower binding patterns other than object rest patterns. For example, + // array spread patterns are preserved. + // + // Certain patterns such as "{a: {...a}, b: {...b}, ...c}" may need to be + // split multiple times. In this case the "capturedKeys" argument allows + // the visitor to pass on captured keys to the tail-recursive call that + // handles the properties after the split. + visit = func(expr ast.Expr, init ast.Expr, capturedKeys []func() ast.Expr) { + switch e := expr.Data.(type) { + case *ast.EArray: + // Split on the first binding with a nested rest binding pattern + for i, item := range e.Items { + // "let [a, {...b}, c] = d" + if containsRestBinding[item.Data] { + splitArrayPattern(e.Items[:i], item, append([]ast.Expr{}, e.Items[i+1:]...), init, e.IsSingleLine) + return + } + } + + case *ast.EObject: + last := len(e.Properties) - 1 + endsWithRestBinding := last >= 0 && e.Properties[last].Kind == ast.PropertySpread + + // Split on the first binding with a nested rest binding pattern + for i, _ := range e.Properties { + property := &e.Properties[i] + + // "let {a, ...b} = c" + if property.Kind == ast.PropertySpread { + lowerObjectRestPattern(e.Properties[:i], *property.Value, init, capturedKeys, e.IsSingleLine) + return + } + + // Save a copy of this key so the rest binding can exclude it + if endsWithRestBinding { + key, capturedKey := p.captureKeyForObjectRest(property.Key) + property.Key = key + capturedKeys = append(capturedKeys, capturedKey) + } + + // "let {a: {...b}, c} = d" + if containsRestBinding[property.Value.Data] { + splitObjectPattern(e.Properties[:i+1], e.Properties[i+1:], init, capturedKeys, e.IsSingleLine) + return + } + } + } + + assign(expr, init) + } + + visit(rootExpr, rootInit, nil) + return true +} + +// Returns "typeof ref === 'symbol' ? ref : ref + ''" +func symbolOrString(loc ast.Loc, ref ast.Ref) ast.Expr { + return ast.Expr{loc, &ast.EIf{ + Test: ast.Expr{loc, &ast.EBinary{ + Op: ast.BinOpStrictEq, + Left: ast.Expr{loc, &ast.EUnary{ + Op: ast.UnOpTypeof, + Value: ast.Expr{loc, &ast.EIdentifier{ref}}, + }}, + Right: ast.Expr{loc, &ast.EString{lexer.StringToUTF16("symbol")}}, + }}, + Yes: ast.Expr{loc, &ast.EIdentifier{ref}}, + No: ast.Expr{loc, &ast.EBinary{ + Op: ast.BinOpAdd, + Left: ast.Expr{loc, &ast.EIdentifier{ref}}, + Right: ast.Expr{loc, &ast.EString{}}, + }}, + }} +} + +// Save a copy of the key for the call to "__rest" later on. Certain +// expressions can be converted to keys more efficiently than others. +func (p *parser) captureKeyForObjectRest(originalKey ast.Expr) (finalKey ast.Expr, capturedKey func() ast.Expr) { + loc := originalKey.Loc + finalKey = originalKey + + switch k := originalKey.Data.(type) { + case *ast.EString: + capturedKey = func() ast.Expr { return ast.Expr{loc, &ast.EString{k.Value}} } + + case *ast.ENumber: + // Emit it as the number plus a string (i.e. call toString() on it). + // It's important to do it this way instead of trying to print the + // float as a string because Go's floating-point printer doesn't + // behave exactly the same as JavaScript and if they are different, + // the generated code will be wrong. + capturedKey = func() ast.Expr { + return ast.Expr{loc, &ast.EBinary{ + Op: ast.BinOpAdd, + Left: ast.Expr{loc, &ast.ENumber{k.Value}}, + Right: ast.Expr{loc, &ast.EString{}}, + }} + } + + case *ast.EIdentifier: + capturedKey = func() ast.Expr { + return p.callRuntime(loc, "__restKey", []ast.Expr{ast.Expr{loc, &ast.EIdentifier{k.Ref}}}) + } + + default: + // If it's an arbitrary expression, it probably has a side effect. + // Stash it in a temporary reference so we don't evaluate it twice. + tempRef := p.generateTempRef(tempRefNeedsDeclare, "") + finalKey = ast.Assign(ast.Expr{loc, &ast.EIdentifier{tempRef}}, originalKey) + capturedKey = func() ast.Expr { + return p.callRuntime(loc, "__restKey", []ast.Expr{ast.Expr{loc, &ast.EIdentifier{tempRef}}}) + } + } + + return +} + // Lower class fields for environments that don't support them. This either // takes a statement or an expression. func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) { diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 6902b337901..08c9c2f2c3d 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -3,12 +3,27 @@ package runtime const Code = ` let __defineProperty = Object.defineProperty let __hasOwnProperty = Object.prototype.hasOwnProperty + let __getOwnPropertySymbols = Object.getOwnPropertySymbols let __getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor let __propertyIsEnumerable = Object.prototype.propertyIsEnumerable export let __pow = Math.pow export let __assign = Object.assign + // For object rest patterns + export let __restKey = key => typeof key === 'symbol' ? key : key + '' + export let __rest = (source, exclude) => { + let target = {} + for (let prop in source) + if (__hasOwnProperty.call(source, prop) && exclude.indexOf(prop) < 0) + target[prop] = source[prop] + if (source != null && __getOwnPropertySymbols) + for (let prop of __getOwnPropertySymbols(source)) + if (exclude.indexOf(prop) < 0 && __propertyIsEnumerable.call(source, prop)) + target[prop] = source[prop] + return target + } + // Wraps a CommonJS closure and returns a require() function export let __commonJS = (callback, module) => () => { if (!module) { diff --git a/scripts/destructuring-fuzzer.js b/scripts/destructuring-fuzzer.js new file mode 100644 index 00000000000..79b80d9a24d --- /dev/null +++ b/scripts/destructuring-fuzzer.js @@ -0,0 +1,385 @@ +function generateTestCase(assign) { + let sideEffectCount = 0 + let patternCount = 0 + let limit = 10 + let depth = 0 + + function choice(n) { + return Math.random() * n | 0 + } + + function patternAndValue() { + patternCount++ + switch (choice(3)) { + case 0: return array() + case 1: return object() + case 2: return [assign(), choice(10), choice(10)] + } + } + + function sideEffect(result) { + return `s(${sideEffectCount++}${result ? `, ${result}` : ''})` + } + + function indent(open, items, close) { + let tab = ' ' + items = items.map(i => `\n${tab.repeat(depth + 1)}${i}`).join(',') + return `${open}${items}\n${tab.repeat(depth)}${close}` + } + + function id() { + return String.fromCharCode('a'.charCodeAt(0) + choice(3)) + } + + function array() { + let count = 1 + choice(2) + let pattern = [] + let value = [] + + depth++ + for (let i = 0; i < count; i++) { + if (patternCount > limit) break + let [pat, val, defVal] = patternAndValue() + switch (choice(3)) { + case 0: + pattern.push(pat) + value.push(val) + break + case 1: + pattern.push(`${pat} = ${sideEffect(defVal)}`) + value.push(val) + break + case 2: + pattern.push(`${pat} = ${sideEffect(defVal)}`) + value.push(defVal) + break + } + if (choice(10) < 8) value.push(val) + } + if (choice(2)) { + pattern.push(`...${assign()}`) + if (choice(10) < 8) value.push(choice(10)) + } + depth-- + + return [ + indent('[', pattern, ']'), + indent('[', value, ']'), + '[]', + ] + } + + function object() { + let count = 1 + choice(2) + let pattern = [] + let value = [] + let valKeys = new Set() + + depth++ + for (let i = 0; i < count; i++) { + if (patternCount > limit) break + let valKey = id() + if (valKeys.has(valKey)) continue + valKeys.add(valKey) + let patKey = choice() ? valKey : `[${sideEffect(`'${valKey}'`)}]` + let [pat, val, defVal] = patternAndValue() + switch (choice(3)) { + case 0: + pattern.push(`${patKey}: ${pat}`) + value.push(`${valKey}: ${val}`) + break + case 1: + pattern.push(`${patKey}: ${pat} = ${sideEffect(defVal)}`) + value.push(`${valKey}: ${val}`) + break + case 2: + pattern.push(`${patKey}: ${pat} = ${sideEffect(defVal)}`) + value.push(`${valKey}: ${defVal}`) + break + } + } + if (choice(2)) { + pattern.push(`...${assign()}`) + if (choice(10) < 8) value.push(`${id()}: ${choice(10)}`) + } + depth-- + + return [ + indent('{', pattern, '}'), + indent('{', value, '}'), + '{}', + ] + } + + return choice(2) ? array() : object() +} + +function evaluate(code) { + let effectTrace = [] + let assignTarget = {} + let sideEffect = (id, value) => (effectTrace.push(id), value) + new Function('a', 's', code)(assignTarget, sideEffect) + return JSON.stringify({ assignTarget, effectTrace }) +} + +function generateTestCases(trials) { + let testCases = [] + + while (testCases.length < trials) { + let ids = [] + let assignCount = 0 + let [pattern, value] = generateTestCase(() => { + let id = `_${assignCount++}` + ids.push(id) + return id + }) + try { + evaluate(`(${pattern.replace(/_/g, 'a._')} = ${value});`) + testCases.push([pattern, value, ids]) + } catch (e) { + } + } + + return testCases +} + +function AssignmentOperator([pattern, value]) { + let ts = `(${pattern.replace(/_/g, 'a._')} = ${value});` + let js = ts + return { js, ts } +} + +function NamespaceExport([pattern, value]) { + let ts = `namespace a { export var ${`${pattern} = ${value}`} }` + let js = `(${pattern.replace(/_/g, 'a._')} = ${value});` + return { js, ts } +} + +function ConstDeclaration([pattern, value, ids]) { + let ts = `const ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}` + let js = ts + return { js, ts } +} + +function LetDeclaration([pattern, value, ids]) { + let ts = `let ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}` + let js = ts + return { js, ts } +} + +function VarDeclaration([pattern, value, ids]) { + let ts = `var ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}` + let js = ts + return { js, ts } +} + +function TryCatchBinding([pattern, value, ids]) { + let ts = `try { throw ${value} } catch (${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` + let js = ts + return { js, ts } +} + +function FunctionStatementArguments([pattern, value, ids]) { + let ts = `function foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }\nfoo(${value});` + let js = ts + return { js, ts } +} + +function FunctionExpressionArguments([pattern, value, ids]) { + let ts = `(function(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} })(${value});` + let js = ts + return { js, ts } +} + +function ArrowFunctionArguments([pattern, value, ids]) { + let ts = `((${pattern}) => { ${ids.map(id => `a.${id} = ${id};`).join('\n')} })(${value});` + let js = ts + return { js, ts } +} + +function ObjectMethodArguments([pattern, value, ids]) { + let ts = `({ foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } }).foo(${value});` + let js = ts + return { js, ts } +} + +function ClassStatementMethodArguments([pattern, value, ids]) { + let ts = `class Foo { foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } }\nnew Foo().foo(${value});` + let js = ts + return { js, ts } +} + +function ClassExpressionMethodArguments([pattern, value, ids]) { + let ts = `(new (class { foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } })).foo(${value});` + let js = ts + return { js, ts } +} + +function ForLoopConst([pattern, value, ids]) { + let ts = `var i; for (const ${pattern} = ${value}; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` + let js = ts + return { js, ts } +} + +function ForLoopLet([pattern, value, ids]) { + let ts = `for (let ${pattern} = ${value}, i = 0; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` + let js = ts + return { js, ts } +} + +function ForLoopVar([pattern, value, ids]) { + let ts = `for (var ${pattern} = ${value}, i = 0; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` + let js = ts + return { js, ts } +} + +function ForLoop([pattern, value]) { + let ts = `for (${pattern.replace(/_/g, 'a._')} = ${value}; 0; ) ;` + let js = ts + return { js, ts } +} + +function ForOfLoopConst([pattern, value, ids]) { + let ts = `for (const ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` + let js = ts + return { js, ts } +} + +function ForOfLoopLet([pattern, value, ids]) { + let ts = `for (let ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` + let js = ts + return { js, ts } +} + +function ForOfLoopVar([pattern, value, ids]) { + let ts = `for (var ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` + let js = ts + return { js, ts } +} + +function ForOfLoop([pattern, value]) { + let ts = `for (${pattern.replace(/_/g, 'a._')} of [${value}]) ;` + let js = ts + return { js, ts } +} + +async function verify(test, transform, testCases) { + let indent = t => t.replace(/\n/g, '\n ') + let newline = false + console.log(`${test.name} (${transform.name}):`) + + await concurrentMap(testCases, 20, async (testCase) => { + let { js, ts } = test(testCase) + let expected + try { + expected = evaluate(js) + } catch (e) { + return + } + + let transformed + let actual + try { + transformed = await transform(ts) + actual = evaluate(transformed) + } catch (e) { + actual = e + '' + } + + if (actual !== expected) { + process.stdout.write('X') + newline = true + + if (process.argv.indexOf('--verbose') >= 0) { + console.log('\n' + '='.repeat(80)) + console.log(indent(`Original code:\n${ts}`)) + console.log(indent(`Transformed code:\n${transformed}`)) + console.log(indent(`Expected output:\n${expected}`)) + console.log(indent(`Actual output:\n${actual}`)) + newline = false + } + } else { + process.stdout.write('-') + newline = true + } + }) + + if (newline) process.stdout.write('\n') +} + +function concurrentMap(items, batch, callback) { + return new Promise((resolve, reject) => { + let index = 0 + let pending = 0 + let next = () => { + if (index === items.length && pending === 0) { + resolve() + } else if (index < items.length) { + let item = items[index++] + pending++ + callback(item).then(() => { + pending-- + next() + }, e => { + items.length = 0 + reject(e) + }) + } + } + for (let i = 0; i < batch; i++)next() + }) +} + +async function main() { + let rimraf = require('rimraf') + let path = require('path') + let installDir = path.join(__dirname, '.destructuring-fuzzer') + + let es = require('./esbuild').installForTests(installDir) + let esbuild = async (x) => (await es.transform(x, { target: 'es6', loader: 'ts' })).js.trim() + + console.log(` +Options: + --verbose = Print details for failures + +Legend: + - = The transform function passed + X = The transform function failed +`) + + let tests = [ + AssignmentOperator, + NamespaceExport, + ConstDeclaration, + LetDeclaration, + VarDeclaration, + TryCatchBinding, + FunctionStatementArguments, + FunctionExpressionArguments, + ArrowFunctionArguments, + ObjectMethodArguments, + ClassStatementMethodArguments, + ClassExpressionMethodArguments, + ForLoopConst, + ForLoopLet, + ForLoopVar, + ForLoop, + ForOfLoopConst, + ForOfLoopLet, + ForOfLoopVar, + ForOfLoop, + ] + let transforms = [ + esbuild, + ] + let testCases = generateTestCases(100) + + for (let transform of transforms) { + for (let test of tests) + await verify(test, transform, testCases) + } + + rimraf.sync(installDir, { disableGlob: true }) +} + +main().catch(e => setTimeout(() => { throw e })) diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 9da6114313a..e619da858d6 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -277,7 +277,6 @@ let transformTests = { // Future syntax forAwait: ({ service }) => futureSyntax(service, 'async function foo() { for await (let x of y) {} }', 'es2017', 'es2018'), bigInt: ({ service }) => futureSyntax(service, '123n', 'es2019', 'es2020'), - objRest: ({ service }) => futureSyntax(service, 'let {...x} = y', 'es2017', 'es2018'), nonIdArrayRest: ({ service }) => futureSyntax(service, 'let [...[x]] = y', 'es2015', 'es2016'), // Future syntax: async generator functions