Files
bun.sh/src/ast/E.zig
taylor.fish edf13bd91d Refactor BabyList (#22502)
(For internal tracking: fixes STAB-1129, STAB-1145, STAB-1146,
STAB-1150, STAB-1126, STAB-1147, STAB-1148, STAB-1149, STAB-1158)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-09 20:41:10 -07:00

1509 lines
53 KiB
Zig

/// This represents an internal property name that can be mangled. The symbol
/// referenced by this expression should be a "SymbolMangledProp" symbol.
pub const NameOfSymbol = struct {
ref: Ref = Ref.None,
/// If true, a preceding comment contains "@__KEY__"
///
/// Currently not used
has_property_key_comment: bool = false,
};
pub const Array = struct {
items: ExprNodeList = ExprNodeList{},
comma_after_spread: ?logger.Loc = null,
is_single_line: bool = false,
is_parenthesized: bool = false,
was_originally_macro: bool = false,
close_bracket_loc: logger.Loc = logger.Loc.Empty,
pub fn push(this: *Array, allocator: std.mem.Allocator, item: Expr) !void {
try this.items.append(allocator, item);
}
pub inline fn slice(this: Array) []Expr {
return this.items.slice();
}
pub fn inlineSpreadOfArrayLiterals(
this: *Array,
allocator: std.mem.Allocator,
estimated_count: usize,
) !ExprNodeList {
var out: bun.BabyList(Expr) = try .initCapacity(
allocator,
// This over-allocates a little but it's fine
estimated_count + @as(usize, this.items.len),
);
out.expandToCapacity();
var remain = out.slice();
for (this.items.slice()) |item| {
switch (item.data) {
.e_spread => |val| {
if (val.value.data == .e_array) {
for (val.value.data.e_array.items.slice()) |inner_item| {
if (inner_item.data == .e_missing) {
remain[0] = Expr.init(E.Undefined, .{}, inner_item.loc);
remain = remain[1..];
} else {
remain[0] = inner_item;
remain = remain[1..];
}
}
// skip empty arrays
// don't include the inlined spread.
continue;
}
// non-arrays are kept in
},
else => {},
}
remain[0] = item;
remain = remain[1..];
}
out.shrinkRetainingCapacity(out.len - remain.len);
return out;
}
pub fn toJS(this: @This(), allocator: std.mem.Allocator, globalObject: *jsc.JSGlobalObject) ToJSError!jsc.JSValue {
const items = this.items.slice();
var array = try jsc.JSValue.createEmptyArray(globalObject, items.len);
array.protect();
defer array.unprotect();
for (items, 0..) |expr, j| {
try array.putIndex(globalObject, @as(u32, @truncate(j)), try expr.data.toJS(allocator, globalObject));
}
return array;
}
/// Assumes each item in the array is a string
pub fn alphabetizeStrings(this: *Array) void {
if (comptime Environment.allow_assert) {
for (this.items.slice()) |item| {
bun.assert(item.data == .e_string);
}
}
std.sort.pdq(Expr, this.items.slice(), {}, Sorter.isLessThan);
}
const Sorter = struct {
pub fn isLessThan(ctx: void, lhs: Expr, rhs: Expr) bool {
return strings.cmpStringsAsc(ctx, lhs.data.e_string.data, rhs.data.e_string.data);
}
};
};
pub const Unary = struct {
op: Op.Code,
value: ExprNodeIndex,
flags: Unary.Flags = .{},
pub const Flags = packed struct(u8) {
/// The expression "typeof (0, x)" must not become "typeof x" if "x"
/// is unbound because that could suppress a ReferenceError from "x".
///
/// Also if we know a typeof operator was originally an identifier, then
/// we know that this typeof operator always has no side effects (even if
/// we consider the identifier by itself to have a side effect).
///
/// Note that there *is* actually a case where "typeof x" can throw an error:
/// when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ
/// checks are not yet handled correctly by Bun, so this possibility is
/// currently ignored.
was_originally_typeof_identifier: bool = false,
/// Similarly the expression "delete (0, x)" must not become "delete x"
/// because that syntax is invalid in strict mode. We also need to make sure
/// we don't accidentally change the return value:
///
/// Returns false:
/// "var a; delete (a)"
/// "var a = Object.freeze({b: 1}); delete (a.b)"
/// "var a = Object.freeze({b: 1}); delete (a?.b)"
/// "var a = Object.freeze({b: 1}); delete (a['b'])"
/// "var a = Object.freeze({b: 1}); delete (a?.['b'])"
///
/// Returns true:
/// "var a; delete (0, a)"
/// "var a = Object.freeze({b: 1}); delete (true && a.b)"
/// "var a = Object.freeze({b: 1}); delete (false || a?.b)"
/// "var a = Object.freeze({b: 1}); delete (null ?? a?.['b'])"
///
/// "var a = Object.freeze({b: 1}); delete (true ? a['b'] : a['b'])"
was_originally_delete_of_identifier_or_property_access: bool = false,
_: u6 = 0,
};
};
pub const Binary = struct {
left: ExprNodeIndex,
right: ExprNodeIndex,
op: Op.Code,
};
pub const Boolean = struct {
value: bool,
pub fn toJS(this: @This(), ctx: *jsc.JSGlobalObject) jsc.C.JSValueRef {
return jsc.C.JSValueMakeBoolean(ctx, this.value);
}
};
pub const Super = struct {};
pub const Null = struct {};
pub const This = struct {};
pub const Undefined = struct {};
pub const New = struct {
target: ExprNodeIndex,
args: ExprNodeList = ExprNodeList{},
// True if there is a comment containing "@__PURE__" or "#__PURE__" preceding
// this call expression. See the comment inside ECall for more details.
can_be_unwrapped_if_unused: CallUnwrap = .never,
close_parens_loc: logger.Loc,
};
pub const NewTarget = struct {
range: logger.Range,
};
pub const ImportMeta = struct {};
pub const ImportMetaMain = struct {
/// If we want to print `!import.meta.main`, set this flag to true
/// instead of wrapping in a unary not. This way, the printer can easily
/// print `require.main != module` instead of `!(require.main == module)`
inverted: bool = false,
};
pub const Special = union(enum) {
/// emits `exports` or `module.exports` depending on `commonjs_named_exports_deoptimized`
module_exports,
/// `import.meta.hot`
hot_enabled,
/// Acts as .e_undefined, but allows property accesses to the rest of the HMR API.
hot_disabled,
/// `import.meta.hot.data` when HMR is enabled. Not reachable when it is disabled.
hot_data,
/// `import.meta.hot.accept` when HMR is enabled. Truthy.
hot_accept,
/// Converted from `hot_accept` to this in js_parser.zig when it is
/// passed strings. Printed as `hmr.hot.acceptSpecifiers`
hot_accept_visited,
/// Prints the resolved specifier string for an import record.
resolved_specifier_string: ImportRecord.Index,
};
pub const Call = struct {
// Node:
target: ExprNodeIndex,
args: ExprNodeList = ExprNodeList{},
optional_chain: ?OptionalChain = null,
is_direct_eval: bool = false,
close_paren_loc: logger.Loc = logger.Loc.Empty,
// True if there is a comment containing "@__PURE__" or "#__PURE__" preceding
// this call expression. This is an annotation used for tree shaking, and
// means that the call can be removed if it's unused. It does not mean the
// call is pure (e.g. it may still return something different if called twice).
//
// Note that the arguments are not considered to be part of the call. If the
// call itself is removed due to this annotation, the arguments must remain
// if they have side effects.
can_be_unwrapped_if_unused: CallUnwrap = .never,
// Used when printing to generate the source prop on the fly
was_jsx_element: bool = false,
pub fn hasSameFlagsAs(a: *Call, b: *Call) bool {
return (a.optional_chain == b.optional_chain and
a.is_direct_eval == b.is_direct_eval and
a.can_be_unwrapped_if_unused == b.can_be_unwrapped_if_unused);
}
};
pub const CallUnwrap = enum(u2) {
never,
if_unused,
if_unused_and_toString_safe,
};
pub const Dot = struct {
// target is Node
target: ExprNodeIndex,
name: string,
name_loc: logger.Loc,
optional_chain: ?OptionalChain = null,
// If true, this property access is known to be free of side-effects. That
// means it can be removed if the resulting value isn't used.
can_be_removed_if_unused: bool = false,
// If true, this property access is a function that, when called, can be
// unwrapped if the resulting value is unused. Unwrapping means discarding
// the call target but keeping any arguments with side effects.
call_can_be_unwrapped_if_unused: CallUnwrap = .never,
pub fn hasSameFlagsAs(a: *Dot, b: *Dot) bool {
return (a.optional_chain == b.optional_chain and
a.is_direct_eval == b.is_direct_eval and
a.can_be_removed_if_unused == b.can_be_removed_if_unused and a.call_can_be_unwrapped_if_unused == b.call_can_be_unwrapped_if_unused);
}
};
pub const Index = struct {
index: ExprNodeIndex,
target: ExprNodeIndex,
optional_chain: ?OptionalChain = null,
pub fn hasSameFlagsAs(a: *E.Index, b: *E.Index) bool {
return (a.optional_chain == b.optional_chain);
}
};
pub const Arrow = struct {
args: []G.Arg = &[_]G.Arg{},
body: G.FnBody,
is_async: bool = false,
has_rest_arg: bool = false,
prefer_expr: bool = false, // Use shorthand if true and "Body" is a single return statement
pub const noop_return_undefined: Arrow = .{
.args = &.{},
.body = .{
.loc = .Empty,
.stmts = &.{},
},
};
};
pub const Function = struct { func: G.Fn };
pub const Identifier = struct {
ref: Ref = Ref.None,
// If we're inside a "with" statement, this identifier may be a property
// access. In that case it would be incorrect to remove this identifier since
// the property access may be a getter or setter with side effects.
must_keep_due_to_with_stmt: bool = false,
// If true, this identifier is known to not have a side effect (i.e. to not
// throw an exception) when referenced. If false, this identifier may or
// not have side effects when referenced. This is used to allow the removal
// of known globals such as "Object" if they aren't used.
can_be_removed_if_unused: bool = false,
// If true, this identifier represents a function that, when called, can be
// unwrapped if the resulting value is unused. Unwrapping means discarding
// the call target but keeping any arguments with side effects.
call_can_be_unwrapped_if_unused: bool = false,
pub inline fn init(ref: Ref) Identifier {
return Identifier{
.ref = ref,
.must_keep_due_to_with_stmt = false,
.can_be_removed_if_unused = false,
.call_can_be_unwrapped_if_unused = false,
};
}
};
/// This is similar to an `Identifier` but it represents a reference to an ES6
/// import item.
///
/// Depending on how the code is linked, the file containing this EImportIdentifier
/// may or may not be in the same module group as the file it was imported from.
///
/// If it's the same module group than we can just merge the import item symbol
/// with the corresponding symbol that was imported, effectively renaming them
/// to be the same thing and statically binding them together.
///
/// But if it's a different module group, then the import must be dynamically
/// evaluated using a property access off the corresponding namespace symbol,
/// which represents the result of a require() call.
///
/// It's stored as a separate type so it's not easy to confuse with a plain
/// identifier. For example, it'd be bad if code trying to convert "{x: x}" into
/// "{x}" shorthand syntax wasn't aware that the "x" in this case is actually
/// "{x: importedNamespace.x}". This separate type forces code to opt-in to
/// doing this instead of opt-out.
pub const ImportIdentifier = struct {
ref: Ref = Ref.None,
/// If true, this was originally an identifier expression such as "foo". If
/// false, this could potentially have been a member access expression such
/// as "ns.foo" off of an imported namespace object.
was_originally_identifier: bool = false,
};
/// This is a dot expression on exports, such as `exports.<ref>`. It is given
/// it's own AST node to allow CommonJS unwrapping, in which this can just be
/// the identifier in the Ref
pub const CommonJSExportIdentifier = struct {
ref: Ref = Ref.None,
base: Base = .exports,
/// The original variant of the dot expression must be known so that in the case that we
/// - fail to convert this to ESM
/// - ALSO see an assignment to `module.exports` (commonjs_module_exports_assigned_deoptimized)
/// It must be known if `exports` or `module.exports` was written in source
/// code, as the distinction will alter behavior. The fixup happens in the printer when
/// printing this node.
pub const Base = enum {
exports,
module_dot_exports,
};
};
// This is similar to EIdentifier but it represents class-private fields and
// methods. It can be used where computed properties can be used, such as
// EIndex and Property.
pub const PrivateIdentifier = struct {
ref: Ref,
};
/// In development mode, the new JSX transform has a few special props
/// - `React.jsxDEV(type, arguments, key, isStaticChildren, source, self)`
/// - `arguments`:
/// ```{ ...props, children: children, }```
/// - `source`: https://github.com/babel/babel/blob/ef87648f3f05ccc393f89dea7d4c7c57abf398ce/packages/babel-plugin-transform-react-jsx-source/src/index.js#L24-L48
/// ```{
/// fileName: string | null,
/// columnNumber: number | null,
/// lineNumber: number | null,
/// }```
/// - `children`:
/// - static the function is React.jsxsDEV, "jsxs" instead of "jsx"
/// - one child? the function is React.jsxDEV,
/// - no children? the function is React.jsxDEV and children is an empty array.
/// `isStaticChildren`: https://github.com/facebook/react/blob/4ca62cac45c288878d2532e5056981d177f9fdac/packages/react/src/jsx/ReactJSXElementValidator.js#L369-L384
/// This flag means children is an array of JSX Elements literals.
/// The documentation on this is sparse, but it appears that
/// React just calls Object.freeze on the children array.
/// Object.freeze, historically, is quite a bit slower[0] than just not doing that.
/// Given that...I am choosing to always pass "false" to this.
/// This also skips extra state that we'd need to track.
/// If React Fast Refresh ends up using this later, then we can revisit this decision.
/// [0]: https://github.com/automerge/automerge/issues/177
pub const JSXElement = struct {
/// JSX tag name
/// <div> => E.String.init("div")
/// <MyComponent> => E.Identifier{.ref = symbolPointingToMyComponent }
/// null represents a fragment
tag: ?ExprNodeIndex = null,
/// JSX props
properties: G.Property.List = G.Property.List{},
/// JSX element children <div>{this_is_a_child_element}</div>
children: ExprNodeList = ExprNodeList{},
// needed to make sure parse and visit happen in the same order
key_prop_index: i32 = -1,
flags: Flags.JSXElement.Bitset = Flags.JSXElement.Bitset{},
close_tag_loc: logger.Loc = logger.Loc.Empty,
pub const SpecialProp = enum {
__self, // old react transform used this as a prop
__source,
key,
ref,
any,
pub const Map = ComptimeStringMap(SpecialProp, .{
.{ "__self", .__self },
.{ "__source", .__source },
.{ "key", .key },
.{ "ref", .ref },
});
};
};
pub const Missing = struct {
pub fn jsonStringify(_: *const @This(), writer: anytype) !void {
return try writer.write(null);
}
};
pub const Number = struct {
value: f64,
const double_digit = [_]string{ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "100" };
const neg_double_digit = [_]string{ "-0", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", "-16", "-17", "-18", "-19", "-20", "-21", "-22", "-23", "-24", "-25", "-26", "-27", "-28", "-29", "-30", "-31", "-32", "-33", "-34", "-35", "-36", "-37", "-38", "-39", "-40", "-41", "-42", "-43", "-44", "-45", "-46", "-47", "-48", "-49", "-50", "-51", "-52", "-53", "-54", "-55", "-56", "-57", "-58", "-59", "-60", "-61", "-62", "-63", "-64", "-65", "-66", "-67", "-68", "-69", "-70", "-71", "-72", "-73", "-74", "-75", "-76", "-77", "-78", "-79", "-80", "-81", "-82", "-83", "-84", "-85", "-86", "-87", "-88", "-89", "-90", "-91", "-92", "-93", "-94", "-95", "-96", "-97", "-98", "-99", "-100" };
/// String concatenation with numbers is required by the TypeScript compiler for
/// "constant expression" handling in enums. We can match the behavior of a JS VM
/// by calling out to the APIs in WebKit which are responsible for this operation.
///
/// This can return `null` in wasm builds to avoid linking JSC
pub fn toString(this: Number, allocator: std.mem.Allocator) ?string {
return toStringFromF64(this.value, allocator);
}
pub fn toStringFromF64(value: f64, allocator: std.mem.Allocator) ?string {
if (value == @trunc(value) and (value < std.math.maxInt(i32) and value > std.math.minInt(i32))) {
const int_value = @as(i64, @intFromFloat(value));
const abs = @as(u64, @intCast(@abs(int_value)));
// do not allocate for a small set of constant numbers: -100 through 100
if (abs < double_digit.len) {
return if (int_value < 0)
neg_double_digit[abs]
else
double_digit[abs];
}
return std.fmt.allocPrint(allocator, "{d}", .{@as(i32, @intCast(int_value))}) catch return null;
}
if (std.math.isNan(value)) {
return "NaN";
}
if (std.math.isNegativeInf(value)) {
return "-Infinity";
}
if (std.math.isInf(value)) {
return "Infinity";
}
if (Environment.isNative) {
var buf: [124]u8 = undefined;
return bun.handleOom(allocator.dupe(u8, bun.fmt.FormatDouble.dtoa(&buf, value)));
} else {
// do not attempt to implement the spec here, it would be error prone.
}
return null;
}
pub inline fn toU64(self: Number) u64 {
return self.to(u64);
}
pub inline fn toUsize(self: Number) usize {
return self.to(usize);
}
pub inline fn toU32(self: Number) u32 {
return self.to(u32);
}
pub inline fn toU16(self: Number) u16 {
return self.to(u16);
}
pub fn to(self: Number, comptime T: type) T {
return @as(T, @intFromFloat(@min(@max(@trunc(self.value), 0), comptime @min(std.math.floatMax(f64), std.math.maxInt(T)))));
}
pub fn jsonStringify(self: *const Number, writer: anytype) !void {
return try writer.write(self.value);
}
pub fn toJS(this: @This()) jsc.JSValue {
return jsc.JSValue.jsNumber(this.value);
}
};
pub const BigInt = struct {
value: string,
pub var empty = BigInt{ .value = "" };
pub fn jsonStringify(self: *const @This(), writer: anytype) !void {
return try writer.write(self.value);
}
pub fn toJS(_: @This()) jsc.JSValue {
// TODO:
return jsc.JSValue.jsNumber(0);
}
};
pub const Object = struct {
properties: G.Property.List = G.Property.List{},
comma_after_spread: ?logger.Loc = null,
is_single_line: bool = false,
is_parenthesized: bool = false,
was_originally_macro: bool = false,
close_brace_loc: logger.Loc = logger.Loc.Empty,
// used in TOML parser to merge properties
pub const Rope = struct {
head: Expr,
next: ?*Rope = null,
pub fn append(this: *Rope, expr: Expr, allocator: std.mem.Allocator) OOM!*Rope {
if (this.next) |next| {
return try next.append(expr, allocator);
}
const rope = try allocator.create(Rope);
rope.* = .{ .head = expr };
this.next = rope;
return rope;
}
};
pub fn get(self: *const Object, key: string) ?Expr {
return if (asProperty(self, key)) |query| query.expr else @as(?Expr, null);
}
pub fn toJS(this: *Object, allocator: std.mem.Allocator, globalObject: *jsc.JSGlobalObject) ToJSError!jsc.JSValue {
var obj = jsc.JSValue.createEmptyObject(globalObject, this.properties.len);
obj.protect();
defer obj.unprotect();
const props: []const G.Property = this.properties.slice();
for (props) |prop| {
if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.value == null) {
return error.@"Cannot convert argument type to JS";
}
const key = try prop.key.?.data.toJS(allocator, globalObject);
const value = try prop.value.?.toJS(allocator, globalObject);
try obj.putToPropertyKey(globalObject, key, value);
}
return obj;
}
pub fn put(self: *Object, allocator: std.mem.Allocator, key: string, expr: Expr) !void {
if (asProperty(self, key)) |query| {
self.properties.ptr[query.i].value = expr;
} else {
try self.properties.append(allocator, .{
.key = Expr.init(E.String, E.String.init(key), expr.loc),
.value = expr,
});
}
}
pub fn putString(self: *Object, allocator: std.mem.Allocator, key: string, value: string) !void {
return try put(self, allocator, key, Expr.init(E.String, E.String.init(value), logger.Loc.Empty));
}
pub const SetError = error{ OutOfMemory, Clobber };
pub fn set(self: *const Object, key: Expr, allocator: std.mem.Allocator, value: Expr) SetError!void {
if (self.hasProperty(key.data.e_string.data)) return error.Clobber;
try self.properties.append(allocator, .{
.key = key,
.value = value,
});
}
pub const RopeQuery = struct {
expr: Expr,
rope: *const Rope,
};
// this is terribly, shamefully slow
pub fn setRope(self: *Object, rope: *const Rope, allocator: std.mem.Allocator, value: Expr) SetError!void {
if (self.get(rope.head.data.e_string.data)) |existing| {
switch (existing.data) {
.e_array => |array| {
if (rope.next == null) {
try array.push(allocator, value);
return;
}
if (array.items.last()) |last| {
if (last.data != .e_object) {
return error.Clobber;
}
try last.data.e_object.setRope(rope.next.?, allocator, value);
return;
}
try array.push(allocator, value);
return;
},
.e_object => |object| {
if (rope.next != null) {
try object.setRope(rope.next.?, allocator, value);
return;
}
return error.Clobber;
},
else => {
return error.Clobber;
},
}
}
var value_ = value;
if (rope.next) |next| {
var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc);
try obj.data.e_object.setRope(next, allocator, value);
value_ = obj;
}
try self.properties.append(allocator, .{
.key = rope.head,
.value = value_,
});
}
pub fn getOrPutObject(self: *Object, rope: *const Rope, allocator: std.mem.Allocator) SetError!Expr {
if (self.get(rope.head.data.e_string.data)) |existing| {
switch (existing.data) {
.e_array => |array| {
if (rope.next == null) {
return error.Clobber;
}
if (array.items.last()) |last| {
if (last.data != .e_object) {
return error.Clobber;
}
return try last.data.e_object.getOrPutObject(rope.next.?, allocator);
}
return error.Clobber;
},
.e_object => |object| {
if (rope.next != null) {
return try object.getOrPutObject(rope.next.?, allocator);
}
// success
return existing;
},
else => {
return error.Clobber;
},
}
}
if (rope.next) |next| {
var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc);
const out = try obj.data.e_object.getOrPutObject(next, allocator);
try self.properties.append(allocator, .{
.key = rope.head,
.value = obj,
});
return out;
}
const out = Expr.init(E.Object, E.Object{}, rope.head.loc);
try self.properties.append(allocator, .{
.key = rope.head,
.value = out,
});
return out;
}
pub fn getOrPutArray(self: *Object, rope: *const Rope, allocator: std.mem.Allocator) SetError!Expr {
if (self.get(rope.head.data.e_string.data)) |existing| {
switch (existing.data) {
.e_array => |array| {
if (rope.next == null) {
return existing;
}
if (array.items.last()) |last| {
if (last.data != .e_object) {
return error.Clobber;
}
return try last.data.e_object.getOrPutArray(rope.next.?, allocator);
}
return error.Clobber;
},
.e_object => |object| {
if (rope.next == null) {
return error.Clobber;
}
return try object.getOrPutArray(rope.next.?, allocator);
},
else => {
return error.Clobber;
},
}
}
if (rope.next) |next| {
var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc);
const out = try obj.data.e_object.getOrPutArray(next, allocator);
try self.properties.append(allocator, .{
.key = rope.head,
.value = obj,
});
return out;
}
const out = Expr.init(E.Array, E.Array{}, rope.head.loc);
try self.properties.append(allocator, .{
.key = rope.head,
.value = out,
});
return out;
}
pub fn hasProperty(obj: *const Object, name: string) bool {
for (obj.properties.slice()) |prop| {
const key = prop.key orelse continue;
if (std.meta.activeTag(key.data) != .e_string) continue;
if (key.data.e_string.eql(string, name)) return true;
}
return false;
}
pub fn asProperty(obj: *const Object, name: string) ?Expr.Query {
for (obj.properties.slice(), 0..) |prop, i| {
const value = prop.value orelse continue;
const key = prop.key orelse continue;
if (std.meta.activeTag(key.data) != .e_string) continue;
const key_str = key.data.e_string;
if (key_str.eql(string, name)) {
return Expr.Query{
.expr = value,
.loc = key.loc,
.i = @as(u32, @truncate(i)),
};
}
}
return null;
}
/// Assumes each key in the property is a string
pub fn alphabetizeProperties(this: *Object) void {
if (comptime Environment.isDebug) {
for (this.properties.slice()) |prop| {
bun.assert(prop.key.?.data == .e_string);
}
}
std.sort.pdq(G.Property, this.properties.slice(), {}, Sorter.isLessThan);
}
pub fn packageJSONSort(this: *Object) void {
std.sort.pdq(G.Property, this.properties.slice(), {}, PackageJSONSort.Fields.isLessThan);
}
const PackageJSONSort = struct {
const Fields = enum(u8) {
name = 0,
version = 1,
author = 2,
repository = 3,
config = 4,
main = 5,
module = 6,
dependencies = 7,
devDependencies = 8,
optionalDependencies = 9,
peerDependencies = 10,
exports = 11,
__fake = 12,
pub const Map = ComptimeStringMap(Fields, .{
.{ "name", Fields.name },
.{ "version", Fields.version },
.{ "author", Fields.author },
.{ "repository", Fields.repository },
.{ "config", Fields.config },
.{ "main", Fields.main },
.{ "module", Fields.module },
.{ "dependencies", Fields.dependencies },
.{ "devDependencies", Fields.devDependencies },
.{ "optionalDependencies", Fields.optionalDependencies },
.{ "peerDependencies", Fields.peerDependencies },
.{ "exports", Fields.exports },
});
pub fn isLessThan(ctx: void, lhs: G.Property, rhs: G.Property) bool {
var lhs_key_size: u8 = @intFromEnum(Fields.__fake);
var rhs_key_size: u8 = @intFromEnum(Fields.__fake);
if (lhs.key != null and lhs.key.?.data == .e_string) {
lhs_key_size = @intFromEnum(Map.get(lhs.key.?.data.e_string.data) orelse Fields.__fake);
}
if (rhs.key != null and rhs.key.?.data == .e_string) {
rhs_key_size = @intFromEnum(Map.get(rhs.key.?.data.e_string.data) orelse Fields.__fake);
}
return switch (std.math.order(lhs_key_size, rhs_key_size)) {
.lt => true,
.gt => false,
.eq => strings.cmpStringsAsc(ctx, lhs.key.?.data.e_string.data, rhs.key.?.data.e_string.data),
};
}
};
};
const Sorter = struct {
pub fn isLessThan(ctx: void, lhs: G.Property, rhs: G.Property) bool {
return strings.cmpStringsAsc(ctx, lhs.key.?.data.e_string.data, rhs.key.?.data.e_string.data);
}
};
};
pub const Spread = struct { value: ExprNodeIndex };
/// JavaScript string literal type
pub const String = struct {
// A version of this where `utf8` and `value` are stored in a packed union, with len as a single u32 was attempted.
// It did not improve benchmarks. Neither did converting this from a heap-allocated type to a stack-allocated type.
// TODO: change this to *const anyopaque and change all uses to either .slice8() or .slice16()
data: []const u8 = "",
prefer_template: bool = false,
// A very simple rope implementation
// We only use this for string folding, so this is kind of overkill
// We don't need to deal with substrings
next: ?*String = null,
end: ?*String = null,
rope_len: u32 = 0,
is_utf16: bool = false,
pub fn isIdentifier(this: *String, allocator: std.mem.Allocator) bool {
if (!this.isUTF8()) {
return bun.js_lexer.isIdentifierUTF16(this.slice16());
}
return bun.js_lexer.isIdentifier(this.slice(allocator));
}
pub const class = E.String{ .data = "class" };
pub fn push(this: *String, other: *String) void {
bun.assert(this.isUTF8());
bun.assert(other.isUTF8());
if (other.rope_len == 0) {
other.rope_len = @truncate(other.data.len);
}
if (this.rope_len == 0) {
this.rope_len = @truncate(this.data.len);
}
this.rope_len += other.rope_len;
if (this.next == null) {
this.next = other;
this.end = other;
} else {
var end = this.end.?;
while (end.next != null) end = end.end.?;
end.next = other;
this.end = other;
}
}
/// Cloning the rope string is rarely needed, see `foldStringAddition`'s
/// comments and the 'edgecase/EnumInliningRopeStringPoison' test
pub fn cloneRopeNodes(s: String) String {
var root = s;
if (root.next != null) {
var current: ?*String = &root;
while (true) {
const node = current.?;
if (node.next) |next| {
node.next = Expr.Data.Store.append(String, next.*);
current = node.next;
} else {
root.end = node;
break;
}
}
}
return root;
}
pub fn toUTF8(this: *String, allocator: std.mem.Allocator) !void {
if (!this.is_utf16) return;
this.data = try strings.toUTF8Alloc(allocator, this.slice16());
this.is_utf16 = false;
}
pub fn init(value: anytype) String {
const Value = @TypeOf(value);
if (Value == []u16 or Value == []const u16) {
return .{
.data = @as([*]const u8, @ptrCast(value.ptr))[0..value.len],
.is_utf16 = true,
};
}
return .{ .data = value };
}
/// E.String containing non-ascii characters may not fully work.
/// https://github.com/oven-sh/bun/issues/11963
/// More investigation is needed.
pub fn initReEncodeUTF8(utf8: []const u8, allocator: std.mem.Allocator) String {
return if (bun.strings.isAllASCII(utf8))
init(utf8)
else
init(bun.handleOom(bun.strings.toUTF16AllocForReal(allocator, utf8, false, false)));
}
pub fn slice8(this: *const String) []const u8 {
bun.assert(!this.is_utf16);
return this.data;
}
pub fn slice16(this: *const String) []const u16 {
bun.assert(this.is_utf16);
return @as([*]const u16, @ptrCast(@alignCast(this.data.ptr)))[0..this.data.len];
}
pub fn resolveRopeIfNeeded(this: *String, allocator: std.mem.Allocator) void {
if (this.next == null or !this.isUTF8()) return;
var bytes = bun.handleOom(std.ArrayList(u8).initCapacity(allocator, this.rope_len));
bytes.appendSliceAssumeCapacity(this.data);
var str = this.next;
while (str) |part| {
bun.handleOom(bytes.appendSlice(part.data));
str = part.next;
}
this.data = bytes.items;
this.next = null;
}
pub fn slice(this: *String, allocator: std.mem.Allocator) []const u8 {
this.resolveRopeIfNeeded(allocator);
return bun.handleOom(this.string(allocator));
}
fn stringCompareForJavaScript(comptime T: type, a: []const T, b: []const T) std.math.Order {
const a_slice = a[0..@min(a.len, b.len)];
const b_slice = b[0..@min(a.len, b.len)];
for (a_slice, b_slice) |a_char, b_char| {
const delta: i32 = @as(i32, a_char) - @as(i32, b_char);
if (delta != 0) {
return if (delta < 0) .lt else .gt;
}
}
return std.math.order(a.len, b.len);
}
/// Compares two strings lexicographically for JavaScript semantics.
/// Both strings must share the same encoding (UTF-8 vs UTF-16).
pub inline fn order(this: *const String, other: *const String) std.math.Order {
bun.debugAssert(this.isUTF8() == other.isUTF8());
if (this.isUTF8()) {
return stringCompareForJavaScript(u8, this.data, other.data);
} else {
return stringCompareForJavaScript(u16, this.slice16(), other.slice16());
}
}
pub var empty = String{};
pub var @"true" = String{ .data = "true" };
pub var @"false" = String{ .data = "false" };
pub var @"null" = String{ .data = "null" };
pub var @"undefined" = String{ .data = "undefined" };
pub fn clone(str: *const String, allocator: std.mem.Allocator) !String {
return String{
.data = try allocator.dupe(u8, str.data),
.prefer_template = str.prefer_template,
.is_utf16 = !str.isUTF8(),
};
}
pub fn cloneSliceIfNecessary(str: *const String, allocator: std.mem.Allocator) ![]const u8 {
if (str.isUTF8()) {
return allocator.dupe(u8, str.string(allocator) catch unreachable);
}
return str.string(allocator);
}
pub fn javascriptLength(s: *const String) ?u32 {
if (s.rope_len > 0) {
// We only support ascii ropes for now
return s.rope_len;
}
if (s.isUTF8()) {
if (!strings.isAllASCII(s.data)) {
return null;
}
return @truncate(s.data.len);
}
return @truncate(s.slice16().len);
}
pub inline fn len(s: *const String) usize {
return if (s.rope_len > 0) s.rope_len else s.data.len;
}
pub inline fn isUTF8(s: *const String) bool {
return !s.is_utf16;
}
pub inline fn isBlank(s: *const String) bool {
return s.len() == 0;
}
pub inline fn isPresent(s: *const String) bool {
return s.len() > 0;
}
pub fn eql(s: *const String, comptime _t: type, other: anytype) bool {
if (s.isUTF8()) {
switch (_t) {
@This() => {
if (other.isUTF8()) {
return strings.eqlLong(s.data, other.data, true);
} else {
return strings.utf16EqlString(other.slice16(), s.data);
}
},
[]const u8 => {
return strings.eqlLong(s.data, other, true);
},
[]u16, []const u16 => {
return strings.utf16EqlString(other, s.data);
},
else => {
@compileError("Invalid type");
},
}
} else {
switch (_t) {
@This() => {
if (other.isUTF8()) {
return strings.utf16EqlString(s.slice16(), other.data);
} else {
return std.mem.eql(u16, other.slice16(), s.slice16());
}
},
[]const u8 => {
return strings.utf16EqlString(s.slice16(), other);
},
[]u16, []const u16 => {
return std.mem.eql(u16, other.slice16(), s.slice16());
},
else => {
@compileError("Invalid type");
},
}
}
}
pub fn eqlComptime(s: *const String, comptime value: []const u8) bool {
bun.assert(s.next == null);
return if (s.isUTF8())
strings.eqlComptime(s.data, value)
else
strings.eqlComptimeUTF16(s.slice16(), value);
}
pub fn hasPrefixComptime(s: *const String, comptime value: anytype) bool {
if (s.data.len < value.len)
return false;
return if (s.isUTF8())
strings.eqlComptime(s.data[0..value.len], value)
else
strings.eqlComptimeUTF16(s.slice16()[0..value.len], value);
}
pub fn string(s: *const String, allocator: std.mem.Allocator) OOM![]const u8 {
if (s.isUTF8()) {
return s.data;
} else {
return strings.toUTF8Alloc(allocator, s.slice16());
}
}
pub fn stringZ(s: *const String, allocator: std.mem.Allocator) OOM![:0]const u8 {
if (s.isUTF8()) {
return allocator.dupeZ(u8, s.data);
} else {
return strings.toUTF8AllocZ(allocator, s.slice16());
}
}
pub fn stringCloned(s: *const String, allocator: std.mem.Allocator) OOM![]const u8 {
if (s.isUTF8()) {
return allocator.dupe(u8, s.data);
} else {
return strings.toUTF8Alloc(allocator, s.slice16());
}
}
pub fn hash(s: *const String) u64 {
if (s.isBlank()) return 0;
if (s.isUTF8()) {
// hash utf-8
return bun.hash(s.data);
} else {
// hash utf-16
return bun.hash(@as([*]const u8, @ptrCast(s.slice16().ptr))[0 .. s.slice16().len * 2]);
}
}
pub fn toJS(s: *String, allocator: std.mem.Allocator, globalObject: *jsc.JSGlobalObject) !jsc.JSValue {
s.resolveRopeIfNeeded(allocator);
if (!s.isPresent()) {
var emp = bun.String.empty;
return emp.toJS(globalObject);
}
if (s.isUTF8()) {
if (try strings.toUTF16Alloc(allocator, s.slice8(), false, false)) |utf16| {
var out, const chars = bun.String.createUninitialized(.utf16, utf16.len);
@memcpy(chars, utf16);
return out.transferToJS(globalObject);
} else {
var out, const chars = bun.String.createUninitialized(.latin1, s.slice8().len);
@memcpy(chars, s.slice8());
return out.transferToJS(globalObject);
}
} else {
var out, const chars = bun.String.createUninitialized(.utf16, s.slice16().len);
@memcpy(chars, s.slice16());
return out.transferToJS(globalObject);
}
}
pub fn toZigString(s: *String, allocator: std.mem.Allocator) jsc.ZigString {
if (s.isUTF8()) {
return jsc.ZigString.fromUTF8(s.slice(allocator));
} else {
return jsc.ZigString.initUTF16(s.slice16());
}
}
pub fn format(s: String, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
comptime bun.assert(fmt.len == 0);
try writer.writeAll("E.String");
if (s.next == null) {
try writer.writeAll("(");
if (s.isUTF8()) {
try writer.print("\"{s}\"", .{s.data});
} else {
try writer.print("\"{}\"", .{bun.fmt.utf16(s.slice16())});
}
try writer.writeAll(")");
} else {
try writer.writeAll("(rope: [");
var it: ?*const String = &s;
while (it) |part| {
if (part.isUTF8()) {
try writer.print("\"{s}\"", .{part.data});
} else {
try writer.print("\"{}\"", .{bun.fmt.utf16(part.slice16())});
}
it = part.next;
if (it != null) try writer.writeAll(" ");
}
try writer.writeAll("])");
}
}
pub fn jsonStringify(s: *const String, writer: anytype) !void {
var buf = [_]u8{0} ** 4096;
var i: usize = 0;
for (s.slice16()) |char| {
buf[i] = @as(u8, @intCast(char));
i += 1;
if (i >= 4096) {
break;
}
}
return try writer.write(buf[0..i]);
}
};
// value is in the Node
pub const TemplatePart = struct {
value: ExprNodeIndex,
tail_loc: logger.Loc,
tail: Template.Contents,
};
pub const Template = struct {
tag: ?ExprNodeIndex = null,
parts: []TemplatePart = &.{},
head: Contents,
pub const Contents = union(Tag) {
cooked: E.String,
raw: string,
const Tag = enum {
cooked,
raw,
};
pub fn isUTF8(contents: Contents) bool {
return contents == .cooked and contents.cooked.isUTF8();
}
};
/// "`a${'b'}c`" => "`abc`"
pub fn fold(
this: *Template,
allocator: std.mem.Allocator,
loc: logger.Loc,
) Expr {
if (this.tag != null or (this.head == .cooked and !this.head.cooked.isUTF8())) {
// we only fold utf-8/ascii for now
return Expr{
.data = .{ .e_template = this },
.loc = loc,
};
}
bun.assert(this.head == .cooked);
if (this.parts.len == 0) {
return Expr.init(E.String, this.head.cooked, loc);
}
var parts = std.ArrayList(TemplatePart).initCapacity(allocator, this.parts.len) catch unreachable;
var head = Expr.init(E.String, this.head.cooked, loc);
for (this.parts) |part_src| {
var part = part_src;
bun.assert(part.tail == .cooked);
part.value = part.value.unwrapInlined();
switch (part.value.data) {
.e_number => {
if (part.value.data.e_number.toString(allocator)) |s| {
part.value = Expr.init(E.String, E.String.init(s), part.value.loc);
}
},
.e_null => {
part.value = Expr.init(E.String, E.String.init("null"), part.value.loc);
},
.e_boolean => {
part.value = Expr.init(E.String, E.String.init(if (part.value.data.e_boolean.value)
"true"
else
"false"), part.value.loc);
},
.e_undefined => {
part.value = Expr.init(E.String, E.String.init("undefined"), part.value.loc);
},
.e_big_int => |value| {
part.value = Expr.init(E.String, E.String.init(value.value), part.value.loc);
},
else => {},
}
if (part.value.data == .e_string and part.tail.cooked.isUTF8() and part.value.data.e_string.isUTF8()) {
if (parts.items.len == 0) {
if (part.value.data.e_string.len() > 0) {
head.data.e_string.push(Expr.init(E.String, part.value.data.e_string.*, logger.Loc.Empty).data.e_string);
}
if (part.tail.cooked.len() > 0) {
head.data.e_string.push(Expr.init(E.String, part.tail.cooked, part.tail_loc).data.e_string);
}
continue;
} else {
var prev_part = &parts.items[parts.items.len - 1];
bun.assert(prev_part.tail == .cooked);
if (prev_part.tail.cooked.isUTF8()) {
if (part.value.data.e_string.len() > 0) {
prev_part.tail.cooked.push(Expr.init(E.String, part.value.data.e_string.*, logger.Loc.Empty).data.e_string);
}
if (part.tail.cooked.len() > 0) {
prev_part.tail.cooked.push(Expr.init(E.String, part.tail.cooked, part.tail_loc).data.e_string);
}
} else {
parts.appendAssumeCapacity(part);
}
}
} else {
parts.appendAssumeCapacity(part);
}
}
if (parts.items.len == 0) {
parts.deinit();
head.data.e_string.resolveRopeIfNeeded(allocator);
return head;
}
return Expr.init(E.Template, .{
.tag = null,
.parts = parts.items,
.head = .{ .cooked = head.data.e_string.* },
}, loc);
}
};
pub const RegExp = struct {
value: string,
// This exists for JavaScript bindings
// The RegExp constructor expects flags as a second argument.
// We want to avoid re-lexing the flags, so we store them here.
// This is the index of the first character in a flag, not the "/"
// /foo/gim
// ^
flags_offset: ?u16 = null,
pub var empty = RegExp{ .value = "" };
pub fn pattern(this: RegExp) string {
// rewind until we reach the /foo/gim
// ^
// should only ever be a single character
// but we're being cautious
if (this.flags_offset) |i_| {
var i = i_;
while (i > 0 and this.value[i] != '/') {
i -= 1;
}
return std.mem.trim(u8, this.value[0..i], "/");
}
return std.mem.trim(u8, this.value, "/");
}
pub fn flags(this: RegExp) string {
// rewind until we reach the /foo/gim
// ^
// should only ever be a single character
// but we're being cautious
if (this.flags_offset) |i| {
return this.value[i..];
}
return "";
}
pub fn jsonStringify(self: *const RegExp, writer: anytype) !void {
return try writer.write(self.value);
}
};
pub const Await = struct {
value: ExprNodeIndex,
};
pub const Yield = struct {
value: ?ExprNodeIndex = null,
is_star: bool = false,
};
pub const If = struct {
test_: ExprNodeIndex,
yes: ExprNodeIndex,
no: ExprNodeIndex,
};
pub const RequireString = struct {
import_record_index: u32 = 0,
unwrapped_id: u32 = std.math.maxInt(u32),
};
pub const RequireResolveString = struct {
import_record_index: u32,
// close_paren_loc: logger.Loc = logger.Loc.Empty,
};
pub const InlinedEnum = struct {
value: ExprNodeIndex,
comment: string,
};
pub const Import = struct {
expr: ExprNodeIndex,
options: ExprNodeIndex = Expr.empty,
import_record_index: u32,
/// TODO:
/// Comments inside "import()" expressions have special meaning for Webpack.
/// Preserving comments inside these expressions makes it possible to use
/// esbuild as a TypeScript-to-JavaScript frontend for Webpack to improve
/// performance. We intentionally do not interpret these comments in esbuild
/// because esbuild is not Webpack. But we do preserve them since doing so is
/// harmless, easy to maintain, and useful to people. See the Webpack docs for
/// more info: https://webpack.js.org/api/module-methods/#magic-comments.
// leading_interior_comments: []G.Comment = &([_]G.Comment{}),
pub fn isImportRecordNull(this: *const Import) bool {
return this.import_record_index == std.math.maxInt(u32);
}
pub fn importRecordLoader(import: *const Import) ?bun.options.Loader {
// This logic is duplicated in js_printer.zig fn parsePath()
const obj = import.options.data.as(.e_object) orelse
return null;
const with = obj.get("with") orelse obj.get("assert") orelse
return null;
const with_obj = with.data.as(.e_object) orelse
return null;
const str = (with_obj.get("type") orelse
return null).data.as(.e_string) orelse
return null;
if (!str.is_utf16) if (bun.options.Loader.fromString(str.data)) |loader| {
if (loader == .sqlite) {
const embed = with_obj.get("embed") orelse return loader;
const embed_str = embed.data.as(.e_string) orelse return loader;
if (embed_str.eqlComptime("true")) {
return .sqlite_embedded;
}
}
return loader;
};
return null;
}
};
pub const Class = G.Class;
const string = []const u8;
const stringZ = [:0]const u8;
const std = @import("std");
const bun = @import("bun");
const ComptimeStringMap = bun.ComptimeStringMap;
const Environment = bun.Environment;
const ImportRecord = bun.ImportRecord;
const OOM = bun.OOM;
const jsc = bun.jsc;
const logger = bun.logger;
const strings = bun.strings;
const Loader = bun.options.Loader;
const js_ast = bun.ast;
const E = js_ast.E;
const Expr = js_ast.Expr;
const ExprNodeIndex = js_ast.ExprNodeIndex;
const ExprNodeList = js_ast.ExprNodeList;
const Flags = js_ast.Flags;
const G = js_ast.G;
const Op = js_ast.Op;
const OptionalChain = js_ast.OptionalChain;
const Ref = js_ast.Ref;
const ToJSError = js_ast.ToJSError;