diff options
Diffstat (limited to 'src/js_parser')
-rw-r--r-- | src/js_parser/imports.zig | 40 | ||||
-rw-r--r-- | src/js_parser/js_parser.zig | 10245 | ||||
-rw-r--r-- | src/js_parser/js_parser_test.zig | 499 |
3 files changed, 10784 insertions, 0 deletions
diff --git a/src/js_parser/imports.zig b/src/js_parser/imports.zig new file mode 100644 index 000000000..d5197930d --- /dev/null +++ b/src/js_parser/imports.zig @@ -0,0 +1,40 @@ +pub const std = @import("std"); +pub const logger = @import("../logger.zig"); +pub const js_lexer = @import("../js_lexer.zig"); +pub const importRecord = @import("../import_record.zig"); +pub const js_ast = @import("../js_ast.zig"); +pub const options = @import("../options.zig"); +pub const alloc = @import("../alloc.zig"); +pub const js_printer = @import("../js_printer.zig"); +pub const renamer = @import("../renamer.zig"); +pub const fs = @import("../fs.zig"); + +pub usingnamespace @import("../strings.zig"); +pub usingnamespace @import("../ast/base.zig"); +pub usingnamespace js_ast.G; +pub usingnamespace @import("../defines.zig"); + +pub const ImportKind = importRecord.ImportKind; +pub const BindingNodeIndex = js_ast.BindingNodeIndex; + +pub const StmtNodeIndex = js_ast.StmtNodeIndex; +pub const ExprNodeIndex = js_ast.ExprNodeIndex; +pub const ExprNodeList = js_ast.ExprNodeList; +pub const StmtNodeList = js_ast.StmtNodeList; +pub const BindingNodeList = js_ast.BindingNodeList; +pub const assert = std.debug.assert; + +pub const LocRef = js_ast.LocRef; +pub const S = js_ast.S; +pub const B = js_ast.B; +pub const G = js_ast.G; +pub const T = js_lexer.T; +pub const E = js_ast.E; +pub const Stmt = js_ast.Stmt; +pub const Expr = js_ast.Expr; +pub const Binding = js_ast.Binding; +pub const Symbol = js_ast.Symbol; +pub const Level = js_ast.Op.Level; +pub const Op = js_ast.Op; +pub const Scope = js_ast.Scope; +pub const locModuleScope = logger.Loc.Empty; diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig new file mode 100644 index 000000000..07a9f037f --- /dev/null +++ b/src/js_parser/js_parser.zig @@ -0,0 +1,10245 @@ +usingnamespace @import("imports.zig"); + +pub fn ExpressionTransposer(comptime ctx: type, visitor: fn (ptr: *ctx, arg: Expr, state: anytype) Expr) type { + return struct { + context: *Context, + + pub fn init(c: *Context) @This() { + return @This(){ + .context = c, + }; + } + + pub fn maybeTransposeIf(self: *@This(), arg: Expr, state: anytype) Expr { + switch (arg.data) { + .e_if => |ex| { + ex.yes = self.maybeTransposeIf(ex.yes, state); + ex.no = self.maybeTransposeIf(ex.no, state); + return arg; + }, + else => { + return visitor(self.context, arg, state); + }, + } + } + pub const Context = ctx; + }; +} + +pub fn locAfterOp(e: E.Binary) logger.Loc { + if (e.left.loc.start < e.right.loc.start) { + return e.right.loc; + } else { + // handle the case when we have transposed the operands + return e.left.loc; + } +} + +pub const ImportScanner = struct { + stmts: []Stmt = &([_]Stmt{}), + kept_import_equals: bool = false, + removed_import_equals: bool = false, + pub fn scan(p: *P, stmts: []Stmt) !ImportScanner { + var scanner = ImportScanner{}; + var stmts_end: usize = 0; + + for (stmts) |_stmt| { + // zls needs the hint, it seems. + const stmt: Stmt = _stmt; + switch (stmt.data) { + .s_import => |st| { + var record: ImportRecord = p.import_records.items[st.import_record_index]; + + // The official TypeScript compiler always removes unused imported + // symbols. However, we deliberately deviate from the official + // TypeScript compiler's behavior doing this in a specific scenario: + // we are not bundling, symbol renaming is off, and the tsconfig.json + // "importsNotUsedAsValues" setting is present and is not set to + // "remove". + // + // This exists to support the use case of compiling partial modules for + // compile-to-JavaScript languages such as Svelte. These languages try + // to reference imports in ways that are impossible for esbuild to know + // about when esbuild is only given a partial module to compile. Here + // is an example of some Svelte code that might use esbuild to convert + // TypeScript to JavaScript: + // + // <script lang="ts"> + // import Counter from './Counter.svelte'; + // export let name: string = 'world'; + // </script> + // <main> + // <h1>Hello {name}!</h1> + // <Counter /> + // </main> + // + // Tools that use esbuild to compile TypeScript code inside a Svelte + // file like this only give esbuild the contents of the <script> tag. + // These tools work around this missing import problem when using the + // official TypeScript compiler by hacking the TypeScript AST to + // remove the "unused import" flags. This isn't possible in esbuild + // because esbuild deliberately does not expose an AST manipulation + // API for performance reasons. + // + // We deviate from the TypeScript compiler's behavior in this specific + // case because doing so is useful for these compile-to-JavaScript + // languages and is benign in other cases. The rationale is as follows: + // + // * If "importsNotUsedAsValues" is absent or set to "remove", then + // we don't know if these imports are values or types. It's not + // safe to keep them because if they are types, the missing imports + // will cause run-time failures because there will be no matching + // exports. It's only safe keep imports if "importsNotUsedAsValues" + // is set to "preserve" or "error" because then we can assume that + // none of the imports are types (since the TypeScript compiler + // would generate an error in that case). + // + // * If we're bundling, then we know we aren't being used to compile + // a partial module. The parser is seeing the entire code for the + // module so it's safe to remove unused imports. And also we don't + // want the linker to generate errors about missing imports if the + // imported file is also in the bundle. + // + // * If identifier minification is enabled, then using esbuild as a + // partial-module transform library wouldn't work anyway because + // the names wouldn't match. And that means we're minifying so the + // user is expecting the output to be as small as possible. So we + // should omit unused imports. + // + // const keep_unused_imports = !p.options.trim_unused_imports; + var did_remove_star_loc = false; + const keep_unused_imports = true; + + // TypeScript always trims unused imports. This is important for + // correctness since some imports might be fake (only in the type + // system and used for type-only imports). + if (!keep_unused_imports) { + var found_imports = false; + var is_unused_in_typescript = false; + + if (st.default_name) |default_name| { + found_imports = true; + var symbol = p.symbols.items[default_name.ref.?.inner_index]; + + // TypeScript has a separate definition of unused + if (p.options.ts and p.ts_use_counts.items[default_name.ref.?.inner_index] != 0) { + is_unused_in_typescript = false; + } + + // Remove the symbol if it's never used outside a dead code region + if (symbol.use_count_estimate == 0) { + st.default_name = null; + } + } + + // Remove the star import if it's unused + if (st.star_name_loc) |star_name| { + found_imports = true; + const symbol = p.symbols.items[st.namespace_ref.inner_index]; + + // TypeScript has a separate definition of unused + if (p.options.ts and p.ts_use_counts.items[st.namespace_ref.inner_index] != 0) { + is_unused_in_typescript = false; + } + + // Remove the symbol if it's never used outside a dead code region + if (symbol.use_count_estimate == 0) { + // Make sure we don't remove this if it was used for a property + // access while bundling + var has_any = false; + + if (p.import_items_for_namespace.get(st.namespace_ref)) |entry| { + if (entry.count() > 0) { + has_any = true; + } + } + + if (!has_any) { + st.star_name_loc = null; + did_remove_star_loc = true; + } + } + } + + // Remove items if they are unused + if (st.items.len > 0) { + found_imports = false; + var items_end: usize = 0; + var i: usize = 0; + while (i < st.items.len) : (i += 1) { + const item = st.items[i]; + const ref = item.name.ref.?; + const symbol: Symbol = p.symbols.items[ref.inner_index]; + + // TypeScript has a separate definition of unused + if (p.options.ts and p.ts_use_counts.items[ref.inner_index] != 0) { + is_unused_in_typescript = false; + } + + // Remove the symbol if it's never used outside a dead code region + if (symbol.use_count_estimate != 0) { + st.items[items_end] = item; + items_end += 1; + } + } + + if (items_end < st.items.len - 1) { + var list = List(js_ast.ClauseItem).fromOwnedSlice(p.allocator, st.items); + list.shrinkAndFree(items_end); + st.items = list.toOwnedSlice(); + } + } + + // -- Original Comment -- + // Omit this statement if we're parsing TypeScript and all imports are + // unused. Note that this is distinct from the case where there were + // no imports at all (e.g. "import 'foo'"). In that case we want to keep + // the statement because the user is clearly trying to import the module + // for side effects. + // + // This culling is important for correctness when parsing TypeScript + // because a) the TypeScript compiler does ths and we want to match it + // and b) this may be a fake module that only exists in the type system + // and doesn't actually exist in reality. + // + // We do not want to do this culling in JavaScript though because the + // module may have side effects even if all imports are unused. + // -- Original Comment -- + + // jarred: I think, in this project, we want this behavior, even in JavaScript. + // I think this would be a big performance improvement. + // The less you import, the less code you transpile. + // Side-effect imports are nearly always done through identifier-less imports + // e.g. `import 'fancy-stylesheet-thing/style.css';` + // This is a breaking change though. We can make it an option with some guardrail + // so maybe if it errors, it shows a suggestion "retry without trimming unused imports" + if (found_imports and !p.options.preserve_unused_imports_ts) { + // Ignore import records with a pre-filled source index. These are + // for injected files and we definitely do not want to trim these. + if (!Ref.isSourceIndexNull(record.source_index)) { + record.is_unused = true; + continue; + } + } + } + + if (p.options.trim_unused_imports) { + if (st.star_name_loc != null or did_remove_star_loc) { + // -- Original Comment -- + // If we're bundling a star import and the namespace is only ever + // used for property accesses, then convert each unique property to + // a clause item in the import statement and remove the star import. + // That will cause the bundler to bundle them more efficiently when + // both this module and the imported module are in the same group. + // + // Before: + // + // import * as ns from 'foo' + // console.log(ns.a, ns.b) + // + // After: + // + // import {a, b} from 'foo' + // console.log(a, b) + // + // This is not done if the namespace itself is used, because in that + // case the code for the namespace will have to be generated. This is + // determined by the symbol count because the parser only counts the + // star import as used if it was used for something other than a + // property access: + // + // import * as ns from 'foo' + // console.log(ns, ns.a, ns.b) + // + // -- Original Comment -- + + // jarred: we don't use the same grouping mechanism as esbuild + // but, we do this anyway. + // The reasons why are: + // * It makes static analysis for other tools simpler. + // * I imagine browsers may someday do some optimizations + // when it's "easier" to know only certain modules are used + // For example, if you're importing a component from a design system + // it's really stupid to import all 1,000 components from that design system + // when you just want <Button /> + const namespace_ref = st.namespace_ref; + const convert_star_to_clause = p.symbols.items[namespace_ref.inner_index].use_count_estimate == 0; + + if (convert_star_to_clause and !keep_unused_imports) { + st.star_name_loc = null; + } + + // "importItemsForNamespace" has property accesses off the namespace + if (p.import_items_for_namespace.get(namespace_ref)) |import_items| { + var count = import_items.count(); + if (count > 0) { + // Sort keys for determinism + var sorted: []string = try p.allocator.alloc(string, count); + var iter = import_items.iterator(); + var i: usize = 0; + while (iter.next()) |item| { + sorted[i] = item.key; + i += 1; + } + strings.sortAsc(sorted); + + if (convert_star_to_clause) { + // Create an import clause for these items. Named imports will be + // automatically created later on since there is now a clause. + var items = try p.allocator.alloc(js_ast.ClauseItem, count); + try p.declared_symbols.ensureUnusedCapacity(count); + i = 0; + for (sorted) |alias| { + const name: LocRef = import_items.get(alias) orelse unreachable; + const original_name = p.symbols.items[name.ref.?.inner_index].original_name; + items[i] = js_ast.ClauseItem{ + .alias = alias, + .alias_loc = name.loc, + .name = name, + .original_name = original_name, + }; + p.declared_symbols.appendAssumeCapacity(js_ast.DeclaredSymbol{ + .ref = name.ref.?, + .is_top_level = true, + }); + + i += 1; + } + + if (st.items.len > 0) { + p.panic("The syntax \"import {{x}}, * as y from 'path'\" isn't valid", .{}); + } + + st.items = items; + } else { + // If we aren't converting this star import to a clause, still + // create named imports for these property accesses. This will + // cause missing imports to generate useful warnings. + // + // It will also improve bundling efficiency for internal imports + // by still converting property accesses off the namespace into + // bare identifiers even if the namespace is still needed. + + for (sorted) |alias| { + const name: LocRef = import_items.get(alias) orelse unreachable; + + try p.named_imports.put(name.ref.?, js_ast.NamedImport{ + .alias = alias, + .alias_loc = name.loc, + .namespace_ref = st.namespace_ref, + .import_record_index = st.import_record_index, + }); + + // Make sure the printer prints this as a property access + var symbol: Symbol = p.symbols.items[name.ref.?.inner_index]; + symbol.namespace_alias = G.NamespaceAlias{ .namespace_ref = st.namespace_ref, .alias = alias }; + p.symbols.items[name.ref.?.inner_index] = symbol; + } + } + } + } + } + } + + try p.import_records_for_current_part.append(st.import_record_index); + + if (st.star_name_loc != null) { + record.contains_import_star = true; + } + + if (st.default_name != null) { + record.contains_default_alias = true; + } else { + for (st.items) |item| { + if (strings.eql(item.alias, "default")) { + record.contains_default_alias = true; + break; + } + } + } + }, + .s_function => |st| { + if (st.func.flags.is_export) { + if (st.func.name) |name| { + try p.recordExport(name.loc, p.symbols.items[name.ref.?.inner_index].original_name, name.ref.?); + } else { + try p.log.addRangeError(p.source, logger.Range{ .loc = st.func.open_parens_loc, .len = 2 }, "Exported functions must have a name"); + } + } + }, + .s_class => |st| { + if (st.is_export) { + if (st.class.class_name) |name| { + try p.recordExport(name.loc, p.symbols.items[name.ref.?.inner_index].original_name, name.ref.?); + } else { + try p.log.addRangeError(p.source, logger.Range{ .loc = st.class.body_loc, .len = 0 }, "Exported classes must have a name"); + } + } + }, + .s_local => |st| { + if (st.is_export) { + for (st.decls) |decl| { + p.recordExportedBinding(decl.binding); + } + } + + // Remove unused import-equals statements, since those likely + // correspond to types instead of values + if (st.was_ts_import_equals and !st.is_export and st.decls.len > 0) { + var decl = st.decls[0]; + + // Skip to the underlying reference + var value = decl.value; + if (decl.value) |val| { + while (true) { + if (@as(Expr.Tag, val.data) == .e_dot) { + value = val.data.e_dot.target; + } else { + break; + } + } + } + + // Is this an identifier reference and not a require() call? + if (value) |val| { + if (@as(Expr.Tag, val.data) == .e_identifier) { + // Is this import statement unused? + if (@as(Binding.Tag, decl.binding.data) == .b_identifier and p.symbols.items[decl.binding.data.b_identifier.ref.inner_index].use_count_estimate == 0) { + p.ignoreUsage(val.data.e_identifier.ref); + + scanner.removed_import_equals = true; + continue; + } else { + scanner.kept_import_equals = true; + } + } + } + } + }, + .s_export_default => |st| { + try p.recordExport(st.default_name.loc, "default", st.default_name.ref.?); + }, + .s_export_clause => |st| { + for (st.items) |item| { + try p.recordExport(item.alias_loc, item.alias, item.name.ref.?); + } + }, + .s_export_star => |st| { + try p.import_records_for_current_part.append(st.import_record_index); + + if (st.alias) |alias| { + // "export * as ns from 'path'" + try p.named_imports.put(st.namespace_ref, js_ast.NamedImport{ + .alias = null, + .alias_is_star = true, + .alias_loc = alias.loc, + .namespace_ref = Ref.None, + .import_record_index = st.import_record_index, + .is_exported = true, + }); + try p.recordExport(alias.loc, alias.original_name, st.namespace_ref); + } else { + // "export * from 'path'" + try p.export_star_import_records.append(st.import_record_index); + } + }, + .s_export_from => |st| { + try p.import_records_for_current_part.append(st.import_record_index); + + for (st.items) |item| { + const ref = item.name.ref orelse p.panic("Expected export from item to have a name {s}", .{st}); + // Note that the imported alias is not item.Alias, which is the + // exported alias. This is somewhat confusing because each + // SExportFrom statement is basically SImport + SExportClause in one. + try p.named_imports.put(ref, js_ast.NamedImport{ + .alias_is_star = false, + .alias = item.original_name, + .alias_loc = item.name.loc, + .namespace_ref = st.namespace_ref, + .import_record_index = st.import_record_index, + .is_exported = true, + }); + try p.recordExport(item.name.loc, item.alias, ref); + } + }, + else => {}, + } + + stmts[stmts_end] = stmt; + stmts_end += 1; + } + scanner.stmts = stmts[0..stmts_end]; + return scanner; + } +}; + +pub const SideEffects = enum { + could_have_side_effects, + no_side_effects, + + pub const Result = struct { + side_effects: SideEffects, + ok: bool = false, + value: bool = false, + }; + + pub fn toNumber(data: Expr.Data) ?f64 { + switch (data) { + .e_null => |e| { + return 0; + }, + .e_undefined => |e| { + return std.math.nan_f64; + }, + .e_boolean => |e| { + return if (e.value) 1.0 else 0.0; + }, + .e_number => |e| { + return e.value; + }, + else => {}, + } + + return null; + } + + pub fn isPrimitiveToReorder(data: Expr.Data) bool { + switch (data) { + .e_null, .e_undefined, .e_string, .e_boolean, .e_number, .e_big_int => { + return true; + }, + else => { + return false; + }, + } + } + + pub const Equality = struct { equal: bool = false, ok: bool = false }; + + // Returns "equal, ok". If "ok" is false, then nothing is known about the two + // values. If "ok" is true, the equality or inequality of the two values is + // stored in "equal". + pub fn eql(left: Expr.Data, right: Expr.Data) Equality { + var equality = Equality{}; + switch (left) { + .e_null => |l| { + equality.equal = @as(Expr.Tag, right) == Expr.Tag.e_null; + equality.ok = equality.equal; + }, + .e_undefined => |l| { + equality.equal = @as(Expr.Tag, right) == Expr.Tag.e_undefined; + equality.ok = equality.equal; + }, + .e_boolean => |l| { + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_boolean; + equality.equal = l.value == right.e_boolean.value; + }, + .e_number => |l| { + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_number; + equality.equal = l.value == right.e_number.value; + }, + .e_big_int => |l| { + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_big_int; + equality.equal = strings.eql(l.value, right.e_big_int.value); + }, + .e_string => |l| { + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_string; + equality.equal = std.mem.eql(u16, l.value, right.e_string.value); + }, + else => {}, + } + + return equality; + } + + // Returns true if this expression is known to result in a primitive value (i.e. + // null, undefined, boolean, number, bigint, or string), even if the expression + // cannot be removed due to side effects. + pub fn isPrimitiveWithSideEffects(data: Expr.Data) bool { + switch (data) { + .e_null, .e_undefined, .e_boolean, .e_number, .e_big_int, .e_string => { + return true; + }, + .e_unary => |e| { + switch (e.op) { + // number or bigint + .un_pos, + .un_neg, + .un_cpl, + .un_pre_dec, + .un_pre_inc, + .un_post_dec, + .un_post_inc, + // boolean + .un_not, + .un_delete, + // undefined + .un_void, + // string + .un_typeof, + => { + return true; + }, + else => {}, + } + }, + .e_binary => |e| { + switch (e.op) { + // boolean + .bin_lt, + .bin_le, + .bin_gt, + .bin_ge, + .bin_in, + .bin_instanceof, + .bin_loose_eq, + .bin_loose_ne, + .bin_strict_eq, + .bin_strict_ne, + // string, number, or bigint + .bin_add, + .bin_add_assign, + // number or bigint + .bin_sub, + .bin_mul, + .bin_div, + .bin_rem, + .bin_pow, + .bin_sub_assign, + .bin_mul_assign, + .bin_div_assign, + .bin_rem_assign, + .bin_pow_assign, + .bin_shl, + .bin_shr, + .bin_u_shr, + .bin_shl_assign, + .bin_shr_assign, + .bin_u_shr_assign, + .bin_bitwise_or, + .bin_bitwise_and, + .bin_bitwise_xor, + .bin_bitwise_or_assign, + .bin_bitwise_and_assign, + .bin_bitwise_xor_assign, + => { + return true; + }, + + // These always return one of the arguments unmodified + .bin_logical_and, .bin_logical_or, .bin_nullish_coalescing, .bin_logical_and_assign, .bin_logical_or_assign, .bin_nullish_coalescing_assign => { + return isPrimitiveWithSideEffects(e.left.data) and isPrimitiveWithSideEffects(e.right.data); + }, + .bin_comma => { + return isPrimitiveWithSideEffects(e.right.data); + }, + } + }, + .e_if => { + return isPrimitiveWithSideEffects(e.yes.data) and isPrimitiveWithSideEffects(e.no.data); + }, + else => {}, + } + return false; + } + + // Returns true if the result of the "typeof" operator on this expression is + // statically determined and this expression has no side effects (i.e. can be + // removed without consequence). + pub fn toTypeof(data: Expr.Data) ?string { + switch (data) { + .e_null => { + return "object"; + }, + .e_undefined => { + return "undefined"; + }, + .e_boolean => { + return "boolean"; + }, + .e_number => { + return "number"; + }, + .e_big_int => { + return "bigint"; + }, + .e_string => { + return "string"; + }, + .e_function, .e_arrow => { + return "function"; + }, + else => {}, + } + + return null; + } + + pub fn toNullOrUndefined(exp: Expr.Data) Result { + switch (exp) { + // Never null or undefined + .e_boolean, .e_number, .e_string, .e_reg_exp, .e_function, .e_arrow, .e_big_int => { + return Result{ .value = false, .side_effects = SideEffects.no_side_effects, .ok = true }; + }, + + .e_object, .e_array, .e_class => { + return Result{ .value = false, .side_effects = .could_have_side_effects, .ok = true }; + }, + + // always anull or undefined + .e_null, .e_undefined => { + return Result{ .value = true, .side_effects = .could_have_side_effects, .ok = true }; + }, + + .e_unary => |e| { + switch (e.op) { + // Always number or bigint + .un_pos, .un_neg, .un_cpl, .un_pre_dec, .un_pre_inc, .un_post_dec, .un_post_inc => { + return Result{ .ok = true, .value = false, .side_effects = SideEffects.could_have_side_effects }; + }, + // Always undefined + .un_not, .un_typeof, .un_delete => { + return Result{ .value = true, .side_effects = .could_have_side_effects, .ok = true }; + }, + + .un_void => { + return Result{ .value = true, .side_effects = .could_have_side_effects, .ok = true }; + }, + + else => {}, + } + }, + + .e_binary => |e| { + switch (e.op) { + // always string or number or bigint + .bin_add, + .bin_add_assign, + // always number or bigint + .bin_sub, + .bin_mul, + .bin_div, + .bin_rem, + .bin_pow, + .bin_sub_assign, + .bin_mul_assign, + .bin_div_assign, + .bin_rem_assign, + .bin_pow_assign, + .bin_shl, + .bin_shr, + .bin_u_shr, + .bin_shl_assign, + .bin_shr_assign, + .bin_u_shr_assign, + .bin_bitwise_or, + .bin_bitwise_and, + .bin_bitwise_xor, + .bin_bitwise_or_assign, + .bin_bitwise_and_assign, + .bin_bitwise_xor_assign, + // always boolean + .bin_lt, + .bin_le, + .bin_gt, + .bin_ge, + .bin_in, + .bin_instanceof, + .bin_loose_eq, + .bin_loose_ne, + .bin_strict_eq, + .bin_strict_ne, + => { + return Result{ .ok = true, .value = false, .side_effects = SideEffects.could_have_side_effects }; + }, + + .bin_comma => { + const res = toNullOrUndefined(e.right.data); + if (res.ok) { + return Result{ .ok = true, .value = res.value, .side_effects = SideEffects.could_have_side_effects }; + } + }, + else => {}, + } + }, + else => {}, + } + + return Result{ .ok = false, .value = false, .side_effects = SideEffects.could_have_side_effects }; + } + + pub fn toBoolean(exp: Expr.Data) Result { + switch (exp) { + .e_null, .e_undefined => { + return Result{ .ok = true, .value = false, .side_effects = .no_side_effects }; + }, + .e_boolean => |e| { + return Result{ .ok = true, .value = e.value, .side_effects = .no_side_effects }; + }, + .e_number => |e| { + return Result{ .ok = true, .value = e.value != 0.0 and !std.math.isNan(e.value), .side_effects = .no_side_effects }; + }, + .e_big_int => |e| { + return Result{ .ok = true, .value = !strings.eql(e.value, "0"), .side_effects = .no_side_effects }; + }, + .e_string => |e| { + return Result{ .ok = true, .value = e.value.len > 0, .side_effects = .no_side_effects }; + }, + .e_function, .e_arrow, .e_reg_exp => { + return Result{ .ok = true, .value = true, .side_effects = .no_side_effects }; + }, + .e_object, .e_array, .e_class => { + return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects }; + }, + .e_unary => |e_| { + switch (e_.op) { + .un_void => { + return Result{ .ok = true, .value = false, .side_effects = .could_have_side_effects }; + }, + .un_typeof => { + // Never an empty string + + return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects }; + }, + .un_not => { + var result = toBoolean(e_.value.data); + if (result.ok) { + result.value = !result.value; + return result; + } + }, + else => {}, + } + }, + .e_binary => |e_| { + switch (e_.op) { + .bin_logical_or => { + // "anything || truthy" is truthy + const result = toBoolean(e_.right.data); + if (result.value and result.ok) { + return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects }; + } + }, + .bin_logical_and => { + // "anything && falsy" is falsy + const result = toBoolean(e_.right.data); + if (!result.value and result.ok) { + return Result{ .ok = true, .value = false, .side_effects = .could_have_side_effects }; + } + }, + .bin_comma => { + // "anything, truthy/falsy" is truthy/falsy + var result = toBoolean(e_.right.data); + if (result.ok) { + result.side_effects = .could_have_side_effects; + return result; + } + }, + else => {}, + } + }, + else => {}, + } + + return Result{ .ok = false, .value = false, .side_effects = SideEffects.could_have_side_effects }; + } +}; + +const ExprOrLetStmt = struct { + stmt_or_expr: js_ast.StmtOrExpr, + decls: []G.Decl = &([_]G.Decl{}), +}; + +const FunctionKind = enum { stmt, expr }; + +const EightLetterMatcher = strings.ExactSizeMatcher(8); + +const AsyncPrefixExpression = enum { + none, + is_yield, + is_async, + is_await, + + pub fn find(ident: string) AsyncPrefixExpression { + if (ident.len != 5) { + return .none; + } + + switch (EightLetterMatcher.match(ident)) { + EightLetterMatcher.case("yield") => { + return .is_yield; + }, + EightLetterMatcher.case("await") => { + return .is_await; + }, + EightLetterMatcher.case("async") => { + return .is_async; + }, + + else => { + return .none; + }, + } + } +}; + +const IdentifierOpts = struct { + assign_target: js_ast.AssignTarget = js_ast.AssignTarget.none, + is_delete_target: bool = false, + was_originally_identifier: bool = false, +}; + +fn statementCaresAboutScope(stmt: Stmt) bool { + switch (stmt.data) { + .s_block, + .s_empty, + .s_debugger, + .s_expr, + .s_if, + .s_for, + .s_for_in, + .s_for_of, + .s_do_while, + .s_while, + .s_with, + .s_try, + .s_switch, + .s_return, + .s_throw, + .s_break, + .s_continue, + .s_directive, + => { + return false; + }, + .s_local => |s| { + return s.kind != .k_var; + }, + else => { + return true; + }, + } +} + +const ExprIn = struct { + // This tells us if there are optional chain expressions (EDot, EIndex, or + // ECall) that are chained on to this expression. Because of the way the AST + // works, chaining expressions on to this expression means they are our + // parent expressions. + // + // Some examples: + // + // a?.b.c // EDot + // a?.b[c] // EIndex + // a?.b() // ECall + // + // Note that this is false if our parent is a node with a OptionalChain + // value of OptionalChainStart. That means it's the start of a new chain, so + // it's not considered part of this one. + // + // Some examples: + // + // a?.b?.c // EDot + // a?.b?.[c] // EIndex + // a?.b?.() // ECall + // + // Also note that this is false if our parent is a node with a OptionalChain + // value of OptionalChainNone. That means it's outside parentheses, which + // means it's no longer part of the chain. + // + // Some examples: + // + // (a?.b).c // EDot + // (a?.b)[c] // EIndex + // (a?.b)() // ECall + // + has_chain_parent: bool = false, + + // If our parent is an ECall node with an OptionalChain value of + // OptionalChainStart, then we will need to store the value for the "this" of + // that call somewhere if the current expression is an optional chain that + // ends in a property access. That's because the value for "this" will be + // used twice: once for the inner optional chain and once for the outer + // optional chain. + // + // Example: + // + // // Original + // a?.b?.(); + // + // // Lowered + // var _a; + // (_a = a == null ? void 0 : a.b) == null ? void 0 : _a.call(a); + // + // In the example above we need to store "a" as the value for "this" so we + // can substitute it back in when we call "_a" if "_a" is indeed present. + // See also "thisArgFunc" and "thisArgWrapFunc" in "exprOut". + store_this_arg_for_parent_optional_chain: bool = false, + + // Certain substitutions of identifiers are disallowed for assignment targets. + // For example, we shouldn't transform "undefined = 1" into "void 0 = 1". This + // isn't something real-world code would do but it matters for conformance + // tests. + assign_target: js_ast.AssignTarget = js_ast.AssignTarget.none, +}; + +const ExprOut = struct { + // True if the child node is an optional chain node (EDot, EIndex, or ECall + // with an IsOptionalChain value of true) + child_contains_optional_chain: bool = false, +}; + +const Tup = std.meta.Tuple; + +// This function exists to tie all of these checks together in one place +fn isEvalOrArguments(name: string) bool { + return strings.eql(name, "eval") or strings.eql(name, "arguments"); +} + +const PrependTempRefsOpts = struct { + fn_body_loc: ?logger.Loc = null, + kind: StmtsKind = StmtsKind.none, +}; + +pub const StmtsKind = enum { + none, + loop_body, + fn_body, +}; + +fn notimpl() noreturn { + std.debug.panic("Not implemented yet!!", .{}); +} + +fn lexerpanic() noreturn { + std.debug.panic("LexerPanic", .{}); +} + +fn fail() noreturn { + std.debug.panic("Something went wrong :cry;", .{}); +} + +const ExprBindingTuple = struct { expr: ?ExprNodeIndex = null, binding: ?Binding = null, override_expr: ?ExprNodeIndex = null }; + +const TempRef = struct { + ref: Ref, + value: ?Expr = null, +}; + +const ImportNamespaceCallOrConstruct = struct { + ref: js_ast.Ref, + is_construct: bool = false, +}; + +const ThenCatchChain = struct { + next_target: js_ast.Expr.Data, + has_multiple_args: bool = false, + has_catch: bool = false, +}; + +const ParsedPath = struct { loc: logger.Loc, text: string }; + +const StrictModeFeature = enum { + with_statement, + delete_bare_name, + for_in_var_init, + eval_or_arguments, + reserved_word, + legacy_octal_literal, + legacy_octal_escape, + if_else_function_stmt, +}; + +const SymbolMergeResult = enum { + forbidden, + replace_with_new, + overwrite_with_new, + keep_existing, + become_private_get_set_pair, + become_private_static_get_set_pair, +}; + +const Map = std.AutoHashMap; + +const List = std.ArrayList; +const LocList = List(logger.Loc); +const StmtList = List(Stmt); + +const SymbolUseMap = Map(js_ast.Ref, js_ast.Symbol.Use); +const StringRefMap = std.StringHashMap(js_ast.Ref); +const StringBoolMap = std.StringHashMap(bool); +const RefBoolMap = Map(js_ast.Ref, bool); +const RefRefMap = Map(js_ast.Ref, js_ast.Ref); +const ImportRecord = importRecord.ImportRecord; +const Flags = js_ast.Flags; +const ScopeOrder = struct { + loc: logger.Loc, + scope: *js_ast.Scope, +}; +const EnumValueType = enum { + unknown, + string, + numeric, +}; + +const ParenExprOpts = struct { + async_range: logger.Range = logger.Range.None, + is_async: bool = false, + force_arrow_fn: bool = false, +}; + +const AwaitOrYield = enum { + allow_ident, + allow_expr, + forbid_all, +}; + +// This is function-specific information used during parsing. It is saved and +// restored on the call stack around code that parses nested functions and +// arrow expressions. +const FnOrArrowDataParse = struct { + async_range: ?logger.Range = null, + allow_await: AwaitOrYield = AwaitOrYield.allow_ident, + allow_yield: AwaitOrYield = AwaitOrYield.allow_ident, + allow_super_call: bool = false, + is_top_level: bool = false, + is_constructor: bool = false, + is_typescript_declare: bool = false, + arrow_arg_errors: ?DeferredArrowArgErrors = null, + + // In TypeScript, forward declarations of functions have no bodies + allow_missing_body_for_type_script: bool = false, + + // Allow TypeScript decorators in function arguments + allow_ts_decorators: bool = false, + + pub fn i() FnOrArrowDataParse { + return FnOrArrowDataParse{ .allow_await = AwaitOrYield.forbid_all }; + } +}; + +// This is function-specific information used during visiting. It is saved and +// restored on the call stack around code that parses nested functions and +// arrow expressions. +const FnOrArrowDataVisit = struct { + super_index_ref: ?*js_ast.Ref = null, + + is_arrow: bool = false, + is_async: bool = false, + is_inside_loop: bool = false, + is_inside_switch: bool = false, + is_outside_fn_or_arrow: bool = false, + + // This is used to silence unresolvable imports due to "require" calls inside + // a try/catch statement. The assumption is that the try/catch statement is + // there to handle the case where the reference to "require" crashes. + try_body_count: i32 = 0, +}; + +// This is function-specific information used during visiting. It is saved and +// restored on the call stack around code that parses nested functions (but not +// nested arrow functions). +const FnOnlyDataVisit = struct { + // This is a reference to the magic "arguments" variable that exists inside + // functions in JavaScript. It will be non-nil inside functions and nil + // otherwise. + arguments_ref: ?js_ast.Ref = null, + + // Arrow functions don't capture the value of "this" and "arguments". Instead, + // the values are inherited from the surrounding context. If arrow functions + // are turned into regular functions due to lowering, we will need to generate + // local variables to capture these values so they are preserved correctly. + this_capture_ref: ?js_ast.Ref = null, + arguments_capture_ref: ?js_ast.Ref = null, + + // Inside a static class property initializer, "this" expressions should be + // replaced with the class name. + this_class_static_ref: ?js_ast.Ref = null, + + // If we're inside an async arrow function and async functions are not + // supported, then we will have to convert that arrow function to a generator + // function. That means references to "arguments" inside the arrow function + // will have to reference a captured variable instead of the real variable. + is_inside_async_arrow_fn: bool = false, + + // If false, the value for "this" is the top-level module scope "this" value. + // That means it's "undefined" for ECMAScript modules and "exports" for + // CommonJS modules. We track this information so that we can substitute the + // correct value for these top-level "this" references at compile time instead + // of passing the "this" expression through to the output and leaving the + // interpretation up to the run-time behavior of the generated code. + // + // If true, the value for "this" is nested inside something (either a function + // or a class declaration). That means the top-level module scope "this" value + // has been shadowed and is now inaccessible. + is_this_nested: bool = false, +}; + +// Due to ES6 destructuring patterns, there are many cases where it's +// impossible to distinguish between an array or object literal and a +// destructuring assignment until we hit the "=" operator later on. +// This object defers errors about being in one state or the other +// until we discover which state we're in. +const DeferredErrors = struct { + // These are errors for expressions + invalid_expr_default_value: ?logger.Range = null, + invalid_expr_after_question: ?logger.Range = null, + array_spread_feature: ?logger.Range = null, + + pub fn isEmpty(self: *DeferredErrors) bool { + return self.invalid_expr_default_value == null and self.invalid_expr_after_question == null and self.array_spread_feature == null; + } + + pub fn mergeInto(self: *DeferredErrors, to: *DeferredErrors) void { + if (self.invalid_expr_default_value) |inv| { + to.invalid_expr_default_value = inv; + } + + if (self.invalid_expr_after_question) |inv| { + to.invalid_expr_after_question = inv; + } + + if (self.array_spread_feature) |inv| { + to.array_spread_feature = inv; + } + } + + var None = DeferredErrors{ + .invalid_expr_default_value = null, + .invalid_expr_after_question = null, + .array_spread_feature = null, + }; +}; + +const ImportClause = struct { + items: []js_ast.ClauseItem = &([_]js_ast.ClauseItem{}), + is_single_line: bool = false, +}; + +const ModuleType = enum { esm }; + +const PropertyOpts = struct { + async_range: logger.Range = logger.Range.None, + is_async: bool = false, + is_generator: bool = false, + + // Class-related options + is_static: bool = false, + is_class: bool = false, + class_has_extends: bool = false, + allow_ts_decorators: bool = false, + ts_decorators: []Expr = &[_]Expr{}, +}; + +pub const Parser = struct { + options: Options, + lexer: js_lexer.Lexer, + log: *logger.Log, + source: logger.Source, + define: *Define, + allocator: *std.mem.Allocator, + p: ?*P, + + pub const Options = struct { + jsx: options.JSX, + ts: bool = false, + ascii_only: bool = true, + keep_names: bool = true, + mangle_syntax: bool = false, + mange_identifiers: bool = false, + omit_runtime_for_tests: bool = false, + ignore_dce_annotations: bool = true, + preserve_unused_imports_ts: bool = false, + use_define_for_class_fields: bool = false, + suppress_warnings_about_weird_code: bool = true, + moduleType: ModuleType = ModuleType.esm, + trim_unused_imports: bool = true, + }; + + pub fn parse(self: *Parser) !js_ast.Result { + if (self.p == null) { + self.p = try P.init(self.allocator, self.log, self.source, self.define, self.lexer, self.options); + } + + var result: js_ast.Result = undefined; + + if (self.p) |p| { + // Parse the file in the first pass, but do not bind symbols + var opts = ParseStatementOptions{ .is_module_scope = true }; + debugl("<p.parseStmtsUpTo>"); + const stmts = try p.parseStmtsUpTo(js_lexer.T.t_end_of_file, &opts); + debugl("</p.parseStmtsUpTo>"); + try p.prepareForVisitPass(); + + // ESM is always strict mode. I don't think we need this. + // // Strip off a leading "use strict" directive when not bundling + // var directive = ""; + + // Insert a variable for "import.meta" at the top of the file if it was used. + // We don't need to worry about "use strict" directives because this only + // happens when bundling, in which case we are flatting the module scopes of + // all modules together anyway so such directives are meaningless. + // if (!p.import_meta_ref.isSourceIndexNull()) { + // // heap so it lives beyond this function call + // var decls = try p.allocator.alloc(G.Decl, 1); + // decls[0] = Decl{ .binding = p.b(B.Identifier{ + // .ref = p.import_meta_ref, + // }, logger.Loc.Empty), .value = p.e(E.Object{}, logger.Loc.Empty) }; + // var importMetaStatement = p.s(S.Local{ + // .kind = .k_const, + // .decls = decls, + // }, logger.Loc.Empty); + // } + + debugl("<p.appendPart>"); + var before = List(js_ast.Part).init(p.allocator); + var after = List(js_ast.Part).init(p.allocator); + var parts = List(js_ast.Part).init(p.allocator); + try p.appendPart(&parts, stmts); + // for (stmts) |stmt| { + // var _stmts = ([_]Stmt{stmt}); + + // switch (stmt.data) { + // // Split up top-level multi-declaration variable statements + + // .s_local => |local| { + // for (local.decls) |decl| { + // var decls = try p.allocator.alloc(Decl, 1); + // var clone = S.Local{ + // .kind = local.kind, + // .decls = decls, + // .is_export = local.is_export, + // .was_ts_import_equals = local.was_ts_import_equals, + // }; + // _stmts[0] = p.s(clone, stmt.loc); + + // try p.appendPart(&parts, &_stmts); + // } + // }, + // // Move imports (and import-like exports) to the top of the file to + // // ensure that if they are converted to a require() call, the effects + // // will take place before any other statements are evaluated. + // .s_import, .s_export_from, .s_export_star => { + // try p.appendPart(&before, &_stmts); + // }, + + // .s_export_equals => { + // try p.appendPart(&after, &_stmts); + // }, + // else => { + // try p.appendPart(&parts, &_stmts); + // }, + // } + // } + // p.popScope(); + var parts_slice: []js_ast.Part = undefined; + + if (before.items.len > 0 or after.items.len > 0) { + const before_len = before.items.len; + const after_len = after.items.len; + const parts_len = parts.items.len; + var _parts = try p.allocator.alloc( + js_ast.Part, + before_len + + after_len + + parts_len, + ); + if (before_len > 0) { + std.mem.copy(js_ast.Part, _parts, before.toOwnedSlice()); + } + if (parts_len > 0) { + std.mem.copy(js_ast.Part, _parts[before_len .. before_len + parts_len], parts.toOwnedSlice()); + } + + if (after_len > 0) { + std.mem.copy(js_ast.Part, _parts[before_len + parts_len .. _parts.len], after.toOwnedSlice()); + } + parts_slice = _parts; + } else { + after.deinit(); + before.deinit(); + parts_slice = parts.toOwnedSlice(); + } + debugl("</p.appendPart>"); + + // Pop the module scope to apply the "ContainsDirectEval" rules + // p.popScope(); + debugl("<result.Ast>"); + result.ast = try p.toAST(parts_slice); + result.ok = true; + debugl("</result.Ast>"); + + // result = p.toAST(parts); + // result.source_map_comment = p.lexer.source_mapping_url; + } + + return result; + } + + pub fn init(transform: options.TransformOptions, log: *logger.Log, source: *logger.Source, define: *Define, allocator: *std.mem.Allocator) !Parser { + const lexer = try js_lexer.Lexer.init(log, source, allocator); + return Parser{ + .options = Options{ + .ts = transform.loader == .tsx or transform.loader == .ts, + .jsx = options.JSX{ + .parse = transform.loader == .tsx or transform.loader == .jsx, + .factory = transform.jsx_factory, + .fragment = transform.jsx_fragment, + }, + }, + .allocator = allocator, + .lexer = lexer, + .define = define, + .source = source.*, + .log = log, + .p = null, + }; + } +}; + +const FindLabelSymbolResult = struct { ref: Ref, is_loop: bool, found: bool = false }; + +const FindSymbolResult = struct { + ref: Ref, + declare_loc: ?logger.Loc = null, + is_inside_with_scope: bool = false, +}; +const ExportClauseResult = struct { clauses: []js_ast.ClauseItem = &([_]js_ast.ClauseItem{}), is_single_line: bool = false }; + +const DeferredTsDecorators = struct { + values: []js_ast.Expr, + + // If this turns out to be a "declare class" statement, we need to undo the + // scopes that were potentially pushed while parsing the decorator arguments. + scope_index: usize, +}; + +const LexicalDecl = enum(u8) { forbid, allow_all, allow_fn_inside_if, allow_fn_inside_label }; + +const ParseClassOptions = struct { + ts_decorators: []Expr = &[_]Expr{}, + allow_ts_decorators: bool = false, + is_type_script_declare: bool = false, +}; + +const ParseStatementOptions = struct { + ts_decorators: ?DeferredTsDecorators = null, + lexical_decl: LexicalDecl = .forbid, + is_module_scope: bool = false, + is_namespace_scope: bool = false, + is_export: bool = false, + is_name_optional: bool = false, // For "export default" pseudo-statements, + is_typescript_declare: bool = false, +}; + +// P is for Parser! +// public only because of Binding.ToExpr +pub const P = struct { + allocator: *std.mem.Allocator, + options: Parser.Options, + log: *logger.Log, + define: *Define, + source: logger.Source, + lexer: js_lexer.Lexer, + allow_in: bool = false, + allow_private_identifiers: bool = false, + has_top_level_return: bool = false, + latest_return_had_semicolon: bool = false, + has_import_meta: bool = false, + has_es_module_syntax: bool = false, + top_level_await_keyword: logger.Range, + fn_or_arrow_data_parse: FnOrArrowDataParse, + fn_or_arrow_data_visit: FnOrArrowDataVisit, + fn_only_data_visit: FnOnlyDataVisit, + allocated_names: List(string), + latest_arrow_arg_loc: logger.Loc = logger.Loc.Empty, + forbid_suffix_after_as_loc: logger.Loc = logger.Loc.Empty, + current_scope: *js_ast.Scope = undefined, + scopes_for_current_part: List(*js_ast.Scope), + symbols: List(js_ast.Symbol), + ts_use_counts: List(u32), + exports_ref: js_ast.Ref = js_ast.Ref.None, + require_ref: js_ast.Ref = js_ast.Ref.None, + module_ref: js_ast.Ref = js_ast.Ref.None, + import_meta_ref: js_ast.Ref = js_ast.Ref.None, + promise_ref: ?js_ast.Ref = null, + + data: js_ast.AstData, + + injected_define_symbols: []js_ast.Ref, + symbol_uses: SymbolUseMap, + declared_symbols: List(js_ast.DeclaredSymbol), + runtime_imports: StringRefMap, + duplicate_case_checker: void, + non_bmp_identifiers: StringBoolMap, + legacy_octal_literals: void, + // legacy_octal_literals: map[js_ast.E]logger.Range, + + // For strict mode handling + hoistedRefForSloppyModeBlockFn: void, + + // For lowering private methods + weak_map_ref: ?js_ast.Ref, + weak_set_ref: ?js_ast.Ref, + private_getters: RefRefMap, + private_setters: RefRefMap, + + // These are for TypeScript + should_fold_numeric_constants: bool = false, + emitted_namespace_vars: RefBoolMap, + is_exported_inside_namespace: RefRefMap, + known_enum_values: Map(js_ast.Ref, std.StringHashMap(f64)), + local_type_names: StringBoolMap, + + // This is the reference to the generated function argument for the namespace, + // which is different than the reference to the namespace itself: + // + // namespace ns { + // } + // + // The code above is transformed into something like this: + // + // var ns1; + // (function(ns2) { + // })(ns1 or (ns1 = {})); + // + // This variable is "ns2" not "ns1". It is only used during the second + // "visit" pass. + enclosing_namespace_arg_ref: ?js_ast.Ref = null, + + // Imports (both ES6 and CommonJS) are tracked at the top level + import_records: List(ImportRecord), + import_records_for_current_part: List(u32), + export_star_import_records: List(u32), + + // These are for handling ES6 imports and exports + es6_import_keyword: logger.Range = logger.Range.None, + es6_export_keyword: logger.Range = logger.Range.None, + enclosing_class_keyword: logger.Range = logger.Range.None, + import_items_for_namespace: Map(js_ast.Ref, std.StringHashMap(js_ast.LocRef)), + is_import_item: RefBoolMap, + named_imports: Map(js_ast.Ref, js_ast.NamedImport), + named_exports: std.StringHashMap(js_ast.NamedExport), + top_level_symbol_to_parts: Map(js_ast.Ref, List(u32)), + import_namespace_cc_map: Map(ImportNamespaceCallOrConstruct, bool), + + // The parser does two passes and we need to pass the scope tree information + // from the first pass to the second pass. That's done by tracking the calls + // to pushScopeForParsePass() and popScope() during the first pass in + // scopesInOrder. + // + // Then, when the second pass calls pushScopeForVisitPass() and popScope(), + // we consume entries from scopesInOrder and make sure they are in the same + // order. This way the second pass can efficiently use the same scope tree + // as the first pass without having to attach the scope tree to the AST. + // + // We need to split this into two passes because the pass that declares the + // symbols must be separate from the pass that binds identifiers to declared + // symbols to handle declaring a hoisted "var" symbol in a nested scope and + // binding a name to it in a parent or sibling scope. + scopes_in_order: List(ScopeOrder), + + // These properties are for the visit pass, which runs after the parse pass. + // The visit pass binds identifiers to declared symbols, does constant + // folding, substitutes compile-time variable definitions, and lowers certain + // syntactic constructs as appropriate. + stmt_expr_value: Expr.Data, + call_target: Expr.Data, + delete_target: Expr.Data, + loop_body: Stmt.Data, + module_scope: *js_ast.Scope = undefined, + is_control_flow_dead: bool = false, + + // Inside a TypeScript namespace, an "export declare" statement can be used + // to cause a namespace to be emitted even though it has no other observable + // effect. This flag is used to implement this feature. + // + // Specifically, namespaces should be generated for all of the following + // namespaces below except for "f", which should not be generated: + // + // namespace a { export declare const a } + // namespace b { export declare let [[b]] } + // namespace c { export declare function c() } + // namespace d { export declare class d {} } + // namespace e { export declare enum e {} } + // namespace f { export declare namespace f {} } + // + // The TypeScript compiler compiles this into the following code (notice "f" + // is missing): + // + // var a; (function (a_1) {})(a or (a = {})); + // var b; (function (b_1) {})(b or (b = {})); + // var c; (function (c_1) {})(c or (c = {})); + // var d; (function (d_1) {})(d or (d = {})); + // var e; (function (e_1) {})(e or (e = {})); + // + // Note that this should not be implemented by declaring symbols for "export + // declare" statements because the TypeScript compiler doesn't generate any + // code for these statements, so these statements are actually references to + // global variables. There is one exception, which is that local variables + // *should* be declared as symbols because they are replaced with. This seems + // like very arbitrary behavior but it's what the TypeScript compiler does, + // so we try to match it. + // + // Specifically, in the following code below "a" and "b" should be declared + // and should be substituted with "ns.a" and "ns.b" but the other symbols + // shouldn't. References to the other symbols actually refer to global + // variables instead of to symbols that are exported from the namespace. + // This is the case as of TypeScript 4.3. I assume this is a TypeScript bug: + // + // namespace ns { + // export declare const a + // export declare let [[b]] + // export declare function c() + // export declare class d { } + // export declare enum e { } + // console.log(a, b, c, d, e) + // } + // + // The TypeScript compiler compiles this into the following code: + // + // var ns; + // (function (ns) { + // console.log(ns.a, ns.b, c, d, e); + // })(ns or (ns = {})); + // + // Relevant issue: https://github.com/evanw/esbuild/issues/1158 + has_non_local_export_declare_inside_namespace: bool = false, + + // This helps recognize the "await import()" pattern. When this is present, + // warnings about non-string import paths will be omitted inside try blocks. + await_target: ?js_ast.Expr.Data = null, + + to_expr_wrapper_namespace: Binding2ExprWrapper.Namespace, + to_expr_wrapper_hoisted: Binding2ExprWrapper.Hoisted, + + // This helps recognize the "import().catch()" pattern. We also try to avoid + // warning about this just like the "try { await import() }" pattern. + then_catch_chain: ThenCatchChain, + + // Temporary variables used for lowering + temp_refs_to_declare: List(TempRef), + temp_ref_count: i32 = 0, + + // When bundling, hoisted top-level local variables declared with "var" in + // nested scopes are moved up to be declared in the top-level scope instead. + // The old "var" statements are turned into regular assignments instead. This + // makes it easier to quickly scan the top-level statements for "var" locals + // with the guarantee that all will be found. + relocated_top_level_vars: List(js_ast.LocRef), + + // ArrowFunction is a special case in the grammar. Although it appears to be + // a PrimaryExpression, it's actually an AssignmentExpression. This means if + // a AssignmentExpression ends up producing an ArrowFunction then nothing can + // come after it other than the comma operator, since the comma operator is + // the only thing above AssignmentExpression under the Expression rule: + // + // AssignmentExpression: + // ArrowFunction + // ConditionalExpression + // LeftHandSideExpression = AssignmentExpression + // LeftHandSideExpression AssignmentOperator AssignmentExpression + // + // Expression: + // AssignmentExpression + // Expression , AssignmentExpression + // + after_arrow_body_loc: logger.Loc = logger.Loc.Empty, + import_transposer: ImportTransposer, + require_transposer: RequireTransposer, + require_resolve_transposer: RequireResolveTransposer, + + const TransposeState = struct { + is_await_target: bool = false, + is_then_catch_target: bool = false, + loc: logger.Loc, + }; + + pub fn transposeImport(p: *P, arg: Expr, state: anytype) Expr { + // The argument must be a string + if (@as(Expr.Tag, arg.data) == .e_string) { + // Ignore calls to import() if the control flow is provably dead here. + // We don't want to spend time scanning the required files if they will + // never be used. + if (p.is_control_flow_dead) { + return p.e(E.Null{}, arg.loc); + } + const str = arg.data.e_string; + + const import_record_index = p.addImportRecord(.dynamic, arg.loc, p.lexer.utf16ToString(str.value)); + p.import_records.items[import_record_index].handles_import_errors = (state.is_await_target and p.fn_or_arrow_data_visit.try_body_count != 0) or state.is_then_catch_target; + p.import_records_for_current_part.append(import_record_index) catch unreachable; + return p.e(E.Import{ + .expr = arg, + .import_record_index = @intCast(Ref.Int, import_record_index), + // .leading_interior_comments = arg.data.e_string. + }, state.loc); + } + + // Use a debug log so people can see this if they want to + const r = js_lexer.rangeOfIdentifier(&p.source, state.loc); + p.log.addRangeDebug(p.source, r, "This \"import\" expression will not be bundled because the argument is not a string literal") catch unreachable; + + return p.e(E.Import{ + .expr = arg, + .import_record_index = Ref.None.source_index, + }, state.loc); + } + + pub fn transposeRequireResolve(p: *P, arg: Expr, transpose_state: anytype) Expr { + return arg; + } + + pub fn transposeRequire(p: *P, arg: Expr, transpose_state: anytype) Expr { + return arg; + } + + const ImportTransposer = ExpressionTransposer(P, P.transposeImport); + const RequireTransposer = ExpressionTransposer(P, P.transposeRequire); + const RequireResolveTransposer = ExpressionTransposer(P, P.transposeRequireResolve); + + const Binding2ExprWrapper = struct { + pub const Namespace = Binding.ToExpr(P, P.wrapIdentifierNamespace); + pub const Hoisted = Binding.ToExpr(P, P.wrapIdentifierHoisting); + }; + + pub fn s(p: *P, t: anytype, loc: logger.Loc) Stmt { + if (@typeInfo(@TypeOf(t)) == .Pointer) { + return Stmt.init(t, loc); + } else { + return Stmt.alloc(p.allocator, t, loc); + } + } + pub fn e(p: *P, t: anytype, loc: logger.Loc) Expr { + if (@typeInfo(@TypeOf(t)) == .Pointer) { + return Expr.init(t, loc); + } else { + return Expr.alloc(p.allocator, t, loc); + } + } + + pub fn b(p: *P, t: anytype, loc: logger.Loc) Binding { + if (@typeInfo(@TypeOf(t)) == .Pointer) { + return Binding.init(t, loc); + } else { + return Binding.alloc(p.allocator, t, loc); + } + } + + pub fn deinit(parser: *P) void { + parser.allocated_names.deinit(); + parser.scopes_for_current_part.deinit(); + parser.symbols.deinit(); + parser.ts_use_counts.deinit(); + parser.declared_symbols.deinit(); + parser.known_enum_values.deinit(); + parser.import_records.deinit(); + parser.import_records_for_current_part.deinit(); + parser.export_star_import_records.deinit(); + parser.import_items_for_namespace.deinit(); + parser.named_imports.deinit(); + parser.top_level_symbol_to_parts.deinit(); + parser.import_namespace_cc_map.deinit(); + parser.scopes_in_order.deinit(); + parser.temp_refs_to_declare.deinit(); + parser.relocated_top_level_vars.deinit(); + } + + pub fn findSymbol(p: *P, loc: logger.Loc, name: string) !FindSymbolResult { + var ref: Ref = undefined; + var declare_loc: logger.Loc = undefined; + var is_inside_with_scope = false; + var did_forbid_argumen = false; + var scope = p.current_scope; + + while (true) { + + // Track if we're inside a "with" statement body + if (scope.kind == .with) { + is_inside_with_scope = true; + } + + // Forbid referencing "arguments" inside class bodies + if (scope.forbid_arguments and strings.eql(name, "arguments") and !did_forbid_argumen) { + const r = js_lexer.rangeOfIdentifier(&p.source, loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot access \"{s}\" here", .{name}) catch unreachable; + did_forbid_argumen = true; + } + + // Is the symbol a member of this scope? + if (scope.members.get(name)) |member| { + ref = member.ref; + declare_loc = member.loc; + break; + } + + if (scope.parent) |parent| { + scope = parent; + } else { + // Allocate an "unbound" symbol + p.checkForNonBMPCodePoint(loc, name); + ref = try p.newSymbol(.unbound, name); + declare_loc = loc; + try p.module_scope.members.put(name, js_ast.Scope.Member{ .ref = ref, .loc = logger.Loc.Empty }); + break; + } + } + + // If we had to pass through a "with" statement body to get to the symbol + // declaration, then this reference could potentially also refer to a + // property on the target object of the "with" statement. We must not rename + // it or we risk changing the behavior of the code. + if (is_inside_with_scope) { + p.symbols.items[ref.inner_index].must_not_be_renamed = true; + } + + // Track how many times we've referenced this symbol + p.recordUsage(&ref); + return FindSymbolResult{ + .ref = ref, + .declare_loc = declare_loc, + .is_inside_with_scope = is_inside_with_scope, + }; + } + + pub fn recordExportedBinding(p: *P, binding: Binding) void { + switch (binding.data) { + .b_missing => {}, + .b_identifier => |ident| { + p.recordExport(binding.loc, p.symbols.items[ident.ref.inner_index].original_name, ident.ref) catch unreachable; + }, + .b_array => |array| { + for (array.items) |prop| { + p.recordExportedBinding(prop.binding); + } + }, + .b_object => |obj| { + for (obj.properties) |prop| { + p.recordExportedBinding(prop.value); + } + }, + else => { + p.panic("Unexpected binding export type {s}", .{binding}); + }, + } + } + + pub fn recordExport(p: *P, loc: logger.Loc, alias: string, ref: Ref) !void { + if (p.named_exports.get(alias)) |name| { + // Duplicate exports are an error + var notes = try p.allocator.alloc(logger.Data, 1); + notes[0] = logger.Data{ + .text = try std.fmt.allocPrint(p.allocator, "\"{s}\" was originally exported here", .{alias}), + .location = logger.Location.init_or_nil(p.source, js_lexer.rangeOfIdentifier(&p.source, name.alias_loc)), + }; + try p.log.addRangeErrorFmtWithNotes( + p.source, + js_lexer.rangeOfIdentifier(&p.source, loc), + p.allocator, + notes, + "Multiple exports with the same name {s}", + .{alias}, + ); + } else { + try p.named_exports.put(alias, js_ast.NamedExport{ .alias_loc = loc, .ref = ref }); + } + } + + pub fn recordUsage(p: *P, ref: *const js_ast.Ref) void { + // The use count stored in the symbol is used for generating symbol names + // during minification. These counts shouldn't include references inside dead + // code regions since those will be culled. + if (!p.is_control_flow_dead) { + p.symbols.items[ref.inner_index].use_count_estimate += 1; + var use = p.symbol_uses.get(ref.*) orelse unreachable; + use.count_estimate += 1; + p.symbol_uses.put(ref.*, use) catch unreachable; + } + + // The correctness of TypeScript-to-JavaScript conversion relies on accurate + // symbol use counts for the whole file, including dead code regions. This is + // tracked separately in a parser-only data structure. + if (p.options.ts) { + p.ts_use_counts.items[ref.inner_index] += 1; + } + } + + pub fn findSymbolHelper(self: *P, loc: logger.Loc, name: string) ?js_ast.Ref { + if (self.findSymbol(loc, name)) |sym| { + return sym.ref; + } + + return null; + } + + pub fn symbolForDefineHelper(self: *P, i: usize) ?js_ast.Ref { + if (self.injected_define_symbols.items.len > i) { + return self.injected_define_symbols.items[i]; + } + + return null; + } + + pub fn logArrowArgErrors(p: *P, errors: *DeferredArrowArgErrors) void { + if (errors.invalid_expr_await.len > 0) { + var r = errors.invalid_expr_await; + p.log.addRangeError(p.source, r, "Cannot use an \"await\" expression here") catch unreachable; + } + + if (errors.invalid_expr_yield.len > 0) { + var r = errors.invalid_expr_yield; + p.log.addRangeError(p.source, r, "Cannot use a \"yield\" expression here") catch unreachable; + } + } + + pub fn keyNameForError(p: *P, key: js_ast.Expr) string { + switch (key.data) { + .e_string => { + return p.lexer.raw(); + }, + .e_private_identifier => { + return p.lexer.raw(); + // return p.loadNameFromRef() + }, + else => { + return "property"; + }, + } + } + + pub fn canMergeSymbols(p: *P, scope: *js_ast.Scope, existing: Symbol.Kind, new: Symbol.Kind) SymbolMergeResult { + if (existing == .unbound) { + return .replace_with_new; + } + + // In TypeScript, imports are allowed to silently collide with symbols within + // the module. Presumably this is because the imports may be type-only: + // + // import {Foo} from 'bar' + // class Foo {} + // + if (p.options.ts and existing == .import) { + return .replace_with_new; + } + + // "enum Foo {} enum Foo {}" + // "namespace Foo { ... } enum Foo {}" + if (new == .ts_enum and (existing == .ts_enum or existing == .ts_namespace)) { + return .replace_with_new; + } + + // "namespace Foo { ... } namespace Foo { ... }" + // "function Foo() {} namespace Foo { ... }" + // "enum Foo {} namespace Foo { ... }" + if (new == .ts_namespace) { + switch (existing) { + .ts_namespace, .hoisted_function, .generator_or_async_function, .ts_enum, .class => { + return .keep_existing; + }, + else => {}, + } + } + + // "var foo; var foo;" + // "var foo; function foo() {}" + // "function foo() {} var foo;" + // "function *foo() {} function *foo() {}" but not "{ function *foo() {} function *foo() {} }" + if (Symbol.isKindHoistedOrFunction(new) and Symbol.isKindHoistedOrFunction(existing) and (scope.kind == .entry or scope.kind == .function_body or + (Symbol.isKindHoisted(new) and Symbol.isKindHoisted(existing)))) + { + return .keep_existing; + } + + // "get #foo() {} set #foo() {}" + // "set #foo() {} get #foo() {}" + if ((existing == .private_get and new == .private_set) or + (existing == .private_set and new == .private_get)) + { + return .become_private_get_set_pair; + } + if ((existing == .private_static_get and new == .private_static_set) or + (existing == .private_static_set and new == .private_static_get)) + { + return .become_private_static_get_set_pair; + } + + // "try {} catch (e) { var e }" + if (existing == .catch_identifier and new == .hoisted) { + return .replace_with_new; + } + + // "function() { var arguments }" + if (existing == .arguments and new == .hoisted) { + return .keep_existing; + } + + // "function() { let arguments }" + if (existing == .arguments and new != .hoisted) { + return .overwrite_with_new; + } + + return .forbidden; + } + + pub fn handleIdentifier(p: *P, loc: logger.Loc, ident: *E.Identifier, _original_name: ?string, opts: IdentifierOpts) Expr { + const ref = ident.ref; + + if ((opts.assign_target != .none or opts.is_delete_target) and p.symbols.items[ref.inner_index].kind == .import) { + // Create an error for assigning to an import namespace + const r = js_lexer.rangeOfIdentifier(&p.source, loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot assign to import \"{s}\"", .{ + p.symbols.items[ref.inner_index].original_name, + }) catch unreachable; + } + + // Substitute an EImportIdentifier now if this is an import item + if (p.is_import_item.contains(ref)) { + return p.e( + E.ImportIdentifier{ .ref = ref, .was_originally_identifier = opts.was_originally_identifier }, + loc, + ); + } + + // Substitute a namespace export reference now if appropriate + if (p.options.ts) { + if (p.is_exported_inside_namespace.get(ref)) |ns_ref| { + const name = p.symbols.items[ref.inner_index].original_name; + + // If this is a known enum value, inline the value of the enum + if (p.known_enum_values.get(ns_ref)) |enum_values| { + if (enum_values.get(name)) |number| { + return p.e(E.Number{ .value = number }, loc); + } + } + + // Otherwise, create a property access on the namespace + p.recordUsage(&ns_ref); + + return p.e(E.Dot{ .target = p.e(E.Identifier{ .ref = ns_ref }, loc), .name = name, .name_loc = loc }, loc); + } + } + + if (_original_name) |original_name| { + const result = p.findSymbol(loc, original_name) catch unreachable; + ident.ref = result.ref; + } + + return p.e(ident, loc); + } + + pub fn prepareForVisitPass(p: *P) !void { + try p.pushScopeForVisitPass(js_ast.Scope.Kind.entry, locModuleScope); + p.fn_or_arrow_data_visit.is_outside_fn_or_arrow = true; + p.module_scope = p.current_scope; + p.has_es_module_syntax = p.es6_import_keyword.len > 0 or p.es6_export_keyword.len > 0 or p.top_level_await_keyword.len > 0; + + // ECMAScript modules are always interpreted as strict mode. This has to be + // done before "hoistSymbols" because strict mode can alter hoisting (!). + if (p.es6_import_keyword.len > 0) { + p.module_scope.recursiveSetStrictMode(js_ast.StrictModeKind.implicit_strict_mode_import); + } else if (p.es6_export_keyword.len > 0) { + p.module_scope.recursiveSetStrictMode(js_ast.StrictModeKind.implicit_strict_mode_export); + } else if (p.top_level_await_keyword.len > 0) { + p.module_scope.recursiveSetStrictMode(js_ast.StrictModeKind.implicit_strict_mode_top_level_await); + } + + p.hoistSymbols(p.module_scope); + + p.require_ref = try p.newSymbol(.unbound, "require"); + p.exports_ref = try p.newSymbol(.hoisted, "exports"); + p.module_ref = try p.newSymbol(.hoisted, "module"); + } + + pub fn hoistSymbols(p: *P, scope: *js_ast.Scope) void { + if (!scope.kindStopsHoisting()) { + var iter = scope.members.iterator(); + nextMember: while (iter.next()) |res| { + var symbol = p.symbols.items[res.value.ref.inner_index]; + if (!symbol.isHoisted()) { + continue :nextMember; + } + } + } + } + + pub fn unshiftScopeOrder(self: *P) !ScopeOrder { + if (self.scopes_in_order.items.len == 0) { + var scope = try js_ast.Scope.initPtr(self.allocator); + return ScopeOrder{ + .scope = scope, + .loc = logger.Loc.Empty, + }; + } else { + return self.scopes_in_order.orderedRemove(0); + } + } + + pub fn pushScopeForVisitPass(p: *P, kind: js_ast.Scope.Kind, loc: logger.Loc) !void { + var order = try p.unshiftScopeOrder(); + + // Sanity-check that the scopes generated by the first and second passes match + if (!order.loc.eql(loc) or order.scope.kind != kind) { + p.panic("Expected scope ({s}, {d}) in {s}, found scope ({s}, {d})", .{ kind, loc.start, p.source.path.pretty, order.scope.kind, order.loc.start }); + } + + p.current_scope = order.scope; + + try p.scopes_for_current_part.append(order.scope); + } + + pub fn pushScopeForParsePass(p: *P, kind: js_ast.Scope.Kind, loc: logger.Loc) !usize { + debugl("<pushScopeForParsePass>"); + defer debugl("</pushScopeForParsePass>"); + var scope = try Scope.initPtr(p.allocator); + scope.kind = kind; + scope.label_ref = null; + + var parent: *Scope = undefined; + + if (kind != .entry) { + parent = p.current_scope; + scope.parent = parent; + try parent.children.append(scope); + scope.strict_mode = parent.strict_mode; + } + + p.current_scope = scope; + + // Enforce that scope locations are strictly increasing to help catch bugs + // where the pushed scopes are mistmatched between the first and second passes + if (p.scopes_in_order.items.len > 0) { + const prev_start = p.scopes_in_order.items[p.scopes_in_order.items.len - 1].loc.start; + if (prev_start >= loc.start) { + p.panic("Scope location {d} must be greater than {d}", .{ loc.start, prev_start }); + } + } + + // Copy down function arguments into the function body scope. That way we get + // errors if a statement in the function body tries to re-declare any of the + // arguments. + if (kind == js_ast.Scope.Kind.function_body) { + if (parent.kind != js_ast.Scope.Kind.function_args) { + p.panic("Internal error", .{}); + } + + var iter = scope.parent.?.members.iterator(); + while (iter.next()) |entry| { + // // Don't copy down the optional function expression name. Re-declaring + // // the name of a function expression is allowed. + const adjacent_symbols = p.symbols.items[entry.value.ref.inner_index]; + if (adjacent_symbols.kind != .hoisted_function) { + try scope.members.put(entry.key, entry.value); + } + } + } + + // Remember the length in case we call popAndDiscardScope() later + const scope_index = p.scopes_in_order.items.len; + try p.scopes_in_order.append(ScopeOrder{ .loc = loc, .scope = scope }); + + return scope_index; + } + + // Note: do not write to "p.log" in this function. Any errors due to conversion + // from expression to binding should be written to "invalidLog" instead. That + // way we can potentially keep this as an expression if it turns out it's not + // needed as a binding after all. + pub fn convertExprToBinding(p: *P, expr: ExprNodeIndex, invalid_loc: *LocList) ?Binding { + switch (expr.data) { + .e_missing => { + return p.b(B.Missing{}, expr.loc); + }, + .e_identifier => |ex| { + return p.b(B.Identifier{ .ref = ex.ref }, expr.loc); + }, + .e_array => |ex| { + if (ex.comma_after_spread) |spread| { + invalid_loc.append(spread) catch unreachable; + } + + if (ex.is_parenthesized) { + invalid_loc.append(p.source.rangeOfOperatorBefore(expr.loc, "(").loc) catch unreachable; + } + + // p.markSyntaxFeature(Destructing) + var items = List(js_ast.ArrayBinding).init(p.allocator); + for (items.items) |item| { + var is_spread = true; + switch (item.default_value.?.data) { + .e_identifier => {}, + else => { + // nested rest binding + // p.markSyntaxFeature(compat.NestedRestBinding, p.source.RangeOfOperatorAfter(item.Loc, "[")) + }, + } + var _expr = expr; + const res = p.convertExprToBindingAndInitializer(&_expr, invalid_loc, is_spread); + assert(res.binding != null); + items.append(js_ast.ArrayBinding{ .binding = res.binding orelse unreachable, .default_value = res.override_expr }) catch unreachable; + } + + return p.b(B.Array{ + .items = items.toOwnedSlice(), + .has_spread = ex.comma_after_spread != null, + .is_single_line = ex.is_single_line, + }, expr.loc); + }, + .e_object => |ex| { + if (ex.comma_after_spread) |sp| { + invalid_loc.append(sp) catch unreachable; + } + + if (ex.is_parenthesized) { + invalid_loc.append(p.source.rangeOfOperatorBefore(expr.loc, "(").loc) catch unreachable; + } + // p.markSyntaxFeature(compat.Destructuring, p.source.RangeOfOperatorAfter(expr.Loc, "{")) + + var properties = List(B.Property).init(p.allocator); + for (ex.properties) |item| { + if (item.flags.is_method or item.kind == .get or item.kind == .set) { + invalid_loc.append(item.key.?.loc) catch unreachable; + continue; + } + var value = &(item.value orelse unreachable); + const tup = p.convertExprToBindingAndInitializer(value, invalid_loc, false); + const initializer = tup.expr orelse item.initializer; + + properties.append(B.Property{ + .flags = Flags.Property{ + .is_spread = item.kind == .spread, + .is_computed = item.flags.is_computed, + }, + + .key = item.key orelse p.panic("Internal error: Expected {s} to have a key.", .{item}), + .value = tup.binding orelse p.panic("Internal error: Expected {s} to have a binding.", .{tup}), + .default_value = initializer, + }) catch unreachable; + } + + return p.b(B.Object{ + .properties = properties.toOwnedSlice(), + .is_single_line = ex.is_single_line, + }, expr.loc); + }, + else => { + invalid_loc.append(expr.loc) catch unreachable; + return null; + }, + } + + return null; + } + + pub fn convertExprToBindingAndInitializer(p: *P, expr: *ExprNodeIndex, invalid_log: *LocList, is_spread: bool) ExprBindingTuple { + var initializer: ?ExprNodeIndex = null; + var override: ?ExprNodeIndex = null; + // zig syntax is sometimes painful + switch (expr.*.data) { + .e_binary => |bin| { + if (bin.op == .bin_assign) { + initializer = bin.right; + override = bin.left; + } + }, + else => {}, + } + + var bind = p.convertExprToBinding(expr.*, invalid_log); + if (initializer) |initial| { + const equalsRange = p.source.rangeOfOperatorBefore(initial.loc, "="); + if (is_spread) { + p.log.addRangeError(p.source, equalsRange, "A rest argument cannot have a default initializer") catch unreachable; + } else { + // p.markSyntaxFeature(); + } + } + return ExprBindingTuple{ .binding = bind, .expr = initializer }; + } + + pub fn forbidLexicalDecl(p: *P, loc: logger.Loc) !void { + try p.log.addRangeError(p.source, p.lexer.range(), "Cannot use a declaration in a single-statement context"); + } + + pub fn logExprErrors(p: *P, errors: *DeferredErrors) void { + if (errors.invalid_expr_default_value) |r| { + p.log.addRangeError( + p.source, + r, + "Unexpected \"=\"", + ) catch unreachable; + } + + if (errors.invalid_expr_after_question) |r| { + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Unexpected {s}", .{p.source.contents[r.loc.i()..r.endI()]}) catch unreachable; + } + + // if (errors.array_spread_feature) |err| { + // p.markSyntaxFeature(compat.ArraySpread, errors.arraySpreadFeature) + // } + } + + // This assumes the "function" token has already been parsed + + pub fn parseFnStmt(p: *P, loc: logger.Loc, opts: *ParseStatementOptions, asyncRange: ?logger.Range) !Stmt { + const isGenerator = p.lexer.token == T.t_asterisk; + const isAsync = asyncRange != null; + + // if isGenerator { + // p.markSyntaxFeature(compat.Generator, p.lexer.Range()) + // p.lexer.Next() + // } else if isAsync { + // p.markLoweredSyntaxFeature(compat.AsyncAwait, asyncRange, compat.Generator) + // } + + switch (opts.lexical_decl) { + .forbid => { + try p.forbidLexicalDecl(loc); + }, + + // Allow certain function statements in certain single-statement contexts + .allow_fn_inside_if, .allow_fn_inside_label => { + if (opts.is_typescript_declare or isGenerator or isAsync) { + try p.forbidLexicalDecl(loc); + } + }, + else => {}, + } + + var name: ?js_ast.LocRef = null; + var nameText: string = undefined; + + // The name is optional for "export default function() {}" pseudo-statements + if (!opts.is_name_optional or p.lexer.token == T.t_identifier) { + var nameLoc = p.lexer.loc(); + nameText = p.lexer.identifier; + p.lexer.expect(T.t_identifier); + // Difference + const ref = try p.newSymbol(Symbol.Kind.other, nameText); + name = js_ast.LocRef{ + .loc = nameLoc, + .ref = ref, + }; + } + + // Even anonymous functions can have TypeScript type parameters + if (p.options.ts) { + p.skipTypescriptTypeParameters(); + } + + // Introduce a fake block scope for function declarations inside if statements + var ifStmtScopeIndex: usize = 0; + var hasIfScope = opts.lexical_decl == .allow_fn_inside_if; + if (hasIfScope) { + ifStmtScopeIndex = try p.pushScopeForParsePass(js_ast.Scope.Kind.block, loc); + } + + var scopeIndex = try p.pushScopeForParsePass(js_ast.Scope.Kind.function_args, p.lexer.loc()); + var func = p.parseFn(name, FnOrArrowDataParse{ + .async_range = asyncRange, + .allow_await = if (isAsync) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, + .allow_yield = if (isGenerator) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, + .is_typescript_declare = opts.is_typescript_declare, + + // Only allow omitting the body if we're parsing TypeScript + .allow_missing_body_for_type_script = p.options.ts, + }); + + // Don't output anything if it's just a forward declaration of a function + if (opts.is_typescript_declare or func.body == null) { + p.popAndDiscardScope(scopeIndex); + + // Balance the fake block scope introduced above + if (hasIfScope) { + p.popScope(); + } + + if (opts.is_typescript_declare and opts.is_namespace_scope and opts.is_export) { + p.has_non_local_export_declare_inside_namespace = true; + } + + return p.s(S.TypeScript{}, loc); + } + + p.popScope(); + + // Only declare the function after we know if it had a body or not. Otherwise + // TypeScript code such as this will double-declare the symbol: + // + // function foo(): void; + // function foo(): void {} + // + if (name) |*name_| { + const kind = if (isGenerator or isAsync) Symbol.Kind.generator_or_async_function else Symbol.Kind.hoisted_function; + name_.ref = try p.declareSymbol(kind, name_.loc, nameText); + } + func.name = name; + + func.flags.has_if_scope = hasIfScope; + + func.flags.is_export = opts.is_export; + + // Balance the fake block scope introduced above + if (hasIfScope) { + p.popScope(); + } + + return p.s(S.Function{ + .func = func, + }, func.open_parens_loc); + } + + pub fn popAndDiscardScope(p: *P, scope_index: usize) void { + // Move up to the parent scope + var to_discard = p.current_scope; + var parent = to_discard.parent orelse unreachable; + + p.current_scope = parent; + + // Truncate the scope order where we started to pretend we never saw this scope + p.scopes_in_order.shrinkRetainingCapacity(scope_index); + + var children = parent.children; + // Remove the last child from the parent scope + var last = children.items.len - 1; + if (children.items[last] != to_discard) { + p.panic("Internal error", .{}); + } + + _ = children.popOrNull(); + } + + pub fn parseFn(p: *P, name: ?js_ast.LocRef, opts: FnOrArrowDataParse) G.Fn { + // if data.allowAwait and data.allowYield { + // p.markSyntaxFeature(compat.AsyncGenerator, data.asyncRange) + // } + + var func = G.Fn{ + .name = name, + .flags = Flags.Function{ + .has_rest_arg = false, + .is_async = opts.allow_await == .allow_expr, + .is_generator = opts.allow_yield == .allow_expr, + }, + + .arguments_ref = null, + .open_parens_loc = p.lexer.loc(), + }; + p.lexer.expect(T.t_open_paren); + + // Await and yield are not allowed in function arguments + var old_fn_or_arrow_data = opts; + p.fn_or_arrow_data_parse.allow_await = if (opts.allow_await == .allow_expr) AwaitOrYield.forbid_all else AwaitOrYield.allow_ident; + p.fn_or_arrow_data_parse.allow_yield = if (opts.allow_yield == .allow_expr) AwaitOrYield.forbid_all else AwaitOrYield.allow_ident; + + // If "super()" is allowed in the body, it's allowed in the arguments + p.fn_or_arrow_data_parse.allow_super_call = opts.allow_super_call; + var args = List(G.Arg).init(p.allocator); + while (p.lexer.token != T.t_close_paren) { + // Skip over "this" type annotations + if (p.options.ts and p.lexer.token == T.t_this) { + p.lexer.next(); + if (p.lexer.token == T.t_colon) { + p.lexer.next(); + p.skipTypescriptType(js_ast.Op.Level.lowest); + } + if (p.lexer.token != T.t_comma) { + break; + } + + p.lexer.next(); + continue; + } + + var ts_decorators: []ExprNodeIndex = undefined; + if (opts.allow_ts_decorators) { + ts_decorators = p.parseTypeScriptDecorators(); + } + + if (!func.flags.has_rest_arg and p.lexer.token == T.t_dot_dot_dot) { + // p.markSyntaxFeature + p.lexer.next(); + func.flags.has_rest_arg = true; + } + + var is_typescript_ctor_field = false; + var is_identifier = p.lexer.token == T.t_identifier; + var text = p.lexer.identifier; + var arg = p.parseBinding(); + + if (p.options.ts and is_identifier and opts.is_constructor) { + // Skip over TypeScript accessibility modifiers, which turn this argument + // into a class field when used inside a class constructor. This is known + // as a "parameter property" in TypeScript. + while (true) { + switch (p.lexer.token) { + .t_identifier, .t_open_brace, .t_open_bracket => { + if (!js_lexer.TypeScriptAccessibilityModifier.has(p.lexer.identifier)) { + break; + } + + is_typescript_ctor_field = true; + + // TypeScript requires an identifier binding + if (p.lexer.token != .t_identifier) { + p.lexer.expect(.t_identifier); + } + text = p.lexer.identifier; + + // Re-parse the binding (the current binding is the TypeScript keyword) + arg = p.parseBinding(); + }, + else => { + break; + }, + } + } + + // "function foo(a?) {}" + if (p.lexer.token == .t_question) { + p.lexer.next(); + } + + // "function foo(a: any) {}" + if (p.lexer.token == .t_colon) { + p.lexer.next(); + p.skipTypescriptType(.lowest); + } + } + + var parseStmtOpts = ParseStatementOptions{}; + p.declareBinding(.hoisted, arg, &parseStmtOpts) catch unreachable; + + var default_value: ?ExprNodeIndex = null; + if (!func.flags.has_rest_arg and p.lexer.token == .t_equals) { + // p.markSyntaxFeature + p.lexer.next(); + default_value = p.parseExpr(.comma); + } + + args.append(G.Arg{ + .ts_decorators = ts_decorators, + .binding = arg, + .default = default_value, + + // We need to track this because it affects code generation + .is_typescript_ctor_field = is_typescript_ctor_field, + }) catch unreachable; + + if (p.lexer.token != .t_comma) { + break; + } + + if (func.flags.has_rest_arg) { + // JavaScript does not allow a comma after a rest argument + if (opts.is_typescript_declare) { + // TypeScript does allow a comma after a rest argument in a "declare" context + p.lexer.next(); + } else { + p.lexer.expect(.t_close_paren); + } + + break; + } + + p.lexer.next(); + } + func.args = args.toOwnedSlice(); + + // Reserve the special name "arguments" in this scope. This ensures that it + // shadows any variable called "arguments" in any parent scopes. But only do + // this if it wasn't already declared above because arguments are allowed to + // be called "arguments", in which case the real "arguments" is inaccessible. + if (!p.current_scope.members.contains("arguments")) { + func.arguments_ref = p.declareSymbol(.arguments, func.open_parens_loc, "arguments") catch unreachable; + p.symbols.items[func.arguments_ref.?.inner_index].must_not_be_renamed = true; + } + + p.lexer.expect(.t_close_paren); + p.fn_or_arrow_data_parse = old_fn_or_arrow_data; + + // "function foo(): any {}" + if (p.options.ts and p.lexer.token == .t_colon) { + p.lexer.next(); + p.skipTypescriptReturnType(); + } + + // "function foo(): any;" + if (opts.allow_missing_body_for_type_script and p.lexer.token != .t_open_brace) { + p.lexer.expectOrInsertSemicolon(); + return func; + } + var tempOpts = opts; + func.body = p.parseFnBody(&tempOpts) catch unreachable; + + return func; + } + + // pub fn parseBinding(p: *P) + + // TODO: + pub fn skipTypescriptReturnType(p: *P) void { + notimpl(); + } + + // TODO: + pub fn parseTypeScriptDecorators(p: *P) []ExprNodeIndex { + if (!p.options.ts) { + return &([_]ExprNodeIndex{}); + } + + var decorators = List(ExprNodeIndex).init(p.allocator); + while (p.lexer.token == T.t_at) { + p.lexer.next(); + + // Parse a new/call expression with "exprFlagTSDecorator" so we ignore + // EIndex expressions, since they may be part of a computed property: + // + // class Foo { + // @foo ['computed']() {} + // } + // + // This matches the behavior of the TypeScript compiler. + decorators.append(p.parseExprWithFlags(.new, Expr.EFlags.ts_decorator)) catch unreachable; + } + + return decorators.toOwnedSlice(); + } + + // TODO: + pub fn skipTypescriptType(p: *P, level: js_ast.Op.Level) void { + notimpl(); + } + + // TODO: + pub fn skipTypescriptTypeParameters(p: *P) void { + notimpl(); + } + + fn createDefaultName(p: *P, loc: logger.Loc) !js_ast.LocRef { + var identifier = try std.fmt.allocPrint(p.allocator, "{s}_default", .{p.source.identifier_name}); + + const name = js_ast.LocRef{ .loc = loc, .ref = try p.newSymbol(Symbol.Kind.other, identifier) }; + + var scope = p.current_scope; + + try scope.generated.append(name.ref orelse unreachable); + + return name; + } + + pub fn newSymbol(p: *P, kind: Symbol.Kind, identifier: string) !js_ast.Ref { + const ref = js_ast.Ref{ + .source_index = @intCast(Ref.Int, p.source.index), + .inner_index = @intCast(Ref.Int, p.symbols.items.len), + }; + try p.symbols.append(Symbol{ + .kind = kind, + .original_name = identifier, + .link = null, + }); + + if (p.options.ts) { + try p.ts_use_counts.append(0); + } + + return ref; + } + + pub fn parseLabelName(p: *P) !?js_ast.LocRef { + if (p.lexer.token != .t_identifier or p.lexer.has_newline_before) { + return null; + } + + const name = LocRef{ .loc = p.lexer.loc(), .ref = try p.storeNameInRef(p.lexer.identifier) }; + p.lexer.next(); + return name; + } + + pub fn parseClassStmt(p: *P, loc: logger.Loc, opts: *ParseStatementOptions) Stmt { + var name: ?js_ast.LocRef = null; + var class_keyword = p.lexer.range(); + if (p.lexer.token == .t_class) { + //marksyntaxfeature + p.lexer.next(); + } else { + p.lexer.expected(.t_class); + } + + var is_identifier = p.lexer.token == .t_identifier; + var is_strict_modereserved_word = is_identifier and js_lexer.StrictModeReservedWords.has(p.lexer.identifier); + + if (!opts.is_name_optional or (is_identifier and !is_strict_modereserved_word)) { + var name_loc = p.lexer.loc(); + var name_text = p.lexer.identifier; + if (is_strict_modereserved_word) { + p.lexer.unexpected(); + } + + p.lexer.expect(.t_identifier); + name = LocRef{ .loc = name_loc, .ref = null }; + if (!opts.is_typescript_declare) { + (name orelse unreachable).ref = p.declareSymbol(.class, name_loc, name_text) catch unreachable; + } + } + + // Even anonymous classes can have TypeScript type parameters + if (p.options.ts) { + p.skipTypescriptTypeParameters(); + } + var class_opts = ParseClassOptions{ + .allow_ts_decorators = true, + .is_type_script_declare = opts.is_typescript_declare, + }; + if (opts.ts_decorators) |dec| { + class_opts.ts_decorators = dec.values; + } + + var scope_index = p.pushScopeForParsePass(.class_name, loc) catch unreachable; + var class = p.parseClass(class_keyword, name, class_opts); + + if (opts.is_typescript_declare) { + p.popAndDiscardScope(scope_index); + if (opts.is_namespace_scope and opts.is_export) { + p.has_non_local_export_declare_inside_namespace = true; + } + + return p.s(S.TypeScript{}, loc); + } + + p.popScope(); + return p.s(S.Class{ + .class = class, + .is_export = opts.is_export, + }, loc); + } + + pub fn parseStmt(p: *P, opts: *ParseStatementOptions) !Stmt { + var loc = p.lexer.loc(); + + switch (p.lexer.token) { + .t_semicolon => { + p.lexer.next(); + return Stmt.empty(); + }, + + .t_export => { + var previousExportKeyword = p.es6_export_keyword; + if (opts.is_module_scope) { + p.es6_export_keyword = p.lexer.range(); + } else if (!opts.is_namespace_scope) { + p.lexer.unexpected(); + } + p.lexer.next(); + + // TypeScript decorators only work on class declarations + // "@decorator export class Foo {}" + // "@decorator export abstract class Foo {}" + // "@decorator export default class Foo {}" + // "@decorator export default abstract class Foo {}" + // "@decorator export declare class Foo {}" + // "@decorator export declare abstract class Foo {}" + if (opts.ts_decorators != null and p.lexer.token != js_lexer.T.t_class and p.lexer.token != js_lexer.T.t_default and !p.lexer.isContextualKeyword("abstract") and !p.lexer.isContextualKeyword("declare")) { + p.lexer.expected(js_lexer.T.t_class); + } + + switch (p.lexer.token) { + T.t_class, T.t_const, T.t_function, T.t_var => { + opts.is_export = true; + return p.parseStmt(opts); + }, + + T.t_import => { + // "export import foo = bar" + if (p.options.ts and (opts.is_module_scope or opts.is_namespace_scope)) { + opts.is_export = true; + return p.parseStmt(opts); + } + + p.lexer.unexpected(); + }, + + T.t_enum => { + if (!p.options.ts) { + p.lexer.unexpected(); + } + + opts.is_export = true; + return p.parseStmt(opts); + }, + + T.t_identifier => { + if (p.lexer.isContextualKeyword("let")) { + opts.is_export = true; + return p.parseStmt(opts); + } + + if (opts.is_typescript_declare and p.lexer.isContextualKeyword("as")) { + // "export as namespace ns;" + p.lexer.next(); + p.lexer.expectContextualKeyword("namespace"); + p.lexer.expect(T.t_identifier); + p.lexer.expectOrInsertSemicolon(); + + return p.s(S.TypeScript{}, loc); + } + + if (p.lexer.isContextualKeyword("async")) { + var asyncRange = p.lexer.range(); + p.lexer.next(); + if (p.lexer.has_newline_before) { + try p.log.addRangeError(p.source, asyncRange, "Unexpected newline after \"async\""); + } + + p.lexer.expect(T.t_function); + opts.is_export = true; + return try p.parseFnStmt(loc, opts, asyncRange); + } + + if (p.options.ts) { + notimpl(); + + // switch (p.lexer.identifier) { + // "type" => { + // // "export type foo = ..." + // const typeRange = p.lexer.range(); + // if (p.lexer.has_newline_before) { + // p.lexer.addError(p.source, typeRange.end(), "Unexpected newline after \"type\""); + // return; + // } + + // }, + // } + } + + p.lexer.unexpected(); + lexerpanic(); + }, + + T.t_default => { + if (!opts.is_module_scope and (!opts.is_namespace_scope or !opts.is_typescript_declare)) { + p.lexer.unexpected(); + lexerpanic(); + } + + var defaultLoc = p.lexer.loc(); + p.lexer.next(); + + // TypeScript decorators only work on class declarations + // "@decorator export default class Foo {}" + // "@decorator export default abstract class Foo {}" + if (opts.ts_decorators != null and p.lexer.token != T.t_class and !p.lexer.isContextualKeyword("abstract")) { + p.lexer.expected(T.t_class); + } + + if (p.lexer.isContextualKeyword("async")) { + var async_range = p.lexer.range(); + p.lexer.next(); + var defaultName: js_ast.LocRef = undefined; + if (p.lexer.token == T.t_function and !p.lexer.has_newline_before) { + p.lexer.next(); + var stmtOpts = ParseStatementOptions{ + .is_name_optional = true, + .lexical_decl = .allow_all, + }; + var stmt = try p.parseFnStmt(loc, &stmtOpts, async_range); + if (@as(Stmt.Tag, stmt.data) == .s_type_script) { + // This was just a type annotation + return stmt; + } + + if (stmt.data.s_function.func.name) |name| { + defaultName = js_ast.LocRef{ .loc = defaultLoc, .ref = name.ref }; + } else { + defaultName = try p.createDefaultName(defaultLoc); + } + // this is probably a panic + var value = js_ast.StmtOrExpr{ .stmt = stmt }; + return p.s(S.ExportDefault{ .default_name = defaultName, .value = value }, loc); + } + + defaultName = try createDefaultName(p, loc); + + var expr = p.parseSuffix(try p.parseAsyncPrefixExpr(async_range, Level.comma), Level.comma, null, Expr.EFlags.none); + p.lexer.expectOrInsertSemicolon(); + // this is probably a panic + var value = js_ast.StmtOrExpr{ .expr = expr }; + return p.s(S.ExportDefault{ .default_name = defaultName, .value = value }, loc); + } + + if (p.lexer.token == .t_function or p.lexer.token == .t_class or p.lexer.isContextualKeyword("interface")) { + var _opts = ParseStatementOptions{ + .ts_decorators = opts.ts_decorators, + .is_name_optional = true, + .lexical_decl = .allow_all, + }; + var stmt = p.parseStmt(&_opts) catch unreachable; + + var default_name: js_ast.LocRef = undefined; + + switch (stmt.data) { + // This was just a type annotation + .s_type_script => { + return stmt; + }, + + .s_function => |func_container| { + if (func_container.func.name) |name| { + default_name = LocRef{ .loc = defaultLoc, .ref = name.ref }; + } else {} + }, + .s_class => |class| { + if (class.class.class_name) |name| { + default_name = LocRef{ .loc = defaultLoc, .ref = name.ref }; + } else {} + }, + else => { + p.panic("Internal error: unexpected stmt {s}", .{stmt}); + }, + } + + return p.s( + S.ExportDefault{ .default_name = default_name, .value = js_ast.StmtOrExpr{ .stmt = stmt } }, + loc, + ); + } + + const is_identifier = p.lexer.token == .t_identifier; + const name = p.lexer.identifier; + var expr = p.parseExpr(.comma); + + // Handle the default export of an abstract class in TypeScript + if (p.options.ts and is_identifier and (p.lexer.token == .t_class or opts.ts_decorators != null) and strings.eql(name, "abstract")) { + switch (expr.data) { + .e_identifier => |ident| { + var stmtOpts = ParseStatementOptions{ + .ts_decorators = opts.ts_decorators, + .is_name_optional = true, + }; + const stmt: Stmt = p.parseClassStmt(loc, &stmtOpts); + + // Use the statement name if present, since it's a better name + var default_name: LocRef = undefined; + switch (stmt.data) { + .s_class => |class| { + var ref: Ref = undefined; + var picked = false; + if (class.class.class_name) |loc_ref| { + if (loc_ref.ref) |_ref| { + ref = _ref; + picked = true; + } + } + + if (!picked) { + ref = (createDefaultName(p, defaultLoc) catch unreachable).ref orelse unreachable; + } + default_name = LocRef{ .loc = defaultLoc, .ref = ref }; + }, + else => { + default_name = createDefaultName(p, defaultLoc) catch unreachable; + }, + } + + return p.s(S.ExportDefault{ .default_name = default_name, .value = js_ast.StmtOrExpr{ .stmt = stmt } }, loc); + }, + else => { + p.panic("internal error: unexpected", .{}); + }, + } + } + + p.lexer.expectOrInsertSemicolon(); + return p.s(S.ExportDefault{ .default_name = createDefaultName(p, loc) catch unreachable, .value = js_ast.StmtOrExpr{ .expr = expr } }, loc); + }, + T.t_asterisk => { + if (!opts.is_module_scope and !(opts.is_namespace_scope or !opts.is_typescript_declare)) { + p.lexer.unexpected(); + } + + p.lexer.next(); + var namespace_ref: js_ast.Ref = undefined; + var alias: ?js_ast.G.ExportStarAlias = null; + var path_loc: logger.Loc = undefined; + var path_text: string = undefined; + + if (p.lexer.isContextualKeyword("as")) { + // "export * as ns from 'path'" + const name = p.lexer.identifier; + namespace_ref = p.storeNameInRef(name) catch unreachable; + alias = G.ExportStarAlias{ .loc = p.lexer.loc(), .original_name = name }; + if (!p.lexer.isIdentifierOrKeyword()) { + p.lexer.expect(.t_identifier); + } + p.checkForNonBMPCodePoint((alias orelse unreachable).loc, name); + p.lexer.next(); + p.lexer.expectContextualKeyword("from"); + const parsedPath = p.parsePath(); + path_loc = parsedPath.loc; + path_text = parsedPath.text; + } else { + // "export * from 'path'" + p.lexer.expectContextualKeyword("from"); + const parsedPath = p.parsePath(); + path_loc = parsedPath.loc; + path_text = parsedPath.text; + var path_name = fs.PathName.init(strings.append(p.allocator, path_text, "_star") catch unreachable); + namespace_ref = p.storeNameInRef(path_name.nonUniqueNameString(p.allocator) catch unreachable) catch unreachable; + } + + var import_record_index = p.addImportRecord(ImportKind.stmt, path_loc, path_text); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.ExportStar{ + .namespace_ref = namespace_ref, + .alias = alias, + .import_record_index = import_record_index, + }, loc); + }, + T.t_open_brace => { + if (!opts.is_module_scope and !(opts.is_namespace_scope or !opts.is_typescript_declare)) { + p.lexer.unexpected(); + } + + const export_clause = try p.parseExportClause(); + if (p.lexer.isContextualKeyword("from")) { + p.lexer.expectContextualKeyword("from"); + const parsedPath = p.parsePath(); + const import_record_index = p.addImportRecord(.stmt, parsedPath.loc, parsedPath.text); + var path_name = fs.PathName.init(strings.append(p.allocator, "import_", parsedPath.text) catch unreachable); + const namespace_ref = p.storeNameInRef(path_name.nonUniqueNameString(p.allocator) catch unreachable) catch unreachable; + p.lexer.expectOrInsertSemicolon(); + return p.s(S.ExportFrom{ .items = export_clause.clauses, .is_single_line = export_clause.is_single_line, .namespace_ref = namespace_ref, .import_record_index = import_record_index }, loc); + } + p.lexer.expectOrInsertSemicolon(); + return p.s(S.ExportClause{ .items = export_clause.clauses, .is_single_line = export_clause.is_single_line }, loc); + }, + T.t_equals => { + // "export = value;" + + p.es6_export_keyword = previousExportKeyword; // This wasn't an ESM export statement after all + if (p.options.ts) { + p.lexer.next(); + var value = p.parseExpr(.lowest); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.ExportEquals{ .value = value }, loc); + } + p.lexer.unexpected(); + return Stmt.empty(); + }, + else => { + p.lexer.unexpected(); + return Stmt.empty(); + }, + } + }, + + .t_function => { + p.lexer.next(); + return p.parseFnStmt(loc, opts, null); + }, + .t_enum => { + if (!p.options.ts) { + p.lexer.unexpected(); + } + return p.parseTypescriptEnumStmt(loc, opts); + }, + .t_at => { + // Parse decorators before class statements, which are potentially exported + if (p.options.ts) { + const scope_index = p.scopes_in_order.items.len; + const ts_decorators = p.parseTypeScriptDecorators(); + + // If this turns out to be a "declare class" statement, we need to undo the + // scopes that were potentially pushed while parsing the decorator arguments. + // That can look like any one of the following: + // + // "@decorator declare class Foo {}" + // "@decorator declare abstract class Foo {}" + // "@decorator export declare class Foo {}" + // "@decorator export declare abstract class Foo {}" + // + opts.ts_decorators = DeferredTsDecorators{ + .values = ts_decorators, + .scope_index = scope_index, + }; + + // "@decorator class Foo {}" + // "@decorator abstract class Foo {}" + // "@decorator declare class Foo {}" + // "@decorator declare abstract class Foo {}" + // "@decorator export class Foo {}" + // "@decorator export abstract class Foo {}" + // "@decorator export declare class Foo {}" + // "@decorator export declare abstract class Foo {}" + // "@decorator export default class Foo {}" + // "@decorator export default abstract class Foo {}" + if (p.lexer.token != .t_class and p.lexer.token != .t_export and !p.lexer.isContextualKeyword("abstract") and !p.lexer.isContextualKeyword("declare")) { + p.lexer.expected(.t_class); + } + + return p.parseStmt(opts); + } + // notimpl(); + }, + .t_class => { + if (opts.lexical_decl != .allow_all) { + try p.forbidLexicalDecl(loc); + } + + return p.parseClassStmt(loc, opts); + }, + .t_var => { + p.lexer.next(); + const decls = p.parseAndDeclareDecls(.hoisted, opts); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.Local{ .kind = .k_var, .decls = decls, .is_export = opts.is_export }, loc); + }, + .t_const => { + if (opts.lexical_decl != .allow_all) { + try p.forbidLexicalDecl(loc); + } + // p.markSyntaxFeature(compat.Const, p.lexer.Range()) + + p.lexer.next(); + + if (p.options.ts and p.lexer.token == T.t_enum) { + return p.parseTypescriptEnumStmt(loc, opts); + } + + const decls = p.parseAndDeclareDecls(.cconst, opts); + p.lexer.expectOrInsertSemicolon(); + + if (!opts.is_typescript_declare) { + try p.requireInitializers(decls); + } + + return p.s(S.Local{ .kind = .k_const, .decls = decls, .is_export = opts.is_export }, loc); + }, + .t_if => { + p.lexer.next(); + p.lexer.expect(.t_open_paren); + const test_ = p.parseExpr(.lowest); + p.lexer.expect(.t_close_paren); + var stmtOpts = ParseStatementOptions{ + .lexical_decl = .allow_fn_inside_if, + }; + const yes = p.parseStmt(&stmtOpts) catch unreachable; + var no: ?Stmt = null; + if (p.lexer.token == .t_else) { + p.lexer.next(); + stmtOpts = ParseStatementOptions{ + .lexical_decl = .allow_fn_inside_if, + }; + no = p.parseStmt(&stmtOpts) catch unreachable; + } + + return p.s(S.If{ + .test_ = test_, + .yes = yes, + .no = no, + }, loc); + }, + .t_do => { + p.lexer.next(); + var stmtOpts = ParseStatementOptions{}; + const body = p.parseStmt(&stmtOpts) catch unreachable; + p.lexer.expect(.t_while); + p.lexer.expect(.t_open_paren); + const test_ = p.parseExpr(.lowest); + p.lexer.expect(.t_close_paren); + + // This is a weird corner case where automatic semicolon insertion applies + // even without a newline present + if (p.lexer.token == .t_semicolon) { + p.lexer.next(); + } + return p.s(S.DoWhile{ .body = body, .test_ = test_ }, loc); + }, + .t_while => { + p.lexer.next(); + + p.lexer.expect(.t_open_paren); + const test_ = p.parseExpr(.lowest); + const body_loc = p.lexer.loc(); + p.lexer.expect(.t_close_paren); + + var stmtOpts = ParseStatementOptions{}; + + // Push a scope so we make sure to prevent any bare identifiers referenced + // within the body from being renamed. Renaming them might change the + // semantics of the code. + _ = try p.pushScopeForParsePass(.with, body_loc); + const body = p.parseStmt(&stmtOpts) catch unreachable; + p.popScope(); + + return p.s(S.With{ .body = body, .value = test_, .body_loc = body_loc }, loc); + }, + .t_with => { + p.lexer.next(); + p.lexer.expect(.t_open_paren); + const test_ = p.parseExpr(.lowest); + const body_loc = p.lexer.loc(); + p.lexer.expect(.t_close_paren); + }, + .t_switch => { + p.lexer.next(); + + p.lexer.expect(.t_open_paren); + const test_ = p.parseExpr(.lowest); + p.lexer.expect(.t_close_paren); + + const body_loc = p.lexer.loc(); + _ = try p.pushScopeForParsePass(.block, body_loc); + defer p.popScope(); + + p.lexer.expect(.t_open_brace); + var cases = List(js_ast.Case).init(p.allocator); + var foundDefault = false; + var stmtOpts = ParseStatementOptions{ .lexical_decl = .allow_all }; + var value: ?js_ast.Expr = null; + while (p.lexer.token != .t_close_brace) { + var body = List(js_ast.Stmt).init(p.allocator); + value = null; + if (p.lexer.token == .t_default) { + if (foundDefault) { + try p.log.addRangeError(p.source, p.lexer.range(), "Multiple default clauses are not allowed"); + fail(); + } + + foundDefault = true; + p.lexer.next(); + p.lexer.expect(.t_colon); + } else { + p.lexer.expect(.t_case); + value = p.parseExpr(.lowest); + p.lexer.expect(.t_colon); + } + + caseBody: while (true) { + switch (p.lexer.token) { + .t_close_brace, .t_case, .t_default => { + break :caseBody; + }, + else => { + stmtOpts = ParseStatementOptions{ .lexical_decl = .allow_all }; + try body.append(p.parseStmt(&stmtOpts) catch unreachable); + }, + } + } + try cases.append(js_ast.Case{ .value = value, .body = body.toOwnedSlice(), .loc = logger.Loc.Empty }); + } + p.lexer.expect(.t_close_brace); + return p.s(S.Switch{ .test_ = test_, .body_loc = body_loc, .cases = cases.toOwnedSlice() }, loc); + }, + .t_try => { + p.lexer.next(); + const body_loc = p.lexer.loc(); + p.lexer.expect(.t_open_brace); + _ = try p.pushScopeForParsePass(.block, loc); + var stmtOpts = ParseStatementOptions{}; + const body = p.parseStmtsUpTo(.t_close_brace, &stmtOpts) catch unreachable; + p.popScope(); + p.lexer.next(); + + var catch_: ?js_ast.Catch = null; + var finally: ?js_ast.Finally = null; + + if (p.lexer.token == .t_catch) { + const catch_loc = p.lexer.loc(); + _ = try p.pushScopeForParsePass(.block, catch_loc); + p.lexer.next(); + var binding: ?js_ast.Binding = null; + + // The catch binding is optional, and can be omitted + // jarred: TIL! + if (p.lexer.token != .t_open_brace) { + p.lexer.expect(.t_open_paren); + const value = p.parseBinding(); + + // Skip over types + if (p.options.ts and p.lexer.token == .t_colon) { + p.lexer.expect(.t_colon); + p.skipTypescriptType(.lowest); + } + + p.lexer.expect(.t_close_paren); + + // Bare identifiers are a special case + var kind = Symbol.Kind.other; + switch (value.data) { + .b_identifier => { + kind = .catch_identifier; + }, + else => {}, + } + stmtOpts = ParseStatementOptions{}; + try p.declareBinding(kind, value, &stmtOpts); + } + + p.lexer.expect(.t_open_brace); + stmtOpts = ParseStatementOptions{}; + const stmts = p.parseStmtsUpTo(.t_close_brace, &stmtOpts) catch unreachable; + p.lexer.next(); + catch_ = js_ast.Catch{ + .loc = catch_loc, + .binding = binding, + .body = stmts, + }; + p.popScope(); + } + + if (p.lexer.token == .t_finally or catch_ == null) { + const finally_loc = p.lexer.loc(); + _ = try p.pushScopeForParsePass(.block, finally_loc); + p.lexer.expect(.t_finally); + p.lexer.expect(.t_open_brace); + stmtOpts = ParseStatementOptions{}; + const stmts = p.parseStmtsUpTo(.t_close_brace, &stmtOpts) catch unreachable; + p.lexer.next(); + finally = js_ast.Finally{ .loc = finally_loc, .stmts = stmts }; + p.popScope(); + } + + return p.s( + S.Try{ .body_loc = body_loc, .body = body, .catch_ = catch_, .finally = finally }, + loc, + ); + }, + .t_for => { + _ = try p.pushScopeForParsePass(.block, loc); + defer p.popScope(); + + p.lexer.next(); + + // "for await (let x of y) {}" + var isForAwait = p.lexer.isContextualKeyword("await"); + if (isForAwait) { + const await_range = p.lexer.range(); + if (p.fn_or_arrow_data_parse.allow_await != .allow_expr) { + try p.log.addRangeError(p.source, await_range, "Cannot use \"await\" outside an async function"); + isForAwait = false; + } else { + // TODO: improve error handling here + // didGenerateError := p.markSyntaxFeature(compat.ForAwait, awaitRange) + if (p.fn_or_arrow_data_parse.is_top_level) { + p.top_level_await_keyword = await_range; + // p.markSyntaxFeature(compat.TopLevelAwait, awaitRange) + } + } + p.lexer.next(); + } + + p.lexer.expect(.t_open_paren); + + var init_: ?Stmt = null; + var test_: ?Expr = null; + var update: ?Expr = null; + + // "in" expressions aren't allowed here + p.allow_in = false; + + var bad_let_range: ?logger.Range = null; + if (p.lexer.isContextualKeyword("let")) { + bad_let_range = p.lexer.range(); + } + + var decls: []G.Decl = &([_]G.Decl{}); + var init_loc = p.lexer.loc(); + var is_var = false; + switch (p.lexer.token) { + // for (var ) + .t_var => { + is_var = true; + p.lexer.next(); + var stmtOpts = ParseStatementOptions{}; + decls = p.parseAndDeclareDecls(.hoisted, &stmtOpts); + init_ = p.s(S.Local{ .kind = .k_const, .decls = decls }, init_loc); + }, + // for (const ) + .t_const => { + p.lexer.next(); + var stmtOpts = ParseStatementOptions{}; + decls = p.parseAndDeclareDecls(.cconst, &stmtOpts); + init_ = p.s(S.Local{ .kind = .k_const, .decls = decls }, init_loc); + }, + // for (;) + .t_semicolon => {}, + else => { + var stmtOpts = ParseStatementOptions{ .lexical_decl = .allow_all }; + + const res = try p.parseExprOrLetStmt(&stmtOpts); + switch (res.stmt_or_expr) { + .stmt => |stmt| { + bad_let_range = null; + init_ = stmt; + }, + .expr => |expr| { + init_ = p.s(S.SExpr{ + .value = expr, + }, init_loc); + }, + } + }, + } + + // "in" expressions are allowed again + p.allow_in = true; + + // Detect for-of loops + if (p.lexer.isContextualKeyword("of") or isForAwait) { + if (bad_let_range) |r| { + try p.log.addRangeError(p.source, r, "\"let\" must be wrapped in parentheses to be used as an expression here"); + fail(); + } + + if (isForAwait and !p.lexer.isContextualKeyword("of")) { + if (init_) |init_stmt| { + p.lexer.expectedString("\"of\""); + } else { + p.lexer.unexpected(); + } + } + + try p.forbidInitializers(decls, "of", false); + p.lexer.next(); + const value = p.parseExpr(.comma); + p.lexer.expect(.t_close_paren); + var stmtOpts = ParseStatementOptions{}; + const body = p.parseStmt(&stmtOpts) catch unreachable; + return p.s(S.ForOf{ .is_await = isForAwait, .init = init_ orelse unreachable, .value = value, .body = body }, loc); + } + + // Detect for-in loops + if (p.lexer.token == .t_in) { + try p.forbidInitializers(decls, "in", false); + p.lexer.next(); + const value = p.parseExpr(.comma); + p.lexer.expect(.t_close_paren); + var stmtOpts = ParseStatementOptions{}; + const body = p.parseStmt(&stmtOpts) catch unreachable; + return p.s(S.ForIn{ .init = init_ orelse unreachable, .value = value, .body = body }, loc); + } + + // Only require "const" statement initializers when we know we're a normal for loop + if (init_) |init_stmt| { + switch (init_stmt.data) { + .s_local => |local| { + if (local.kind == .k_const) { + try p.requireInitializers(decls); + } + }, + else => {}, + } + } + + p.lexer.expect(.t_semicolon); + if (p.lexer.token != .t_semicolon) { + test_ = p.parseExpr(.lowest); + } + + p.lexer.expect(.t_semicolon); + + if (p.lexer.token != .t_close_paren) { + update = p.parseExpr(.lowest); + } + + p.lexer.expect(.t_close_paren); + var stmtOpts = ParseStatementOptions{}; + const body = p.parseStmt(&stmtOpts) catch unreachable; + return p.s( + S.For{ .init = init_, .test_ = test_, .update = update, .body = body }, + loc, + ); + }, + .t_import => { + const previous_import_keyword = p.es6_import_keyword; + p.es6_import_keyword = p.lexer.range(); + p.lexer.next(); + var stmt: S.Import = S.Import{ + .namespace_ref = undefined, + .import_record_index = std.math.maxInt(u32), + }; + var was_originally_bare_import = false; + + // "export import foo = bar" + if ((opts.is_export or (opts.is_namespace_scope and !opts.is_typescript_declare)) and p.lexer.token != .t_identifier) { + p.lexer.expected(.t_identifier); + } + + switch (p.lexer.token) { + // "import('path')" + // "import.meta" + .t_open_paren, .t_dot => { + p.es6_import_keyword = previous_import_keyword; // this wasn't an esm import statement after all + const expr = p.parseSuffix(p.parseImportExpr(loc, .lowest), .lowest, null, Expr.EFlags.none); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.SExpr{ + .value = expr, + }, loc); + }, + .t_string_literal, .t_no_substitution_template_literal => { + // "import 'path'" + if (!opts.is_module_scope and (!opts.is_namespace_scope or !opts.is_typescript_declare)) { + p.lexer.unexpected(); + fail(); + } + was_originally_bare_import = true; + }, + .t_asterisk => { + // "import * as ns from 'path'" + if (!opts.is_module_scope and (!opts.is_namespace_scope or !opts.is_typescript_declare)) { + p.lexer.unexpected(); + fail(); + } + + p.lexer.next(); + p.lexer.expectContextualKeyword("as"); + stmt = S.Import{ + .namespace_ref = try p.storeNameInRef(p.lexer.identifier), + .star_name_loc = p.lexer.loc(), + .import_record_index = std.math.maxInt(u32), + }; + p.lexer.expect(.t_identifier); + p.lexer.expectContextualKeyword("from"); + }, + .t_open_brace => { + // "import {item1, item2} from 'path'" + if (!opts.is_module_scope and (!opts.is_namespace_scope or !opts.is_typescript_declare)) { + p.lexer.unexpected(); + fail(); + } + var importClause = try p.parseImportClause(); + stmt = S.Import{ + .namespace_ref = undefined, + .import_record_index = std.math.maxInt(u32), + .items = importClause.items, + .is_single_line = importClause.is_single_line, + }; + p.lexer.expectContextualKeyword("from"); + }, + .t_identifier => { + // "import defaultItem from 'path'" + // "import foo = bar" + if (!opts.is_module_scope and (!opts.is_namespace_scope)) { + p.lexer.unexpected(); + fail(); + } + + const default_name = p.lexer.identifier; + stmt = S.Import{ .namespace_ref = undefined, .import_record_index = std.math.maxInt(u32), .default_name = LocRef{ + .loc = p.lexer.loc(), + .ref = try p.storeNameInRef(default_name), + } }; + p.lexer.next(); + + if (p.options.ts) { + // Skip over type-only imports + if (strings.eql(default_name, "type")) { + switch (p.lexer.token) { + .t_identifier => { + if (!strings.eql(p.lexer.identifier, "from")) { + // "import type foo from 'bar';" + p.lexer.next(); + p.lexer.expectContextualKeyword("from"); + _ = p.parsePath(); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.TypeScript{}, loc); + } + }, + .t_asterisk => { + // "import type * as foo from 'bar';" + p.lexer.next(); + p.lexer.expectContextualKeyword("as"); + p.lexer.expect(.t_identifier); + p.lexer.expectContextualKeyword("from"); + _ = p.parsePath(); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.TypeScript{}, loc); + }, + + .t_open_brace => { + // "import type {foo} from 'bar';" + _ = try p.parseImportClause(); + p.lexer.expectContextualKeyword("from"); + _ = p.parsePath(); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.TypeScript{}, loc); + }, + else => {}, + } + } + + // Parse TypeScript import assignment statements + p.es6_import_keyword = previous_import_keyword; // This wasn't an ESM import statement after all; + return p.parseTypeScriptImportEqualsStmt(loc, opts, logger.Loc.Empty, default_name); + } + + if (p.lexer.token == .t_comma) { + p.lexer.next(); + + switch (p.lexer.token) { + // "import defaultItem, * as ns from 'path'" + .t_asterisk => { + p.lexer.next(); + p.lexer.expectContextualKeyword("as"); + stmt.namespace_ref = try p.storeNameInRef(p.lexer.identifier); + stmt.star_name_loc = p.lexer.loc(); + p.lexer.expect(.t_identifier); + }, + // "import defaultItem, {item1, item2} from 'path'" + .t_open_brace => { + const importClause = try p.parseImportClause(); + stmt.items = importClause.items; + stmt.is_single_line = importClause.is_single_line; + }, + else => { + p.lexer.unexpected(); + }, + } + } + + p.lexer.expectContextualKeyword("from"); + }, + else => { + p.lexer.unexpected(); + fail(); + }, + } + + const path = p.parsePath(); + stmt.import_record_index = p.addImportRecord(.stmt, path.loc, path.text); + p.import_records.items[stmt.import_record_index].was_originally_bare_import = was_originally_bare_import; + p.lexer.expectOrInsertSemicolon(); + + if (stmt.star_name_loc) |star| { + stmt.namespace_ref = try p.declareSymbol(.import, star, p.loadNameFromRef(stmt.namespace_ref)); + } else { + var path_name = fs.PathName.init(strings.append(p.allocator, "import_", path.text) catch unreachable); + const name = try path_name.nonUniqueNameString(p.allocator); + stmt.namespace_ref = try p.newSymbol(.other, name); + var scope: *Scope = p.current_scope; + try scope.generated.append(stmt.namespace_ref); + } + + var item_refs = std.StringHashMap(LocRef).init(p.allocator); + + // Link the default item to the namespace + if (stmt.default_name) |*name_loc| { + const name = p.loadNameFromRef(name_loc.ref orelse unreachable); + const ref = try p.declareSymbol(.import, name_loc.loc, name); + try p.is_import_item.put(ref, true); + name_loc.ref = ref; + } + + if (stmt.items.len > 0) { + try item_refs.ensureCapacity(@intCast(u32, stmt.items.len)); + for (stmt.items) |*item| { + const name = p.loadNameFromRef(item.name.ref orelse unreachable); + const ref = try p.declareSymbol(.import, item.name.loc, name); + p.checkForNonBMPCodePoint(item.alias_loc, item.alias); + try p.is_import_item.put(ref, true); + item.name.ref = ref; + item_refs.putAssumeCapacity(item.alias, LocRef{ .loc = item.name.loc, .ref = ref }); + } + } + + // Track the items for this namespace + try p.import_items_for_namespace.put(stmt.namespace_ref, item_refs); + return p.s(stmt, loc); + }, + .t_break => { + p.lexer.next(); + const name = try p.parseLabelName(); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.Break{ .label = name }, loc); + }, + .t_continue => { + p.lexer.next(); + const name = try p.parseLabelName(); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.Continue{ .label = name }, loc); + }, + .t_return => { + p.lexer.next(); + var value: ?Expr = null; + if ((p.lexer.token != .t_semicolon and + !p.lexer.has_newline_before and + p.lexer.token != .t_close_brace and + p.lexer.token != .t_end_of_file)) + { + value = p.parseExpr(.lowest); + } + p.latest_return_had_semicolon = p.lexer.token == .t_semicolon; + p.lexer.expectOrInsertSemicolon(); + + return p.s(S.Return{ .value = value }, loc); + }, + .t_throw => { + p.lexer.next(); + if (p.lexer.has_newline_before) { + try p.log.addError(p.source, logger.Loc{ + .start = loc.start + 5, + }, "Unexpected newline after \"throw\""); + fail(); + } + const expr = p.parseExpr(.lowest); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.Throw{ .value = expr }, loc); + }, + .t_debugger => { + p.lexer.next(); + p.lexer.expectOrInsertSemicolon(); + return p.s(S.Debugger{}, loc); + }, + .t_open_brace => { + _ = try p.pushScopeForParsePass(.block, loc); + defer p.popScope(); + p.lexer.next(); + var stmtOpts = ParseStatementOptions{}; + const stmts = p.parseStmtsUpTo(.t_close_brace, &stmtOpts) catch unreachable; + p.lexer.next(); + return p.s(S.Block{ + .stmts = stmts, + }, loc); + }, + + else => { + const is_identifier = p.lexer.token == .t_identifier; + const name = p.lexer.identifier; + var emiss = E.Missing{}; + // Parse either an async function, an async expression, or a normal expression + var expr: Expr = Expr{ .loc = loc, .data = Expr.Data{ .e_missing = &emiss } }; + if (is_identifier and strings.eql(p.lexer.raw(), "async")) { + var async_range = p.lexer.range(); + p.lexer.next(); + if (p.lexer.token == .t_function and !p.lexer.has_newline_before) { + p.lexer.next(); + return try p.parseFnStmt(async_range.loc, opts, async_range); + } + + expr = p.parseSuffix(try p.parseAsyncPrefixExpr(async_range, .lowest), .lowest, null, Expr.EFlags.none); + } else { + const exprOrLet = try p.parseExprOrLetStmt(opts); + switch (exprOrLet.stmt_or_expr) { + .stmt => |stmt| { + p.lexer.expectOrInsertSemicolon(); + return stmt; + }, + .expr => |_expr| { + expr = _expr; + }, + } + } + + if (is_identifier) { + switch (expr.data) { + .e_identifier => |ident| { + if (p.lexer.token == .t_colon and opts.ts_decorators == null) { + _ = try p.pushScopeForParsePass(.label, loc); + defer p.popScope(); + + // Parse a labeled statement + p.lexer.next(); + + const _name = LocRef{ .loc = expr.loc, .ref = ident.ref }; + var nestedOpts = ParseStatementOptions{}; + + switch (opts.lexical_decl) { + .allow_all, .allow_fn_inside_label => { + nestedOpts.lexical_decl = .allow_fn_inside_label; + }, + else => {}, + } + var stmt = p.parseStmt(&nestedOpts) catch unreachable; + return p.s(S.Label{ .name = _name, .stmt = stmt }, loc); + } + }, + else => {}, + } + + if (p.options.ts) { + if (js_lexer.TypescriptStmtKeyword.List.get(name)) |ts_stmt| { + switch (ts_stmt) { + .ts_stmt_type => { + if (p.lexer.token == .t_identifier and !p.lexer.has_newline_before) { + // "type Foo = any" + var stmtOpts = ParseStatementOptions{ .is_module_scope = opts.is_module_scope }; + p.skipTypescriptTypeStmt(&stmtOpts); + return p.s(S.TypeScript{}, loc); + } + }, + .ts_stmt_namespace, .ts_stmt_module => { + // "namespace Foo {}" + // "module Foo {}" + // "declare module 'fs' {}" + // "declare module 'fs';" + if (((opts.is_module_scope or opts.is_namespace_scope) and (p.lexer.token == .t_identifier or + (p.lexer.token == .t_string_literal and opts.is_typescript_declare)))) + { + return p.parseTypescriptNamespaceTmt(loc, opts); + } + }, + .ts_stmt_interface => { + // "interface Foo {}" + var stmtOpts = ParseStatementOptions{ .is_module_scope = opts.is_module_scope }; + + p.skipTypeScriptInterfaceStmt(&stmtOpts); + return p.s(S.TypeScript{}, loc); + }, + .ts_stmt_abstract => { + if (p.lexer.token == .t_class or opts.ts_decorators != null) { + return p.parseClassStmt(loc, opts); + } + }, + .ts_stmt_global => { + // "declare module 'fs' { global { namespace NodeJS {} } }" + if (opts.is_namespace_scope and opts.is_typescript_declare and p.lexer.token == .t_open_brace) { + p.lexer.next(); + _ = p.parseStmtsUpTo(.t_close_brace, opts) catch unreachable; + p.lexer.next(); + return p.s(S.TypeScript{}, loc); + } + }, + .ts_stmt_declare => { + opts.lexical_decl = .allow_all; + opts.is_typescript_declare = true; + + // "@decorator declare class Foo {}" + // "@decorator declare abstract class Foo {}" + if (opts.ts_decorators != null and p.lexer.token != .t_class and !p.lexer.isContextualKeyword("abstract")) { + p.lexer.expected(.t_class); + } + + // "declare global { ... }" + if (p.lexer.isContextualKeyword("global")) { + p.lexer.next(); + p.lexer.expect(.t_open_brace); + _ = p.parseStmtsUpTo(.t_close_brace, opts) catch unreachable; + p.lexer.next(); + return p.s(S.TypeScript{}, loc); + } + + // "declare const x: any" + const stmt = p.parseStmt(opts) catch unreachable; + if (opts.ts_decorators) |decs| { + p.discardScopesUpTo(decs.scope_index); + } + + // Unlike almost all uses of "declare", statements that use + // "export declare" with "var/let/const" inside a namespace affect + // code generation. They cause any declared bindings to be + // considered exports of the namespace. Identifier references to + // those names must be converted into property accesses off the + // namespace object: + // + // namespace ns { + // export declare const x + // export function y() { return x } + // } + // + // (ns as any).x = 1 + // console.log(ns.y()) + // + // In this example, "return x" must be replaced with "return ns.x". + // This is handled by replacing each "export declare" statement + // inside a namespace with an "export var" statement containing all + // of the declared bindings. That "export var" statement will later + // cause identifiers to be transformed into property accesses. + if (opts.is_namespace_scope and opts.is_export) { + var decls: []G.Decl = &([_]G.Decl{}); + switch (stmt.data) { + .s_local => |local| { + var _decls = try List(G.Decl).initCapacity(p.allocator, local.decls.len); + for (local.decls) |decl| { + try extractDeclsForBinding(decl.binding, &_decls); + } + decls = _decls.toOwnedSlice(); + }, + else => {}, + } + + if (decls.len > 0) { + return p.s(S.Local{ + .kind = .k_var, + .is_export = true, + .decls = decls, + }, loc); + } + } + + return p.s(S.TypeScript{}, loc); + }, + } + } + } + } + + p.lexer.expectOrInsertSemicolon(); + return p.s(S.SExpr{ .value = expr }, loc); + }, + } + + return js_ast.Stmt.empty(); + } + + pub fn discardScopesUpTo(p: *P, scope_index: usize) void { + // Remove any direct children from their parent + var scope = p.current_scope; + var children = scope.children; + for (p.scopes_in_order.items[scope_index..]) |child| { + if (child.scope.parent == p.current_scope) { + var i: usize = children.items.len - 1; + while (i >= 0) { + if (children.items[i] == child.scope) { + _ = children.orderedRemove(i); + break; + } + i -= 1; + } + } + } + + // Truncate the scope order where we started to pretend we never saw this scope + p.scopes_in_order.shrinkAndFree(scope_index); + } + + pub fn skipTypescriptTypeStmt(p: *P, opts: *ParseStatementOptions) void { + notimpl(); + } + + pub fn parseTypescriptNamespaceTmt(p: *P, loc: logger.Loc, opts: *ParseStatementOptions) Stmt { + notimpl(); + } + + pub fn skipTypeScriptInterfaceStmt(p: *P, opts: *ParseStatementOptions) void { + notimpl(); + } + + pub fn parseTypeScriptImportEqualsStmt(p: *P, loc: logger.Loc, opts: *ParseStatementOptions, default_name_loc: logger.Loc, default_name: string) Stmt { + notimpl(); + } + + pub fn parseClauseAlias(p: *P, kind: string) !string { + const loc = p.lexer.loc(); + + // The alias may now be a string (see https://github.com/tc39/ecma262/pull/2154) + if (p.lexer.token == .t_string_literal) { + if (p.lexer.utf16ToStringWithValidation(p.lexer.string_literal)) |alias| { + return alias; + } else |err| { + const r = p.source.rangeOfString(loc); + // TODO: improve error message + try p.log.addRangeErrorFmt(p.source, r, p.allocator, "Invalid {s} alias because it contains an unpaired Unicode surrogate (like emoji)", .{kind}); + return p.source.textForRange(r); + } + } + + // The alias may be a keyword + if (!p.lexer.isIdentifierOrKeyword()) { + p.lexer.expect(.t_identifier); + } + + const alias = p.lexer.identifier; + p.checkForNonBMPCodePoint(loc, alias); + return alias; + } + + pub fn parseImportClause( + p: *P, + ) !ImportClause { + var items = List(js_ast.ClauseItem).init(p.allocator); + p.lexer.expect(.t_open_brace); + var is_single_line = !p.lexer.has_newline_before; + + while (p.lexer.token != .t_close_brace) { + // The alias may be a keyword; + const isIdentifier = p.lexer.token == .t_identifier; + const alias_loc = p.lexer.loc(); + const alias = try p.parseClauseAlias("import"); + var name = LocRef{ .loc = alias_loc, .ref = try p.storeNameInRef(alias) }; + var original_name = alias; + p.lexer.next(); + + if (p.lexer.isContextualKeyword("as")) { + p.lexer.next(); + original_name = p.lexer.identifier; + name = LocRef{ .loc = alias_loc, .ref = try p.storeNameInRef(alias) }; + p.lexer.expect(.t_identifier); + } else if (!isIdentifier) { + // An import where the name is a keyword must have an alias + p.lexer.expectedString("\"as\""); + } + + // Reject forbidden names + if (isEvalOrArguments(original_name)) { + const r = js_lexer.rangeOfIdentifier(&p.source, name.loc); + try p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot use \"{s}\" as an identifier here", .{original_name}); + } + + try items.append(js_ast.ClauseItem{ + .alias = alias, + .alias_loc = alias_loc, + .name = name, + .original_name = original_name, + }); + + if (p.lexer.token != .t_comma) { + break; + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + + p.lexer.next(); + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + + p.lexer.expect(.t_close_brace); + return ImportClause{ .items = items.toOwnedSlice(), .is_single_line = is_single_line }; + } + + pub fn forbidInitializers(p: *P, decls: []G.Decl, loop_type: string, is_var: bool) !void { + if (decls.len > 1) { + try p.log.addErrorFmt(p.source, decls[0].binding.loc, p.allocator, "for-{s} loops must have a single declaration", .{loop_type}); + } else if (decls.len == 1) { + if (decls[0].value) |value| { + if (is_var) { + + // This is a weird special case. Initializers are allowed in "var" + // statements with identifier bindings. + return; + } + + try p.log.addErrorFmt(p.source, value.loc, p.allocator, "for-{s} loop variables cannot have an initializer", .{loop_type}); + } + } + } + + pub fn parseExprOrLetStmt(p: *P, opts: *ParseStatementOptions) !ExprOrLetStmt { + var let_range = p.lexer.range(); + var raw = p.lexer.raw(); + + if (p.lexer.token != .t_identifier or !strings.eql(raw, "let")) { + return ExprOrLetStmt{ .stmt_or_expr = js_ast.StmtOrExpr{ .expr = p.parseExpr(.lowest) } }; + } + + p.lexer.next(); + + switch (p.lexer.token) { + .t_identifier, .t_open_bracket, .t_open_brace => { + if (opts.lexical_decl == .allow_all or !p.lexer.has_newline_before or p.lexer.token == .t_open_bracket) { + if (opts.lexical_decl != .allow_all) { + try p.forbidLexicalDecl(let_range.loc); + } + + const decls = p.parseAndDeclareDecls(.other, opts); + return ExprOrLetStmt{ + .stmt_or_expr = js_ast.StmtOrExpr{ + .stmt = p.s(S.Local{ + .kind = .k_let, + .decls = decls, + .is_export = opts.is_export, + }, let_range.loc), + }, + .decls = decls, + }; + } + }, + else => {}, + } + + const ref = p.storeNameInRef(raw) catch unreachable; + const expr = p.e(E.Identifier{ .ref = ref }, let_range.loc); + return ExprOrLetStmt{ .stmt_or_expr = js_ast.StmtOrExpr{ .expr = p.parseExpr(.lowest) } }; + } + + pub fn requireInitializers(p: *P, decls: []G.Decl) !void { + for (decls) |decl| { + if (decl.value == null) { + switch (decl.binding.data) { + .b_identifier => |ident| { + const r = js_lexer.rangeOfIdentifier(&p.source, decl.binding.loc); + try p.log.addRangeErrorFmt(p.source, r, p.allocator, "The constant \"{s}\" must be initialized", .{p.symbols.items[ident.ref.inner_index].original_name}); + // return;/ + }, + else => { + try p.log.addError(p.source, decl.binding.loc, "This constant must be initialized"); + }, + } + } + } + } + + pub fn parseBinding(p: *P) Binding { + var loc = p.lexer.loc(); + + switch (p.lexer.token) { + .t_identifier => { + const name = p.lexer.identifier; + if ((p.fn_or_arrow_data_parse.allow_await != .allow_ident and strings.eql(name, "await")) or (p.fn_or_arrow_data_parse.allow_yield != .allow_ident and strings.eql(name, "yield"))) { + // TODO: add fmt to addRangeError + p.log.addRangeError(p.source, p.lexer.range(), "Cannot use \"yield\" or \"await\" here.") catch unreachable; + } + + const ref = p.storeNameInRef(name) catch unreachable; + p.lexer.next(); + return p.b(B.Identifier{ .ref = ref }, loc); + }, + .t_open_bracket => { + p.lexer.next(); + var is_single_line = !p.lexer.has_newline_before; + var items = List(js_ast.ArrayBinding).init(p.allocator); + var has_spread = false; + + // "in" expressions are allowed + var old_allow_in = p.allow_in; + p.allow_in = true; + while (p.lexer.token != .t_close_bracket) { + if (p.lexer.token == .t_comma) { + items.append(js_ast.ArrayBinding{ + .binding = p.b( + B.Missing{}, + p.lexer.loc(), + ), + }) catch unreachable; + } else { + if (p.lexer.token == .t_dot_dot_dot) { + p.lexer.next(); + has_spread = true; + + // This was a bug in the ES2015 spec that was fixed in ES2016 + if (p.lexer.token != .t_identifier) { + // p.markSyntaxFeature(compat.NestedRestBinding, p.lexer.Range()) + + } + } + + const binding = p.parseBinding(); + + var default_value: ?Expr = null; + if (!has_spread and p.lexer.token == .t_equals) { + p.lexer.next(); + default_value = p.parseExpr(.comma); + } + + items.append(js_ast.ArrayBinding{ .binding = binding, .default_value = default_value }) catch unreachable; + + // Commas after spread elements are not allowed + if (has_spread and p.lexer.token == .t_comma) { + p.log.addRangeError(p.source, p.lexer.range(), "Unexpected \",\" after rest pattern") catch unreachable; + fail(); + } + } + + if (p.lexer.token != .t_comma) { + break; + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + p.lexer.next(); + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + } + p.allow_in = old_allow_in; + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + p.lexer.expect(.t_close_bracket); + return p.b(B.Array{ + .items = items.toOwnedSlice(), + .has_spread = has_spread, + .is_single_line = is_single_line, + }, loc); + }, + .t_open_brace => { + // p.markSyntaxFeature(compat.Destructuring, p.lexer.Range()) + p.lexer.next(); + var is_single_line = false; + var properties = List(js_ast.B.Property).init(p.allocator); + + // "in" expressions are allowed + var old_allow_in = p.allow_in; + p.allow_in = true; + + while (p.lexer.token != .t_close_brace) { + var property = p.parsePropertyBinding(); + properties.append(property) catch unreachable; + + // Commas after spread elements are not allowed + if (property.flags.is_spread and p.lexer.token == .t_comma) { + p.log.addRangeError(p.source, p.lexer.range(), "Unexpected \",\" after rest pattern") catch unreachable; + fail(); + } + + if (p.lexer.token != .t_comma) { + break; + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + p.lexer.next(); + if (p.lexer.has_newline_before) { + is_single_line = false; + } + } + + p.allow_in = old_allow_in; + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + p.lexer.expect(.t_close_brace); + + return p.b(B.Object{ + .properties = properties.toOwnedSlice(), + .is_single_line = is_single_line, + }, loc); + }, + else => {}, + } + + p.lexer.expect(.t_identifier); + return p.b(B.Missing{}, loc); + } + + pub fn parsePropertyBinding(p: *P) B.Property { + var key: js_ast.Expr = undefined; + var is_computed = false; + + switch (p.lexer.token) { + .t_dot_dot_dot => { + p.lexer.next(); + const value = p.b(B.Identifier{ + .ref = p.storeNameInRef(p.lexer.identifier) catch unreachable, + }, p.lexer.loc()); + p.lexer.expect(.t_identifier); + return B.Property{ + // This "key" diverges from esbuild, but is due to Go always having a zero value. + .key = p.e(E.Missing{}, logger.Loc.Empty), + + .flags = Flags.Property{ .is_spread = true }, + .value = value, + }; + }, + .t_numeric_literal => { + key = p.e(E.Number{ + .value = p.lexer.number, + }, p.lexer.loc()); + // check for legacy octal literal + p.lexer.next(); + }, + .t_string_literal => { + key = p.parseStringLiteral(); + }, + .t_big_integer_literal => { + key = p.e(E.BigInt{ + .value = p.lexer.identifier, + }, p.lexer.loc()); + // p.markSyntaxFeature(compat.BigInt, p.lexer.Range()) + p.lexer.next(); + }, + .t_open_bracket => { + is_computed = true; + p.lexer.next(); + key = p.parseExpr(.comma); + p.lexer.expect(.t_close_bracket); + }, + else => { + const name = p.lexer.identifier; + const loc = p.lexer.loc(); + + if (!p.lexer.isIdentifierOrKeyword()) { + p.lexer.expect(.t_identifier); + } + + p.lexer.next(); + + key = p.e(E.String{ + .value = p.lexer.stringToUTF16(name), + }, loc); + + if (p.lexer.token != .t_colon and p.lexer.token != .t_open_paren) { + const ref = p.storeNameInRef(name) catch unreachable; + const value = p.b(B.Identifier{ .ref = ref }, loc); + var default_value: ?Expr = null; + if (p.lexer.token == .t_equals) { + p.lexer.next(); + default_value = p.parseExpr(.comma); + } + + return B.Property{ + .key = key, + .value = value, + .default_value = default_value, + }; + } + }, + } + + p.lexer.expect(.t_colon); + const value = p.parseBinding(); + + var default_value: ?Expr = null; + if (p.lexer.token == .t_equals) { + p.lexer.next(); + default_value = p.parseExpr(.comma); + } + + return B.Property{ + .flags = Flags.Property{ + .is_computed = is_computed, + }, + .key = key, + .value = value, + .default_value = default_value, + }; + } + + pub fn parseAndDeclareDecls(p: *P, kind: Symbol.Kind, opts: *ParseStatementOptions) []G.Decl { + var decls = List(G.Decl).initCapacity(p.allocator, 1) catch unreachable; + + while (true) { + // Forbid "let let" and "const let" but not "var let" + if ((kind == .other or kind == .cconst) and p.lexer.isContextualKeyword("let")) { + p.log.addRangeError(p.source, p.lexer.range(), "Cannot use \"let\" as an identifier here") catch unreachable; + } + + var value: ?js_ast.Expr = null; + var local = p.parseBinding(); + p.declareBinding(kind, local, opts) catch unreachable; + + // Skip over types + if (p.options.ts) { + // "let foo!" + var is_definite_assignment_assertion = p.lexer.token == .t_exclamation; + if (is_definite_assignment_assertion) { + p.lexer.next(); + } + + // "let foo: number" + if (is_definite_assignment_assertion or p.lexer.token == .t_colon) { + p.lexer.expect(.t_colon); + p.skipTypescriptType(.lowest); + } + } + + if (p.lexer.token == .t_equals) { + p.lexer.next(); + value = p.parseExpr(.comma); + } + + decls.append(G.Decl{ + .binding = local, + .value = value, + }) catch unreachable; + + if (p.lexer.token != .t_comma) { + break; + } + p.lexer.next(); + } + + return decls.toOwnedSlice(); + } + + pub fn parseTypescriptEnumStmt(p: *P, loc: logger.Loc, opts: *ParseStatementOptions) Stmt { + notimpl(); + // return Stmt.empty(); + } + + pub fn parseExportClause(p: *P) !ExportClauseResult { + var items = List(js_ast.ClauseItem).initCapacity(p.allocator, 1) catch unreachable; + var first_keyword_item_loc = logger.Loc{}; + p.lexer.expect(.t_open_brace); + var is_single_line = !p.lexer.has_newline_before; + + while (p.lexer.token != .t_close_brace) { + var alias = try p.parseClauseAlias("export"); + var alias_loc = p.lexer.loc(); + + var name = LocRef{ + .loc = alias_loc, + .ref = p.storeNameInRef(alias) catch unreachable, + }; + var original_name = alias; + + // The name can actually be a keyword if we're really an "export from" + // statement. However, we won't know until later. Allow keywords as + // identifiers for now and throw an error later if there's no "from". + // + // // This is fine + // export { default } from 'path' + // + // // This is a syntax error + // export { default } + // + if (p.lexer.token != .t_identifier) { + if (!p.lexer.isIdentifierOrKeyword()) { + p.lexer.expect(.t_identifier); + } + if (first_keyword_item_loc.start == 0) { + first_keyword_item_loc = p.lexer.loc(); + } + } + + p.checkForNonBMPCodePoint(alias_loc, alias); + p.lexer.next(); + + if (p.lexer.isContextualKeyword("as")) { + p.lexer.next(); + alias = try p.parseClauseAlias("export"); + alias_loc = p.lexer.loc(); + + p.lexer.next(); + } + + items.append(js_ast.ClauseItem{ + .alias = alias, + .alias_loc = alias_loc, + .name = name, + .original_name = original_name, + }) catch unreachable; + + // we're done if there's no comma + if (p.lexer.token != .t_comma) { + break; + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + p.lexer.next(); + if (p.lexer.has_newline_before) { + is_single_line = false; + } + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + p.lexer.expect(.t_close_brace); + + // Throw an error here if we found a keyword earlier and this isn't an + // "export from" statement after all + if (first_keyword_item_loc.start != 0 and !p.lexer.isContextualKeyword("from")) { + const r = js_lexer.rangeOfIdentifier(&p.source, first_keyword_item_loc); + p.lexer.addRangeError(r, "Expected identifier but found \"{s}\"", .{p.source.textForRange(r)}, true); + } + + return ExportClauseResult{ + .clauses = items.toOwnedSlice(), + .is_single_line = is_single_line, + }; + } + + pub fn parsePath(p: *P) ParsedPath { + var path = ParsedPath{ + .loc = p.lexer.loc(), + .text = p.lexer.utf16ToString(p.lexer.string_literal), + }; + + if (p.lexer.token == .t_no_substitution_template_literal) { + p.lexer.next(); + } else { + p.lexer.expect(.t_string_literal); + } + + return path; + } + + // TODO: + pub fn checkForNonBMPCodePoint(p: *P, loc: logger.Loc, name: string) void {} + + pub fn parseStmtsUpTo(p: *P, eend: js_lexer.T, opts: *ParseStatementOptions) ![]Stmt { + var stmts = StmtList.init(p.allocator); + + var returnWithoutSemicolonStart: i32 = -1; + opts.lexical_decl = .allow_all; + var isDirectivePrologue = true; + + run: while (true) { + for (p.lexer.comments_to_preserve_before.items) |comment| { + try stmts.append(p.s(S.Comment{ + .text = comment.text, + }, p.lexer.loc())); + } + + if (p.lexer.token == eend) { + break; + } + + var stmt = p.parseStmt(opts) catch break :run; + + // Skip TypeScript types entirely + if (p.options.ts) { + switch (stmt.data) { + .s_type_script => { + continue; + }, + else => {}, + } + } + + // Parse one or more directives at the beginning + if (isDirectivePrologue) { + isDirectivePrologue = false; + switch (stmt.data) { + .s_expr => |expr| { + switch (expr.value.data) { + .e_string => |str| { + if (!str.prefer_template) { + stmt.data = Stmt.Data{ + .s_directive = p.m(S.Directive{ + .value = str.value, + // .legacy_octal_loc = str.legacy_octal_loc, + }), + }; + isDirectivePrologue = true; + + if (strings.eqlUtf16("use strict", str.value)) { + // Track "use strict" directives + p.current_scope.strict_mode = .explicit_strict_mode; + } else if (strings.eqlUtf16("use asm", str.value)) { + stmt.data = Stmt.Data{ .s_empty = p.m(S.Empty{}) }; + } + } + }, + else => {}, + } + }, + else => {}, + } + } + + try stmts.append(stmt); + + // Warn about ASI and return statements. Here's an example of code with + // this problem: https://github.com/rollup/rollup/issues/3729 + if (!p.options.suppress_warnings_about_weird_code) { + var needsCheck = true; + switch (stmt.data) { + .s_return => |ret| { + if (ret.value == null and !p.latest_return_had_semicolon) { + returnWithoutSemicolonStart = stmt.loc.start; + needsCheck = false; + } + }, + else => {}, + } + + if (needsCheck and returnWithoutSemicolonStart != -1) { + switch (stmt.data) { + .s_expr => |exp| { + try p.log.addWarning( + p.source, + logger.Loc{ .start = returnWithoutSemicolonStart + 6 }, + "The following expression is not returned because of an automatically-inserted semicolon", + ); + }, + else => {}, + } + + returnWithoutSemicolonStart = -1; + } + } + } + + return stmts.toOwnedSlice(); + } + + pub fn markStrictModeFeature(p: *P, feature: StrictModeFeature, r: logger.Range, detail: string) !void { + var text: string = undefined; + var can_be_transformed = false; + switch (feature) { + .with_statement => { + text = "With statements"; + }, + .delete_bare_name => { + text = "\"delete\" of a bare identifier"; + }, + .for_in_var_init => { + text = "Variable initializers within for-in loops"; + can_be_transformed = true; + }, + .eval_or_arguments => { + text = try std.fmt.allocPrint(p.allocator, "Declarations with the name {s}", .{detail}); + }, + .reserved_word => { + text = try std.fmt.allocPrint(p.allocator, "{s} is a reserved word and", .{detail}); + }, + .legacy_octal_literal => { + text = "Legacy octal literals"; + }, + .legacy_octal_escape => { + text = "Legacy octal escape sequences"; + }, + .if_else_function_stmt => { + text = "Function declarations inside if statements"; + }, + // else => { + // text = "This feature"; + // }, + } + + var scope = p.current_scope; + if (p.isStrictMode()) { + var why: string = ""; + var notes: []logger.Data = undefined; + var where: logger.Range = undefined; + switch (scope.strict_mode) { + .implicit_strict_mode_import => { + where = p.es6_import_keyword; + }, + .implicit_strict_mode_export => { + where = p.es6_export_keyword; + }, + .implicit_strict_mode_top_level_await => { + where = p.top_level_await_keyword; + }, + .implicit_strict_mode_class => { + why = "All code inside a class is implicitly in strict mode"; + where = p.enclosing_class_keyword; + }, + else => {}, + } + if (why.len == 0) { + why = try std.fmt.allocPrint(p.allocator, "This file is implicitly in strict mode because of the \"{s}\" keyword here", .{p.source.textForRange(where)}); + } + + try p.log.addRangeErrorWithNotes(p.source, r, try std.fmt.allocPrint(p.allocator, "{s} cannot be used in strict mode", .{text}), &([_]logger.Data{logger.rangeData(p.source, where, why)})); + } else if (!can_be_transformed and p.isStrictModeOutputFormat()) { + try p.log.addRangeError(p.source, r, try std.fmt.allocPrint(p.allocator, "{s} cannot be used with \"esm\" due to strict mode", .{text})); + } + } + + pub fn isStrictMode(p: *P) bool { + return p.current_scope.strict_mode != .sloppy_mode; + } + + pub fn isStrictModeOutputFormat(p: *P) bool { + return true; + } + + pub fn declareSymbol(p: *P, kind: Symbol.Kind, loc: logger.Loc, name: string) !Ref { + // p.checkForNonBMPCodePoint(loc, name) + + // Forbid declaring a symbol with a reserved word in strict mode + if (p.isStrictMode() and js_lexer.StrictModeReservedWords.has(name)) { + try p.markStrictModeFeature(.reserved_word, js_lexer.rangeOfIdentifier(&p.source, loc), name); + } + + // Allocate a new symbol + var ref = try p.newSymbol(kind, name); + + const scope = p.current_scope; + if (scope.members.get(name)) |existing| { + var symbol: Symbol = p.symbols.items[@intCast(usize, existing.ref.inner_index)]; + + switch (p.canMergeSymbols(scope, symbol.kind, kind)) { + .forbidden => { + const r = js_lexer.rangeOfIdentifier(&p.source, loc); + var notes: []logger.Data = undefined; + notes = &([_]logger.Data{logger.rangeData(p.source, r, try std.fmt.allocPrint(p.allocator, "{s} has already been declared", .{name}))}); + try p.log.addRangeErrorWithNotes(p.source, r, try std.fmt.allocPrint(p.allocator, "{s} was originally declared here", .{name}), notes); + return existing.ref; + }, + .keep_existing => { + ref = existing.ref; + }, + .replace_with_new => { + symbol.link = ref; + }, + .become_private_get_set_pair => { + ref = existing.ref; + symbol.kind = .private_get_set_pair; + }, + .become_private_static_get_set_pair => { + ref = existing.ref; + symbol.kind = .private_static_get_set_pair; + }, + + .overwrite_with_new => {}, + // else => unreachable, + } + } + + try scope.members.put(name, js_ast.Scope.Member{ .ref = ref, .loc = loc }); + return ref; + } + + pub fn validateFunctionName(p: *P, func: G.Fn, kind: FunctionKind) void { + if (func.name) |name| { + const original_name = p.symbols.items[name.ref.?.inner_index].original_name; + + if (func.flags.is_async and strings.eql(original_name, "await")) { + p.log.addRangeError( + p.source, + js_lexer.rangeOfIdentifier(&p.source, name.loc), + "An async function cannot be named \"await\"", + ) catch unreachable; + } else if (kind == .expr and func.flags.is_generator and strings.eql(original_name, "yield")) { + p.log.addRangeError( + p.source, + js_lexer.rangeOfIdentifier(&p.source, name.loc), + "An generator function expression cannot be named \"yield\"", + ) catch unreachable; + } + } + } + + pub fn parseFnExpr(p: *P, loc: logger.Loc, is_async: bool, async_range: logger.Range) !Expr { + p.lexer.next(); + const is_generator = p.lexer.token == T.t_asterisk; + if (is_generator) { + // p.markSyntaxFeature() + p.lexer.next(); + } else if (is_async) { + // p.markLoweredSyntaxFeature(compat.AsyncAwait, asyncRange, compat.Generator) + } + + var name: ?js_ast.LocRef = null; + + _ = p.pushScopeForParsePass(.function_args, loc) catch unreachable; + defer p.popScope(); + + if (p.lexer.token == .t_identifier) { + name = js_ast.LocRef{ + .loc = loc, + .ref = null, + }; + + if (p.lexer.identifier.len > 0 and !strings.eql(p.lexer.identifier, "arguments")) { + (name orelse unreachable).ref = try p.declareSymbol(.hoisted_function, (name orelse unreachable).loc, p.lexer.identifier); + } else { + (name orelse unreachable).ref = try p.newSymbol(.hoisted_function, p.lexer.identifier); + } + debug("FUNC NAME {s}", .{p.lexer.identifier}); + p.lexer.next(); + } + + if (p.options.ts) { + p.skipTypescriptTypeParameters(); + } + + var func = p.parseFn(name, FnOrArrowDataParse{ + .async_range = async_range, + .allow_await = if (is_async) .allow_expr else .allow_ident, + .allow_yield = if (is_generator) .allow_expr else .allow_ident, + }); + + p.validateFunctionName(func, .expr); + + return p.e(js_ast.E.Function{ + .func = func, + }, loc); + } + + pub fn parseFnBody(p: *P, data: *FnOrArrowDataParse) !G.FnBody { + var oldFnOrArrowData = p.fn_or_arrow_data_parse; + var oldAllowIn = p.allow_in; + p.fn_or_arrow_data_parse = data.*; + p.allow_in = true; + + const loc = p.lexer.loc(); + _ = try p.pushScopeForParsePass(Scope.Kind.function_body, p.lexer.loc()); + defer p.popScope(); + + p.lexer.expect(.t_open_brace); + var opts = ParseStatementOptions{}; + const stmts = p.parseStmtsUpTo(.t_close_brace, &opts) catch unreachable; + p.lexer.next(); + + p.allow_in = oldAllowIn; + p.fn_or_arrow_data_parse = oldFnOrArrowData; + return G.FnBody{ .loc = loc, .stmts = stmts }; + } + + pub fn parseArrowBody(p: *P, args: []js_ast.G.Arg, data: *FnOrArrowDataParse) !E.Arrow { + var arrow_loc = p.lexer.loc(); + + // Newlines are not allowed before "=>" + if (p.lexer.has_newline_before) { + try p.log.addRangeError(p.source, p.lexer.range(), "Unexpected newline before \"=>\""); + fail(); + } + + p.lexer.expect(T.t_equals_greater_than); + + for (args) |arg| { + var opts = ParseStatementOptions{}; + try p.declareBinding(Symbol.Kind.hoisted, arg.binding, &opts); + } + + data.allow_super_call = p.fn_or_arrow_data_parse.allow_super_call; + if (p.lexer.token == .t_open_brace) { + var body = try p.parseFnBody(data); + p.after_arrow_body_loc = p.lexer.loc(); + return E.Arrow{ .args = args, .body = body }; + } + + _ = try p.pushScopeForParsePass(Scope.Kind.function_body, arrow_loc); + defer p.popScope(); + + var old_fn_or_arrow_data = p.fn_or_arrow_data_parse; + p.fn_or_arrow_data_parse = data.*; + + var expr = p.parseExpr(Level.comma); + p.fn_or_arrow_data_parse = old_fn_or_arrow_data; + var stmts = try p.allocator.alloc(Stmt, 1); + stmts[0] = p.s(S.Return{ .value = expr }, arrow_loc); + + return E.Arrow{ .args = args, .prefer_expr = true, .body = G.FnBody{ .loc = arrow_loc, .stmts = stmts } }; + } + + pub fn declareBinding(p: *P, kind: Symbol.Kind, binding: BindingNodeIndex, opts: *ParseStatementOptions) !void { + switch (binding.data) { + .b_missing => {}, + .b_identifier => |bind| { + if (!opts.is_typescript_declare or (opts.is_namespace_scope and opts.is_export)) { + bind.ref = try p.declareSymbol(kind, binding.loc, p.loadNameFromRef(bind.ref)); + } + }, + + .b_array => |bind| { + for (bind.items) |item| { + p.declareBinding(kind, item.binding, opts) catch unreachable; + } + }, + + .b_object => |bind| { + for (bind.properties) |*prop| { + const value = prop.value; + p.declareBinding(kind, value, opts) catch unreachable; + } + }, + + else => { + // @compileError("Missing binding type"); + }, + } + } + + // Saves us from allocating a slice to the heap + pub fn parseArrowBodySingleArg(p: *P, arg: G.Arg, data: anytype) !E.Arrow { + switch (@TypeOf(data)) { + FnOrArrowDataParse => { + var args = [_]G.Arg{arg}; + + var d = data; + + return p.parseArrowBody(args[0..], &d); + }, + *FnOrArrowDataParse => { + var args = [_]G.Arg{arg}; + return p.parseArrowBody(args[0..], data); + }, + else => unreachable, + } + } + + // This is where the allocate memory to the heap for AST objects. + // This is a short name to keep the code more readable. + // It also swallows errors, but I think that's correct here. + // We can handle errors via the log. + // We'll have to deal with @wasmHeapGrow or whatever that thing is. + pub fn mm(self: *P, comptime ast_object_type: type, instance: anytype) callconv(.Inline) *ast_object_type { + var obj = self.allocator.create(ast_object_type) catch unreachable; + obj.* = instance; + return obj; + } + + // mmmm memmory allocation + pub fn m(self: *P, kind: anytype) callconv(.Inline) *@TypeOf(kind) { + return self.mm(@TypeOf(kind), kind); + } + + // Doing this the fast way is too complicated for now. + pub fn storeNameInRef(p: *P, name: string) !js_ast.Ref { + // allocated_names is lazily allocated + if (p.allocated_names.capacity > 0) { + const inner_index = @intCast(Ref.Int, p.allocated_names.items.len); + try p.allocated_names.append(name); + return js_ast.Ref{ .source_index = std.math.maxInt(Ref.Int), .inner_index = inner_index }; + } else { + p.allocated_names = try @TypeOf(p.allocated_names).initCapacity(p.allocator, 1); + p.allocated_names.appendAssumeCapacity(name); + return js_ast.Ref{ .source_index = std.math.maxInt(Ref.Int), .inner_index = 0 }; + } + } + + pub fn loadNameFromRef(p: *P, ref: js_ast.Ref) string { + assert(ref.inner_index < p.allocated_names.items.len); + return p.allocated_names.items[ref.inner_index]; + } + + // This parses an expression. This assumes we've already parsed the "async" + // keyword and are currently looking at the following token. + pub fn parseAsyncPrefixExpr(p: *P, async_range: logger.Range, level: Level) !Expr { + // "async function() {}" + if (!p.lexer.has_newline_before and p.lexer.token == T.t_function) { + return try p.parseFnExpr(async_range.loc, true, async_range); + } + + // Check the precedence level to avoid parsing an arrow function in + // "new async () => {}". This also avoids parsing "new async()" as + // "new (async())()" instead. + if (!p.lexer.has_newline_before and level.lt(.member)) { + switch (p.lexer.token) { + // "async => {}" + .t_equals_greater_than => { + if (level.lte(.assign)) { + const arg = G.Arg{ .binding = p.b( + B.Identifier{ + .ref = try p.storeNameInRef("async"), + }, + async_range.loc, + ) }; + _ = p.pushScopeForParsePass(.function_args, async_range.loc) catch unreachable; + defer p.popScope(); + var arrow_body = try p.parseArrowBodySingleArg(arg, FnOrArrowDataParse{}); + return p.e(arrow_body, async_range.loc); + } + }, + // "async x => {}" + .t_identifier => { + if (level.lte(.assign)) { + // p.markLoweredSyntaxFeature(); + const ref = try p.storeNameInRef(p.lexer.identifier); + var arg = G.Arg{ .binding = p.b(B.Identifier{ + .ref = ref, + }, p.lexer.loc()) }; + p.lexer.next(); + + _ = try p.pushScopeForParsePass(.function_args, async_range.loc); + defer p.popScope(); + + var arrowBody = try p.parseArrowBodySingleArg(arg, FnOrArrowDataParse{ + .allow_await = .allow_expr, + }); + arrowBody.is_async = true; + return p.e(arrowBody, async_range.loc); + } + }, + + // "async()" + // "async () => {}" + .t_open_paren => { + p.lexer.next(); + return p.parseParenExpr(async_range.loc, ParenExprOpts{ .is_async = true, .async_range = async_range }); + }, + + // "async<T>()" + // "async <T>() => {}" + .t_less_than => { + if (p.options.ts and p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking()) { + p.lexer.next(); + return p.parseParenExpr(async_range.loc, ParenExprOpts{ .is_async = true, .async_range = async_range }); + } + }, + + else => {}, + } + } + + // "async" + // "async + 1" + return p.e( + E.Identifier{ .ref = try p.storeNameInRef("async") }, + async_range.loc, + ); + } + + pub fn trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking(self: *P) bool { + notimpl(); + } + + pub fn parseExprOrBindings(p: *P, level: Level, errors: ?*DeferredErrors) Expr { + return p.parseExprCommon(level, errors, Expr.EFlags.none); + } + + pub fn parseExpr(p: *P, level: Level) Expr { + return p.parseExprCommon(level, null, Expr.EFlags.none); + } + + pub fn parseExprWithFlags(p: *P, level: Level, flags: Expr.EFlags) Expr { + return p.parseExprCommon(level, null, flags); + } + + pub fn parseExprCommon(p: *P, level: Level, errors: ?*DeferredErrors, flags: Expr.EFlags) Expr { + const had_pure_comment_before = p.lexer.has_pure_comment_before and !p.options.ignore_dce_annotations; + var expr = p.parsePrefix(level, errors, flags); + + // There is no formal spec for "__PURE__" comments but from reverse- + // engineering, it looks like they apply to the next CallExpression or + // NewExpression. So in "/* @__PURE__ */ a().b() + c()" the comment applies + // to the expression "a().b()". + + if (had_pure_comment_before and level.lt(.call)) { + expr = p.parseSuffix(expr, @intToEnum(Level, @enumToInt(Level.call) - 1), errors, flags); + switch (expr.data) { + .e_call => |ex| { + ex.can_be_unwrapped_if_unused = true; + }, + .e_new => |ex| { + ex.can_be_unwrapped_if_unused = true; + }, + else => {}, + } + } + + return p.parseSuffix(expr, level, errors, flags); + } + + pub fn addImportRecord(p: *P, kind: ImportKind, loc: logger.Loc, name: string) u32 { + var index = p.import_records.items.len; + const record = ImportRecord{ + .kind = kind, + .range = p.source.rangeOfString(loc), + .path = fs.Path.init(name), + }; + p.import_records.append(record) catch unreachable; + return @intCast(u32, index); + } + + pub fn popScope(p: *P) void { + const current_scope = p.current_scope; + // We cannot rename anything inside a scope containing a direct eval() call + if (current_scope.contains_direct_eval) { + var iter = current_scope.members.iterator(); + while (iter.next()) |member| { + + // Using direct eval when bundling is not a good idea in general because + // esbuild must assume that it can potentially reach anything in any of + // the containing scopes. We try to make it work but this isn't possible + // in some cases. + // + // For example, symbols imported using an ESM import are a live binding + // to the underlying symbol in another file. This is emulated during + // scope hoisting by erasing the ESM import and just referencing the + // underlying symbol in the flattened bundle directly. However, that + // symbol may have a different name which could break uses of direct + // eval: + // + // // Before bundling + // import { foo as bar } from './foo.js' + // console.log(eval('bar')) + // + // // After bundling + // let foo = 123 // The contents of "foo.js" + // console.log(eval('bar')) + // + // There really isn't any way to fix this. You can't just rename "foo" to + // "bar" in the example above because there may be a third bundled file + // that also contains direct eval and imports the same symbol with a + // different conflicting import alias. And there is no way to store a + // live binding to the underlying symbol in a variable with the import's + // name so that direct eval can access it: + // + // // After bundling + // let foo = 123 // The contents of "foo.js" + // const bar = /* cannot express a live binding to "foo" here */ + // console.log(eval('bar')) + // + // Technically a "with" statement could potentially make this work (with + // a big hit to performance), but they are deprecated and are unavailable + // in strict mode. This is a non-starter since all ESM code is strict mode. + // + // So while we still try to obey the requirement that all symbol names are + // pinned when direct eval is present, we make an exception for top-level + // symbols in an ESM file when bundling is enabled. We make no guarantee + // that "eval" will be able to reach these symbols and we allow them to be + // renamed or removed by tree shaking. + // if (p.currentScope.parent == null and p.has_es_module_syntax) { + // continue; + // } + + p.symbols.items[member.value.ref.inner_index].must_not_be_renamed = true; + } + } + + p.current_scope = current_scope.parent orelse p.panic("Internal error: attempted to call popScope() on the topmost scope", .{}); + } + + pub fn markExprAsParenthesized(p: *P, expr: *Expr) void { + switch (expr.data) { + .e_array => |ex| { + ex.is_parenthesized = true; + }, + .e_object => |ex| { + ex.is_parenthesized = true; + }, + else => { + return; + }, + } + } + + pub fn parseYieldExpr(p: *P, loc: logger.Loc) Expr { + // Parse a yield-from expression, which yields from an iterator + const isStar = p.lexer.token == T.t_asterisk; + + if (isStar) { + if (p.lexer.has_newline_before) { + p.lexer.unexpected(); + } + p.lexer.next(); + } + + var value: ?ExprNodeIndex = null; + switch (p.lexer.token) { + .t_close_brace, .t_close_paren, .t_colon, .t_comma, .t_semicolon => {}, + else => { + if (isStar or !p.lexer.has_newline_before) { + value = p.parseExpr(.yield); + } + }, + } + + return p.e(E.Yield{ + .value = value, + .is_star = isStar, + }, loc); + } + + pub fn parseProperty(p: *P, kind: Property.Kind, opts: *PropertyOpts, errors: ?*DeferredErrors) ?G.Property { + var key: Expr = undefined; + var key_range = p.lexer.range(); + var is_computed = false; + + switch (p.lexer.token) { + .t_numeric_literal => { + key = p.e(E.Number{ + .value = p.lexer.number, + }, p.lexer.loc()); + // p.checkForLegacyOctalLiteral() + p.lexer.next(); + }, + .t_string_literal => { + key = p.parseStringLiteral(); + }, + .t_big_integer_literal => { + key = p.e(E.BigInt{ .value = p.lexer.identifier }, p.lexer.loc()); + // markSyntaxFeature + p.lexer.next(); + }, + .t_private_identifier => { + if (!opts.is_class or opts.ts_decorators.len > 0) { + p.lexer.expected(.t_identifier); + } + + key = p.e(E.PrivateIdentifier{ .ref = p.storeNameInRef(p.lexer.identifier) catch unreachable }, p.lexer.loc()); + p.lexer.next(); + }, + .t_open_bracket => { + is_computed = true; + // p.markSyntaxFeature(compat.objectExtensions, p.lexer.range()) + p.lexer.next(); + const wasIdentifier = p.lexer.token == .t_identifier; + const expr = p.parseExpr(.comma); + + // Handle index signatures + if (p.options.ts and p.lexer.token == .t_colon and wasIdentifier and opts.is_class) { + switch (expr.data) { + .e_identifier => |ident| { + p.lexer.next(); + p.skipTypescriptType(.lowest); + p.lexer.expect(.t_close_bracket); + p.lexer.expect(.t_colon); + p.skipTypescriptType(.lowest); + p.lexer.expectOrInsertSemicolon(); + + // Skip this property entirely + return null; + }, + else => {}, + } + } + + p.lexer.expect(.t_close_bracket); + key = expr; + }, + .t_asterisk => { + if (kind != .normal or opts.is_generator) { + p.lexer.unexpected(); + } + + p.lexer.next(); + opts.is_generator = true; + return p.parseProperty(.normal, opts, errors); + }, + + else => { + const name = p.lexer.identifier; + const raw = p.lexer.raw(); + const name_range = p.lexer.range(); + + if (!p.lexer.isIdentifierOrKeyword()) { + p.lexer.expect(.t_identifier); + } + + p.lexer.next(); + + // Support contextual keywords + if (kind == .normal and !opts.is_generator) { + // Does the following token look like a key? + var couldBeModifierKeyword = p.lexer.isIdentifierOrKeyword(); + if (!couldBeModifierKeyword) { + switch (p.lexer.token) { + .t_open_bracket, .t_numeric_literal, .t_string_literal, .t_asterisk, .t_private_identifier => { + couldBeModifierKeyword = true; + }, + else => {}, + } + } + + // If so, check for a modifier keyword + if (couldBeModifierKeyword) { + // TODO: micro-optimization, use a smaller list for non-typescript files. + if (js_lexer.PropertyModifierKeyword.List.get(name)) |keyword| { + switch (keyword) { + .p_get => { + if (!opts.is_async and strings.eql(raw, name)) { + // p.markSyntaxFeautre(ObjectAccessors, name_range) + return p.parseProperty(.get, opts, null); + } + }, + + .p_set => { + if (!opts.is_async and strings.eql(raw, name)) { + // p.markSyntaxFeautre(ObjectAccessors, name_range) + return p.parseProperty(.set, opts, null); + } + }, + .p_async => { + if (!opts.is_async and strings.eql(raw, name) and !p.lexer.has_newline_before) { + opts.is_async = true; + opts.async_range = name_range; + + // p.markSyntaxFeautre(ObjectAccessors, name_range) + return p.parseProperty(kind, opts, null); + } + }, + .p_static => { + if (!opts.is_static and !opts.is_async and opts.is_class and strings.eql(raw, name)) { + opts.is_static = true; + return p.parseProperty(kind, opts, null); + } + }, + .p_private, .p_protected, .p_public, .p_readonly, .p_abstract, .p_declare, .p_override => { + // Skip over TypeScript keywords + if (opts.is_class and p.options.ts and strings.eql(raw, name)) { + return p.parseProperty(kind, opts, null); + } + }, + } + } + } + } + + key = p.e(E.String{ + .value = p.lexer.stringToUTF16(name), + }, name_range.loc); + + // Parse a shorthand property + if (!opts.is_class and kind == .normal and p.lexer.token != .t_colon and p.lexer.token != .t_open_paren and p.lexer.token != .t_less_than and !opts.is_generator and !js_lexer.Keywords.has(name)) { + if ((p.fn_or_arrow_data_parse.allow_await != .allow_ident and strings.eql(name, "await")) or (p.fn_or_arrow_data_parse.allow_yield != .allow_ident and strings.eql(name, "yield"))) { + // TODO: add fmt to addRangeError + p.log.addRangeError(p.source, name_range, "Cannot use \"yield\" or \"await\" here.") catch unreachable; + } + + const ref = p.storeNameInRef(name) catch unreachable; + const value = p.e(E.Identifier{ .ref = ref }, key.loc); + + // Destructuring patterns have an optional default value + var initializer: ?Expr = null; + if (errors != null and p.lexer.token == .t_equals) { + (errors orelse unreachable).invalid_expr_default_value = p.lexer.range(); + p.lexer.next(); + initializer = p.parseExpr(.comma); + } + + return G.Property{ + .kind = kind, + .key = key, + .value = value, + .initializer = initializer, + .flags = Flags.Property{ .was_shorthand = true }, + }; + } + }, + } + + if (p.options.ts) { + // "class X { foo?: number }" + // "class X { foo!: number }" + if (opts.is_class and (p.lexer.token == .t_question or p.lexer.token == .t_exclamation)) { + p.lexer.next(); + } + + // "class X { foo?<T>(): T }" + // "const x = { foo<T>(): T {} }" + p.skipTypescriptTypeParameters(); + } + + // Parse a class field with an optional initial value + if (opts.is_class and kind == .normal and !opts.is_async and !opts.is_generator and p.lexer.token != .t_open_paren) { + var initializer: ?Expr = null; + + // Forbid the names "constructor" and "prototype" in some cases + if (!is_computed) { + switch (key.data) { + .e_string => |str| { + if (std.mem.eql(u16, str.value, std.unicode.utf8ToUtf16LeStringLiteral("constructor")) or (opts.is_static and std.mem.eql(u16, str.value, std.unicode.utf8ToUtf16LeStringLiteral("prototype")))) { + // TODO: fmt error message to include string value. + p.log.addRangeError(p.source, key_range, "Invalid field name") catch unreachable; + } + }, + else => {}, + } + } + + // Skip over types + if (p.options.ts and p.lexer.token == .t_colon) { + p.lexer.next(); + p.skipTypescriptType(.lowest); + } + + if (p.lexer.token == .t_equals) { + p.lexer.next(); + initializer = p.parseExpr(.comma); + } + + // Special-case private identifiers + switch (key.data) { + .e_private_identifier => |private| { + const name = p.loadNameFromRef(private.ref); + if (strings.eql(name, "#constructor")) { + p.log.addRangeError(p.source, key_range, "Invalid field name \"#constructor\"") catch unreachable; + } + + var declare: js_ast.Symbol.Kind = undefined; + if (opts.is_static) { + declare = .private_static_field; + } else { + declare = .private_field; + } + private.ref = p.declareSymbol(declare, key.loc, name) catch unreachable; + }, + else => {}, + } + + p.lexer.expectOrInsertSemicolon(); + + return G.Property{ + .ts_decorators = opts.ts_decorators, + .kind = kind, + .flags = Flags.Property{ + .is_computed = is_computed, + .is_static = opts.is_static, + }, + .key = key, + .initializer = initializer, + }; + } + + // Parse a method expression + if (p.lexer.token == .t_open_paren or kind != .normal or opts.is_class or opts.is_async or opts.is_generator) { + if (p.lexer.token == .t_open_paren and kind != .get and kind != .set) { + // markSyntaxFeature object extensions + } + + const loc = p.lexer.loc(); + const scope_index = p.pushScopeForParsePass(.function_args, loc) catch unreachable; + var is_constructor = false; + + // Forbid the names "constructor" and "prototype" in some cases + if (opts.is_class and !is_computed) { + switch (key.data) { + .e_string => |str| { + if (!opts.is_static and strings.eqlUtf16("constructor", str.value)) { + if (kind == .get) { + p.log.addRangeError(p.source, key_range, "Class constructor cannot be a getter") catch unreachable; + } else if (kind == .set) { + p.log.addRangeError(p.source, key_range, "Class constructor cannot be a setter") catch unreachable; + } else if (opts.is_async) { + p.log.addRangeError(p.source, key_range, "Class constructor cannot be an async function") catch unreachable; + } else if (opts.is_generator) { + p.log.addRangeError(p.source, key_range, "Class constructor cannot be a generator function") catch unreachable; + } else { + is_constructor = true; + } + } else if (opts.is_static and strings.eqlUtf16("prototype", str.value)) { + p.log.addRangeError(p.source, key_range, "Invalid static method name \"prototype\"") catch unreachable; + } + }, + else => {}, + } + } + + var func = p.parseFn(null, FnOrArrowDataParse{ + .async_range = opts.async_range, + .allow_await = if (opts.is_async) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, + .allow_yield = if (opts.is_generator) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, + .allow_super_call = opts.class_has_extends and is_constructor, + .allow_ts_decorators = opts.allow_ts_decorators, + .is_constructor = is_constructor, + + // Only allow omitting the body if we're parsing TypeScript class + .allow_missing_body_for_type_script = p.options.ts and opts.is_class, + }); + + // "class Foo { foo(): void; foo(): void {} }" + if (func.body == null) { + // Skip this property entirely + p.popAndDiscardScope(scope_index); + return null; + } + + p.popScope(); + func.flags.is_unique_formal_parameters = true; + const value = p.e(E.Function{ .func = func }, loc); + + // Enforce argument rules for accessors + switch (kind) { + .get => { + if (func.args.len > 0) { + const r = js_lexer.rangeOfIdentifier(&p.source, func.args[0].binding.loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Getter {s} must have zero arguments", .{p.keyNameForError(key)}) catch unreachable; + } + }, + .set => { + if (func.args.len != 1) { + var r = js_lexer.rangeOfIdentifier(&p.source, if (func.args.len > 0) func.args[0].binding.loc else loc); + if (func.args.len > 1) { + r = js_lexer.rangeOfIdentifier(&p.source, func.args[1].binding.loc); + } + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Setter {s} must have exactly 1 argument (there are {d})", .{ p.keyNameForError(key), func.args.len }) catch unreachable; + } + }, + else => {}, + } + + // Special-case private identifiers + switch (key.data) { + .e_private_identifier => |private| { + var declare: Symbol.Kind = undefined; + var suffix: string = undefined; + switch (kind) { + .get => { + if (opts.is_static) { + declare = .private_static_get; + } else { + declare = .private_get; + } + suffix = "_get"; + }, + .set => { + if (opts.is_static) { + declare = .private_static_set; + } else { + declare = .private_set; + } + suffix = "_set"; + }, + else => { + if (opts.is_static) { + declare = .private_static_method; + } else { + declare = .private_method; + } + suffix = "_fn"; + }, + } + + const name = p.loadNameFromRef(private.ref); + if (strings.eql(name, "#constructor")) { + p.log.addRangeError(p.source, key_range, "Invalid method name \"#constructor\"") catch unreachable; + } + private.ref = p.declareSymbol(declare, key.loc, name) catch unreachable; + }, + else => {}, + } + + return G.Property{ + .ts_decorators = opts.ts_decorators, + .kind = kind, + .flags = Flags.Property{ + .is_computed = is_computed, + .is_method = true, + .is_static = opts.is_static, + }, + .key = key, + .value = value, + }; + } + + p.lexer.expect(.t_colon); + + const value = p.parseExprOrBindings(.comma, errors); + + return G.Property{ + .ts_decorators = &[_]Expr{}, + .kind = kind, + .flags = Flags.Property{ + .is_computed = is_computed, + }, + .key = key, + .value = value, + }; + } + + // By the time we call this, the identifier and type parameters have already + // been parsed. We need to start parsing from the "extends" clause. + pub fn parseClass(p: *P, class_keyword: logger.Range, name: ?js_ast.LocRef, class_opts: ParseClassOptions) G.Class { + var extends: ?Expr = null; + + if (p.lexer.token == .t_extends) { + p.lexer.next(); + extends = p.parseExpr(.new); + + // TypeScript's type argument parser inside expressions backtracks if the + // first token after the end of the type parameter list is "{", so the + // parsed expression above will have backtracked if there are any type + // arguments. This means we have to re-parse for any type arguments here. + // This seems kind of wasteful to me but it's what the official compiler + // does and it probably doesn't have that high of a performance overhead + // because "extends" clauses aren't that frequent, so it should be ok. + if (p.options.ts) { + p.skipTypeScriptTypeArguments(false); // isInsideJSXElement + } + } + + if (p.options.ts and p.lexer.isContextualKeyword("implements")) { + p.lexer.next(); + + while (true) { + p.skipTypescriptType(.lowest); + if (p.lexer.token != .t_comma) { + break; + } + p.lexer.next(); + } + } + + var body_loc = p.lexer.loc(); + p.lexer.expect(T.t_open_brace); + var properties = List(G.Property).init(p.allocator); + + // Allow "in" and private fields inside class bodies + const old_allow_in = p.allow_in; + const old_allow_private_identifiers = p.allow_private_identifiers; + p.allow_in = true; + p.allow_private_identifiers = true; + + // A scope is needed for private identifiers + const scopeIndex = p.pushScopeForParsePass(.class_body, body_loc) catch unreachable; + + var opts = PropertyOpts{ .is_class = true, .allow_ts_decorators = class_opts.allow_ts_decorators, .class_has_extends = extends != null }; + while (p.lexer.token != T.t_close_brace) { + if (p.lexer.token == .t_semicolon) { + p.lexer.next(); + continue; + } + + opts = PropertyOpts{ .is_class = true, .allow_ts_decorators = class_opts.allow_ts_decorators, .class_has_extends = extends != null }; + + // Parse decorators for this property + const first_decorator_loc = p.lexer.loc(); + if (opts.allow_ts_decorators) { + opts.ts_decorators = p.parseTypeScriptDecorators(); + } else { + opts.ts_decorators = &[_]Expr{}; + } + + // This property may turn out to be a type in TypeScript, which should be ignored + if (p.parseProperty(.normal, &opts, null)) |property| { + properties.append(property) catch unreachable; + + // Forbid decorators on class constructors + if (opts.ts_decorators.len > 0) { + switch ((property.key orelse p.panic("Internal error: Expected property {s} to have a key.", .{property})).data) { + .e_string => |str| { + if (strings.eqlUtf16("constructor", str.value)) { + p.log.addError(p.source, first_decorator_loc, "TypeScript does not allow decorators on class constructors") catch unreachable; + } + }, + else => {}, + } + } + } + } + + if (class_opts.is_type_script_declare) { + p.popAndDiscardScope(scopeIndex); + } else { + p.popScope(); + } + + p.allow_in = old_allow_in; + p.allow_private_identifiers = old_allow_private_identifiers; + + p.lexer.expect(.t_close_brace); + + return G.Class{ + .class_name = name, + .extends = extends, + .ts_decorators = class_opts.ts_decorators, + .class_keyword = class_keyword, + .body_loc = body_loc, + .properties = properties.toOwnedSlice(), + }; + } + + pub fn skipTypeScriptTypeArguments(p: *P, isInsideJSXElement: bool) void { + notimpl(); + } + + pub fn parseTemplateParts(p: *P, include_raw: bool) std.meta.Tuple(&[_]type{ []E.TemplatePart, logger.Loc }) { + var parts = List(E.TemplatePart).initCapacity(p.allocator, 1) catch unreachable; + // Allow "in" inside template literals + var oldAllowIn = p.allow_in; + p.allow_in = true; + var legacy_octal_loc = logger.Loc.Empty; + + parseTemplatePart: while (true) { + p.lexer.next(); + var value = p.parseExpr(.lowest); + var tail_loc = p.lexer.loc(); + p.lexer.rescanCloseBraceAsTemplateToken(); + var tail = p.lexer.string_literal; + var tail_raw: string = ""; + + if (include_raw) { + tail_raw = p.lexer.rawTemplateContents(); + } else if (p.lexer.legacy_octal_loc.start > tail_loc.start) { + legacy_octal_loc = p.lexer.legacy_octal_loc; + } + + parts.append(E.TemplatePart{ + .value = value, + .tail_loc = tail_loc, + .tail = tail, + .tail_raw = tail_raw, + }) catch unreachable; + + if (p.lexer.token == .t_template_tail) { + p.lexer.next(); + break :parseTemplatePart; + } + std.debug.assert(p.lexer.token != .t_end_of_file); + } + + p.allow_in = oldAllowIn; + + return .{ .@"0" = parts.toOwnedSlice(), .@"1" = legacy_octal_loc }; + } + + // This assumes the caller has already checked for TStringLiteral or TNoSubstitutionTemplateLiteral + pub fn parseStringLiteral(p: *P) Expr { + var legacy_octal_loc: logger.Loc = logger.Loc.Empty; + var loc = p.lexer.loc(); + if (p.lexer.legacy_octal_loc.start > loc.start) { + legacy_octal_loc = p.lexer.legacy_octal_loc; + } + + const expr = p.e(E.String{ + .value = p.lexer.string_literal, + .legacy_octal_loc = legacy_octal_loc, + .prefer_template = p.lexer.token == .t_no_substitution_template_literal, + }, loc); + p.lexer.next(); + return expr; + } + + pub fn parseCallArgs(p: *P) []Expr { + // Allow "in" inside call arguments + const old_allow_in = p.allow_in; + p.allow_in = true; + + var args = List(Expr).init(p.allocator); + p.lexer.expect(.t_open_paren); + + while (p.lexer.token != .t_close_paren) { + const loc = p.lexer.loc(); + const is_spread = p.lexer.token == .t_dot_dot_dot; + if (is_spread) { + // p.mark_syntax_feature(compat.rest_argument, p.lexer.range()); + p.lexer.next(); + } + var arg = p.parseExpr(.comma); + if (is_spread) { + arg = p.e(E.Spread{ .value = arg }, loc); + } + args.append(arg) catch unreachable; + if (p.lexer.token != .t_comma) { + break; + } + p.lexer.next(); + } + + p.lexer.expect(.t_close_paren); + p.allow_in = old_allow_in; + return args.toOwnedSlice(); + } + + pub fn parseSuffix(p: *P, left: Expr, level: Level, errors: ?*DeferredErrors, flags: Expr.EFlags) Expr { + return _parseSuffix(p, left, level, errors orelse &DeferredErrors.None, flags); + } + pub fn _parseSuffix(p: *P, _left: Expr, level: Level, errors: *DeferredErrors, flags: Expr.EFlags) Expr { + var expr: Expr = undefined; + var left = _left; + var loc = p.lexer.loc(); + var optional_chain: ?js_ast.OptionalChain = null; + + while (true) { + if (p.lexer.loc().start == p.after_arrow_body_loc.start) { + while (true) { + switch (p.lexer.token) { + .t_comma => { + if (level.gte(.comma)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ + .op = .bin_comma, + .left = left, + .right = p.parseExpr(.comma), + }, left.loc); + }, + else => { + return left; + }, + } + } + } + + // Stop now if this token is forbidden to follow a TypeScript "as" cast + if (p.lexer.loc().start == p.forbid_suffix_after_as_loc.start) { + return left; + } + + // Reset the optional chain flag by default. That way we won't accidentally + // treat "c.d" as OptionalChainContinue in "a?.b + c.d". + var old_optional_chain = optional_chain; + optional_chain = null; + + switch (p.lexer.token) { + .t_dot => { + p.lexer.next(); + if (p.lexer.token == .t_private_identifier and p.allow_private_identifiers) { + // "a.#b" + // "a?.b.#c" + switch (left.data) { + .e_super => { + p.lexer.expected(.t_identifier); + }, + else => {}, + } + + var name = p.lexer.identifier; + var name_loc = p.lexer.loc(); + p.lexer.next(); + const ref = p.storeNameInRef(name) catch unreachable; + left = p.e(E.Index{ + .target = left, + .index = p.e( + E.PrivateIdentifier{ + .ref = ref, + }, + name_loc, + ), + .optional_chain = old_optional_chain, + }, left.loc); + } else { + // "a.b" + // "a?.b.c" + if (!p.lexer.isIdentifierOrKeyword()) { + p.lexer.expect(.t_identifier); + } + + var name = p.lexer.identifier; + var name_loc = p.lexer.loc(); + p.lexer.next(); + + left = p.e(E.Dot{ .target = left, .name = name, .name_loc = name_loc, .optional_chain = old_optional_chain }, left.loc); + } + + optional_chain = old_optional_chain; + }, + .t_question_dot => { + p.lexer.next(); + var optional_start = js_ast.OptionalChain.start; + + // TODO: Remove unnecessary optional chains + // if p.options.mangleSyntax { + // if isNullOrUndefined, _, ok := toNullOrUndefinedWithSideEffects(left.Data); ok and !isNullOrUndefined { + // optionalStart = js_ast.OptionalChainNone + // } + // } + + switch (p.lexer.token) { + .t_open_bracket => { + // "a?.[b]" + p.lexer.next(); + + // allow "in" inside the brackets; + const old_allow_in = p.allow_in; + p.allow_in = true; + + const index = p.parseExpr(.lowest); + + p.allow_in = old_allow_in; + + p.lexer.expect(.t_close_bracket); + left = p.e( + E.Index{ .target = left, .index = index, .optional_chain = optional_start }, + left.loc, + ); + }, + + .t_open_paren => { + // "a?.()" + if (level.gte(.call)) { + return left; + } + + left = p.e(E.Call{ + .target = left, + .args = p.parseCallArgs(), + .optional_chain = optional_start, + }, left.loc); + }, + .t_less_than => { + // "a?.<T>()" + if (!p.options.ts) { + p.lexer.expected(.t_identifier); + } + + p.skipTypeScriptTypeArguments(false); + if (p.lexer.token != .t_open_paren) { + p.lexer.expected(.t_open_paren); + } + + if (level.gte(.call)) { + return left; + } + + left = p.e( + E.Call{ .target = left, .args = p.parseCallArgs(), .optional_chain = optional_start }, + left.loc, + ); + }, + else => { + if (p.lexer.token == .t_private_identifier and p.allow_private_identifiers) { + // "a?.#b" + const name = p.lexer.identifier; + const name_loc = p.lexer.loc(); + p.lexer.next(); + const ref = p.storeNameInRef(name) catch unreachable; + left = p.e(E.Index{ + .target = left, + .index = p.e( + E.PrivateIdentifier{ + .ref = ref, + }, + name_loc, + ), + .optional_chain = optional_start, + }, left.loc); + } else { + // "a?.b" + if (!p.lexer.isIdentifierOrKeyword()) { + p.lexer.expect(.t_identifier); + } + const name = p.lexer.identifier; + const name_loc = p.lexer.loc(); + p.lexer.next(); + + left = p.e(E.Dot{ + .target = left, + .name = name, + .name_loc = name_loc, + .optional_chain = optional_start, + }, left.loc); + } + }, + } + + // Only continue if we have started + if (optional_start == .start) { + optional_start = .ccontinue; + } + }, + .t_no_substitution_template_literal => { + if (old_optional_chain != null) { + p.log.addRangeError(p.source, p.lexer.range(), "Template literals cannot have an optional chain as a tag") catch unreachable; + } + // p.markSyntaxFeature(compat.TemplateLiteral, p.lexer.Range()); + const head = p.lexer.string_literal; + const head_raw = p.lexer.rawTemplateContents(); + p.lexer.next(); + left = p.e(E.Template{ + .tag = left, + .head = head, + .head_raw = head_raw, + .legacy_octal_loc = logger.Loc.Empty, + }, left.loc); + }, + .t_template_head => { + if (old_optional_chain != null) { + p.log.addRangeError(p.source, p.lexer.range(), "Template literals cannot have an optional chain as a tag") catch unreachable; + } + // p.markSyntaxFeature(compat.TemplateLiteral, p.lexer.Range()); + const head = p.lexer.string_literal; + const head_raw = p.lexer.rawTemplateContents(); + const partsGroup = p.parseTemplateParts(true); + p.lexer.next(); + const tag = left; + left = p.e(E.Template{ .tag = tag, .head = head, .head_raw = head_raw, .parts = partsGroup.@"0" }, left.loc); + }, + .t_open_bracket => { + // When parsing a decorator, ignore EIndex expressions since they may be + // part of a computed property: + // + // class Foo { + // @foo ['computed']() {} + // } + // + // This matches the behavior of the TypeScript compiler. + if (flags != .ts_decorator) { + return left; + } + + p.lexer.next(); + + // Allow "in" inside the brackets + const old_allow_in = p.allow_in; + p.allow_in = true; + + const index = p.parseExpr(.lowest); + + p.allow_in = old_allow_in; + + p.lexer.expect(.t_close_bracket); + + left = p.e(E.Index{ + .target = left, + .index = index, + .optional_chain = old_optional_chain, + }, left.loc); + optional_chain = old_optional_chain; + }, + .t_open_paren => { + if (level.gte(.call)) { + return left; + } + + left = p.e( + E.Call{ + .target = left, + .args = p.parseCallArgs(), + .optional_chain = old_optional_chain, + }, + left.loc, + ); + optional_chain = old_optional_chain; + }, + .t_question => { + if (level.gte(.conditional)) { + return left; + } + p.lexer.next(); + + // Stop now if we're parsing one of these: + // "(a?) => {}" + // "(a?: b) => {}" + // "(a?, b?) => {}" + if (p.options.ts and left.loc.start == p.latest_arrow_arg_loc.start and (p.lexer.token == .t_colon or + p.lexer.token == .t_close_paren or p.lexer.token == .t_comma)) + { + if (errors.isEmpty()) { + p.lexer.unexpected(); + } + errors.invalid_expr_after_question = p.lexer.range(); + return left; + } + + // Allow "in" in between "?" and ":" + const old_allow_in = p.allow_in; + p.allow_in = true; + + const yes = p.parseExpr(.comma); + + p.allow_in = old_allow_in; + p.lexer.expect(.t_colon); + const no = p.parseExpr(.comma); + + left = p.e(E.If{ + .test_ = left, + .yes = yes, + .no = no, + }, left.loc); + }, + .t_exclamation => { + // Skip over TypeScript non-null assertions + if (p.lexer.has_newline_before) { + return left; + } + + if (!p.options.ts) { + p.lexer.unexpected(); + } + + if (level.gte(.postfix)) { + return left; + } + + p.lexer.next(); + optional_chain = old_optional_chain; + }, + .t_minus_minus => { + if (p.lexer.has_newline_before or level.gte(.postfix)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Unary{ .op = .un_post_dec, .value = left }, left.loc); + }, + .t_plus_plus => { + if (p.lexer.has_newline_before or level.gte(.postfix)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Unary{ .op = .un_post_inc, .value = left }, left.loc); + }, + .t_comma => { + if (level.gte(.comma)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_comma, .left = left, .right = p.parseExpr(.comma) }, left.loc); + }, + .t_plus => { + if (level.gte(.add)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_add, .left = left, .right = p.parseExpr(.add) }, left.loc); + }, + .t_plus_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_add_assign, .left = left, .right = p.parseExpr(@intToEnum(Op.Level, @enumToInt(Op.Level.assign) - 1)) }, left.loc); + }, + .t_minus => { + if (level.gte(.add)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_sub, .left = left, .right = p.parseExpr(.add) }, left.loc); + }, + .t_minus_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_sub_assign, .left = left, .right = p.parseExpr(Op.Level.assign.sub(1)) }, left.loc); + }, + .t_asterisk => { + if (level.gte(.multiply)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_mul, .left = left, .right = p.parseExpr(.multiply) }, left.loc); + }, + .t_asterisk_asterisk => { + if (level.gte(.exponentiation)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_pow, .left = left, .right = p.parseExpr(Op.Level.exponentiation.sub(1)) }, left.loc); + }, + .t_asterisk_asterisk_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_pow_assign, .left = left, .right = p.parseExpr(Op.Level.assign.sub(1)) }, left.loc); + }, + .t_asterisk_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_mul_assign, .left = left, .right = p.parseExpr(Op.Level.assign.sub(1)) }, left.loc); + }, + .t_percent => { + if (level.gte(.multiply)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_rem, .left = left, .right = p.parseExpr(Op.Level.multiply) }, left.loc); + }, + .t_percent_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_rem_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_slash => { + if (level.gte(.multiply)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_div, .left = left, .right = p.parseExpr(Level.multiply) }, left.loc); + }, + .t_slash_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_div_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_equals_equals => { + if (level.gte(.equals)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_loose_eq, .left = left, .right = p.parseExpr(Level.equals) }, left.loc); + }, + .t_exclamation_equals => { + if (level.gte(.equals)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_loose_ne, .left = left, .right = p.parseExpr(Level.equals) }, left.loc); + }, + .t_equals_equals_equals => { + if (level.gte(.equals)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_strict_eq, .left = left, .right = p.parseExpr(Level.equals) }, left.loc); + }, + .t_exclamation_equals_equals => { + if (level.gte(.equals)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_strict_ne, .left = left, .right = p.parseExpr(Level.equals) }, left.loc); + }, + .t_less_than => { + // TypeScript allows type arguments to be specified with angle brackets + // inside an expression. Unlike in other languages, this unfortunately + // appears to require backtracking to parse. + if (p.options.ts and p.trySkipTypeScriptTypeArgumentsWithBacktracking()) { + optional_chain = old_optional_chain; + continue; + } + + if (level.gte(.compare)) { + return left; + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_lt, .left = left, .right = p.parseExpr(.compare) }, left.loc); + }, + .t_less_than_equals => { + if (level.gte(.compare)) { + return left; + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_le, .left = left, .right = p.parseExpr(.compare) }, left.loc); + }, + .t_greater_than => { + if (level.gte(.compare)) { + return left; + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_gt, .left = left, .right = p.parseExpr(.compare) }, left.loc); + }, + .t_greater_than_equals => { + if (level.gte(.compare)) { + return left; + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_ge, .left = left, .right = p.parseExpr(.compare) }, left.loc); + }, + .t_less_than_less_than => { + if (level.gte(.shift)) { + return left; + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_shl, .left = left, .right = p.parseExpr(.shift) }, left.loc); + }, + .t_less_than_less_than_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_shl_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_greater_than_greater_than => { + if (level.gte(.shift)) { + return left; + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_shr, .left = left, .right = p.parseExpr(.shift) }, left.loc); + }, + .t_greater_than_greater_than_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_shl_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_greater_than_greater_than_greater_than => { + if (level.gte(.shift)) { + return left; + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_u_shr, .left = left, .right = p.parseExpr(.shift) }, left.loc); + }, + .t_greater_than_greater_than_greater_than_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_u_shr_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_question_question => { + if (level.gte(.nullish_coalescing)) { + return left; + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_nullish_coalescing, .left = left, .right = p.parseExpr(.nullish_coalescing) }, left.loc); + }, + .t_question_question_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_nullish_coalescing_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_bar_bar => { + if (level.gte(.logical_or)) { + return left; + } + + // Prevent "||" inside "??" from the right + if (level.eql(.nullish_coalescing)) { + p.lexer.unexpected(); + } + + p.lexer.next(); + const right = p.parseExpr(.logical_or); + left = p.e(E.Binary{ .op = Op.Code.bin_logical_or, .left = left, .right = right }, left.loc); + + if (level.lt(.nullish_coalescing)) { + left = p.parseSuffix(left, Level.nullish_coalescing.add(1), null, flags); + + if (p.lexer.token == .t_question_question) { + p.lexer.unexpected(); + } + } + }, + .t_bar_bar_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_logical_or_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_ampersand_ampersand => { + if (level.gte(.logical_and)) { + return left; + } + + // Prevent "&&" inside "??" from the right + if (level.eql(.nullish_coalescing)) { + p.lexer.unexpected(); + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_logical_and, .left = left, .right = p.parseExpr(.logical_and) }, left.loc); + + // Prevent "&&" inside "??" from the left + if (level.lt(.nullish_coalescing)) { + left = p.parseSuffix(left, Level.nullish_coalescing.add(1), null, flags); + + if (p.lexer.token == .t_question_question) { + p.lexer.unexpected(); + } + } + }, + .t_ampersand_ampersand_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_logical_and_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_bar => { + if (level.gte(.bitwise_or)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_bitwise_or, .left = left, .right = p.parseExpr(.bitwise_or) }, left.loc); + }, + .t_bar_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_bitwise_or_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_ampersand => { + if (level.gte(.bitwise_and)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_bitwise_and, .left = left, .right = p.parseExpr(.bitwise_and) }, left.loc); + }, + .t_ampersand_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_shl_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_caret => { + if (level.gte(.bitwise_xor)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_bitwise_xor, .left = left, .right = p.parseExpr(.bitwise_xor) }, left.loc); + }, + .t_caret_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_bitwise_xor_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_equals => { + if (level.gte(.assign)) { + return left; + } + + p.lexer.next(); + + left = p.e(E.Binary{ .op = .bin_assign, .left = left, .right = p.parseExpr(Level.assign.sub(1)) }, left.loc); + }, + .t_in => { + if (level.gte(.compare) or !p.allow_in) { + return left; + } + + // Warn about "!a in b" instead of "!(a in b)" + switch (left.data) { + .e_unary => |unary| { + if (unary.op == .un_not) { + // TODO: + // p.log.addRangeWarning(source: ?Source, r: Range, text: string) + } + }, + else => {}, + } + + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_in, .left = left, .right = p.parseExpr(.compare) }, left.loc); + }, + .t_instanceof => { + if (level.gte(.compare)) { + return left; + } + + // Warn about "!a instanceof b" instead of "!(a instanceof b)". Here's an + // example of code with this problem: https://github.com/mrdoob/three.js/pull/11182. + if (!p.options.suppress_warnings_about_weird_code) { + switch (left.data) { + .e_unary => |unary| { + if (unary.op == .un_not) { + // TODO: + // p.log.addRangeWarning(source: ?Source, r: Range, text: string) + } + }, + else => {}, + } + } + p.lexer.next(); + left = p.e(E.Binary{ .op = .bin_instanceof, .left = left, .right = p.parseExpr(.compare) }, left.loc); + }, + else => { + // Handle the TypeScript "as" operator + if (p.options.ts and level.lt(.compare) and !p.lexer.has_newline_before and p.lexer.isContextualKeyword("as")) { + p.lexer.next(); + p.skipTypescriptType(.lowest); + + // These tokens are not allowed to follow a cast expression. This isn't + // an outright error because it may be on a new line, in which case it's + // the start of a new expression when it's after a cast: + // + // x = y as z + // (something); + // + switch (p.lexer.token) { + .t_plus_plus, + .t_minus_minus, + .t_no_substitution_template_literal, + .t_template_head, + .t_open_paren, + .t_open_bracket, + .t_question_dot, + => { + p.forbid_suffix_after_as_loc = p.lexer.loc(); + return left; + }, + else => {}, + } + + if (p.lexer.token.isAssign()) { + p.forbid_suffix_after_as_loc = p.lexer.loc(); + return left; + } + continue; + } + + return left; + }, + } + } + } + + pub fn panic(p: *P, comptime str: string, args: anytype) noreturn { + p.log.addRangeErrorFmt(p.source, p.lexer.range(), p.allocator, str, args) catch unreachable; + + var fixedBuffer = [_]u8{0} ** 4096; + var stream = std.io.fixedBufferStream(&fixedBuffer); + + p.log.print(stream.writer()) catch unreachable; + std.debug.panic("{s}", .{fixedBuffer}); + } + + pub fn _parsePrefix(p: *P, level: Level, errors: *DeferredErrors, flags: Expr.EFlags) Expr { + const loc = p.lexer.loc(); + const l = @enumToInt(level); + std.debug.print("Parse Prefix {s}:{s} @{s} ", .{ p.lexer.token, p.lexer.raw(), @tagName(level) }); + + switch (p.lexer.token) { + .t_super => { + const superRange = p.lexer.range(); + p.lexer.next(); + + switch (p.lexer.token) { + .t_open_paren => { + if (l < @enumToInt(Level.call) and p.fn_or_arrow_data_parse.allow_super_call) { + return p.e(E.Super{}, loc); + } + }, + .t_dot, .t_open_bracket => { + return p.e(E.Super{}, loc); + }, + else => {}, + } + + p.log.addRangeError(p.source, superRange, "Unexpected \"super\"") catch unreachable; + return p.e(E.Super{}, loc); + }, + .t_open_paren => { + p.lexer.next(); + + // Arrow functions aren't allowed in the middle of expressions + if (level.gt(.assign)) { + const oldAllowIn = p.allow_in; + p.allow_in = true; + + var value = p.parseExpr(Level.lowest); + p.markExprAsParenthesized(&value); + p.lexer.expect(.t_close_paren); + p.allow_in = oldAllowIn; + return value; + } + + return p.parseParenExpr(loc, ParenExprOpts{}) catch unreachable; + }, + .t_false => { + p.lexer.next(); + return p.e(E.Boolean{ .value = false }, loc); + }, + .t_true => { + p.lexer.next(); + return p.e(E.Boolean{ .value = true }, loc); + }, + .t_null => { + p.lexer.next(); + return p.e(E.Null{}, loc); + }, + .t_this => { + p.lexer.next(); + return p.e(E.This{}, loc); + }, + .t_identifier => { + const name = p.lexer.identifier; + const name_range = p.lexer.range(); + const raw = p.lexer.raw(); + p.lexer.next(); + + // Handle async and await expressions + switch (AsyncPrefixExpression.find(name)) { + .is_async => { + if (AsyncPrefixExpression.find(raw) != .is_async) { + return p.parseAsyncPrefixExpr(name_range, level) catch unreachable; + } + }, + + .is_await => { + switch (p.fn_or_arrow_data_parse.allow_await) { + .forbid_all => { + p.log.addRangeError(p.source, name_range, "The keyword \"await\" cannot be used here.") catch unreachable; + }, + .allow_expr => { + if (AsyncPrefixExpression.find(raw) != .is_await) { + p.log.addRangeError(p.source, name_range, "The keyword \"await\" cannot be escaped.") catch unreachable; + } else { + if (p.fn_or_arrow_data_parse.is_top_level) { + p.top_level_await_keyword = name_range; + } + + if (p.fn_or_arrow_data_parse.arrow_arg_errors) |*args| { + args.invalid_expr_await = name_range; + } + + const value = p.parseExpr(.prefix); + if (p.lexer.token == T.t_asterisk_asterisk) { + p.lexer.unexpected(); + } + + return p.e(E.Await{ .value = value }, loc); + } + }, + .allow_ident => {}, + } + }, + + .is_yield => { + switch (p.fn_or_arrow_data_parse.allow_yield) { + .forbid_all => { + p.log.addRangeError(p.source, name_range, "The keyword \"yield\" cannot be used here") catch unreachable; + }, + .allow_expr => { + if (AsyncPrefixExpression.find(raw) != .is_yield) { + p.log.addRangeError(p.source, name_range, "The keyword \"yield\" cannot be escaped") catch unreachable; + } else { + if (level.gte(.assign)) { + p.log.addRangeError(p.source, name_range, "Cannot use a \"yield\" here without parentheses") catch unreachable; + } + const value = p.parseExpr(.prefix); + + if (p.fn_or_arrow_data_parse.arrow_arg_errors) |*args| { + args.invalid_expr_yield = name_range; + } + + if (p.lexer.token == T.t_asterisk_asterisk) { + p.lexer.unexpected(); + } + + return p.e(E.Yield{ .value = value }, loc); + } + }, + .allow_ident => { + // Try to gracefully recover if "yield" is used in the wrong place + if (!p.lexer.has_newline_before) { + switch (p.lexer.token) { + .t_null, .t_identifier, .t_false, .t_true, .t_numeric_literal, .t_big_integer_literal, .t_string_literal => { + p.log.addRangeError(p.source, name_range, "Cannot use \"yield\" outside a generator function") catch unreachable; + }, + else => {}, + } + } + }, + } + }, + .none => {}, + } + + // Handle the start of an arrow expression + if (p.lexer.token == .t_equals_greater_than) { + const ref = p.storeNameInRef(name) catch unreachable; + var args = p.allocator.alloc(Arg, 1) catch unreachable; + args[0] = Arg{ .binding = p.b(B.Identifier{ + .ref = ref, + }, loc) }; + + _ = p.pushScopeForParsePass(.function_args, loc) catch unreachable; + defer p.popScope(); + return p.e(p.parseArrowBody(args, p.m(FnOrArrowDataParse{})) catch unreachable, loc); + } + + const ref = p.storeNameInRef(name) catch unreachable; + + return p.e(E.Identifier{ + .ref = ref, + }, loc); + }, + .t_string_literal, .t_no_substitution_template_literal => { + return p.parseStringLiteral(); + }, + .t_template_head => { + var legacy_octal_loc = logger.Loc.Empty; + var head = p.lexer.string_literal; + var head_raw = p.lexer.raw(); + if (p.lexer.legacy_octal_loc.start > loc.start) { + legacy_octal_loc = p.lexer.legacy_octal_loc; + } + + var resp = p.parseTemplateParts(false); + const parts: []E.TemplatePart = resp.@"0"; + const tail_legacy_octal_loc: logger.Loc = resp.@"1"; + if (tail_legacy_octal_loc.start > 0) { + legacy_octal_loc = tail_legacy_octal_loc; + } + // Check if TemplateLiteral is unsupported. We don't care for this product.` + // if () + + return p.e(E.Template{ .head = head, .parts = parts, .legacy_octal_loc = legacy_octal_loc, .head_raw = head_raw }, loc); + }, + .t_numeric_literal => { + const value = p.e(E.Number{ .value = p.lexer.number }, loc); + // p.checkForLegacyOctalLiteral() + p.lexer.next(); + return value; + }, + .t_big_integer_literal => { + const value = p.lexer.identifier; + // markSyntaxFeature bigInt + p.lexer.next(); + return p.e(E.BigInt{ .value = value }, loc); + }, + .t_slash, .t_slash_equals => { + p.lexer.scanRegExp(); + const value = p.lexer.raw(); + p.lexer.next(); + return p.e(E.RegExp{ .value = value }, loc); + }, + .t_void => { + p.lexer.next(); + const value = p.parseExpr(.prefix); + if (p.lexer.token == .t_asterisk_asterisk) { + p.lexer.unexpected(); + } + + return p.e(E.Unary{ + .op = .un_void, + .value = value, + }, loc); + }, + .t_typeof => { + p.lexer.next(); + const value = p.parseExpr(.prefix); + if (p.lexer.token == .t_asterisk_asterisk) { + p.lexer.unexpected(); + } + + return p.e(E.Unary{ .op = .un_typeof, .value = value }, loc); + }, + .t_delete => { + p.lexer.next(); + const value = p.parseExpr(.prefix); + if (p.lexer.token == .t_asterisk_asterisk) { + p.lexer.unexpected(); + } + // TODO: add error deleting private identifier + // const private = value.data.e_private_identifier; + // if (private) |private| { + // const name = p.loadNameFromRef(private.ref); + // p.log.addRangeError(index.loc, ) + // } + + return p.e(E.Unary{ .op = .un_delete, .value = value }, loc); + }, + .t_plus => { + p.lexer.next(); + const value = p.parseExpr(.prefix); + if (p.lexer.token == .t_asterisk_asterisk) { + p.lexer.unexpected(); + } + + return p.e(E.Unary{ .op = .un_pos, .value = value }, loc); + }, + .t_minus => { + p.lexer.next(); + const value = p.parseExpr(.prefix); + if (p.lexer.token == .t_asterisk_asterisk) { + p.lexer.unexpected(); + } + + return p.e(E.Unary{ .op = .un_neg, .value = value }, loc); + }, + .t_tilde => { + p.lexer.next(); + const value = p.parseExpr(.prefix); + if (p.lexer.token == .t_asterisk_asterisk) { + p.lexer.unexpected(); + } + + return p.e(E.Unary{ .op = .un_cpl, .value = value }, loc); + }, + .t_exclamation => { + p.lexer.next(); + const value = p.parseExpr(.prefix); + if (p.lexer.token == .t_asterisk_asterisk) { + p.lexer.unexpected(); + } + + return p.e(E.Unary{ .op = .un_not, .value = value }, loc); + }, + .t_minus_minus => { + p.lexer.next(); + return p.e(E.Unary{ .op = .un_pre_dec, .value = p.parseExpr(.prefix) }, loc); + }, + .t_plus_plus => { + p.lexer.next(); + return p.e(E.Unary{ .op = .un_pre_inc, .value = p.parseExpr(.prefix) }, loc); + }, + .t_function => { + return p.parseFnExpr(loc, false, logger.Range.None) catch unreachable; + }, + .t_class => { + const classKeyword = p.lexer.range(); + // markSyntaxFEatuer class + p.lexer.next(); + var name: ?js_ast.LocRef = null; + + _ = p.pushScopeForParsePass(.class_name, loc) catch unreachable; + + // Parse an optional class name + if (p.lexer.token == .t_identifier and !js_lexer.StrictModeReservedWords.has(p.lexer.identifier)) { + name = js_ast.LocRef{ .loc = p.lexer.loc(), .ref = p.newSymbol(.other, p.lexer.identifier) catch unreachable }; + p.lexer.next(); + } + + // Even anonymous classes can have TypeScript type parameters + if (p.options.ts) { + p.skipTypescriptTypeParameters(); + } + + const class = p.parseClass(classKeyword, name, ParseClassOptions{}); + p.popScope(); + return p.e(class, loc); + }, + .t_new => { + p.lexer.next(); + + // Special-case the weird "new.target" expression here + + const target = p.parseExprWithFlags(.member, flags); + var args: []Expr = &([_]Expr{}); + + if (p.options.ts) { + // Skip over TypeScript non-null assertions + if (p.lexer.token == .t_exclamation and !p.lexer.has_newline_before) { + p.lexer.next(); + } + + // Skip over TypeScript type arguments here if there are any + if (p.lexer.token == .t_less_than) { + _ = p.trySkipTypeScriptTypeArgumentsWithBacktracking(); + } + } + + if (p.lexer.token == .t_open_paren) { + args = p.parseCallArgs(); + } + + return p.e(E.New{ + .target = target, + .args = args, + }, loc); + }, + .t_open_bracket => { + p.lexer.next(); + var is_single_line = !p.lexer.has_newline_before; + var items = List(Expr).init(p.allocator); + var self_errors = DeferredErrors{}; + var comma_after_spread = logger.Loc{}; + + // Allow "in" inside arrays + const old_allow_in = p.allow_in; + p.allow_in = true; + + while (p.lexer.token != .t_close_bracket) { + switch (p.lexer.token) { + .t_comma => { + items.append(p.e(E.Missing{}, p.lexer.loc())) catch unreachable; + }, + .t_dot_dot_dot => { + // this might be wrong. + errors.array_spread_feature = p.lexer.range(); + + const dots_loc = p.lexer.loc(); + p.lexer.next(); + items.append( + p.parseExprOrBindings(.comma, &self_errors), + ) catch unreachable; + }, + else => { + items.append( + p.parseExprOrBindings(.comma, &self_errors), + ) catch unreachable; + }, + } + + if (p.lexer.token != .t_comma) { + break; + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + + p.lexer.next(); + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + + p.lexer.expect(.t_close_bracket); + p.allow_in = old_allow_in; + + if (p.willNeedBindingPattern()) {} else if (errors.isEmpty()) { + // Is this an expression? + p.logExprErrors(&self_errors); + } else { + // In this case, we can't distinguish between the two yet + self_errors.mergeInto(errors); + } + return p.e(E.Array{ + .items = items.toOwnedSlice(), + .comma_after_spread = comma_after_spread, + .is_single_line = is_single_line, + }, loc); + }, + .t_open_brace => { + p.lexer.next(); + var is_single_line = !p.lexer.has_newline_before; + var properties = List(G.Property).init(p.allocator); + var self_errors = DeferredErrors{}; + var comma_after_spread = logger.Loc{}; + + // Allow "in" inside object literals + const old_allow_in = p.allow_in; + p.allow_in = true; + + while (p.lexer.token != .t_close_brace) { + if (p.lexer.token == .t_dot_dot_dot) { + p.lexer.next(); + properties.append(G.Property{ .kind = .spread, .value = p.parseExpr(.comma) }) catch unreachable; + + // Commas are not allowed here when destructuring + if (p.lexer.token == .t_comma) { + comma_after_spread = p.lexer.loc(); + } + } else { + // This property may turn out to be a type in TypeScript, which should be ignored + var propertyOpts = PropertyOpts{}; + if (p.parseProperty(.normal, &propertyOpts, &self_errors)) |prop| { + properties.append(prop) catch unreachable; + } + } + + if (p.lexer.token != .t_comma) { + break; + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + + p.lexer.next(); + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + } + + if (p.lexer.has_newline_before) { + is_single_line = false; + } + + p.lexer.expect(.t_close_brace); + p.allow_in = old_allow_in; + + if (p.willNeedBindingPattern()) {} else if (errors.isEmpty()) { + // Is this an expression? + p.logExprErrors(&self_errors); + } else { + // In this case, we can't distinguish between the two yet + self_errors.mergeInto(errors); + } + return p.e(E.Object{ + .properties = properties.toOwnedSlice(), + .comma_after_spread = comma_after_spread, + .is_single_line = is_single_line, + }, loc); + }, + .t_less_than => { + // This is a very complicated and highly ambiguous area of TypeScript + // syntax. Many similar-looking things are overloaded. + // + // TS: + // + // A type cast: + // <A>(x) + // <[]>(x) + // <A[]>(x) + // + // An arrow function with type parameters: + // <A>(x) => {} + // <A, B>(x) => {} + // <A = B>(x) => {} + // <A extends B>(x) => {} + // + // TSX: + // + // A JSX element: + // <A>(x) => {}</A> + // <A extends>(x) => {}</A> + // <A extends={false}>(x) => {}</A> + // + // An arrow function with type parameters: + // <A, B>(x) => {} + // <A extends B>(x) => {} + // + // A syntax error: + // <[]>(x) + // <A[]>(x) + // <A>(x) => {} + // <A = B>(x) => {} + if (p.options.ts and p.options.jsx.parse) { + var oldLexer = p.lexer; + + p.lexer.next(); + // Look ahead to see if this should be an arrow function instead + var is_ts_arrow_fn = false; + + if (p.lexer.token == .t_identifier) { + p.lexer.next(); + if (p.lexer.token == .t_comma) { + is_ts_arrow_fn = true; + } else if (p.lexer.token == .t_extends) { + p.lexer.next(); + is_ts_arrow_fn = p.lexer.token != .t_equals and p.lexer.token != .t_greater_than; + } + } + + // Restore the lexer + p.lexer = oldLexer; + + if (is_ts_arrow_fn) { + p.skipTypescriptTypeParameters(); + p.lexer.expect(.t_open_paren); + return p.parseParenExpr(loc, ParenExprOpts{ .force_arrow_fn = true }) catch unreachable; + } + } + + if (p.options.jsx.parse) { + notimpl(); + } + + if (p.options.ts) { + notimpl(); + } + + p.lexer.unexpected(); + return p.e(E.Missing{}, logger.Loc.Empty); + }, + .t_import => { + p.lexer.next(); + return p.parseImportExpr(loc, level); + }, + else => { + p.lexer.unexpected(); + return p.e(E.Missing{}, logger.Loc.Empty); + }, + } + + return p.e(E.Missing{}, logger.Loc.Empty); + } + + pub fn jsxStringsToMemberExpression(p: *P, loc: logger.Loc, fragment: string) Expr { + notimpl(); + } + + // Note: The caller has already parsed the "import" keyword + pub fn parseImportExpr(p: *P, loc: logger.Loc, level: Level) Expr { + // Parse an "import.meta" expression + if (p.lexer.token == .t_dot) { + p.es6_import_keyword = js_lexer.rangeOfIdentifier(&p.source, loc); + p.lexer.next(); + if (p.lexer.isContextualKeyword("meta")) { + const r = p.lexer.range(); + p.lexer.next(); + p.has_import_meta = true; + return p.e(E.ImportMeta{}, loc); + } else { + p.lexer.expectedString("\"meta\""); + } + } + + if (level.gt(.call)) { + const r = js_lexer.rangeOfIdentifier(&p.source, loc); + p.log.addRangeError(p.source, r, "Cannot use an \"import\" expression here without parentheses") catch unreachable; + } + // allow "in" inside call arguments; + var old_allow_in = p.allow_in; + p.allow_in = true; + + p.lexer.preserve_all_comments_before = true; + p.lexer.expect(.t_open_paren); + const comments = p.lexer.comments_to_preserve_before.toOwnedSlice(); + p.lexer.preserve_all_comments_before = false; + + const value = p.parseExpr(.comma); + p.lexer.expect(.t_close_paren); + + p.allow_in = old_allow_in; + return p.e(E.Import{ .expr = value, .leading_interior_comments = comments, .import_record_index = 0 }, loc); + } + + pub fn parseJSXElement(loc: logger.Loc) Expr { + // Parse the tag + //var startRange, startText, startTag := p.parseJSXTag();÷ + notimpl(); + return p.e(E.Missing{}, logger.Loc.Empty); + } + + pub fn willNeedBindingPattern(p: *P) bool { + switch (p.lexer.token) { + .t_equals => { + // "[a] = b;" + return true; + }, + .t_in => { + // "for ([a] in b) {}" + return !p.allow_in; + }, + .t_identifier => { + // "for ([a] of b) {}" + return p.allow_in and p.lexer.isContextualKeyword("of"); + }, + else => { + return false; + }, + } + } + + pub fn trySkipTypeScriptTypeArgumentsWithBacktracking(p: *P) bool { + notimpl(); + // return false; + } + pub fn parsePrefix(p: *P, level: Level, errors: ?*DeferredErrors, flags: Expr.EFlags) Expr { + return p._parsePrefix(level, errors orelse &DeferredErrors.None, flags); + } + + pub fn appendPart(p: *P, parts: *List(js_ast.Part), stmts: []Stmt) !void { + p.symbol_uses = SymbolUseMap.init(p.allocator); + p.declared_symbols.deinit(); + p.declared_symbols = @TypeOf(p.declared_symbols).init(p.allocator); + p.import_records_for_current_part.deinit(); + p.import_records_for_current_part = @TypeOf(p.import_records_for_current_part).init(p.allocator); + p.scopes_for_current_part.deinit(); + p.scopes_for_current_part = @TypeOf(p.scopes_for_current_part).init(p.allocator); + var opts = PrependTempRefsOpts{}; + var partStmts = List(Stmt).fromOwnedSlice(p.allocator, stmts); + try p.visitStmtsAndPrependTempRefs(&partStmts, &opts); + + // Insert any relocated variable statements now + if (p.relocated_top_level_vars.items.len > 0) { + var already_declared = RefBoolMap.init(p.allocator); + for (p.relocated_top_level_vars.items) |*local| { + // Follow links because "var" declarations may be merged due to hoisting + while (local.ref != null) { + const link = p.symbols.items[local.ref.?.inner_index].link orelse break; + if (link.isNull()) { + break; + } + local.ref = link; + } + const ref = local.ref orelse continue; + if (!already_declared.contains(ref)) { + try already_declared.put(ref, true); + + const decls = try p.allocator.alloc(G.Decl, 1); + decls[0] = Decl{ + .binding = p.b(B.Identifier{ .ref = ref }, local.loc), + }; + try partStmts.append(p.s(S.Local{ .decls = decls }, local.loc)); + } + } + p.relocated_top_level_vars.deinit(); + p.relocated_top_level_vars = @TypeOf(p.relocated_top_level_vars).init(p.allocator); + + // Follow links because "var" declarations may be merged due to hoisting + + // while (true) { + // const link = p.symbols.items[local.ref.inner_index].link; + // } + } + + if (partStmts.items.len > 0) { + const _stmts = partStmts.toOwnedSlice(); + var part = js_ast.Part{ + .stmts = _stmts, + .symbol_uses = p.symbol_uses, + .declared_symbols = p.declared_symbols.toOwnedSlice(), + .import_record_indices = p.import_records_for_current_part.toOwnedSlice(), + .scopes = p.scopes_for_current_part.toOwnedSlice(), + .can_be_removed_if_unused = p.stmtsCanBeRemovedIfUnused(_stmts), + }; + try parts.append(part); + } + } + + pub fn bindingCanBeRemovedIfUnused(p: *P, binding: Binding) bool { + switch (binding.data) { + .b_array => |bi| { + for (bi.items) |item| { + if (!p.bindingCanBeRemovedIfUnused(item.binding)) { + return false; + } + + if (item.default_value) |default| { + if (!p.exprCanBeRemovedIfUnused(default)) { + return false; + } + } + } + }, + .b_object => |bi| { + for (bi.properties) |property| { + if (!property.flags.is_spread and !p.exprCanBeRemovedIfUnused(property.key)) { + return false; + } + + if (!p.bindingCanBeRemovedIfUnused(property.value)) { + return false; + } + + if (property.default_value) |default| { + if (!p.exprCanBeRemovedIfUnused(default)) { + return false; + } + } + } + }, + else => {}, + } + + return true; + } + + pub fn stmtsCanBeRemovedIfUnused(p: *P, stmts: []Stmt) bool { + for (stmts) |stmt| { + switch (stmt.data) { + // These never have side effects + .s_function, .s_empty => {}, + + // Let these be removed if they are unused. Note that we also need to + // check if the imported file is marked as "sideEffects: false" before we + // can remove a SImport statement. Otherwise the import must be kept for + // its side effects. + .s_import => |st| {}, + .s_class => |st| { + if (!p.classCanBeRemovedIfUnused(&st.class)) { + return false; + } + }, + .s_expr => |st| { + if (st.does_not_affect_tree_shaking) {} else if (!p.exprCanBeRemovedIfUnused(st.value)) { + return false; + } + }, + .s_local => |st| { + for (st.decls) |decl| { + if (!p.bindingCanBeRemovedIfUnused(decl.binding)) { + return false; + } + + if (decl.value) |decl_value| { + if (!p.exprCanBeRemovedIfUnused(decl_value)) { + return false; + } + } + } + }, + + // Exports are tracked separately, so this isn't necessary + .s_export_clause, .s_export_from => {}, + + .s_export_default => |st| { + switch (st.value) { + .stmt => |s2| { + switch (s2.data) { + // These never have side effects + .s_function => {}, + .s_class => |class| { + if (!p.classCanBeRemovedIfUnused(&class.class)) { + return false; + } + }, + else => { + std.debug.panic("Unexpected type in export default: {s}", .{s2}); + }, + } + }, + .expr => |exp| { + if (!p.exprCanBeRemovedIfUnused(exp)) { + return false; + } + }, + } + }, + else => { + return false; + }, + } + } + + return true; + } + + pub fn visitStmtsAndPrependTempRefs(p: *P, stmts: *List(Stmt), opts: *PrependTempRefsOpts) !void { + var old_temp_refs = p.temp_refs_to_declare; + var old_temp_ref_count = p.temp_ref_count; + p.temp_refs_to_declare.deinit(); + p.temp_refs_to_declare = @TypeOf(p.temp_refs_to_declare).init(p.allocator); + p.temp_ref_count = 0; + + try p.visitStmts(stmts, opts.kind); + + // Prepend values for "this" and "arguments" + if (opts.fn_body_loc != null) { + // Capture "this" + if (p.fn_only_data_visit.this_capture_ref) |ref| { + try p.temp_refs_to_declare.append(TempRef{ + .ref = ref, + .value = p.e(E.This{}, opts.fn_body_loc orelse p.panic("Internal error: Expected opts.fn_body_loc to exist", .{})), + }); + } + } + } + + pub fn recordDeclaredSymbol(p: *P, ref: Ref) !void { + try p.declared_symbols.append(js_ast.DeclaredSymbol{ + .ref = ref, + .is_top_level = p.current_scope == p.module_scope, + }); + } + + pub fn visitExpr(p: *P, expr: Expr) Expr { + return p.visitExprInOut(expr, ExprIn{}); + } + + pub fn visitFunc(p: *P, func: *G.Fn, open_parens_loc: logger.Loc) void { + const old_fn_or_arrow_data = p.fn_or_arrow_data_visit; + defer p.fn_or_arrow_data_visit = old_fn_or_arrow_data; + const old_fn_only_data = p.fn_only_data_visit; + defer p.fn_only_data_visit = old_fn_only_data; + p.fn_or_arrow_data_visit = FnOrArrowDataVisit{ .is_async = func.flags.is_async }; + p.fn_only_data_visit = FnOnlyDataVisit{ .is_this_nested = true, .arguments_ref = func.arguments_ref }; + + if (func.name) |name| { + p.recordDeclaredSymbol(name.ref.?) catch unreachable; + const symbol_name = p.symbols.items[name.ref.?.inner_index].original_name; + if (isEvalOrArguments(symbol_name)) { + p.markStrictModeFeature(.eval_or_arguments, js_lexer.rangeOfIdentifier(&p.source, name.loc), symbol_name) catch unreachable; + } + } + + p.pushScopeForVisitPass(.function_args, open_parens_loc) catch unreachable; + defer p.popScope(); + p.visitArgs( + func.args, + VisitArgsOpts{ + .has_rest_arg = func.flags.has_rest_arg, + .body = func.body.?.stmts, + .is_unique_formal_parameters = true, + }, + ); + + var body = func.body orelse p.panic("Expected visitFunc to have body {s}", .{func}); + p.pushScopeForVisitPass(.function_body, body.loc) catch unreachable; + defer p.popScope(); + var stmts = List(Stmt).fromOwnedSlice(p.allocator, body.stmts); + var temp_opts = PrependTempRefsOpts{ .kind = StmtsKind.fn_body, .fn_body_loc = body.loc }; + p.visitStmtsAndPrependTempRefs(&stmts, &temp_opts) catch unreachable; + + body.stmts = stmts.toOwnedSlice(); + + func.body = body; + } + + pub fn maybeKeepExprSymbolName(p: *P, expr: Expr, original_name: string, was_anonymous_named_expr: bool) Expr { + return if (was_anonymous_named_expr) p.keepExprSymbolName(expr, original_name) else expr; + } + + pub fn valueForThis(p: *P, loc: logger.Loc) ?Expr { + // Substitute "this" if we're inside a static class property initializer + if (p.fn_only_data_visit.this_class_static_ref) |*ref| { + p.recordUsage(ref); + return p.e(E.Identifier{ .ref = ref.* }, loc); + } + + // oroigianlly was !=- modepassthrough + if (!p.fn_only_data_visit.is_this_nested) { + if (p.has_es_module_syntax) { + // In an ES6 module, "this" is supposed to be undefined. Instead of + // doing this at runtime using "fn.call(undefined)", we do it at + // compile time using expression substitution here. + return p.e(E.Undefined{}, loc); + } else { + // In a CommonJS module, "this" is supposed to be the same as "exports". + // Instead of doing this at runtime using "fn.call(module.exports)", we + // do it at compile time using expression substitution here. + p.recordUsage(&p.exports_ref); + return p.e(E.Identifier{ .ref = p.exports_ref }, loc); + } + } + + return null; + } + + pub fn visitExprInOut(p: *P, expr: Expr, in: ExprIn) Expr { + switch (expr.data) { + .e_null, .e_super, .e_boolean, .e_big_int, .e_reg_exp, .e_new_target, .e_undefined => {}, + .e_string => |e_| { + // If you're using this, you're probably not using 0-prefixed legacy octal notation + // if e.LegacyOctalLoc.Start > 0 { + }, + .e_number => |e_| { + // idc about legacy octal loc + }, + .e_this => |e_| { + if (p.valueForThis(expr.loc)) |exp| { + return exp; + } + + // // Capture "this" inside arrow functions that will be lowered into normal + // // function expressions for older language environments + // if p.fnOrArrowDataVisit.isArrow && p.options.unsupportedJSFeatures.Has(compat.Arrow) && p.fnOnlyDataVisit.isThisNested { + // return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.captureThis()}}, exprOut{} + // } + }, + + .e_import_meta => |exp| { + const is_delete_target = std.meta.activeTag(p.delete_target) == .e_import_meta and exp == p.delete_target.e_import_meta; + + if (p.define.dots.get("meta")) |meta| { + for (meta) |define| { + if (p.isDotDefineMatch(expr, define.parts)) { + // Substitute user-specified defines + return p.valueForDefine(expr.loc, in.assign_target, is_delete_target, &define.data); + } + } + } + + if (!p.import_meta_ref.isNull()) { + p.recordUsage(&p.import_meta_ref); + return p.e(E.Identifier{ .ref = p.import_meta_ref }, expr.loc); + } + }, + .e_spread => |exp| { + exp.value = p.visitExpr(exp.value); + }, + .e_identifier => |e_| { + const is_delete_target = @as(Expr.Tag, p.delete_target) == .e_identifier and e_ == p.delete_target.e_identifier; + + const name = p.loadNameFromRef(e_.ref); + if (p.isStrictMode() and js_lexer.StrictModeReservedWords.has(name)) { + p.markStrictModeFeature(.reserved_word, js_lexer.rangeOfIdentifier(&p.source, expr.loc), name) catch unreachable; + } + + const result = p.findSymbol(expr.loc, name) catch unreachable; + + e_.must_keep_due_to_with_stmt = result.is_inside_with_scope; + e_.ref = result.ref; + + // Handle assigning to a constant + if (in.assign_target != .none and p.symbols.items[result.ref.inner_index].kind == .cconst) { + const r = js_lexer.rangeOfIdentifier(&p.source, expr.loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot assign to {s} because it is a constant", .{name}) catch unreachable; + } + + var original_name: ?string = null; + + // Substitute user-specified defines for unbound symbols + if (p.symbols.items[e_.ref.inner_index].kind == .unbound and !result.is_inside_with_scope and !is_delete_target) { + if (p.define.identifiers.get(name)) |def| { + if (!def.isUndefined()) { + const newvalue = p.valueForDefine(expr.loc, in.assign_target, is_delete_target, &def); + + // Don't substitute an identifier for a non-identifier if this is an + // assignment target, since it'll cause a syntax error + if (@as(Expr.Tag, newvalue.data) == .e_identifier or in.assign_target == .none) { + return newvalue; + } + + original_name = def.original_name; + } + + // Copy the side effect flags over in case this expression is unused + if (def.can_be_removed_if_unused) { + e_.can_be_removed_if_unused = true; + } + if (def.call_can_be_unwrapped_if_unused and !p.options.ignore_dce_annotations) { + e_.call_can_be_unwrapped_if_unused = true; + } + } + } + + return p.handleIdentifier(expr.loc, e_, original_name, IdentifierOpts{ + .assign_target = in.assign_target, + .is_delete_target = is_delete_target, + .was_originally_identifier = true, + }); + }, + .e_private_identifier => |e_| { + p.panic("Unexpected private identifier. This is an internal error - not your fault.", .{}); + }, + .e_jsx_element => |e_| { + const tag = tagger: { + if (e_.tag) |_tag| { + break :tagger p.visitExpr(_tag); + } else { + break :tagger p.jsxStringsToMemberExpression(expr.loc, p.options.jsx.fragment); + } + }; + + // Visit properties + var i: usize = 0; + while (i < e_.properties.len) : (i += 1) { + const property = e_.properties[i]; + if (property.kind != .spread) { + e_.properties[i].key = p.visitExpr(e_.properties[i].key.?); + } + + if (property.value != null) { + e_.properties[i].value = p.visitExpr(e_.properties[i].value.?); + } + + if (property.initializer != null) { + e_.properties[i].initializer = p.visitExpr(e_.properties[i].initializer.?); + } + } + + // Arguments to createElement() + const args = p.allocator.alloc(Expr, 1 + e_.children.len) catch unreachable; + i = 1; + if (e_.properties.len > 0) { + args[0] = p.e(E.Object{ .properties = e_.properties }, expr.loc); + } else { + args[0] = p.e(E.Null{}, expr.loc); + } + + for (e_.children) |child| { + args[i] = p.visitExpr(child); + i += 1; + } + + // Call createElement() + return p.e(E.Call{ + .target = p.jsxStringsToMemberExpression(expr.loc, p.options.jsx.factory), + .args = args, + // Enable tree shaking + .can_be_unwrapped_if_unused = !p.options.ignore_dce_annotations, + }, expr.loc); + }, + + .e_template => |e_| { + if (e_.tag) |tag| { + e_.tag = p.visitExpr(tag); + } + + var i: usize = 0; + while (i < e_.parts.len) : (i += 1) { + e_.parts[i].value = p.visitExpr(e_.parts[i].value); + } + }, + + .e_binary => |e_| { + switch (e_.left.data) { + // Special-case private identifiers + .e_private_identifier => |private| { + if (e_.op == .bin_in) { + const name = p.loadNameFromRef(private.ref); + const result = p.findSymbol(e_.left.loc, name) catch unreachable; + private.ref = result.ref; + + // Unlike regular identifiers, there are no unbound private identifiers + const symbol: Symbol = p.symbols.items[result.ref.inner_index]; + if (!Symbol.isKindPrivate(symbol.kind)) { + const r = logger.Range{ .loc = e_.left.loc, .len = @intCast(i32, name.len) }; + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Private name \"{s}\" must be declared in an enclosing class", .{name}) catch unreachable; + } + + e_.right = p.visitExpr(e_.right); + // privateSymbolNeedsToBeLowered + return expr; + } + }, + else => {}, + } + + const is_call_target = @as(Expr.Tag, p.call_target) == .e_binary and e_ == p.call_target.e_binary; + const is_stmt_expr = @as(Expr.Tag, p.stmt_expr_value) == .e_binary and e_ == p.stmt_expr_value.e_binary; + const was_anonymous_named_expr = p.isAnonymousNamedExpr(e_.right); + + e_.left = p.visitExprInOut(e_.left, ExprIn{ + .assign_target = e_.op.binaryAssignTarget(), + }); + + // Mark the control flow as dead if the branch is never taken + switch (e_.op) { + .bin_logical_or => { + const side_effects = SideEffects.toBoolean(e_.left.data); + if (side_effects.ok and side_effects.value) { + // "true || dead" + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + e_.right = p.visitExpr(e_.right); + p.is_control_flow_dead = old; + } else { + e_.right = p.visitExpr(e_.right); + } + }, + .bin_logical_and => { + const side_effects = SideEffects.toBoolean(e_.left.data); + if (side_effects.ok and side_effects.value) { + // "false && dead" + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + e_.right = p.visitExpr(e_.right); + p.is_control_flow_dead = old; + } else { + e_.right = p.visitExpr(e_.right); + } + }, + .bin_nullish_coalescing => { + const side_effects = SideEffects.toNullOrUndefined(e_.left.data); + if (side_effects.ok and side_effects.value) { + // "false && dead" + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + e_.right = p.visitExpr(e_.right); + p.is_control_flow_dead = old; + } else { + e_.right = p.visitExpr(e_.right); + } + }, + else => { + e_.right = p.visitExpr(e_.right); + }, + } + + // Always put constants on the right for equality comparisons to help + // reduce the number of cases we have to check during pattern matching. We + // can only reorder expressions that do not have any side effects. + switch (e_.op) { + .bin_loose_eq, .bin_loose_ne, .bin_strict_eq, .bin_strict_ne => { + if (SideEffects.isPrimitiveToReorder(e_.left.data) and !SideEffects.isPrimitiveToReorder(e_.right.data)) { + const _left = e_.left; + const _right = e_.right; + e_.left = _right; + e_.right = _left; + } + }, + else => {}, + } + + switch (e_.op) { + .bin_comma => { + // notimpl(); + }, + .bin_loose_eq => { + const equality = SideEffects.eql(e_.left.data, e_.right.data); + if (equality.ok) { + return p.e( + E.Boolean{ .value = equality.equal }, + expr.loc, + ); + } + + // const after_op_loc = locAfterOp(e_.); + // TODO: warn about equality check + // TODO: warn about typeof string + + }, + .bin_strict_eq => { + const equality = SideEffects.eql(e_.left.data, e_.right.data); + if (equality.ok) { + return p.e(E.Boolean{ .value = equality.ok }, expr.loc); + } + + // const after_op_loc = locAfterOp(e_.); + // TODO: warn about equality check + // TODO: warn about typeof string + }, + .bin_loose_ne => { + const equality = SideEffects.eql(e_.left.data, e_.right.data); + if (equality.ok) { + return p.e(E.Boolean{ .value = !equality.ok }, expr.loc); + } + // const after_op_loc = locAfterOp(e_.); + // TODO: warn about equality check + // TODO: warn about typeof string + + // "x != void 0" => "x != null" + if (@as(Expr.Tag, e_.right.data) == .e_undefined) { + e_.right = p.e(E.Null{}, e_.right.loc); + } + }, + .bin_strict_ne => { + const equality = SideEffects.eql(e_.left.data, e_.right.data); + if (equality.ok) { + return p.e(E.Boolean{ .value = !equality.ok }, expr.loc); + } + }, + .bin_nullish_coalescing => { + const nullorUndefined = SideEffects.toNullOrUndefined(e_.left.data); + if (!nullorUndefined.value) { + return e_.left; + } else if (nullorUndefined.side_effects == .no_side_effects) { + // TODO: + // "(null ?? fn)()" => "fn()" + // "(null ?? this.fn)" => "this.fn" + // "(null ?? this.fn)()" => "(0, this.fn)()" + + } + }, + .bin_logical_or => { + const side_effects = SideEffects.toBoolean(e_.left.data); + if (side_effects.ok and side_effects.value) { + return e_.left; + } else if (side_effects.ok) { + // TODO: + // "(0 || fn)()" => "fn()" + // "(0 || this.fn)" => "this.fn" + // "(0 || this.fn)()" => "(0, this.fn)()" + } + }, + .bin_logical_and => { + const side_effects = SideEffects.toBoolean(e_.left.data); + if (side_effects.ok) { + return e_.left; + } + + // TODO: + // "(1 && fn)()" => "fn()" + // "(1 && this.fn)" => "this.fn" + // "(1 && this.fn)()" => "(0, this.fn)()" + }, + .bin_add => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = vals[0] + vals[1] }, expr.loc); + } + } + + // TODO: fold string addition + }, + .bin_sub => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = vals[0] - vals[1] }, expr.loc); + } + } + }, + .bin_mul => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = vals[0] * vals[1] }, expr.loc); + } + } + }, + .bin_div => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = vals[0] / vals[1] }, expr.loc); + } + } + }, + .bin_rem => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // is this correct? + return p.e(E.Number{ .value = std.math.mod(f64, vals[0], vals[1]) catch 0.0 }, expr.loc); + } + } + }, + .bin_pow => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = std.math.pow(f64, vals[0], vals[1]) }, expr.loc); + } + } + }, + .bin_shl => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) << @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_shr => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_u_shr => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_bitwise_and => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_bitwise_or => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_bitwise_xor => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + // --------------------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------------- + .bin_assign => { + + // Optionally preserve the name + if (@as(Expr.Tag, e_.left.data) == .e_identifier) { + e_.right = p.maybeKeepExprSymbolName(e_.right, p.symbols.items[e_.left.data.e_identifier.ref.inner_index].original_name, was_anonymous_named_expr); + } + }, + .bin_add_assign => { + // notimpl(); + }, + .bin_sub_assign => { + // notimpl(); + }, + .bin_mul_assign => { + // notimpl(); + }, + .bin_div_assign => { + // notimpl(); + }, + .bin_rem_assign => { + // notimpl(); + }, + .bin_pow_assign => { + // notimpl(); + }, + .bin_shl_assign => { + // notimpl(); + }, + .bin_shr_assign => { + // notimpl(); + }, + .bin_u_shr_assign => { + // notimpl(); + }, + .bin_bitwise_or_assign => { + // notimpl(); + }, + .bin_bitwise_and_assign => { + // notimpl(); + }, + .bin_bitwise_xor_assign => { + // notimpl(); + }, + .bin_nullish_coalescing_assign => { + // notimpl(); + }, + .bin_logical_and_assign => { + // notimpl(); + }, + .bin_logical_or_assign => { + // notimpl(); + }, + else => {}, + } + }, + .e_index => |e_| { + const is_call_target = std.meta.activeTag(p.call_target) == .e_index and e_ == p.call_target.e_index; + const is_delete_target = std.meta.activeTag(p.delete_target) == .e_index and e_ == p.delete_target.e_index; + + const target = p.visitExprInOut(e_.target, ExprIn{ + // this is awkward due to a zig compiler bug + .has_chain_parent = (e_.optional_chain orelse js_ast.OptionalChain.start) == js_ast.OptionalChain.ccontinue, + }); + e_.target = target; + + if (e_.optional_chain == null and @as(Expr.Tag, e_.index.data) == .e_string) { + if (p.maybeRewritePropertyAccess( + expr.loc, + in.assign_target, + is_delete_target, + e_.target, + p.lexer.utf16ToString(e_.index.data.e_string.value), + e_.index.loc, + is_call_target, + )) |val| { + return val; + } + } + + // Create an error for assigning to an import namespace when bundling. Even + // though this is a run-time error, we make it a compile-time error when + // bundling because scope hoisting means these will no longer be run-time + // errors. + if ((in.assign_target != .none or is_delete_target) and @as(Expr.Tag, e_.target.data) == .e_identifier) { + const r = js_lexer.rangeOfIdentifier(&p.source, e_.target.loc); + p.log.addRangeErrorFmt( + p.source, + r, + p.allocator, + "Cannot assign to property on import \"{s}\"", + .{p.symbols.items[e_.target.data.e_identifier.ref.inner_index].original_name}, + ) catch unreachable; + } + + return p.e(e_, expr.loc); + }, + .e_unary => |e_| { + switch (e_.op) { + .un_typeof => { + e_.value = p.visitExprInOut(e_.value, ExprIn{ .assign_target = e_.op.unaryAssignTarget() }); + + if (SideEffects.toTypeof(e_.value.data)) |typeof| { + return p.e(E.String{ .value = p.lexer.stringToUTF16(typeof) }, expr.loc); + } + }, + .un_delete => { + e_.value = p.visitExprInOut(e_.value, ExprIn{ .has_chain_parent = true }); + }, + else => { + e_.value = p.visitExprInOut(e_.value, ExprIn{ .assign_target = e_.op.unaryAssignTarget() }); + + // Post-process the unary expression + + switch (e_.op) { + .un_not => { + const side_effects = SideEffects.toBoolean(e_.value.data); + if (side_effects.ok) { + return p.e(E.Boolean{ .value = !side_effects.value }, expr.loc); + } + + // maybe won't do this idk + if (Expr.maybeSimplifyNot(&e_.value, p.allocator)) |exp| { + return exp; + } + }, + .un_void => { + if (p.exprCanBeRemovedIfUnused(e_.value)) { + return p.e(E.Undefined{}, e_.value.loc); + } + }, + .un_pos => { + if (SideEffects.toNumber(e_.value.data)) |num| { + return p.e(E.Number{ .value = num }, expr.loc); + } + }, + .un_neg => { + if (SideEffects.toNumber(e_.value.data)) |num| { + return p.e(E.Number{ .value = -num }, expr.loc); + } + }, + + //////////////////////////////////////////////////////////////////////////////// + + .un_pre_dec => { + // TODO: private fields + }, + .un_pre_inc => { + // TODO: private fields + }, + .un_post_dec => { + // TODO: private fields + }, + .un_post_inc => { + // TODO: private fields + }, + else => {}, + } + }, + } + }, + .e_dot => |e_| { + const is_delete_target = @as(Expr.Tag, p.delete_target) == .e_dot and e_ == p.delete_target.e_dot; + const is_call_target = @as(Expr.Tag, p.call_target) == .e_dot and e_ == p.call_target.e_dot; + + if (p.define.dots.get(e_.name)) |parts| { + for (parts) |define| { + if (p.isDotDefineMatch(expr, define.parts)) { + // Substitute user-specified defines + if (!define.data.isUndefined()) { + // TODO: check this doesn't crash due to the pointer no longer being allocated + return p.valueForDefine(expr.loc, in.assign_target, is_delete_target, &define.data); + } + + // Copy the side effect flags over in case this expression is unused + if (define.data.can_be_removed_if_unused) { + e_.can_be_removed_if_unused = true; + } + + if (define.data.call_can_be_unwrapped_if_unused and !p.options.ignore_dce_annotations) { + e_.call_can_be_unwrapped_if_unused = true; + } + + break; + } + } + } + + // Track ".then().catch()" chains + if (is_call_target and @as(Expr.Tag, p.then_catch_chain.next_target) == .e_dot and p.then_catch_chain.next_target.e_dot == e_) { + if (strings.eql(e_.name, "catch")) { + p.then_catch_chain = ThenCatchChain{ + .next_target = e_.target.data, + .has_catch = true, + }; + } else if (strings.eql(e_.name, "then")) { + p.then_catch_chain = ThenCatchChain{ + .next_target = e_.target.data, + .has_catch = p.then_catch_chain.has_catch or p.then_catch_chain.has_multiple_args, + }; + } + } + + e_.target = p.visitExpr(e_.target); + if (e_.optional_chain == null) { + if (p.maybeRewritePropertyAccess( + expr.loc, + in.assign_target, + is_delete_target, + e_.target, + e_.name, + e_.name_loc, + is_call_target, + )) |_expr| { + return _expr; + } + } + }, + .e_if => |e_| { + const is_call_target = (p.call_target) == .e_if and e_ == p.call_target.e_if; + + e_.test_ = p.visitExpr(e_.test_); + + const side_effects = SideEffects.toBoolean(e_.test_.data); + + if (side_effects.ok) { + // Mark the control flow as dead if the branch is never taken + if (side_effects.value) { + // "true ? live : dead" + e_.yes = p.visitExpr(e_.yes); + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + e_.no = p.visitExpr(e_.no); + p.is_control_flow_dead = old; + } else { + // "false ? dead : live" + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + e_.yes = p.visitExpr(e_.yes); + p.is_control_flow_dead = old; + e_.no = p.visitExpr(e_.no); + } + } else { + e_.yes = p.visitExpr(e_.yes); + e_.no = p.visitExpr(e_.no); + } + }, + .e_await => |e_| { + p.await_target = e_.value.data; + e_.value = p.visitExpr(e_.value); + }, + .e_yield => |e_| { + if (e_.value) |val| { + e_.value = p.visitExpr(val); + } + }, + .e_array => |e_| { + if (in.assign_target != .none) { + if (e_.comma_after_spread) |spread| { + p.log.addRangeError(p.source, logger.Range{ .loc = spread, .len = 1 }, "Unexpected \",\" after rest pattern") catch unreachable; + } + } + + var has_spread = false; + var i: usize = 0; + while (i < e_.items.len) : (i += 1) { + var item = e_.items[i]; + const data = item.data; + switch (data) { + .e_missing => {}, + .e_spread => |spread| { + spread.value = p.visitExprInOut(spread.value, ExprIn{ .assign_target = in.assign_target }); + }, + .e_binary => |e2| { + if (in.assign_target != .none and e2.op == .bin_assign) { + const was_anonymous_named_expr = p.isAnonymousNamedExpr(e2.right); + e2.left = p.visitExprInOut(e2.left, ExprIn{ .assign_target = .replace }); + e2.right = p.visitExpr(e2.right); + + if (@as(Expr.Tag, e2.left.data) == .e_identifier) { + e2.right = p.maybeKeepExprSymbolName( + e2.right, + p.symbols.items[e2.left.data.e_identifier.ref.inner_index].original_name, + was_anonymous_named_expr, + ); + } + } else { + item = p.visitExprInOut(item, ExprIn{ .assign_target = in.assign_target }); + } + }, + else => { + item = p.visitExprInOut(item, ExprIn{ .assign_target = in.assign_target }); + }, + } + e_.items[i] = item; + } + }, + .e_object => |e_| { + if (in.assign_target != .none) { + p.maybeCommaSpreadError(e_.comma_after_spread); + var has_spread = false; + var has_proto = false; + + var i: usize = 0; + while (i < e_.properties.len) : (i += 1) { + var property = e_.properties[i]; + + if (property.kind != .spread) { + const key = p.visitExpr(property.key orelse std.debug.panic("Expected property key", .{})); + e_.properties[i].key = key; + + // Forbid duplicate "__proto__" properties according to the specification + if (!property.flags.is_computed and !property.flags.was_shorthand and !property.flags.is_method and in.assign_target == .none and key.data.isStringValue() and strings.utf16EqlString( + key.data.e_string.value, + "__proto__", + )) { + if (has_proto) { + const r = js_lexer.rangeOfIdentifier(&p.source, key.loc); + p.log.addRangeError(p.source, r, "Cannot specify the \"__proto__\" property more than once per object") catch unreachable; + } + has_proto = true; + } + } else { + has_spread = true; + } + + // Extract the initializer for expressions like "({ a: b = c } = d)" + if (in.assign_target != .none and property.initializer != null and property.value != null) { + switch (property.value.?.data) { + .e_binary => |bin| { + if (bin.op == .bin_assign) { + property.initializer = bin.right; + property.value = bin.left; + } + }, + else => {}, + } + } + + if (property.value != null) { + property.value = p.visitExprInOut(property.value.?, ExprIn{ .assign_target = in.assign_target }); + } + + if (property.initializer != null) { + const was_anonymous_named_expr = p.isAnonymousNamedExpr(property.initializer orelse unreachable); + property.initializer = p.visitExprInOut(property.initializer.?, ExprIn{ .assign_target = in.assign_target }); + + if (property.value) |val| { + if (@as(Expr.Tag, val.data) == .e_identifier) { + property.initializer = p.maybeKeepExprSymbolName( + property.initializer orelse unreachable, + p.symbols.items[val.data.e_identifier.ref.inner_index].original_name, + was_anonymous_named_expr, + ); + } + } + } + + // TODO: can we avoid htis copy + e_.properties[i] = property; + } + } + }, + .e_import => |e_| { + const state = TransposeState{ + .is_await_target = if (p.await_target != null) p.await_target.?.e_import == e_ else false, + .is_then_catch_target = e_ == p.then_catch_chain.next_target.e_import and p.then_catch_chain.has_catch, + .loc = e_.expr.loc, + }; + + e_.expr = p.visitExpr(e_.expr); + return p.import_transposer.maybeTransposeIf(e_.expr, state); + + // TODO: maybeTransposeIfExprChain + }, + .e_call => |e_| { + p.call_target = e_.target.data; + + p.then_catch_chain = ThenCatchChain{ + .next_target = e_.target.data, + .has_multiple_args = e_.args.len >= 2, + .has_catch = @as(Expr.Tag, p.then_catch_chain.next_target) == .e_call and p.then_catch_chain.next_target.e_call == e_ and p.then_catch_chain.has_catch, + }; + + // Prepare to recognize "require.resolve()" calls + // const could_be_require_resolve = (e_.args.len == 1 and @as( + // Expr.Tag, + // e_.target.data, + // ) == .e_dot and e_.target.data.e_dot.optional_chain == null and strings.eql( + // e_.target.dat.e_dot.name, + // "resolve", + // )); + + e_.target = p.visitExprInOut(e_.target, ExprIn{ + .has_chain_parent = (e_.optional_chain orelse js_ast.OptionalChain.start) == .ccontinue, + }); + // TODO: wan about import namespace call + var has_spread = false; + var i: usize = 0; + while (i < e_.args.len) : (i += 1) { + e_.args[i] = p.visitExpr(e_.args[i]); + has_spread = has_spread or @as(Expr.Tag, e_.args[i].data) == .e_spread; + } + + if (e_.optional_chain == null and @as(Expr.Tag, e_.target.data) == .e_identifier and e_.target.data.e_identifier.ref.eql(p.require_ref)) { + // Heuristic: omit warnings inside try/catch blocks because presumably + // the try/catch statement is there to handle the potential run-time + // error from the unbundled require() call failing. + if (e_.args.len == 1) { + return p.require_transposer.maybeTransposeIf(e_.args[0], null); + } else { + const r = js_lexer.rangeOfIdentifier(&p.source, e_.target.loc); + p.log.addRangeDebug(p.source, r, "This call to \"require\" will not be bundled because it has multiple arguments") catch unreachable; + } + } + + return expr; + }, + .e_new => |e_| { + e_.target = p.visitExpr(e_.target); + // p.warnA + + var i: usize = 0; + while (i < e_.args.len) : (i += 1) { + e_.args[i] = p.visitExpr(e_.args[i]); + } + }, + .e_arrow => |e_| { + const old_fn_or_arrow_data = p.fn_or_arrow_data_visit; + p.fn_or_arrow_data_visit = FnOrArrowDataVisit{ + .is_arrow = true, + .is_async = e_.is_async, + }; + defer p.fn_or_arrow_data_visit = old_fn_or_arrow_data; + + // Mark if we're inside an async arrow function. This value should be true + // even if we're inside multiple arrow functions and the closest inclosing + // arrow function isn't async, as long as at least one enclosing arrow + // function within the current enclosing function is async. + const old_inside_async_arrow_fn = p.fn_only_data_visit.is_inside_async_arrow_fn; + p.fn_only_data_visit.is_inside_async_arrow_fn = e_.is_async or p.fn_only_data_visit.is_inside_async_arrow_fn; + defer p.fn_only_data_visit.is_inside_async_arrow_fn = old_inside_async_arrow_fn; + + p.pushScopeForVisitPass(.function_args, expr.loc) catch unreachable; + defer p.popScope(); + + p.visitArgs(e_.args, VisitArgsOpts{ + .has_rest_arg = e_.has_rest_arg, + .body = e_.body.stmts, + .is_unique_formal_parameters = true, + }); + + p.pushScopeForVisitPass(.function_body, e_.body.loc) catch unreachable; + defer p.popScope(); + + var stmts_list = List(Stmt).fromOwnedSlice(p.allocator, e_.body.stmts); + var temp_opts = PrependTempRefsOpts{ .kind = StmtsKind.fn_body }; + p.visitStmtsAndPrependTempRefs(&stmts_list, &temp_opts) catch unreachable; + e_.body.stmts = stmts_list.toOwnedSlice(); + }, + .e_function => |e_| { + p.visitFunc(&e_.func, expr.loc); + if (e_.func.name) |name| { + return p.keepExprSymbolName(expr, p.symbols.items[name.ref.?.inner_index].original_name); + } + }, + .e_class => |e_| { + // This might be wrong. + _ = p.visitClass(expr.loc, e_); + }, + else => {}, + } + return expr; + } + + const VisitArgsOpts = struct { + body: []Stmt = &([_]Stmt{}), + has_rest_arg: bool = false, + + // This is true if the function is an arrow function or a method + is_unique_formal_parameters: bool = false, + }; + + pub fn visitArgs(p: *P, args: []G.Arg, opts: VisitArgsOpts) void { + const strict_loc = fnBodyContainsUseStrict(opts.body); + const has_simple_args = isSimpleParameterList(args, opts.has_rest_arg); + var duplicate_args_check: ?StringBoolMap = null; + // Section 15.2.1 Static Semantics: Early Errors: "It is a Syntax Error if + // FunctionBodyContainsUseStrict of FunctionBody is true and + // IsSimpleParameterList of FormalParameters is false." + if (strict_loc != null and !has_simple_args) { + p.log.addRangeError(p.source, p.source.rangeOfString(strict_loc.?), "Cannot use a \"use strict\" directive in a function with a non-simple parameter list") catch unreachable; + } + + // Section 15.1.1 Static Semantics: Early Errors: "Multiple occurrences of + // the same BindingIdentifier in a FormalParameterList is only allowed for + // functions which have simple parameter lists and which are not defined in + // strict mode code." + if (opts.is_unique_formal_parameters or strict_loc != null or !has_simple_args or p.isStrictMode()) { + duplicate_args_check = StringBoolMap.init(p.allocator); + } + + var i: usize = 0; + var duplicate_args_check_ptr: ?*StringBoolMap = if (duplicate_args_check != null) &duplicate_args_check.? else null; + + while (i < args.len) : (i += 1) { + if (args[i].ts_decorators) |decs| { + args[i].ts_decorators = p.visitTSDecorators(decs); + } + + p.visitBinding(args[i].binding, duplicate_args_check_ptr); + if (args[i].default) |default| { + args[i].default = p.visitExpr(default); + } + } + } + + pub fn visitTSDecorators(p: *P, decs: ExprNodeList) ExprNodeList { + var i: usize = 0; + while (i < decs.len) : (i += 1) { + decs[i] = p.visitExpr(decs[i]); + } + + return decs; + } + + pub fn keepExprSymbolName(p: *P, _value: Expr, name: string) Expr { + var exprs = p.allocator.alloc(Expr, 2) catch unreachable; + exprs[0] = _value; + exprs[1] = p.e(E.String{ + .value = p.lexer.stringToUTF16(name), + }, _value.loc); + var value = p.callRuntime(_value.loc, "__name", exprs); + + // Make sure tree shaking removes this if the function is never used + value.data.e_call.can_be_unwrapped_if_unused = true; + return value; + } + + pub fn fnBodyContainsUseStrict(body: []Stmt) ?logger.Loc { + for (body) |stmt| { + switch (stmt.data) { + .s_comment => { + continue; + }, + .s_directive => |dir| { + if (strings.utf16EqlString(dir.value, "use strict")) { + return stmt.loc; + } + }, + else => {}, + } + } + + return null; + } + + pub fn isSimpleParameterList(args: []G.Arg, has_rest_arg: bool) bool { + if (has_rest_arg) { + return false; + } + + for (args) |arg| { + if (@as(Binding.Tag, arg.binding.data) != .b_identifier or arg.default != null) { + return false; + } + } + + return true; + } + + pub fn classCanBeRemovedIfUnused(p: *P, class: *G.Class) bool { + if (class.extends) |extends| { + if (!p.exprCanBeRemovedIfUnused(extends)) { + return false; + } + } + + for (class.properties) |property| { + if (!p.exprCanBeRemovedIfUnused(property.key orelse unreachable)) { + return false; + } + + if (property.value) |val| { + if (!p.exprCanBeRemovedIfUnused(val)) { + return false; + } + } + + if (property.initializer) |val| { + if (!p.exprCanBeRemovedIfUnused(val)) { + return false; + } + } + } + + return true; + } + + // TODO: + // When React Fast Refresh is enabled, anything that's a JSX component should not be removable + // This is to improve the reliability of fast refresh between page loads. + pub fn exprCanBeRemovedIfUnused(p: *P, expr: Expr) bool { + switch (expr.data) { + .e_null, + .e_undefined, + .e_missing, + .e_boolean, + .e_number, + .e_big_int, + .e_string, + .e_this, + .e_reg_exp, + .e_function, + .e_arrow, + .e_import_meta, + => { + return true; + }, + + .e_dot => |ex| { + return ex.can_be_removed_if_unused; + }, + .e_class => |ex| { + return p.classCanBeRemovedIfUnused(ex); + }, + .e_identifier => |ex| { + if (ex.must_keep_due_to_with_stmt) { + return false; + } + + // Unbound identifiers cannot be removed because they can have side effects. + // One possible side effect is throwing a ReferenceError if they don't exist. + // Another one is a getter with side effects on the global object: + // + // Object.defineProperty(globalThis, 'x', { + // get() { + // sideEffect(); + // }, + // }); + // + // Be very careful about this possibility. It's tempting to treat all + // identifier expressions as not having side effects but that's wrong. We + // must make sure they have been declared by the code we are currently + // compiling before we can tell that they have no side effects. + // + // Note that we currently ignore ReferenceErrors due to TDZ access. This is + // incorrect but proper TDZ analysis is very complicated and would have to + // be very conservative, which would inhibit a lot of optimizations of code + // inside closures. This may need to be revisited if it proves problematic. + if (ex.can_be_removed_if_unused or p.symbols.items[ex.ref.inner_index].kind != .unbound) { + return true; + } + }, + .e_import_identifier => |ex| { + // References to an ES6 import item are always side-effect free in an + // ECMAScript environment. + // + // They could technically have side effects if the imported module is a + // CommonJS module and the import item was translated to a property access + // (which esbuild's bundler does) and the property has a getter with side + // effects. + // + // But this is very unlikely and respecting this edge case would mean + // disabling tree shaking of all code that references an export from a + // CommonJS module. It would also likely violate the expectations of some + // developers because the code *looks* like it should be able to be tree + // shaken. + // + // So we deliberately ignore this edge case and always treat import item + // references as being side-effect free. + return true; + }, + .e_if => |ex| { + return p.exprCanBeRemovedIfUnused(ex.test_) and p.exprCanBeRemovedIfUnused(ex.yes) and p.exprCanBeRemovedIfUnused(ex.no); + }, + .e_array => |ex| { + for (ex.items) |item| { + if (!p.exprCanBeRemovedIfUnused(item)) { + return false; + } + } + + return true; + }, + .e_object => |ex| { + for (ex.properties) |property| { + + // The key must still be evaluated if it's computed or a spread + if (property.kind == .spread or property.flags.is_computed) { + return false; + } + + if (property.value) |val| { + if (!p.exprCanBeRemovedIfUnused(val)) { + return false; + } + } + } + return true; + }, + .e_call => |ex| { + // A call that has been marked "__PURE__" can be removed if all arguments + // can be removed. The annotation causes us to ignore the target. + if (ex.can_be_unwrapped_if_unused) { + for (ex.args) |arg| { + if (!p.exprCanBeRemovedIfUnused(arg)) { + return false; + } + } + } + + return true; + }, + .e_new => |ex| { + // A call that has been marked "__PURE__" can be removed if all arguments + // can be removed. The annotation causes us to ignore the target. + if (ex.can_be_unwrapped_if_unused) { + for (ex.args) |arg| { + if (!p.exprCanBeRemovedIfUnused(arg)) { + return false; + } + } + } + + return true; + }, + .e_unary => |ex| { + switch (ex.op) { + .un_typeof, .un_void, .un_not => { + return p.exprCanBeRemovedIfUnused(ex.value); + }, + else => {}, + } + }, + .e_binary => |ex| { + switch (ex.op) { + .bin_strict_eq, .bin_strict_ne, .bin_comma, .bin_logical_or, .bin_logical_and, .bin_nullish_coalescing => { + return p.exprCanBeRemovedIfUnused(ex.left) and p.exprCanBeRemovedIfUnused(ex.right); + }, + else => {}, + } + }, + else => {}, + } + + return false; + } + + // EDot nodes represent a property access. This function may return an + // expression to replace the property access with. It assumes that the + // target of the EDot expression has already been visited. + pub fn maybeRewritePropertyAccess( + p: *P, + loc: logger.Loc, + assign_target: js_ast.AssignTarget, + is_delete_target: bool, + target: js_ast.Expr, + name: string, + name_loc: logger.Loc, + is_call_target: bool, + ) ?Expr { + if (@as(Expr.Tag, target.data) == .e_identifier) { + const id = target.data.e_identifier; + + // Rewrite property accesses on explicit namespace imports as an identifier. + // This lets us replace them easily in the printer to rebind them to + // something else without paying the cost of a whole-tree traversal during + // module linking just to rewrite these EDot expressions. + if (p.import_items_for_namespace.get(id.ref)) |*import_items| { + var item: LocRef = undefined; + + if (!import_items.contains(name)) { + item = LocRef{ .loc = name_loc, .ref = p.newSymbol(.import, name) catch unreachable }; + p.module_scope.generated.append(item.ref orelse unreachable) catch unreachable; + + import_items.put(name, item) catch unreachable; + p.is_import_item.put(item.ref orelse unreachable, true) catch unreachable; + + var symbol = p.symbols.items[item.ref.?.inner_index]; + // Mark this as generated in case it's missing. We don't want to + // generate errors for missing import items that are automatically + // generated. + symbol.import_item_status = .generated; + } else { + item = import_items.get(name) orelse unreachable; + } + + // Undo the usage count for the namespace itself. This is used later + // to detect whether the namespace symbol has ever been "captured" + // or whether it has just been used to read properties off of. + // + // The benefit of doing this is that if both this module and the + // imported module end up in the same module group and the namespace + // symbol has never been captured, then we don't need to generate + // any code for the namespace at all. + p.ignoreUsage(id.ref); + + // Track how many times we've referenced this symbol + p.recordUsage(&item.ref.?); + var ident = p.allocator.create(E.Identifier) catch unreachable; + ident.ref = item.ref.?; + + return p.handleIdentifier(name_loc, ident, name, IdentifierOpts{ + .assign_target = assign_target, + .is_delete_target = is_delete_target, + // If this expression is used as the target of a call expression, make + // sure the value of "this" is preserved. + .was_originally_identifier = false, + }); + } + + if (is_call_target and id.ref.eql(p.module_ref) and strings.eql(name, "require")) { + p.ignoreUsage(p.module_ref); + p.recordUsage(&p.require_ref); + return p.e(E.Identifier{ .ref = p.require_ref }, name_loc); + } + + // If this is a known enum value, inline the value of the enum + if (p.options.ts) { + if (p.known_enum_values.get(id.ref)) |enum_value_map| { + if (enum_value_map.get(name)) |enum_value| { + return p.e(E.Number{ .value = enum_value }, loc); + } + } + } + } + + return null; + } + + pub fn ignoreUsage(p: *P, ref: Ref) void { + if (!p.is_control_flow_dead) { + p.symbols.items[ref.inner_index].use_count_estimate = std.math.max(p.symbols.items[ref.inner_index].use_count_estimate - 1, 0); + var use = p.symbol_uses.get(ref) orelse p.panic("Expected symbol_uses to exist {s}\n{s}", .{ ref, p.symbol_uses }); + use.count_estimate = std.math.max(use.count_estimate - 1, 0); + if (use.count_estimate == 0) { + _ = p.symbol_uses.remove(ref); + } else { + p.symbol_uses.putAssumeCapacity(ref, use); + } + } + + // Don't roll back the "tsUseCounts" increment. This must be counted even if + // the value is ignored because that's what the TypeScript compiler does. + } + + pub fn visitAndAppendStmt(p: *P, stmts: *List(Stmt), stmt: *Stmt) !void { + switch (stmt.data) { + // These don't contain anything to traverse + + .s_debugger, .s_empty, .s_comment => {}, + .s_type_script => |data| { + // Erase TypeScript constructs from the output completely + return; + }, + .s_directive => |data| { + // if p.isStrictMode() && s.LegacyOctalLoc.Start > 0 { + // p.markStrictModeFeature(legacyOctalEscape, p.source.RangeOfLegacyOctalEscape(s.LegacyOctalLoc), "") + // } + return; + }, + .s_import => |data| { + try p.recordDeclaredSymbol(data.namespace_ref); + + if (data.default_name) |default_name| { + try p.recordDeclaredSymbol(default_name.ref orelse unreachable); + } + + if (data.items.len > 0) { + for (data.items) |*item| { + try p.recordDeclaredSymbol(item.name.ref orelse unreachable); + } + } + }, + .s_export_clause => |data| { + // "export {foo}" + var end: usize = 0; + for (data.items) |*item| { + const name = p.loadNameFromRef(item.name.ref orelse unreachable); + const symbol = try p.findSymbol(item.alias_loc, name); + const ref = symbol.ref; + + if (p.symbols.items[ref.inner_index].kind == .unbound) { + // Silently strip exports of non-local symbols in TypeScript, since + // those likely correspond to type-only exports. But report exports of + // non-local symbols as errors in JavaScript. + if (!p.options.ts) { + const r = js_lexer.rangeOfIdentifier(&p.source, item.name.loc); + try p.log.addRangeErrorFmt(p.source, r, p.allocator, "\"{s}\" is not declared in this file", .{name}); + continue; + } + continue; + } + + item.name.ref = ref; + data.items[end] = item.*; + end += 1; + } + // esbuild: "Note: do not remove empty export statements since TypeScript uses them as module markers" + // jarred: does that mean we can remove them here, since we're not bundling for production? + data.items = data.items[0..end]; + }, + .s_export_from => |data| { + // "export {foo} from 'path'" + const name = p.loadNameFromRef(data.namespace_ref); + data.namespace_ref = try p.newSymbol(.other, name); + try p.current_scope.generated.append(data.namespace_ref); + try p.recordDeclaredSymbol(data.namespace_ref); + + // This is a re-export and the symbols created here are used to reference + for (data.items) |*item| { + const _name = p.loadNameFromRef(item.name.ref orelse unreachable); + const ref = try p.newSymbol(.other, _name); + try p.current_scope.generated.append(data.namespace_ref); + try p.recordDeclaredSymbol(data.namespace_ref); + item.name.ref = ref; + } + }, + .s_export_star => |data| { + // "export {foo} from 'path'" + const name = p.loadNameFromRef(data.namespace_ref); + data.namespace_ref = try p.newSymbol(.other, name); + try p.current_scope.generated.append(data.namespace_ref); + try p.recordDeclaredSymbol(data.namespace_ref); + + // "export * as ns from 'path'" + if (data.alias) |alias| { + // "import * as ns from 'path'" + // "export {ns}" + + // jarred: For now, just always do this transform. + // because Safari doesn't support it and I've seen cases where this breaks + // TODO: backport unsupportedJSFeatures map + p.recordUsage(&data.namespace_ref); + try stmts.ensureCapacity(stmts.items.len + 2); + stmts.appendAssumeCapacity(p.s(S.Import{ .namespace_ref = data.namespace_ref, .star_name_loc = alias.loc, .import_record_index = data.import_record_index }, stmt.loc)); + + var items = try List(js_ast.ClauseItem).initCapacity(p.allocator, 1); + items.appendAssumeCapacity(js_ast.ClauseItem{ .alias = alias.original_name, .original_name = alias.original_name, .alias_loc = alias.loc, .name = LocRef{ .loc = alias.loc, .ref = data.namespace_ref } }); + stmts.appendAssumeCapacity(p.s(S.ExportClause{ .items = items.toOwnedSlice(), .is_single_line = true }, stmt.loc)); + } + }, + .s_export_default => |data| { + try p.recordDeclaredSymbol(data.default_name.ref orelse unreachable); + + switch (data.value) { + .expr => |*expr| { + const was_anonymous_named_expr = expr.isAnonymousNamed(); + data.value.expr = p.visitExpr(expr.*); + + // // Optionally preserve the name + data.value.expr = p.maybeKeepExprSymbolName(expr.*, "default", was_anonymous_named_expr); + + // Discard type-only export default statements + if (p.options.ts) { + switch (expr.data) { + .e_identifier => |ident| { + const symbol = p.symbols.items[ident.ref.inner_index]; + if (symbol.kind == .unbound) { + if (p.local_type_names.get(symbol.original_name)) |local_type| { + if (local_type) { + return; + } + } + } + }, + else => {}, + } + } + }, + + .stmt => |s2| { + switch (s2.data) { + .s_function => |func| { + var name: string = undefined; + if (func.func.name) |func_loc| { + name = p.symbols.items[func_loc.ref.?.inner_index].original_name; + } else { + func.func.name = data.default_name; + name = "default"; + } + + p.visitFunc(&func.func, func.func.open_parens_loc); + stmts.append(stmt.*) catch unreachable; + + if (func.func.name) |name_ref| { + // TODO-REACT-REFRESH-SPOT + stmts.append(p.keepStmtSymbolName(name_ref.loc, name_ref.ref.?, name)) catch unreachable; + } + }, + .s_class => |class| { + var shadow_ref = p.visitClass(s2.loc, &class.class); + stmts.appendSlice(p.lowerClass(js_ast.StmtOrExpr{ .stmt = stmt.* }, shadow_ref)) catch unreachable; + }, + else => {}, + } + }, + } + }, + .s_export_equals => |data| { + // "module.exports = value" + stmts.append( + Expr.assignStmt( + p.e( + E.Dot{ + .target = p.e( + E.Identifier{ + .ref = p.module_ref, + }, + stmt.loc, + ), + .name = "exports", + .name_loc = stmt.loc, + }, + stmt.loc, + ), + p.visitExpr(data.value), + p.allocator, + ), + ) catch unreachable; + p.recordUsage(&p.module_ref); + }, + .s_break => |data| { + if (data.label) |*label| { + const name = p.loadNameFromRef(label.ref orelse p.panic("Expected label to have a ref", .{})); + const res = p.findLabelSymbol(label.loc, name); + + label.ref = res.ref; + } else if (p.fn_or_arrow_data_visit.is_inside_loop and !p.fn_or_arrow_data_visit.is_inside_switch) { + const r = js_lexer.rangeOfIdentifier(&p.source, stmt.loc); + p.log.addRangeError(p.source, r, "Cannot use \"break\" here") catch unreachable; + } + }, + .s_continue => |data| { + if (data.label) |*label| { + const name = p.loadNameFromRef(label.ref orelse p.panic("Expected continue label to have a ref", .{})); + const res = p.findLabelSymbol(label.loc, name); + label.ref = res.ref; + if (res.found and !res.is_loop) { + const r = js_lexer.rangeOfIdentifier(&p.source, stmt.loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot \"continue\" to label {s}", .{name}) catch unreachable; + } + } else if (!p.fn_or_arrow_data_visit.is_inside_loop) { + const r = js_lexer.rangeOfIdentifier(&p.source, stmt.loc); + p.log.addRangeError(p.source, r, "Cannot use \"continue\" here") catch unreachable; + } + }, + .s_label => |data| { + p.pushScopeForVisitPass(.label, stmt.loc) catch unreachable; + const name = p.loadNameFromRef(data.name.ref orelse unreachable); + const ref = p.newSymbol(.label, name) catch unreachable; + data.name.ref = ref; + p.current_scope.label_ref = ref; + switch (data.stmt.data) { + .s_for, .s_for_in, .s_for_of, .s_while, .s_do_while => { + p.current_scope.label_stmt_is_loop = true; + }, + else => {}, + } + + data.stmt = p.visitSingleStmt(data.stmt, StmtsKind.none); + p.popScope(); + }, + .s_local => |data| { + for (data.decls) |*d| { + p.visitBinding(d.binding, null); + + if (d.value != null) { + var val = d.value orelse unreachable; + const was_anonymous_named_expr = p.isAnonymousNamedExpr(val); + + val = p.visitExpr(val); + // go version of defer would cause this to reset the variable + // zig version of defer causes this to set it to the last value of val, at the end of the scope. + defer d.value = val; + + // Optionally preserve the name + switch (d.binding.data) { + .b_identifier => |id| { + val = p.maybeKeepExprSymbolName( + val, + p.symbols.items[id.ref.inner_index].original_name, + was_anonymous_named_expr, + ); + }, + else => {}, + } + } + } + + // Handle being exported inside a namespace + if (data.is_export and p.enclosing_namespace_arg_ref != null) { + for (data.decls) |*d| { + if (d.value) |val| { + p.recordUsage(&(p.enclosing_namespace_arg_ref orelse unreachable)); + // TODO: is it necessary to lowerAssign? why does esbuild do it _most_ of the time? + stmts.append(p.s(S.SExpr{ + .value = Expr.assign(Binding.toExpr(&d.binding, p.to_expr_wrapper_namespace), val, p.allocator), + }, stmt.loc)) catch unreachable; + } + } + + return; + } + + // TODO: do we need to relocate vars? I don't think so. + if (data.kind == .k_var) {} + }, + .s_expr => |data| { + p.stmt_expr_value = data.value.data; + data.value = p.visitExpr(data.value); + + // TODO: + // if (p.options.mangle_syntax) { + + // } + }, + .s_throw => |data| { + data.value = p.visitExpr(data.value); + }, + .s_return => |data| { + if (p.fn_or_arrow_data_visit.is_outside_fn_or_arrow) { + const where = where: { + if (p.es6_export_keyword.len > 0) { + break :where p.es6_export_keyword; + } else if (p.top_level_await_keyword.len > 0) { + break :where p.top_level_await_keyword; + } else { + break :where logger.Range.None; + } + }; + + if (where.len > 0) { + p.log.addRangeError(p.source, where, "Top-level return cannot be used inside an ECMAScript module") catch unreachable; + } + } + + if (data.value) |val| { + data.value = p.visitExpr(val); + + // "return undefined;" can safely just always be "return;" + if (data.value != null and @as(Expr.Tag, data.value.?.data) == .e_undefined) { + // Returning undefined is implicit + data.value = null; + } + } + }, + .s_block => |data| { + { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + + // Pass the "is loop body" status on to the direct children of a block used + // as a loop body. This is used to enable optimizations specific to the + // topmost scope in a loop body block. + const kind = if (std.meta.eql(p.loop_body, stmt.data)) StmtsKind.loop_body else StmtsKind.none; + var _stmts = List(Stmt).init(p.allocator); + p.visitStmts(&_stmts, kind) catch unreachable; + data.stmts = _stmts.toOwnedSlice(); + } + + // trim empty statements + if (data.stmts.len == 0) { + stmts.append(p.s(S.Empty{}, stmt.loc)) catch unreachable; + return; + } else if (data.stmts.len == 1 and !statementCaresAboutScope(data.stmts[0])) { + // Unwrap blocks containing a single statement + stmts.append(data.stmts[0]) catch unreachable; + return; + } + }, + .s_with => |data| { + notimpl(); + }, + .s_while => |data| { + data.test_ = p.visitExpr(data.test_); + data.body = p.visitLoopBody(data.body); + + // TODO: simplify boolean expression + }, + .s_do_while => |data| { + data.test_ = p.visitExpr(data.test_); + data.body = p.visitLoopBody(data.body); + + // TODO: simplify boolean expression + }, + .s_if => |data| { + data.test_ = p.visitExpr(data.test_); + + const effects = SideEffects.toBoolean(data.test_.data); + if (effects.ok and !effects.value) { + const old = p.is_control_flow_dead; + defer p.is_control_flow_dead = old; + p.is_control_flow_dead = true; + data.yes = p.visitSingleStmt(data.yes, StmtsKind.none); + } else { + data.yes = p.visitSingleStmt(data.yes, StmtsKind.none); + } + }, + .s_for => |data| { + { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + if (data.init) |initst| { + _ = p.visitForLoopInit(initst, false); + } + + if (data.test_) |test_| { + data.test_ = p.visitExpr(test_); + + // TODO: boolean with side effects + } + + if (data.update) |update| { + data.update = p.visitExpr(update); + } + + data.body = p.visitLoopBody(data.body); + } + // TODO: Potentially relocate "var" declarations to the top level + + }, + .s_for_in => |data| { + { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + _ = p.visitForLoopInit(data.init, true); + data.value = p.visitExpr(data.value); + data.body = p.visitLoopBody(data.body); + + // TODO: do we need to this? + // // Check for a variable initializer + // if local, ok := s.Init.Data.(*js_ast.SLocal); ok && local.Kind == js_ast.LocalVar && len(local.Decls) == 1 { + // decl := &local.Decls[0] + // if id, ok := decl.Binding.Data.(*js_ast.BIdentifier); ok && decl.Value != nil { + // p.markStrictModeFeature(forInVarInit, p.source.RangeOfOperatorBefore(decl.Value.Loc, "="), "") + + // // Lower for-in variable initializers in case the output is used in strict mode + // stmts = append(stmts, js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SExpr{Value: js_ast.Assign( + // js_ast.Expr{Loc: decl.Binding.Loc, Data: &js_ast.EIdentifier{Ref: id.Ref}}, + // *decl.Value, + // )}}) + // decl.Value = nil + // } + // } + } + }, + .s_for_of => |data| { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + _ = p.visitForLoopInit(data.init, true); + data.value = p.visitExpr(data.value); + data.body = p.visitLoopBody(data.body); + + // TODO: do we need to do this? + // // Potentially relocate "var" declarations to the top level + // if init, ok := s.Init.Data.(*js_ast.SLocal); ok && init.Kind == js_ast.LocalVar { + // if replacement, ok := p.maybeRelocateVarsToTopLevel(init.Decls, relocateVarsForInOrForOf); ok { + // s.Init = replacement + // } + // } + + // p.lowerObjectRestInForLoopInit(s.Init, &s.Body) + }, + .s_try => |data| { + { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + p.fn_or_arrow_data_visit.try_body_count += 1; + defer p.fn_or_arrow_data_visit.try_body_count -= 1; + var _stmts = List(Stmt).fromOwnedSlice(p.allocator, data.body); + p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; + data.body = _stmts.toOwnedSlice(); + } + + if (data.catch_) |*catch_| { + p.pushScopeForVisitPass(.block, catch_.loc) catch unreachable; + defer p.popScope(); + var _stmts = List(Stmt).fromOwnedSlice(p.allocator, data.body); + p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; + catch_.body = _stmts.toOwnedSlice(); + } + + if (data.finally) |*finally| { + p.pushScopeForVisitPass(.block, finally.loc) catch unreachable; + var _stmts = List(Stmt).fromOwnedSlice(p.allocator, data.body); + p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; + finally.stmts = _stmts.toOwnedSlice(); + p.popScope(); + } + }, + .s_switch => |data| { + data.test_ = p.visitExpr(data.test_); + { + p.pushScopeForVisitPass(.block, data.body_loc) catch unreachable; + defer p.popScope(); + var old_is_inside_Swsitch = p.fn_or_arrow_data_visit.is_inside_switch; + p.fn_or_arrow_data_visit.is_inside_switch = true; + defer p.fn_or_arrow_data_visit.is_inside_switch = old_is_inside_Swsitch; + var i: usize = 0; + while (i < data.cases.len) : (i += 1) { + const case = data.cases[i]; + if (case.value) |val| { + data.cases[i].value = p.visitExpr(val); + // TODO: error messages + // Check("case", *c.Value, c.Value.Loc) + // p.warnAboutTypeofAndString(s.Test, *c.Value) + } + var _stmts = List(Stmt).fromOwnedSlice(p.allocator, case.body); + p.visitStmts(&_stmts, StmtsKind.none) catch unreachable; + data.cases[i].body = _stmts.toOwnedSlice(); + } + } + // TODO: duplicate case checker + + }, + .s_function => |data| { + p.visitFunc(&data.func, data.func.open_parens_loc); + + // Handle exporting this function from a namespace + if (data.func.flags.is_export and p.enclosing_namespace_arg_ref != null) { + const enclosing_namespace_arg_ref = p.enclosing_namespace_arg_ref orelse unreachable; + stmts.ensureUnusedCapacity(3) catch unreachable; + stmts.appendAssumeCapacity(stmt.*); + // i wonder if this will crash + stmts.appendAssumeCapacity(Expr.assignStmt(p.e(E.Dot{ + .target = p.e(E.Identifier{ .ref = enclosing_namespace_arg_ref }, stmt.loc), + .name = p.symbols.items[data.func.name.?.ref.?.inner_index].original_name, + .name_loc = data.func.name.?.loc, + }, stmt.loc), p.e(E.Identifier{ .ref = data.func.name.?.ref.? }, data.func.name.?.loc), p.allocator)); + } else { + stmts.ensureUnusedCapacity(2) catch unreachable; + stmts.appendAssumeCapacity(stmt.*); + } + + stmts.appendAssumeCapacity( + // i wonder if this will crash + p.keepStmtSymbolName( + data.func.name.?.loc, + data.func.name.?.ref.?, + p.symbols.items[data.func.name.?.ref.?.inner_index].original_name, + ), + ); + return; + }, + .s_class => |data| { + const shadow_ref = p.visitClass(stmt.loc, &data.class); + + // Remove the export flag inside a namespace + const was_export_inside_namespace = data.is_export and p.enclosing_namespace_arg_ref != null; + if (was_export_inside_namespace) { + data.is_export = false; + } + + // Lower class field syntax for browsers that don't support it + stmts.appendSlice(p.lowerClass(js_ast.StmtOrExpr{ .stmt = stmt.* }, shadow_ref)) catch unreachable; + + // Handle exporting this class from a namespace + if (was_export_inside_namespace) { + stmts.appendAssumeCapacity(Expr.assignStmt(p.e(E.Dot{ + .target = p.e(E.Identifier{ .ref = p.enclosing_namespace_arg_ref.? }, stmt.loc), + .name = p.symbols.items[data.class.class_name.?.ref.?.inner_index].original_name, + .name_loc = data.class.class_name.?.loc, + }, stmt.loc), p.e(E.Identifier{ .ref = data.class.class_name.?.ref.? }, data.class.class_name.?.loc), p.allocator)); + } + + return; + }, + .s_enum => |data| { + p.recordDeclaredSymbol(data.name.ref.?) catch unreachable; + p.pushScopeForVisitPass(.entry, stmt.loc) catch unreachable; + defer p.popScope(); + p.recordDeclaredSymbol(data.arg) catch unreachable; + + // Scan ahead for any variables inside this namespace. This must be done + // ahead of time before visiting any statements inside the namespace + // because we may end up visiting the uses before the declarations. + // We need to convert the uses into property accesses on the namespace. + for (data.values) |value| { + if (!value.ref.isNull()) { + p.is_exported_inside_namespace.put(value.ref, data.arg) catch unreachable; + } + } + + // Values without initializers are initialized to one more than the + // previous value if the previous value is numeric. Otherwise values + // without initializers are initialized to undefined. + var next_numeric_value: f64 = 0.0; + var has_numeric_value = true; + var value_exprs = List(Expr).initCapacity(p.allocator, data.values.len) catch unreachable; + + // Track values so they can be used by constant folding. We need to follow + // links here in case the enum was merged with a preceding namespace + var values_so_far = std.StringHashMap(f64).init(p.allocator); + p.known_enum_values.put(data.name.ref orelse p.panic("Expected data.name.ref", .{}), values_so_far) catch unreachable; + p.known_enum_values.put(data.arg, values_so_far) catch unreachable; + + // We normally don't fold numeric constants because they might increase code + // size, but it's important to fold numeric constants inside enums since + // that's what the TypeScript compiler does. + const old_should_fold_numeric_constants = p.should_fold_numeric_constants; + p.should_fold_numeric_constants = true; + defer p.should_fold_numeric_constants = old_should_fold_numeric_constants; + for (data.values) |*enum_value| { + // gotta allocate here so it lives after this function stack frame goes poof + const name = p.lexer.utf16ToString(enum_value.name); + var assign_target: Expr = undefined; + var enum_value_type: EnumValueType = EnumValueType.unknown; + if (enum_value.value != null) { + enum_value.value = p.visitExpr(enum_value.value.?); + switch (enum_value.value.?.data) { + .e_number => |num| { + values_so_far.put(name, num.value) catch unreachable; + enum_value_type = .numeric; + next_numeric_value = num.value + 1.0; + }, + .e_string => |str| { + enum_value_type = .string; + }, + else => {}, + } + } else if (enum_value_type == .numeric) { + enum_value.value = p.e(E.Number{ .value = next_numeric_value }, enum_value.loc); + values_so_far.put(name, next_numeric_value) catch unreachable; + next_numeric_value += 1; + } else { + enum_value.value = p.e(E.Undefined{}, enum_value.loc); + } + // "Enum['Name'] = value" + + assign_target = Expr.assign(p.e(E.Index{ + .target = p.e( + E.Identifier{ .ref = data.arg }, + enum_value.loc, + ), + .index = p.e( + E.String{ .value = enum_value.name }, + enum_value.loc, + ), + }, enum_value.loc), enum_value.value orelse unreachable, p.allocator); + + p.recordUsage(&data.arg); + + // String-valued enums do not form a two-way map + if (enum_value_type == .string) { + value_exprs.append(assign_target) catch unreachable; + } else { + // "Enum[assignTarget] = 'Name'" + value_exprs.append(Expr.assign(p.e(E.Index{ + .target = p.e( + E.Identifier{ .ref = data.arg }, + enum_value.loc, + ), + .index = assign_target, + }, enum_value.loc), p.e(E.String{ .value = enum_value.name }, enum_value.loc), p.allocator)) catch unreachable; + } + } + p.recordUsage(&data.arg); + + var value_stmts = List(Stmt).initCapacity(p.allocator, value_exprs.items.len) catch unreachable; + // Generate statements from expressions + + for (value_exprs.items) |expr| { + value_stmts.appendAssumeCapacity(p.s(S.SExpr{ .value = expr }, expr.loc)); + } + value_exprs.deinit(); + p.generateClosureForTypescriptNameSpaceOrEnum( + stmts, + stmt.loc, + data.is_export, + data.name.loc, + data.name.ref.?, + data.arg, + value_stmts.toOwnedSlice(), + ); + return; + }, + .s_namespace => |data| { + p.recordDeclaredSymbol(data.name.ref.?) catch unreachable; + + // Scan ahead for any variables inside this namespace. This must be done + // ahead of time before visiting any statements inside the namespace + // because we may end up visiting the uses before the declarations. + // We need to convert the uses into property accesses on the namespace. + for (data.stmts) |child_stmt| { + switch (child_stmt.data) { + .s_local => |local| { + if (local.is_export) { + p.markExportedDeclsInsideNamespace(data.arg, local.decls); + } + }, + else => {}, + } + } + + var prepend_temp_refs = PrependTempRefsOpts{ .kind = StmtsKind.fn_body }; + var prepend_list = List(Stmt).fromOwnedSlice(p.allocator, data.stmts); + + { + const old_enclosing_namespace_arg_ref = p.enclosing_namespace_arg_ref; + p.enclosing_namespace_arg_ref = data.arg; + defer p.enclosing_namespace_arg_ref = old_enclosing_namespace_arg_ref; + p.pushScopeForVisitPass(.entry, stmt.loc) catch unreachable; + defer p.popScope(); + p.recordDeclaredSymbol(data.arg) catch unreachable; + p.visitStmtsAndPrependTempRefs(&prepend_list, &prepend_temp_refs) catch unreachable; + } + + p.generateClosureForTypescriptNameSpaceOrEnum( + stmts, + stmt.loc, + data.is_export, + data.name.loc, + data.name.ref.?, + data.arg, + prepend_list.toOwnedSlice(), + ); + return; + }, + else => { + notimpl(); + }, + } + + // if we get this far, it stays + try stmts.append(stmt.*); + } + + pub fn markExportedDeclsInsideNamespace(p: *P, ns_ref: Ref, decls: []G.Decl) void { + notimpl(); + } + + pub fn generateClosureForTypescriptNameSpaceOrEnum( + p: *P, + stmts: *List(Stmt), + stmt_loc: logger.Loc, + is_export: bool, + name_loc: logger.Loc, + name_ref: Ref, + arg_ref: Ref, + stmts_inside_closure: []Stmt, + ) void { + notimpl(); + } + + pub fn lowerClass(p: *P, stmtorexpr: js_ast.StmtOrExpr, ref: Ref) []Stmt { + switch (stmtorexpr) { + .stmt => |stmt| { + var stmts = p.allocator.alloc(Stmt, 1) catch unreachable; + stmts[0] = stmt; + return stmts; + }, + .expr => |expr| { + var stmts = p.allocator.alloc(Stmt, 1) catch unreachable; + stmts[0] = p.s(S.SExpr{ .value = expr }, expr.loc); + return stmts; + }, + } + } + + pub fn visitForLoopInit(p: *P, stmt: Stmt, is_in_or_of: bool) Stmt { + switch (stmt.data) { + .s_expr => |st| { + const assign_target = if (is_in_or_of) js_ast.AssignTarget.replace else js_ast.AssignTarget.none; + p.stmt_expr_value = st.value.data; + st.value = p.visitExprInOut(st.value, ExprIn{ .assign_target = assign_target }); + }, + .s_local => |st| { + for (st.decls) |*dec| { + p.visitBinding(dec.binding, null); + if (dec.value) |val| { + dec.value = p.visitExpr(val); + } + } + // s.Decls = p.lowerObjectRestInDecls(s.Decls) + // s.Kind = p.selectLocalKind(s.Kind) + }, + else => { + p.panic("Unexpected stmt in visitForLoopInit: {s}", .{stmt}); + }, + } + + return stmt; + } + + // pub fn maybeRelocateVarsToTopLevel(p: *P, decls: []G.Decl, mode: ) + + pub fn wrapIdentifierNamespace( + p: *P, + loc: logger.Loc, + ref: Ref, + ) Expr { + p.recordUsage(&(p.enclosing_namespace_arg_ref orelse unreachable)); + + return p.e(E.Dot{ + .target = p.e(E.Identifier{ .ref = p.enclosing_namespace_arg_ref orelse unreachable }, loc), + .name = p.symbols.items[ref.inner_index].original_name, + .name_loc = loc, + }, loc); + } + + pub fn wrapIdentifierHoisting( + p: *P, + loc: logger.Loc, + ref: Ref, + ) Expr { + p.relocated_top_level_vars.append(LocRef{ .loc = loc, .ref = ref }) catch unreachable; + var _ref = ref; + p.recordUsage(&_ref); + return p.e(E.Identifier{ .ref = _ref }, loc); + } + + pub fn isAnonymousNamedExpr(p: *P, expr: ExprNodeIndex) bool { + switch (expr.data) { + .e_arrow => { + return true; + }, + .e_function => |func| { + return func.func.name == null; + }, + .e_class => |class| { + return class.class_name == null; + }, + else => { + return false; + }, + } + } + + pub fn valueForDefine(p: *P, loc: logger.Loc, assign_target: js_ast.AssignTarget, is_delete_target: bool, define_data: *const DefineData) Expr { + switch (define_data.value) { + .e_identifier => |ident| { + return p.handleIdentifier( + loc, + ident, + define_data.original_name.?, + IdentifierOpts{ + .assign_target = assign_target, + .is_delete_target = is_delete_target, + .was_originally_identifier = true, + }, + ); + }, + else => {}, + } + + return Expr{ + .data = define_data.value, + .loc = loc, + }; + } + + pub fn isDotDefineMatch(p: *P, expr: Expr, parts: []const string) bool { + switch (expr.data) { + .e_dot => |ex| { + if (parts.len > 1) { + // Intermediates must be dot expressions + const last = parts.len - 1; + return strings.eql(parts[last], ex.name) and ex.optional_chain == null and p.isDotDefineMatch(ex.target, parts[0..last]); + } + }, + .e_import_meta => |ex| { + return parts.len == 2 and strings.eql(parts[0], "import") and strings.eql(parts[1], "meta"); + }, + .e_identifier => |ex| { + // The last expression must be an identifier + if (parts.len == 1) { + const name = p.loadNameFromRef(ex.ref); + if (!strings.eql(name, parts[0])) { + return false; + } + + const result = p.findSymbol(expr.loc, name) catch return false; + + // We must not be in a "with" statement scope + if (result.is_inside_with_scope) { + return false; + } + + // The last symbol must be unbound + return p.symbols.items[result.ref.inner_index].kind == .unbound; + } + }, + else => {}, + } + + return false; + } + + pub fn visitBinding(p: *P, binding: BindingNodeIndex, duplicate_arg_check: ?*StringBoolMap) void { + switch (binding.data) { + .b_missing => {}, + .b_identifier => |bind| { + p.recordDeclaredSymbol(bind.ref) catch unreachable; + const name = p.symbols.items[bind.ref.inner_index].original_name; + if (isEvalOrArguments(name)) { + p.markStrictModeFeature(.eval_or_arguments, js_lexer.rangeOfIdentifier(&p.source, binding.loc), name) catch unreachable; + } + + if (duplicate_arg_check) |dup| { + const res = dup.getOrPut(name) catch unreachable; + if (res.found_existing) { + p.log.addRangeErrorFmt( + p.source, + js_lexer.rangeOfIdentifier(&p.source, binding.loc), + p.allocator, + "\"{s}\" cannot be bound multiple times in the same parameter list", + .{name}, + ) catch unreachable; + } + res.entry.value = true; + } + }, + .b_array => |bind| { + for (bind.items) |*item| { + p.visitBinding(item.binding, duplicate_arg_check); + if (item.default_value) |default_value| { + const was_anonymous_named_expr = p.isAnonymousNamedExpr(default_value); + item.default_value = p.visitExpr(default_value); + + switch (item.binding.data) { + .b_identifier => |bind_| { + item.default_value = p.maybeKeepExprSymbolName( + item.default_value orelse unreachable, + p.symbols.items[bind_.ref.inner_index].original_name, + was_anonymous_named_expr, + ); + }, + else => {}, + } + } + } + }, + .b_object => |bind| { + var i: usize = 0; + while (i < bind.properties.len) : (i += 1) { + var property = bind.properties[i]; + if (!property.flags.is_spread) { + property.key = p.visitExpr(property.key); + } + + p.visitBinding(property.value, duplicate_arg_check); + if (property.default_value) |default_value| { + const was_anonymous_named_expr = p.isAnonymousNamedExpr(default_value); + property.default_value = p.visitExpr(default_value); + + switch (property.value.data) { + .b_identifier => |bind_| { + property.default_value = p.maybeKeepExprSymbolName( + property.default_value orelse unreachable, + p.symbols.items[bind_.ref.inner_index].original_name, + was_anonymous_named_expr, + ); + }, + else => {}, + } + } + bind.properties[i] = property; + } + }, + else => { + p.panic("Unexpected binding {s}", .{binding}); + }, + } + } + + pub fn visitLoopBody(p: *P, stmt: StmtNodeIndex) StmtNodeIndex { + const old_is_inside_loop = p.fn_or_arrow_data_visit.is_inside_loop; + p.fn_or_arrow_data_visit.is_inside_loop = true; + defer p.fn_or_arrow_data_visit.is_inside_loop = old_is_inside_loop; + p.loop_body = stmt.data; + return p.visitSingleStmt(stmt, .loop_body); + } + + pub fn visitSingleStmt(p: *P, stmt: Stmt, kind: StmtsKind) Stmt { + const has_if_scope = has_if: { + switch (stmt.data) { + .s_function => |func| { + break :has_if func.func.flags.has_if_scope; + }, + else => { + break :has_if false; + }, + } + }; + + // Introduce a fake block scope for function declarations inside if statements + if (has_if_scope) { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + } + + var stmts = List(Stmt).initCapacity(p.allocator, 1) catch unreachable; + stmts.append(stmt) catch unreachable; + p.visitStmts(&stmts, kind) catch unreachable; + + if (has_if_scope) { + p.popScope(); + } + + return p.stmtsToSingleStmt(stmt.loc, stmts.toOwnedSlice()); + } + + // One statement could potentially expand to several statements + pub fn stmtsToSingleStmt(p: *P, loc: logger.Loc, stmts: []Stmt) Stmt { + if (stmts.len == 0) { + return p.s(S.Empty{}, loc); + } + + if (stmts.len == 1) { + switch (stmts[0].data) { + .s_local => |local| { + // "let" and "const" must be put in a block when in a single-statement context + + if (local.kind == .k_var) { + return stmts[0]; + } + }, + else => { + return stmts[0]; + }, + } + } + + return p.s(S.Block{ .stmts = stmts }, loc); + } + + pub fn findLabelSymbol(p: *P, loc: logger.Loc, name: string) FindLabelSymbolResult { + var res = FindLabelSymbolResult{ .ref = undefined, .is_loop = false }; + + var _scope: ?*Scope = p.current_scope; + + while (_scope) |scope| : (_scope = scope.parent) { + var label_ref = scope.label_ref orelse continue; + + if (!scope.kindStopsHoisting() or (scope.kind != .label) or !strings.eql(name, p.symbols.items[label_ref.inner_index].original_name)) { + continue; + } + + // Track how many times we've referenced this symbol + p.recordUsage(&label_ref); + res.ref = label_ref; + res.is_loop = scope.label_stmt_is_loop; + res.found = true; + break; + } + + const r = js_lexer.rangeOfIdentifier(&p.source, loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "There is no containing label named {s}", .{name}) catch unreachable; + + // Allocate an "unbound" symbol + var ref = p.newSymbol(.unbound, name) catch unreachable; + + // Track how many times we've referenced this symbol + p.recordUsage(&ref); + + return res; + } + + pub fn visitClass(p: *P, name_scope_loc: logger.Loc, class: *G.Class) Ref { + class.ts_decorators = p.visitTSDecorators(class.ts_decorators); + + if (class.class_name) |name| { + p.recordDeclaredSymbol(name.ref.?) catch unreachable; + } + + p.pushScopeForVisitPass(.class_name, name_scope_loc) catch unreachable; + const old_enclosing_class_keyword = p.enclosing_class_keyword; + p.enclosing_class_keyword = class.class_keyword; + p.current_scope.recursiveSetStrictMode(.implicit_strict_mode_class); + var class_name_ref: Ref = if (class.class_name != null) class.class_name.?.ref.? else p.newSymbol(.other, "this") catch unreachable; + + var shadow_ref = Ref.None; + + if (!class_name_ref.eql(Ref.None)) { + // are not allowed to assign to this symbol (it throws a TypeError). + const name = p.symbols.items[class_name_ref.inner_index].original_name; + var identifier = p.allocator.alloc(u8, name.len + 1) catch unreachable; + std.mem.copy(u8, identifier[1..identifier.len], name); + identifier[0] = '_'; + shadow_ref = p.newSymbol(Symbol.Kind.cconst, identifier) catch unreachable; + p.recordDeclaredSymbol(shadow_ref) catch unreachable; + if (class.class_name) |class_name| { + p.current_scope.members.put(identifier, Scope.Member{ .loc = class_name.loc, .ref = shadow_ref }) catch unreachable; + } + } + + if (class.extends) |extends| { + class.extends = p.visitExpr(extends); + } + + p.pushScopeForVisitPass(.class_body, class.body_loc) catch unreachable; + defer p.popScope(); + + var i: usize = 0; + while (i < class.properties.len) : (i += 1) { + var property = &class.properties[i]; + property.ts_decorators = p.visitTSDecorators(property.ts_decorators); + const is_private = if (property.key != null) @as(Expr.Tag, property.key.?.data) == .e_private_identifier else false; + + // Special-case EPrivateIdentifier to allow it here + + if (is_private) { + p.recordDeclaredSymbol(property.key.?.data.e_private_identifier.ref) catch unreachable; + } else if (property.key) |key| { + class.properties[i].key = p.visitExpr(key); + } + + // Make it an error to use "arguments" in a class body + p.current_scope.forbid_arguments = true; + defer p.current_scope.forbid_arguments = false; + + // The value of "this" is shadowed inside property values + const old_is_this_captured = p.fn_only_data_visit.is_this_nested; + const old_this = p.fn_only_data_visit.this_class_static_ref; + p.fn_only_data_visit.is_this_nested = true; + p.fn_only_data_visit.this_class_static_ref = null; + defer p.fn_only_data_visit.is_this_nested = old_is_this_captured; + defer p.fn_only_data_visit.this_class_static_ref = old_this; + + // We need to explicitly assign the name to the property initializer if it + // will be transformed such that it is no longer an inline initializer. + var name_to_keep: ?string = null; + if (is_private) {} else if (!property.flags.is_method and !property.flags.is_computed) { + if (property.key) |key| { + if (@as(Expr.Tag, key.data) == .e_string) { + name_to_keep = p.lexer.utf16ToString(key.data.e_string.value); + } + } + } + + if (property.value) |val| { + if (name_to_keep) |name| { + const was_anon = p.isAnonymousNamedExpr(val); + property.value = p.maybeKeepExprSymbolName(p.visitExpr(val), name, was_anon); + } else { + property.value = p.visitExpr(val); + } + } + + if (property.initializer) |val| { + // if (property.flags.is_static and ) + if (name_to_keep) |name| { + const was_anon = p.isAnonymousNamedExpr(val); + property.initializer = p.maybeKeepExprSymbolName(p.visitExpr(val), name, was_anon); + } else { + property.initializer = p.visitExpr(val); + } + } + } + + if (!shadow_ref.eql(Ref.None)) { + if (p.symbols.items[shadow_ref.inner_index].use_count_estimate == 0) { + // Don't generate a shadowing name if one isn't needed + shadow_ref = Ref.None; + } else if (class.class_name) |class_name| { + // If there was originally no class name but something inside needed one + // (e.g. there was a static property initializer that referenced "this"), + // store our generated name so the class expression ends up with a name. + class.class_name = LocRef{ .loc = name_scope_loc, .ref = class_name_ref }; + p.current_scope.generated.append(class_name_ref) catch unreachable; + p.recordDeclaredSymbol(class_name_ref) catch unreachable; + } + } + + return shadow_ref; + } + + fn keepStmtSymbolName(p: *P, loc: logger.Loc, ref: Ref, name: string) Stmt { + var exprs = p.allocator.alloc(Expr, 2) catch unreachable; + exprs[0] = p.e(E.Identifier{ + .ref = ref, + }, loc); + exprs[1] = p.e(E.String{ .value = strings.toUTF16Alloc(name, p.allocator) catch unreachable }, loc); + return p.s(S.SExpr{ + // I believe that this is a spot we can do $RefreshReg$(name) + .value = p.callRuntime(loc, "__name", exprs), + + // Make sure tree shaking removes this if the function is never used + .does_not_affect_tree_shaking = true, + }, loc); + } + + pub fn callRuntime(p: *P, loc: logger.Loc, name: string, args: []Expr) Expr { + var ref: Ref = undefined; + if (!p.runtime_imports.contains(name)) { + ref = p.newSymbol(.other, name) catch unreachable; + p.module_scope.generated.append(ref) catch unreachable; + p.runtime_imports.put(name, ref) catch unreachable; + } else { + ref = p.runtime_imports.get(name) orelse unreachable; + } + + p.recordUsage(&ref); + return p.e(E.Call{ + .target = p.e(E.Identifier{ + .ref = ref, + }, loc), + .args = args, + }, loc); + } + + fn visitStmts(p: *P, stmts: *List(Stmt), kind: StmtsKind) !void { + // Save the current control-flow liveness. This represents if we are + // currently inside an "if (false) { ... }" block. + var old_is_control_flow_dead = p.is_control_flow_dead; + + // visit all statements first + var visited = try List(Stmt).initCapacity(p.allocator, stmts.items.len); + var before = List(Stmt).init(p.allocator); + var after = List(Stmt).init(p.allocator); + for (stmts.items) |*stmt| { + switch (stmt.data) { + .s_export_equals => { + // TypeScript "export = value;" becomes "module.exports = value;". This + // must happen at the end after everything is parsed because TypeScript + // moves this statement to the end when it generates code. + try p.visitAndAppendStmt(&after, stmt); + continue; + }, + .s_function => |data| { + // Manually hoist block-level function declarations to preserve semantics. + // This is only done for function declarations that are not generators + // or async functions, since this is a backwards-compatibility hack from + // Annex B of the JavaScript standard. + if (!p.current_scope.kindStopsHoisting() and p.symbols.items[data.func.name.?.ref.?.inner_index].kind == .hoisted_function) { + try p.visitAndAppendStmt(&before, stmt); + continue; + } + }, + else => {}, + } + try p.visitAndAppendStmt(&visited, stmt); + } + } + + fn extractDeclsForBinding(binding: Binding, decls: *List(G.Decl)) !void { + switch (binding.data) { + .b_property, .b_missing => {}, + .b_identifier => { + try decls.append(G.Decl{ .binding = binding }); + }, + .b_array => |arr| { + for (arr.items) |item| { + extractDeclsForBinding(item.binding, decls) catch unreachable; + } + }, + .b_object => |obj| { + for (obj.properties) |prop| { + extractDeclsForBinding(prop.value, decls) catch unreachable; + } + }, + } + } + + // This assumes that the open parenthesis has already been parsed by the caller + pub fn parseParenExpr(p: *P, loc: logger.Loc, opts: ParenExprOpts) !Expr { + var items_list = try List(Expr).initCapacity(p.allocator, 1); + var errors = DeferredErrors{}; + var arrowArgErrors = DeferredArrowArgErrors{}; + var spread_range = logger.Range{}; + var type_colon_range = logger.Range{}; + var comma_after_spread = logger.Loc{}; + + // Push a scope assuming this is an arrow function. It may not be, in which + // case we'll need to roll this change back. This has to be done ahead of + // parsing the arguments instead of later on when we hit the "=>" token and + // we know it's an arrow function because the arguments may have default + // values that introduce new scopes and declare new symbols. If this is an + // arrow function, then those new scopes will need to be parented under the + // scope of the arrow function itself. + const scopeIndex = try p.pushScopeForParsePass(.function_args, loc); + + // Allow "in" inside parentheses + var oldAllowIn = p.allow_in; + p.allow_in = true; + + // Forbid "await" and "yield", but only for arrow functions + var old_fn_or_arrow_data = p.fn_or_arrow_data_parse; + p.fn_or_arrow_data_parse.arrow_arg_errors = arrowArgErrors; + + // Scan over the comma-separated arguments or expressions + while (p.lexer.token != .t_close_paren) { + const item_loc = p.lexer.loc(); + const is_spread = p.lexer.token == .t_dot_dot_dot; + + if (is_spread) { + spread_range = p.lexer.range(); + // p.markSyntaxFeature() + p.lexer.next(); + } + + // We don't know yet whether these are arguments or expressions, so parse + p.latest_arrow_arg_loc = p.lexer.loc(); + + var item = p.parseExprOrBindings(.comma, &errors); + + if (is_spread) { + item = p.e(E.Spread{ .value = item }, loc); + } + + // Skip over types + if (p.options.ts and p.lexer.token == .t_colon) { + type_colon_range = p.lexer.range(); + p.lexer.next(); + p.skipTypescriptType(.lowest); + } + + if (p.options.ts and p.lexer.token == .t_equals and !p.forbid_suffix_after_as_loc.eql(p.lexer.loc())) { + p.lexer.next(); + item = Expr.assign(item, p.parseExpr(.comma), p.allocator); + } + + items_list.append(item) catch unreachable; + + if (p.lexer.token != .t_comma) { + break; + } + + // Spread arguments must come last. If there's a spread argument followed + if (is_spread) { + comma_after_spread = p.lexer.loc(); + } + + // Eat the comma token + p.lexer.next(); + } + var items = items_list.toOwnedSlice(); + + // The parenthetical construct must end with a close parenthesis + p.lexer.expect(.t_close_paren); + + // Restore "in" operator status before we parse the arrow function body + p.allow_in = oldAllowIn; + + // Also restore "await" and "yield" expression errors + p.fn_or_arrow_data_parse = old_fn_or_arrow_data; + + // Are these arguments to an arrow function? + if (p.lexer.token == .t_equals_greater_than or opts.force_arrow_fn or (p.options.ts and p.lexer.token == .t_colon)) { + var invalidLog = List(logger.Loc).init(p.allocator); + var args = List(G.Arg).init(p.allocator); + + if (opts.is_async) { + // markl,oweredsyntaxpoksdpokasd + } + + // First, try converting the expressions to bindings + for (items) |*_item| { + var item = _item; + var is_spread = false; + switch (item.data) { + .e_spread => |v| { + is_spread = true; + item = &v.value; + }, + else => {}, + } + + const tuple = p.convertExprToBindingAndInitializer(item, &invalidLog, is_spread); + assert(tuple.binding != null); + // double allocations + args.append(G.Arg{ + .binding = tuple.binding orelse unreachable, + .default = tuple.expr, + }) catch unreachable; + } + + // Avoid parsing TypeScript code like "a ? (1 + 2) : (3 + 4)" as an arrow + // function. The ":" after the ")" may be a return type annotation, so we + // attempt to convert the expressions to bindings first before deciding + // whether this is an arrow function, and only pick an arrow function if + // there were no conversion errors. + if (p.lexer.token == .t_equals_greater_than or (invalidLog.items.len == 0 and (p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() or opts.force_arrow_fn))) { + p.maybeCommaSpreadError(comma_after_spread); + p.logArrowArgErrors(&arrowArgErrors); + } + } + + // If we get here, it's not an arrow function so undo the pushing of the + // scope we did earlier. This needs to flatten any child scopes into the + // parent scope as if the scope was never pushed in the first place. + p.popAndFlattenScope(scopeIndex); + + // If this isn't an arrow function, then types aren't allowed + if (type_colon_range.len > 0) { + try p.log.addRangeError(p.source, type_colon_range, "Unexpected \":\""); + p.panic("", .{}); + } + + // Are these arguments for a call to a function named "async"? + if (opts.is_async) { + p.logExprErrors(&errors); + const async_expr = p.e(E.Identifier{ .ref = try p.storeNameInRef("async") }, loc); + return p.e(E.Call{ .target = async_expr, .args = items }, loc); + } + + // Is this a chain of expressions and comma operators? + + if (items.len > 0) { + p.logExprErrors(&errors); + if (spread_range.len > 0) { + try p.log.addRangeError(p.source, type_colon_range, "Unexpected \"...\""); + p.panic("", .{}); + } + + var value = Expr.joinAllWithComma(items, p.allocator); + p.markExprAsParenthesized(&value); + return value; + } + + // Indicate that we expected an arrow function + p.lexer.expected(.t_equals_greater_than); + p.panic("", .{}); + } + + pub fn popAndFlattenScope(p: *P, scope_index: usize) void { + // Move up to the parent scope + var to_flatten = p.current_scope; + var parent = to_flatten.parent.?; + p.current_scope = parent; + var scopes_in_order = p.scopes_in_order.toOwnedSlice(); + // Erase this scope from the order. This will shift over the indices of all + // the scopes that were created after us. However, we shouldn't have to + // worry about other code with outstanding scope indices for these scopes. + // These scopes were all created in between this scope's push and pop + // operations, so they should all be child scopes and should all be popped + // by the time we get here. + std.mem.copy(ScopeOrder, scopes_in_order[scope_index..scopes_in_order.len], scopes_in_order[scope_index + 1 .. scopes_in_order.len]); + p.scopes_in_order = @TypeOf(p.scopes_in_order).fromOwnedSlice(p.allocator, scopes_in_order); + + // Remove the last child from the parent scope + const last = parent.children.items.len - 1; + assert(parent.children.items[last] == to_flatten); + _ = parent.children.popOrNull(); + + for (to_flatten.children.items) |item| { + item.parent = parent; + parent.children.append(item) catch unreachable; + } + } + + pub fn maybeCommaSpreadError(p: *P, _comma_after_spread: ?logger.Loc) void { + if (_comma_after_spread) |comma_after_spread| { + p.log.addRangeError(p.source, logger.Range{ .loc = comma_after_spread, .len = 1 }, "Unexpected \",\" after rest pattern") catch unreachable; + } + } + + pub fn toAST(p: *P, _parts: []js_ast.Part) !js_ast.Ast { + var parts = _parts; + // Insert an import statement for any runtime imports we generated + if (p.runtime_imports.count() > 0 and !p.options.omit_runtime_for_tests) {} + + var parts_end: usize = 0; + // Handle import paths after the whole file has been visited because we need + // symbol usage counts to be able to remove unused type-only imports in + // TypeScript code. + while (true) { + var kept_import_equals = false; + var removed_import_equals = false; + + var i: usize = 0; + // Potentially remove some statements, then filter out parts to remove any + // with no statements + while (i < parts.len) : (i += 1) { + var part = parts[i]; + _ = p.import_records_for_current_part.toOwnedSlice(); + _ = p.declared_symbols.toOwnedSlice(); + + var result = try ImportScanner.scan(p, part.stmts); + kept_import_equals = kept_import_equals or result.kept_import_equals; + removed_import_equals = removed_import_equals or result.removed_import_equals; + part.import_record_indices = p.import_records_for_current_part.toOwnedSlice(); + part.declared_symbols = p.declared_symbols.toOwnedSlice(); + part.stmts = result.stmts; + if (part.stmts.len > 0) { + if (p.module_scope.contains_direct_eval and part.declared_symbols.len > 0) { + // If this file contains a direct call to "eval()", all parts that + // declare top-level symbols must be kept since the eval'd code may + // reference those symbols. + part.can_be_removed_if_unused = false; + } + parts[parts_end] = part; + parts_end += 1; + } + } + + // We need to iterate multiple times if an import-equals statement was + // removed and there are more import-equals statements that may be removed + if (!kept_import_equals or !removed_import_equals) { + break; + } + } + + parts = parts[0..parts_end]; + // Analyze cross-part dependencies for tree shaking and code splitting + + { + // Map locals to parts + p.top_level_symbol_to_parts = @TypeOf(p.top_level_symbol_to_parts).init(p.allocator); + var i: usize = 0; + while (i < parts.len) : (i += 1) { + const part = parts[i]; + for (part.declared_symbols) |declared| { + if (declared.is_top_level) { + if (p.top_level_symbol_to_parts.contains(declared.ref)) { + try p.top_level_symbol_to_parts.get(declared.ref).?.append(@intCast(u32, i)); + } else { + var list = try List(u32).initCapacity(p.allocator, 1); + list.appendAssumeCapacity(@intCast(u32, i)); + try p.top_level_symbol_to_parts.put(declared.ref, list); + } + } + } + } + + // Each part tracks the other parts it depends on within this file + var local_dependencies = std.AutoHashMap(u32, u32).init(p.allocator); + + i = 0; + while (i < parts.len) : (i += 1) { + const part = parts[i]; + var iter = part.symbol_uses.iterator(); + var dependencies = List(js_ast.Dependency).init(p.allocator); + while (iter.next()) |entry| { + const ref = entry.key; + + if (p.top_level_symbol_to_parts.get(ref)) |tlstp| { + for (tlstp.items) |other_part_index| { + if (!local_dependencies.contains(other_part_index) or other_part_index != i) { + try local_dependencies.put(other_part_index, @intCast(u32, i)); + try dependencies.append(js_ast.Dependency{ + .source_index = p.source.index, + .part_index = other_part_index, + }); + } + } + } + + // Also map from imports to parts that use them + // TODO: will appending to this list like this be a perf issue? + if (p.named_imports.getEntry(ref)) |named_import_entry| { + const named_import = named_import_entry.value; + var buf = try p.allocator.alloc(u32, named_import.local_parts_with_uses.len + 1); + if (named_import.local_parts_with_uses.len > 0) { + std.mem.copy(u32, buf, named_import.local_parts_with_uses); + } + buf[buf.len - 1] = @intCast(u32, i); + named_import_entry.value.local_parts_with_uses = buf; + } + } + } + } + + var exports_kind = js_ast.ExportsKind.none; + const uses_exports_ref = p.symbols.items[p.exports_ref.inner_index].use_count_estimate > 0; + const uses_module_ref = p.symbols.items[p.module_ref.inner_index].use_count_estimate > 0; + + if (p.es6_export_keyword.len > 0 or p.top_level_await_keyword.len > 0) { + exports_kind = .esm; + } else if (uses_exports_ref or uses_module_ref or p.has_top_level_return) { + exports_kind = .cjs; + } else { + exports_kind = .esm; + } + + var wrapper_name = try p.allocator.alloc(u8, "require_".len + p.source.identifier_name.len); + std.mem.copy(u8, wrapper_name[0.."require_".len], "require_"); + std.mem.copy(u8, wrapper_name["require_".len..wrapper_name.len], p.source.identifier_name); + + var wrapper = try p.newSymbol(.other, wrapper_name); + + return js_ast.Ast{ + .parts = parts, + .module_scope = p.module_scope.*, + .symbols = p.symbols.toOwnedSlice(), + .exports_ref = p.exports_ref, + .wrapper_ref = wrapper, + .import_records = p.import_records.toOwnedSlice(), + .export_star_import_records = p.export_star_import_records.toOwnedSlice(), + .top_level_symbol_to_parts = p.top_level_symbol_to_parts, + .approximate_line_count = p.lexer.approximate_newline_count + 1, + .exports_kind = exports_kind, + .named_imports = p.named_imports, + .named_exports = p.named_exports, + .import_keyword = p.es6_import_keyword, + .export_keyword = p.es6_export_keyword, + // .top_Level_await_keyword = p.top_level_await_keyword, + }; + } + + pub fn init(allocator: *std.mem.Allocator, log: *logger.Log, source: logger.Source, define: *Define, lexer: js_lexer.Lexer, opts: Parser.Options) !*P { + var parser = try allocator.create(P); + parser.allocated_names = @TypeOf(parser.allocated_names).init(allocator); + parser.define = define; + parser.scopes_for_current_part = @TypeOf(parser.scopes_for_current_part).init(allocator); + parser.symbols = @TypeOf(parser.symbols).init(allocator); + parser.ts_use_counts = @TypeOf(parser.ts_use_counts).init(allocator); + parser.declared_symbols = @TypeOf(parser.declared_symbols).init(allocator); + parser.known_enum_values = @TypeOf(parser.known_enum_values).init(allocator); + parser.import_records = @TypeOf(parser.import_records).init(allocator); + parser.import_records_for_current_part = @TypeOf(parser.import_records_for_current_part).init(allocator); + parser.export_star_import_records = @TypeOf(parser.export_star_import_records).init(allocator); + parser.import_items_for_namespace = @TypeOf(parser.import_items_for_namespace).init(allocator); + parser.named_imports = @TypeOf(parser.named_imports).init(allocator); + parser.named_exports = @TypeOf(parser.named_exports).init(allocator); + parser.top_level_symbol_to_parts = @TypeOf(parser.top_level_symbol_to_parts).init(allocator); + parser.import_namespace_cc_map = @TypeOf(parser.import_namespace_cc_map).init(allocator); + parser.scopes_in_order = @TypeOf(parser.scopes_in_order).init(allocator); + parser.temp_refs_to_declare = @TypeOf(parser.temp_refs_to_declare).init(allocator); + parser.relocated_top_level_vars = @TypeOf(parser.relocated_top_level_vars).init(allocator); + parser.log = log; + parser.is_import_item = @TypeOf(parser.is_import_item).init(allocator); + parser.allocator = allocator; + parser.runtime_imports = StringRefMap.init(allocator); + parser.options = opts; + parser.to_expr_wrapper_namespace = Binding2ExprWrapper.Namespace.init(parser); + parser.to_expr_wrapper_hoisted = Binding2ExprWrapper.Hoisted.init(parser); + parser.source = source; + parser.import_transposer = @TypeOf(parser.import_transposer).init(parser); + parser.require_transposer = @TypeOf(parser.require_transposer).init(parser); + parser.require_resolve_transposer = @TypeOf(parser.require_resolve_transposer).init(parser); + parser.lexer = lexer; + parser.data = js_ast.AstData.init(allocator); + + _ = try parser.pushScopeForParsePass(.entry, locModuleScope); + + return parser; + } +}; + +// The "await" and "yield" expressions are never allowed in argument lists but +// may or may not be allowed otherwise depending on the details of the enclosing +// function or module. This needs to be handled when parsing an arrow function +// argument list because we don't know if these expressions are not allowed until +// we reach the "=>" token (or discover the absence of one). +// +// Specifically, for await: +// +// // This is ok +// async function foo() { (x = await y) } +// +// // This is an error +// async function foo() { (x = await y) => {} } +// +// And for yield: +// +// // This is ok +// function* foo() { (x = yield y) } +// +// // This is an error +// function* foo() { (x = yield y) => {} } +// +const DeferredArrowArgErrors = struct { + invalid_expr_await: logger.Range = logger.Range.None, + invalid_expr_yield: logger.Range = logger.Range.None, +}; diff --git a/src/js_parser/js_parser_test.zig b/src/js_parser/js_parser_test.zig new file mode 100644 index 000000000..e1b44c62b --- /dev/null +++ b/src/js_parser/js_parser_test.zig @@ -0,0 +1,499 @@ +usingnamespace @import("./imports.zig"); +usingnamespace @import("./js_parser.zig"); + +usingnamespace @import("../linker.zig"); + +const SymbolList = [][]Symbol; + +// const Tester = struct { +// allocator: *std.mem.Allocator, + +// pub const Expectation = struct { +// target: anytype, + +// pub fn report(writer: anytype) void {} + +// pub const Outcome = enum { +// pending, +// pass, +// fail, +// }; + +// const Normalized = struct { +// value: NormalizedValue, +// optional: bool = false, +// pointer: bool = false, +// array_like: bool = false, + +// const NormalizedValue = union(enum) { +// Struct: anytype, +// Number: f64, +// String: anytype, +// }; + +// pub fn parse_valuetype(value: anytype, was_optional: bool, was_pointer: bool, was_arraylike: bool, original_value: anytype) Normalized { +// switch (@typeInfo(@TypeOf(value))) { +// .Pointer => |info| { +// return switch (info.size) { +// .One => { +// return parse_valuetype(value.*, was_optional, true, was_arraylike, original_value); +// }, +// .Many, .C => @compileError("Not supported."), +// .Slice => |slice| { +// return parse_valuetype(value.ptr.*, was_optional, true, true, original_value); +// }, +// }; +// }, +// .Enum => |info| { +// return parse_valuetype(@enumToInt(value), was_optional, was_pointer, was_arraylike, original_value); +// }, +// .Struct => |info| { +// return Normalized{ +// .value = NormalizedValue{ +// .Struct = original_value, +// }, +// .optional = was_optional, +// .pointer = was_pointer, +// .array_like = was_arraylike, +// }; +// }, +// .Int => |info| { +// if (std.meta.bitCount(@TypeOf(value)) == 8) +// return Normalized{ +// .value = NormalizedValue{ +// .Number = @intToFloat(f64, value), +// }, +// .optional = was_optional, +// .pointer = was_pointer, +// .array_like = was_arraylike, +// }; +// }, +// .ComptimeInt => { +// return Normalized{ +// .value = NormalizedValue{ +// .Number = @intToFloat(f64, value), +// }, +// .optional = was_optional, +// .pointer = was_pointer, +// .array_like = was_arraylike, +// }; +// }, +// .Float => |info| { +// return Normalized{ +// .value = NormalizedValue{ +// .Number = @floatCast(f64, value), +// }, +// .optional = was_optional, +// .pointer = was_pointer, +// .array_like = was_arraylike, +// }; +// }, +// } +// } + +// pub fn init(value: anytype) Normalized { +// return Normalized.parse_valuetype(value, false, false, false, value); +// } +// }; + +// fn equals(a: anytype) bool { +// const T = @TypeOf(a); +// const a_info = @typeInfo(T); +// const Tb = @TypeOf(b); +// const b_info = @typeInfo(Tb); + +// const a_final = a_getter: {}; + +// switch (@typeInfo(T)) { +// .Struct => |info| { +// inline for (info.fields) |field_info| { +// if (!eql(@field(a, field_info.name), @field(b, field_info.name))) return false; +// } +// return true; +// }, +// .ErrorUnion => { +// if (a) |a_p| { +// if (b) |b_p| return eql(a_p, b_p) else |_| return false; +// } else |a_e| { +// if (b) |_| return false else |b_e| return a_e == b_e; +// } +// }, +// .Union => |info| { +// if (info.tag_type) |UnionTag| { +// const tag_a = activeTag(a); +// const tag_b = activeTag(b); +// if (tag_a != tag_b) return false; + +// inline for (info.fields) |field_info| { +// if (@field(UnionTag, field_info.name) == tag_a) { +// return eql(@field(a, field_info.name), @field(b, field_info.name)); +// } +// } +// return false; +// } + +// @compileError("cannot compare untagged union type " ++ @typeName(T)); +// }, +// .Array => { +// if (a.len != b.len) return false; +// for (a) |e, i| +// if (!eql(e, b[i])) return false; +// return true; +// }, +// .Vector => |info| { +// var i: usize = 0; +// while (i < info.len) : (i += 1) { +// if (!eql(a[i], b[i])) return false; +// } +// return true; +// }, +// .Pointer => |info| { +// return switch (info.size) { +// .One, .Many, .C => a == b, +// .Slice => |slice| { +// if (a.len != b.len) { +// return false; +// } +// for (a) |e, i| +// if (!eql(e, b[i])) return false; +// }, +// }; +// }, +// .Optional => { +// return eql(a.?, b.?); +// }, +// else => return a == b, +// } +// } + +// pub fn toBe(value: anytype) *Expectation {} +// }; + +// pub fn expect(outcome: anytype) Expectation {} +// pub fn init(allocator: *std.mem.Allocator) Tester { +// return Tester{ .allocator = allocator }; +// } +// }; + +const RED = "\x1b[31;1m"; +const GREEN = "\x1b[32;1m"; +const CYAN = "\x1b[36;1m"; +const WHITE = "\x1b[37;1m"; +const DIM = "\x1b[2m"; +const RESET = "\x1b[0m"; + +pub const Tester = struct { + pass: std.ArrayList(Expectation), + fail: std.ArrayList(Expectation), + allocator: *std.mem.Allocator, + + pub fn t(allocator: *std.mem.Allocator) Tester { + return Tester{ + .allocator = allocator, + .pass = std.ArrayList(Expectation).init(allocator), + .fail = std.ArrayList(Expectation).init(allocator), + }; + } + + pub const Expectation = struct { + expected: string, + result: string, + source: std.builtin.SourceLocation, + + pub fn init(expected: string, result: string, src: std.builtin.SourceLocation) Expectation { + return Expectation{ + .expected = expected, + .result = result, + .source = src, + }; + } + const PADDING = 0; + pub fn print(self: *const @This()) void { + var pad = &([_]u8{' '} ** PADDING); + var stderr = std.io.getStdErr(); + + stderr.writeAll(RESET) catch unreachable; + stderr.writeAll(pad) catch unreachable; + stderr.writeAll(DIM) catch unreachable; + std.fmt.format(stderr.writer(), "{s}:{d}:{d}", .{ self.source.file, self.source.line, self.source.column }) catch unreachable; + stderr.writeAll(RESET) catch unreachable; + stderr.writeAll("\n") catch unreachable; + + stderr.writeAll(pad) catch unreachable; + stderr.writeAll("Expected: ") catch unreachable; + stderr.writeAll(RESET) catch unreachable; + stderr.writeAll(GREEN) catch unreachable; + std.fmt.format(stderr.writer(), "\"{s}\"", .{self.expected}) catch unreachable; + stderr.writeAll(GREEN) catch unreachable; + stderr.writeAll(RESET) catch unreachable; + + stderr.writeAll("\n") catch unreachable; + stderr.writeAll(pad) catch unreachable; + stderr.writeAll("Received: ") catch unreachable; + stderr.writeAll(RESET) catch unreachable; + stderr.writeAll(RED) catch unreachable; + std.fmt.format(stderr.writer(), "\"{s}\"", .{self.result}) catch unreachable; + stderr.writeAll(RED) catch unreachable; + stderr.writeAll(RESET) catch unreachable; + stderr.writeAll("\n") catch unreachable; + } + + pub fn evaluate_outcome(self: *const @This()) Outcome { + for (self.expected) |char, i| { + if (char != self.result[i]) { + return Outcome.fail; + } + } + + return Outcome.pass; + } + }; + + pub const Outcome = enum { + pass, + fail, + }; + pub fn expect(tester: *Tester, expected: string, result: string, src: std.builtin.SourceLocation) callconv(.Inline) bool { + var expectation = Expectation.init(expected, result, src); + switch (expectation.evaluate_outcome()) { + .pass => { + tester.pass.append(expectation) catch unreachable; + return true; + }, + .fail => { + tester.fail.append(expectation) catch unreachable; + return false; + }, + } + } + + const ReportType = enum { + none, + pass, + fail, + some_fail, + + pub fn init(tester: *Tester) ReportType { + if (tester.fail.items.len == 0 and tester.pass.items.len == 0) { + return .none; + } else if (tester.fail.items.len == 0) { + return .pass; + } else if (tester.pass.items.len == 0) { + return .fail; + } else { + return .some_fail; + } + } + }; + + pub fn report(tester: *Tester, src: std.builtin.SourceLocation) void { + var stderr = std.io.getStdErr(); + + if (tester.fail.items.len > 0) { + std.fmt.format(stderr.writer(), "\n\n", .{}) catch unreachable; + } + + for (tester.fail.items) |item| { + item.print(); + std.fmt.format(stderr.writer(), "\n", .{}) catch unreachable; + } + + switch (ReportType.init(tester)) { + .none => { + std.log.info("No expectations.\n\n", .{}); + }, + .pass => { + std.fmt.format(stderr.writer(), "{s}All {d} expectations passed.{s}\n", .{ GREEN, tester.pass.items.len, GREEN }) catch unreachable; + std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable; + std.testing.expect(true); + }, + .fail => { + std.fmt.format(stderr.writer(), "{s}All {d} expectations failed.{s}\n\n", .{ RED, tester.fail.items.len, RED }) catch unreachable; + std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable; + std.testing.expect(false); + }, + .some_fail => { + std.fmt.format(stderr.writer(), "{s}{d} failed{s} and {s}{d} passed{s} of {d} expectations{s}\n\n", .{ + RED, + tester.fail.items.len, + RED ++ RESET, + GREEN, + tester.pass.items.len, + GREEN ++ RESET, + tester.fail.items.len + tester.pass.items.len, + RESET, + }) catch unreachable; + std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable; + std.testing.expect(false); + }, + } + } +}; + +fn expectPrinted(t: *Tester, contents: string, expected: string, src: anytype) !void { + if (alloc.dynamic_manager == null) { + try alloc.setup(std.heap.page_allocator); + } + + debugl("INIT TEST"); + + const opts = try options.TransformOptions.initUncached(alloc.dynamic, "file.js", contents); + var log = logger.Log.init(alloc.dynamic); + var source = logger.Source.initFile(opts.entry_point, alloc.dynamic); + var ast: js_ast.Ast = undefined; + + var define = try Define.init(alloc.dynamic, null); + debugl("INIT PARSER"); + var parser = try Parser.init(opts, &log, &source, define, alloc.dynamic); + debugl("RUN PARSER"); + + var res = try parser.parse(); + ast = res.ast; + var symbols: SymbolList = &([_][]Symbol{ast.symbols}); + var symbol_map = js_ast.Symbol.Map.initList(symbols); + + if (log.msgs.items.len > 0) { + debugl("PRINT LOG ERRORS"); + var fixedBuffer = [_]u8{0} ** 4096; + var stream = std.io.fixedBufferStream(&fixedBuffer); + + try log.print(stream.writer()); + std.debug.print("{s}", .{fixedBuffer}); + } + var linker = Linker{}; + debugl("START AST PRINT"); + + if (PRINT_AST) { + var fixed_buffer = [_]u8{0} ** 512000; + var buf_stream = std.io.fixedBufferStream(&fixed_buffer); + try ast.toJSON(alloc.dynamic, std.io.getStdErr().writer()); + } + + const result = js_printer.printAst(alloc.dynamic, ast, symbol_map, true, js_printer.Options{ .to_module_ref = res.ast.module_ref orelse Ref{ .inner_index = 0 } }, &linker) catch unreachable; + var copied = try std.mem.dupe(alloc.dynamic, u8, result.js); + _ = t.expect(contents, copied, src); + // std.testing.expectEqualStrings(contents, copied); +} + +const PRINT_AST = false; + +test "expectPrint" { + var t_ = Tester.t(std.heap.page_allocator); + var t = &t_; + + try expectPrinted(t, "(-x) ** 2", "(-x) ** 2;\n", @src()); + try expectPrinted(t, "(+x) ** 2", "(+x) ** 2;\n", @src()); + try expectPrinted(t, "(~x) ** 2", "(~x) ** 2;\n", @src()); + try expectPrinted(t, "(!x) ** 2", "(!x) ** 2;\n", @src()); + try expectPrinted(t, "(-1) ** 2", "(-1) ** 2;\n", @src()); + try expectPrinted(t, "(+1) ** 2", "1 ** 2;\n", @src()); + try expectPrinted(t, "(~1) ** 2", "(~1) ** 2;\n", @src()); + try expectPrinted(t, "(!1) ** 2", "false ** 2;\n", @src()); + try expectPrinted(t, "(void x) ** 2", "(void x) ** 2;\n", @src()); + try expectPrinted(t, "(delete x) ** 2", "(delete x) ** 2;\n", @src()); + try expectPrinted(t, "(typeof x) ** 2", "(typeof x) ** 2;\n", @src()); + try expectPrinted(t, "undefined ** 2", "(void 0) ** 2;\n", @src()); + + try expectPrinted(t, "class Foo { foo() {} }", "class Foo {\n foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { *foo() {} }", "class Foo {\n *foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { get foo() {} }", "class Foo {\n get foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { set foo(x) {} }", "class Foo {\n set foo(x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static foo() {} }", "class Foo {\n static foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static *foo() {} }", "class Foo {\n static *foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static get foo() {} }", "class Foo {\n static get foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static set foo(x) {} }", "class Foo {\n static set foo(x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { async foo() {} }", "class Foo {\n async foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static async foo() {} }", "class Foo {\n static async foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static async *foo() {} }", "class Foo {\n static async *foo() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static async *foo() {}\n hey = true; }", "class Foo {\n static async *foo() {\n }\n hey = true;\n}\n", @src()); + + try expectPrinted(t, "class Foo { if() {} }", "class Foo {\n if() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { *if() {} }", "class Foo {\n *if() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { get if() {} }", "class Foo {\n get if() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { set if(x) {} }", "class Foo {\n set if(x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static if() {} }", "class Foo {\n static if() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static *if() {} }", "class Foo {\n static *if() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static get if() {} }", "class Foo {\n static get if() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static set if(x) {} }", "class Foo {\n static set if(x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { async if() {} }", "class Foo {\n async if() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static async if() {} }", "class Foo {\n static async if() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static async *if() {} }", "class Foo {\n static async *if() {\n }\n}\n", @src()); + + try expectPrinted(t, "class Foo { a() {} b() {} }", "class Foo {\n a() {\n }\n b() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} get b() {} }", "class Foo {\n a() {\n }\n get b() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} set b(x) {} }", "class Foo {\n a() {\n }\n set b(x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} static b() {} }", "class Foo {\n a() {\n }\n static b() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} static *b() {} }", "class Foo {\n a() {\n }\n static *b() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} static get b() {} }", "class Foo {\n a() {\n }\n static get b() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} static set b(x) {} }", "class Foo {\n a() {\n }\n static set b(x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} async b() {} }", "class Foo {\n a() {\n }\n async b() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} static async b() {} }", "class Foo {\n a() {\n }\n static async b() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { a() {} static async *b() {} }", "class Foo {\n a() {\n }\n static async *b() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { [arguments] }", "class Foo {\n [arguments];\n}\n", @src()); + try expectPrinted(t, "class Foo { [arguments] = 1 }", "class Foo {\n [arguments] = 1;\n}\n", @src()); + try expectPrinted(t, "class Foo { arguments = 1 }", "class Foo {\n arguments = 1;\n}\n", @src()); + try expectPrinted(t, "class Foo { x = class { arguments = 1 } }", "class Foo {\n x = class {\n arguments = 1;\n };\n}\n", @src()); + try expectPrinted(t, "class Foo { x = function() { arguments } }", "class Foo {\n x = function() {\n arguments;\n };\n}\n", @src()); + try expectPrinted(t, "class Foo { get ['constructor']() {} }", "class Foo {\n get [\"constructor\"]() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { set ['constructor'](x) {} }", "class Foo {\n set [\"constructor\"](x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { *['constructor']() {} }", "class Foo {\n *[\"constructor\"]() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { async ['constructor']() {} }", "class Foo {\n async [\"constructor\"]() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { async *['constructor']() {} }", "class Foo {\n async *[\"constructor\"]() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { get prototype() {} }", "class Foo {\n get prototype() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { get 'prototype'() {} }", "class Foo {\n get prototype() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { set prototype(x) {} }", "class Foo {\n set prototype(x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { set 'prototype'(x) {} }", "class Foo {\n set prototype(x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { *prototype() {} }", "class Foo {\n *prototype() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { *'prototype'() {} }", "class Foo {\n *prototype() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { async prototype() {} }", "class Foo {\n async prototype() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { async 'prototype'() {} }", "class Foo {\n async prototype() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { async *prototype() {} }", "class Foo {\n async *prototype() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { async *'prototype'() {} }", "class Foo {\n async *prototype() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static get ['prototype']() {} }", "class Foo {\n static get [\"prototype\"]() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static set ['prototype'](x) {} }", "class Foo {\n static set [\"prototype\"](x) {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static *['prototype']() {} }", "class Foo {\n static *[\"prototype\"]() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static async ['prototype']() {} }", "class Foo {\n static async [\"prototype\"]() {\n }\n}\n", @src()); + try expectPrinted(t, "class Foo { static async *['prototype']() {} }", "class Foo {\n static async *[\"prototype\"]() {\n }\n}\n", @src()); + try expectPrinted(t, "({ prototype: 1 })", "({prototype: 1});\n", @src()); + try expectPrinted(t, "({ get prototype() {} })", "({get prototype() {\n}});\n", @src()); + try expectPrinted(t, "({ set prototype(x) {} })", "({set prototype(x) {\n}});\n", @src()); + try expectPrinted(t, "({ *prototype() {} })", "({*prototype() {\n}});\n", @src()); + try expectPrinted(t, "({ async prototype() {} })", "({async prototype() {\n}});\n", @src()); + try expectPrinted(t, "({ async* prototype() {} })", "({async *prototype() {\n}});\n", @src()); + // try expectPrinted(t, "class Foo extends Bar { constructor() { super() } }", "class Foo extends Bar {\n constructor() {\n super();\n }\n}\n", @src()); + // try expectPrinted(t, "class Foo extends Bar { constructor() { () => super() } }", "class Foo extends Bar {\n constructor() {\n () => super();\n }\n}\n", @src()); + // try expectPrinted(t, "class Foo extends Bar { constructor() { () => { super() } } }", "class Foo extends Bar {\n constructor() {\n () => {\n super();\n };\n }\n}\n", @src()); + // try expectPrinted(t, "class Foo extends Bar { constructor(x = super()) {} }", "class Foo extends Bar {\n constructor(x = super()) {\n }\n}\n", @src()); + // try expectPrinted(t, "class Foo extends Bar { constructor(x = () => super()) {} }", "class Foo extends Bar {\n constructor(x = () => super()) {\n }\n}\n", @src()); + // try expectPrinted(t, "({foo})", "({foo});\n", @src()); + // try expectPrinted(t, "({foo:0})", "({foo: 0});\n", @src()); + // try expectPrinted(t, "({1e9:0})", "({1e9: 0});\n", @src()); + // try expectPrinted(t, "({1_2_3n:0})", "({123n: 0});\n", @src()); + // try expectPrinted(t, "({0x1_2_3n:0})", "({0x123n: 0});\n", @src()); + // try expectPrinted(t, "({foo() {}})", "({foo() {\n}});\n", @src()); + // try expectPrinted(t, "({*foo() {}})", "({*foo() {\n}});\n", @src()); + // try expectPrinted(t, "({get foo() {}})", "({get foo() {\n}});\n", @src()); + // try expectPrinted(t, "({set foo(x) {}})", "({set foo(x) {\n}});\n", @src()); + + // try expectPrinted(t, "({if:0})", "({if: 0});\n", @src()); + // try expectPrinted(t, "({if() {}})", "({if() {\n}});\n", @src()); + // try expectPrinted(t, "({*if() {}})", "({*if() {\n}});\n", @src()); + // try expectPrinted(t, "({get if() {}})", "({get if() {\n}});\n", @src()); + // try expectPrinted(t, "({set if(x) {}})", "({set if(x) {\n}});\n", @src()); + + // try expectPrinted(t, "await x", "await x;\n", @src()); + // try expectPrinted(t, "await +x", "await +x;\n", @src()); + // try expectPrinted(t, "await -x", "await -x;\n", @src()); + // try expectPrinted(t, "await ~x", "await ~x;\n", @src()); + // try expectPrinted(t, "await !x", "await !x;\n", @src()); + // try expectPrinted(t, "await --x", "await --x;\n", @src()); + // try expectPrinted(t, "await ++x", "await ++x;\n", @src()); + // try expectPrinted(t, "await x--", "await x--;\n", @src()); + // try expectPrinted(t, "await x++", "await x++;\n", @src()); + // try expectPrinted(t, "await void x", "await void x;\n", @src()); + // try expectPrinted(t, "await typeof x", "await typeof x;\n", @src()); + // try expectPrinted(t, "await (x * y)", "await (x * y);\n", @src()); + // try expectPrinted(t, "await (x ** y)", "await (x ** y);\n", @src()); + + t.report(@src()); +} |