/// ** IMPORTANT ** /// ** When making changes to the JavaScript Parser that impact runtime behavior or fix bugs ** /// ** you must also increment the `expected_version` in RuntimeTranspilerCache.zig ** /// ** IMPORTANT ** pub const std = @import("std"); const bun = @import("bun"); pub const logger = bun.logger; pub const js_lexer = bun.js_lexer; pub const importRecord = @import("./import_record.zig"); pub const js_ast = bun.JSAst; pub const options = @import("./options.zig"); pub const js_printer = bun.js_printer; pub const renamer = @import("./renamer.zig"); const _runtime = @import("./runtime.zig"); pub const RuntimeImports = _runtime.Runtime.Imports; pub const RuntimeFeatures = _runtime.Runtime.Features; pub const RuntimeNames = _runtime.Runtime.Names; pub const fs = @import("./fs.zig"); const string = bun.string; const Output = bun.Output; const Environment = bun.Environment; const strings = bun.strings; const default_allocator = bun.default_allocator; const G = js_ast.G; const Define = @import("./defines.zig").Define; const DefineData = @import("./defines.zig").DefineData; const FeatureFlags = @import("./feature_flags.zig"); pub const isPackagePath = @import("./resolver/resolver.zig").isPackagePath; pub const ImportKind = importRecord.ImportKind; pub const BindingNodeIndex = js_ast.BindingNodeIndex; const Decl = G.Decl; const Property = G.Property; const Arg = G.Arg; const Allocator = std.mem.Allocator; 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; const DeclaredSymbol = js_ast.DeclaredSymbol; const JSC = bun.JSC; const Index = @import("./ast/base.zig").Index; fn _disabledAssert(_: bool) void { if (!Environment.allow_assert) @compileError("assert is missing an if (Environment.allow_assert)"); unreachable; } const assert = if (Environment.allow_assert) bun.assert else _disabledAssert; const debug = Output.scoped(.JSParser, false); const ExprListLoc = struct { list: ExprNodeList, loc: logger.Loc, }; pub const LocRef = js_ast.LocRef; pub const S = js_ast.S; pub const B = js_ast.B; 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{ .start = -100 }; const Ref = @import("./ast/base.zig").Ref; pub const StringHashMap = bun.StringHashMap; pub const AutoHashMap = std.AutoHashMap; const StringHashMapUnmanaged = bun.StringHashMapUnmanaged; const ObjectPool = @import("./pool.zig").ObjectPool; const DeferredImportNamespace = struct { namespace: LocRef, import_record_id: u32, }; const SkipTypeParameterResult = enum { did_not_skip_anything, could_be_type_cast, definitely_type_parameters, }; const TypeParameterFlag = packed struct(u8) { /// TypeScript 4.7 allow_in_out_variance_annotations: bool = false, /// TypeScript 5.0 allow_const_modifier: bool = false, /// Allow "<>" without any type parameters allow_empty_type_parameters: bool = false, _: u5 = 0, }; const JSXImport = enum { jsx, jsxDEV, jsxs, Fragment, createElement, pub const Symbols = struct { jsx: ?LocRef = null, jsxDEV: ?LocRef = null, jsxs: ?LocRef = null, Fragment: ?LocRef = null, createElement: ?LocRef = null, pub fn get(noalias this: *const Symbols, name: []const u8) ?Ref { if (strings.eqlComptime(name, "jsx")) return if (this.jsx) |jsx| jsx.ref.? else null; if (strings.eqlComptime(name, "jsxDEV")) return if (this.jsxDEV) |jsx| jsx.ref.? else null; if (strings.eqlComptime(name, "jsxs")) return if (this.jsxs) |jsxs| jsxs.ref.? else null; if (strings.eqlComptime(name, "Fragment")) return if (this.Fragment) |Fragment| Fragment.ref.? else null; if (strings.eqlComptime(name, "createElement")) return if (this.createElement) |createElement| createElement.ref.? else null; return null; } pub fn getWithTag(noalias this: *const Symbols, tag: JSXImport) ?Ref { return switch (tag) { .jsx => if (this.jsx) |jsx| jsx.ref.? else null, .jsxDEV => if (this.jsxDEV) |jsx| jsx.ref.? else null, .jsxs => if (this.jsxs) |jsxs| jsxs.ref.? else null, .Fragment => if (this.Fragment) |Fragment| Fragment.ref.? else null, .createElement => if (this.createElement) |createElement| createElement.ref.? else null, }; } pub fn runtimeImportNames(noalias this: *const Symbols, buf: *[3]string) []const string { var i: usize = 0; if (this.jsxDEV != null) { bun.assert(this.jsx == null); // we should never end up with this in the same file buf[0] = "jsxDEV"; i += 1; } if (this.jsx != null) { bun.assert(this.jsxDEV == null); // we should never end up with this in the same file buf[0] = "jsx"; i += 1; } if (this.jsxs != null) { buf[i] = "jsxs"; i += 1; } if (this.Fragment != null) { buf[i] = "Fragment"; i += 1; } return buf[0..i]; } pub fn sourceImportNames(noalias this: *const Symbols) []const string { return if (this.createElement != null) &[_]string{"createElement"} else &[_]string{}; } }; }; const arguments_str: string = "arguments"; // Dear reader, // There are some things you should know about this file to make it easier for humans to read // "P" is the internal parts of the parser // "p.e" allocates a new Expr // "p.b" allocates a new Binding // "p.s" allocates a new Stmt // We do it this way so if we want to refactor how these are allocated in the future, we only have to modify one function to change it everywhere // Everything in JavaScript is either an Expression, a Binding, or a Statement. // Expression: foo(1) // Statement: let a = 1; // Binding: a // While the names for Expr, Binding, and Stmt are directly copied from esbuild, those were likely inspired by Go's parser. // which is another example of a very fast parser. const ScopeOrderList = std.ArrayListUnmanaged(?ScopeOrder); // kept as a static reference const exports_string_name: string = "exports"; const MacroRefData = struct { import_record_id: u32, // if name is null the macro is imported as a namespace import // import * as macros from "./macros.js" with {type: "macro"}; name: ?string = null, }; const MacroRefs = std.AutoArrayHashMap(Ref, MacroRefData); const Substitution = union(enum) { success: Expr, failure: Expr, continue_: Expr, }; /// Concatenate two `E.String`s, mutating BOTH inputs /// unless `has_inlined_enum_poison` is set. /// /// Currently inlined enum poison refers to where mutation would cause output /// bugs due to inlined enum values sharing `E.String`s. If a new use case /// besides inlined enums comes up to set this to true, please rename the /// variable and document it. fn joinStrings(left: *const E.String, right: *const E.String, has_inlined_enum_poison: bool) E.String { var new = if (has_inlined_enum_poison) // Inlined enums can be shared by multiple call sites. In // this case, we need to ensure that the ENTIRE rope is // cloned. In other situations, the lhs doesn't have any // other owner, so it is fine to mutate `lhs.data.end.next`. // // Consider the following case: // const enum A { // B = "a" + "b", // D = B + "d", // }; // console.log(A.B, A.D); left.cloneRopeNodes() else left.*; // Similarly, the right side has to be cloned for an enum rope too. // // Consider the following case: // const enum A { // B = "1" + "2", // C = ("3" + B) + "4", // }; // console.log(A.B, A.C); const rhs_clone = Expr.Data.Store.append(E.String, if (has_inlined_enum_poison) right.cloneRopeNodes() else right.*); new.push(rhs_clone); new.prefer_template = new.prefer_template or rhs_clone.prefer_template; return new; } /// Transforming the left operand into a string is not safe if it comes from a /// nested AST node. const FoldStringAdditionKind = enum { // "x" + "y" -> "xy" // 1 + "y" -> "1y" normal, // a + "x" + "y" -> a + "xy" // a + 1 + "y" -> a + 1 + y nested_left, }; // NOTE: unlike esbuild's js_ast_helpers.FoldStringAddition, this does mutate // the input AST in the case of rope strings fn foldStringAddition(l: Expr, r: Expr, allocator: std.mem.Allocator, kind: FoldStringAdditionKind) ?Expr { // "See through" inline enum constants // TODO: implement foldAdditionPreProcess to fold some more things :) var lhs = l.unwrapInlined(); var rhs = r.unwrapInlined(); if (kind != .nested_left) { // See comment on `FoldStringAdditionKind` for examples switch (rhs.data) { .e_string, .e_template => { if (lhs.toStringExprWithoutSideEffects(allocator)) |str| { lhs = str; } }, else => {}, } } switch (lhs.data) { .e_string => |left| { if (rhs.toStringExprWithoutSideEffects(allocator)) |str| { rhs = str; } if (left.isUTF8()) { switch (rhs.data) { // "bar" + "baz" => "barbaz" .e_string => |right| { if (right.isUTF8()) { const has_inlined_enum_poison = l.data == .e_inlined_enum or r.data == .e_inlined_enum; return Expr.init(E.String, joinStrings( left, right, has_inlined_enum_poison, ), lhs.loc); } }, // "bar" + `baz${bar}` => `barbaz${bar}` .e_template => |right| { if (right.head.isUTF8()) { return Expr.init(E.Template, E.Template{ .parts = right.parts, .head = .{ .cooked = joinStrings( left, &right.head.cooked, l.data == .e_inlined_enum, ) }, }, l.loc); } }, else => { // other constant-foldable ast nodes would have been converted to .e_string }, } // "'x' + `y${z}`" => "`xy${z}`" if (rhs.data == .e_template and rhs.data.e_template.tag == null) {} } if (left.len() == 0 and rhs.knownPrimitive() == .string) { return rhs; } return null; }, .e_template => |left| { // "`${x}` + 0" => "`${x}` + '0'" if (rhs.toStringExprWithoutSideEffects(allocator)) |str| { rhs = str; } if (left.tag == null) { switch (rhs.data) { // `foo${bar}` + "baz" => `foo${bar}baz` .e_string => |right| { if (right.isUTF8()) { // Mutation of this node is fine because it will be not // be shared by other places. Note that e_template will // be treated by enums as strings, but will not be // inlined unless they could be converted into // .e_string. if (left.parts.len > 0) { const i = left.parts.len - 1; const last = left.parts[i]; if (last.tail.isUTF8()) { left.parts[i].tail = .{ .cooked = joinStrings( &last.tail.cooked, right, r.data == .e_inlined_enum, ) }; return lhs; } } else { if (left.head.isUTF8()) { left.head = .{ .cooked = joinStrings( &left.head.cooked, right, r.data == .e_inlined_enum, ) }; return lhs; } } } }, // `foo${bar}` + `a${hi}b` => `foo${bar}a${hi}b` .e_template => |right| { if (right.tag == null and right.head.isUTF8()) { if (left.parts.len > 0) { const i = left.parts.len - 1; const last = left.parts[i]; if (last.tail.isUTF8() and right.head.isUTF8()) { left.parts[i].tail = .{ .cooked = joinStrings( &last.tail.cooked, &right.head.cooked, r.data == .e_inlined_enum, ) }; left.parts = if (right.parts.len == 0) left.parts else std.mem.concat( allocator, E.TemplatePart, &.{ left.parts, right.parts }, ) catch bun.outOfMemory(); return lhs; } } else { if (left.head.isUTF8() and right.head.isUTF8()) { left.head = .{ .cooked = joinStrings( &left.head.cooked, &right.head.cooked, r.data == .e_inlined_enum, ) }; left.parts = right.parts; return lhs; } } } }, else => { // other constant-foldable ast nodes would have been converted to .e_string }, } } }, else => { // other constant-foldable ast nodes would have been converted to .e_string }, } if (rhs.data.as(.e_string)) |right| { if (right.len() == 0 and lhs.knownPrimitive() == .string) { return lhs; } } return null; } // If we are currently in a hoisted child of the module scope, relocate these // declarations to the top level and return an equivalent assignment statement. // Make sure to check that the declaration kind is "var" before calling this. // And make sure to check that the returned statement is not the zero value. // // This is done to make some transformations non-destructive // Without relocating vars to the top level, simplifying this: // if (false) var foo = 1; // to nothing is unsafe // Because "foo" was defined. And now it's not. pub const RelocateVars = struct { pub const Mode = enum { normal, for_in_or_for_of }; stmt: ?Stmt = null, ok: bool = false, }; 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 ExpressionTransposer( comptime ContextType: type, comptime StateType: type, comptime visitor: fn (noalias ptr: *ContextType, arg: Expr, state: StateType) Expr, ) type { return struct { pub const Context = ContextType; pub const This = @This(); context: *Context, pub fn init(c: *Context) This { return .{ .context = c }; } pub fn maybeTransposeIf(self: *This, arg: Expr, state: StateType) Expr { switch (arg.data) { .e_if => |ex| { return Expr.init(E.If, .{ .yes = self.maybeTransposeIf(ex.yes, state), .no = self.maybeTransposeIf(ex.no, state), .test_ = ex.test_, }, arg.loc); }, else => { return visitor(self.context, arg, state); }, } } pub fn transposeKnownToBeIf(self: *This, arg: Expr, state: StateType) Expr { return Expr.init(E.If, .{ .yes = self.maybeTransposeIf(arg.data.e_if.yes, state), .no = self.maybeTransposeIf(arg.data.e_if.no, state), .test_ = arg.data.e_if.test_, }, arg.loc); } }; } 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; } } const TransposeState = struct { is_await_target: bool = false, is_then_catch_target: bool = false, is_require_immediately_assigned_to_decl: bool = false, loc: logger.Loc = logger.Loc.Empty, import_record_tag: ?ImportRecord.Tag = null, import_loader: ?bun.options.Loader = null, import_options: Expr = Expr.empty, }; const JSXTag = struct { pub const TagType = enum { fragment, tag }; pub const Data = union(TagType) { fragment: u8, tag: Expr, pub fn asExpr(d: *const Data) ?ExprNodeIndex { switch (d.*) { .tag => |tag| { return tag; }, else => { return null; }, } } }; data: Data, range: logger.Range, /// Empty string for fragments. name: string, pub fn parse(comptime P: type, p: *P) anyerror!JSXTag { const loc = p.lexer.loc(); // A missing tag is a fragment if (p.lexer.token == .t_greater_than) { return JSXTag{ .range = logger.Range{ .loc = loc, .len = 0 }, .data = Data{ .fragment = 1 }, .name = "", }; } // The tag is an identifier var name = p.lexer.identifier; var tag_range = p.lexer.range(); try p.lexer.expectInsideJSXElementWithName(.t_identifier, "JSX element name"); // Certain identifiers are strings //
= 'a' and name[0] <= 'z')) { return JSXTag{ .data = Data{ .tag = p.newExpr(E.String{ .data = name, }, loc) }, .range = tag_range, .name = name, }; } // Otherwise, this is an identifier //