mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
545 lines
20 KiB
Zig
545 lines
20 KiB
Zig
const js_ast = bun.JSAst;
|
|
const bun = @import("bun");
|
|
const string = bun.string;
|
|
const Output = bun.Output;
|
|
const Global = bun.Global;
|
|
const Environment = bun.Environment;
|
|
const strings = bun.strings;
|
|
const MutableString = bun.MutableString;
|
|
const stringZ = bun.stringZ;
|
|
const default_allocator = bun.default_allocator;
|
|
const C = bun.C;
|
|
const std = @import("std");
|
|
const Ref = @import("./ast/base.zig").Ref;
|
|
const logger = @import("bun").logger;
|
|
const JSLexer = @import("./js_lexer.zig");
|
|
|
|
pub const NoOpRenamer = struct {
|
|
symbols: js_ast.Symbol.Map,
|
|
source: *const logger.Source,
|
|
|
|
pub fn init(symbols: js_ast.Symbol.Map, source: *const logger.Source) NoOpRenamer {
|
|
return NoOpRenamer{ .symbols = symbols, .source = source };
|
|
}
|
|
|
|
pub const originalName = nameForSymbol;
|
|
|
|
pub fn nameForSymbol(renamer: *NoOpRenamer, ref: Ref) string {
|
|
if (ref.isSourceContentsSlice()) {
|
|
return renamer.source.contents[ref.sourceIndex() .. ref.sourceIndex() + ref.innerIndex()];
|
|
}
|
|
|
|
const resolved = renamer.symbols.follow(ref);
|
|
|
|
if (renamer.symbols.getConst(resolved)) |symbol| {
|
|
return symbol.original_name;
|
|
} else {
|
|
Global.panic("Invalid symbol {s} in {s}", .{ ref, renamer.source.path.text });
|
|
}
|
|
}
|
|
|
|
pub fn toRenamer(this: *NoOpRenamer) Renamer {
|
|
return .{
|
|
.NoOpRenamer = this,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const Renamer = union(enum) {
|
|
NumberRenamer: *NumberRenamer,
|
|
NoOpRenamer: *NoOpRenamer,
|
|
|
|
pub fn symbols(this: Renamer) js_ast.Symbol.Map {
|
|
return switch (this) {
|
|
inline else => |r| r.symbols,
|
|
};
|
|
}
|
|
|
|
pub fn nameForSymbol(renamer: Renamer, ref: Ref) string {
|
|
return switch (renamer) {
|
|
inline else => |r| r.nameForSymbol(ref),
|
|
};
|
|
}
|
|
|
|
pub fn originalName(renamer: Renamer, ref: Ref) ?string {
|
|
return switch (renamer) {
|
|
inline else => |r| r.originalName(ref),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(renamer: Renamer) void {
|
|
switch (renamer) {
|
|
.NumberRenamer => |r| r.deinit(),
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const NumberRenamer = struct {
|
|
symbols: js_ast.Symbol.Map,
|
|
names: []bun.BabyList(string) = &.{},
|
|
allocator: std.mem.Allocator,
|
|
temp_allocator: std.mem.Allocator,
|
|
number_scope_pool: bun.HiveArray(NumberScope, 128).Fallback,
|
|
arena: std.heap.ArenaAllocator,
|
|
root: NumberScope = .{},
|
|
name_stack_fallback: std.heap.StackFallbackAllocator(512) = undefined,
|
|
name_temp_allocator: std.mem.Allocator = undefined,
|
|
|
|
pub fn deinit(self: *NumberRenamer) void {
|
|
self.allocator.free(self.names);
|
|
self.root.deinit(self.temp_allocator);
|
|
self.arena.deinit();
|
|
}
|
|
|
|
pub fn toRenamer(this: *NumberRenamer) Renamer {
|
|
return .{
|
|
.NumberRenamer = this,
|
|
};
|
|
}
|
|
|
|
pub fn originalName(r: *NumberRenamer, ref: Ref) string {
|
|
if (ref.isSourceContentsSlice()) {
|
|
unreachable;
|
|
}
|
|
|
|
const resolved = r.symbols.follow(ref);
|
|
return r.symbols.getConst(resolved).?.original_name;
|
|
}
|
|
|
|
pub fn assignName(r: *NumberRenamer, scope: *NumberScope, input_ref: Ref) void {
|
|
const ref = r.symbols.follow(input_ref);
|
|
|
|
// Don't rename the same symbol more than once
|
|
var inner: *bun.BabyList(string) = &r.names[ref.sourceIndex()];
|
|
if (inner.len > ref.innerIndex() and inner.at(ref.innerIndex()).len > 0) return;
|
|
|
|
// Don't rename unbound symbols, symbols marked as reserved names, labels, or private names
|
|
const symbol = r.symbols.get(ref).?;
|
|
if (symbol.slotNamespace() != .default) {
|
|
return;
|
|
}
|
|
|
|
r.name_stack_fallback.fixed_buffer_allocator.end_index = 0;
|
|
const name = switch (scope.findUnusedName(r.allocator, r.name_temp_allocator, symbol.original_name)) {
|
|
.renamed => |name| name,
|
|
.no_collision => symbol.original_name,
|
|
};
|
|
const new_len = @max(inner.len, ref.innerIndex() + 1);
|
|
if (inner.cap <= new_len) {
|
|
const prev_cap = inner.len;
|
|
inner.ensureUnusedCapacity(r.allocator, new_len - prev_cap) catch unreachable;
|
|
const to_write = inner.ptr[prev_cap..inner.cap];
|
|
@memset(std.mem.sliceAsBytes(to_write).ptr, 0, std.mem.sliceAsBytes(to_write).len);
|
|
}
|
|
inner.len = new_len;
|
|
inner.mut(ref.innerIndex()).* = name;
|
|
}
|
|
|
|
pub fn init(
|
|
allocator: std.mem.Allocator,
|
|
temp_allocator: std.mem.Allocator,
|
|
symbols: js_ast.Symbol.Map,
|
|
root_names: bun.StringHashMapUnmanaged(u32),
|
|
) !*NumberRenamer {
|
|
var renamer = try allocator.create(NumberRenamer);
|
|
renamer.* = NumberRenamer{
|
|
.symbols = symbols,
|
|
.allocator = allocator,
|
|
.temp_allocator = temp_allocator,
|
|
.names = try allocator.alloc(bun.BabyList(string), symbols.symbols_for_source.len),
|
|
.number_scope_pool = undefined,
|
|
.arena = std.heap.ArenaAllocator.init(temp_allocator),
|
|
};
|
|
renamer.name_stack_fallback = .{
|
|
.buffer = undefined,
|
|
.fallback_allocator = renamer.arena.allocator(),
|
|
.fixed_buffer_allocator = undefined,
|
|
};
|
|
renamer.name_temp_allocator = renamer.name_stack_fallback.get();
|
|
renamer.number_scope_pool = bun.HiveArray(NumberScope, 128).Fallback.init(renamer.arena.allocator());
|
|
renamer.root.name_counts = root_names;
|
|
if (comptime Environment.allow_assert) {
|
|
if (std.os.getenv("BUN_DUMP_SYMBOLS") != null)
|
|
symbols.dump();
|
|
}
|
|
|
|
@memset(std.mem.sliceAsBytes(renamer.names).ptr, 0, std.mem.sliceAsBytes(renamer.names).len);
|
|
|
|
return renamer;
|
|
}
|
|
|
|
pub fn assignNamesRecursive(r: *NumberRenamer, scope: *js_ast.Scope, source_index: u32, parent: ?*NumberScope, sorted: *std.ArrayList(u32)) void {
|
|
var s = r.number_scope_pool.get();
|
|
s.* = NumberScope{
|
|
.parent = parent,
|
|
.name_counts = .{},
|
|
};
|
|
defer {
|
|
s.deinit(r.temp_allocator);
|
|
r.number_scope_pool.put(s);
|
|
}
|
|
|
|
assignNamesRecursiveWithNumberScope(r, s, scope, source_index, sorted);
|
|
}
|
|
|
|
fn assignNamesInScope(
|
|
r: *NumberRenamer,
|
|
s: *NumberScope,
|
|
scope: *js_ast.Scope,
|
|
source_index: u32,
|
|
sorted: *std.ArrayList(u32),
|
|
) void {
|
|
{
|
|
sorted.clearRetainingCapacity();
|
|
sorted.ensureUnusedCapacity(scope.members.count()) catch unreachable;
|
|
sorted.items.len = scope.members.count();
|
|
var remaining = sorted.items;
|
|
var value_iter = scope.members.valueIterator();
|
|
while (value_iter.next()) |value_ref| {
|
|
if (comptime Environment.allow_assert)
|
|
std.debug.assert(!value_ref.ref.isSourceContentsSlice());
|
|
|
|
remaining[0] = value_ref.ref.innerIndex();
|
|
remaining = remaining[1..];
|
|
}
|
|
std.debug.assert(remaining.len == 0);
|
|
std.sort.sort(u32, sorted.items, {}, std.sort.asc(u32));
|
|
|
|
for (sorted.items) |inner_index| {
|
|
r.assignName(s, Ref.init(@intCast(Ref.Int, inner_index), source_index, false));
|
|
}
|
|
}
|
|
|
|
for (scope.generated.slice()) |ref| {
|
|
r.assignName(s, ref);
|
|
}
|
|
}
|
|
|
|
pub fn assignNamesRecursiveWithNumberScope(r: *NumberRenamer, initial_scope: *NumberScope, scope_: *js_ast.Scope, source_index: u32, sorted: *std.ArrayList(u32)) void {
|
|
var s = initial_scope;
|
|
var scope = scope_;
|
|
defer if (s != initial_scope) {
|
|
s.deinit(r.temp_allocator);
|
|
r.number_scope_pool.put(s);
|
|
};
|
|
|
|
// Ignore function argument scopes
|
|
if (scope.kind == .function_args and scope.children.len == 1) {
|
|
scope = scope.children.ptr[0];
|
|
std.debug.assert(scope.kind == .function_body);
|
|
}
|
|
|
|
while (true) {
|
|
if (scope.members.count() > 0 or scope.generated.len > 0) {
|
|
var new_child_scope = r.number_scope_pool.get();
|
|
new_child_scope.* = .{
|
|
.parent = s,
|
|
.name_counts = .{},
|
|
};
|
|
s = new_child_scope;
|
|
|
|
r.assignNamesInScope(s, scope, source_index, sorted);
|
|
}
|
|
|
|
if (scope.children.len == 1) {
|
|
scope = scope.children.ptr[0];
|
|
if (scope.kind == .function_args and scope.children.len == 1) {
|
|
scope = scope.children.ptr[0];
|
|
std.debug.assert(scope.kind == .function_body);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Symbols in child scopes may also have to be renamed to avoid conflicts
|
|
for (scope.children.slice()) |child| {
|
|
r.assignNamesRecursiveWithNumberScope(s, child, source_index, sorted);
|
|
}
|
|
}
|
|
|
|
pub fn addTopLevelSymbol(r: *NumberRenamer, ref: Ref) void {
|
|
r.assignName(&r.root, ref);
|
|
}
|
|
|
|
pub fn addTopLevelDeclaredSymbols(r: *NumberRenamer, declared_symbols: js_ast.DeclaredSymbol.List) void {
|
|
var decls = declared_symbols;
|
|
js_ast.DeclaredSymbol.forEachTopLevelSymbol(&decls, r, addTopLevelSymbol);
|
|
}
|
|
|
|
pub fn nameForSymbol(renamer: *NumberRenamer, ref: Ref) string {
|
|
if (ref.isSourceContentsSlice()) {
|
|
bun.unreachablePanic("Unexpected unbound symbol!\n{any}", .{ref});
|
|
}
|
|
|
|
const resolved = renamer.symbols.follow(ref);
|
|
|
|
const source_index = resolved.sourceIndex();
|
|
const inner_index = resolved.innerIndex();
|
|
|
|
const renamed_list = renamer.names[source_index];
|
|
|
|
if (renamed_list.len > inner_index) {
|
|
const renamed = renamed_list.at(inner_index).*;
|
|
if (renamed.len > 0) {
|
|
return renamed;
|
|
}
|
|
}
|
|
|
|
return renamer.symbols.symbols_for_source.at(source_index).at(inner_index).original_name;
|
|
}
|
|
|
|
pub const NumberScope = struct {
|
|
parent: ?*NumberScope = null,
|
|
name_counts: bun.StringHashMapUnmanaged(u32) = .{},
|
|
|
|
pub fn deinit(this: *NumberScope, allocator: std.mem.Allocator) void {
|
|
this.name_counts.deinit(allocator);
|
|
this.* = undefined;
|
|
}
|
|
|
|
pub const NameUse = union(enum) {
|
|
unused: void,
|
|
same_scope: u32,
|
|
used: void,
|
|
|
|
pub fn find(this: *NumberScope, name: []const u8) NameUse {
|
|
// This version doesn't allocate
|
|
if (comptime Environment.allow_assert)
|
|
std.debug.assert(JSLexer.isIdentifier(name));
|
|
|
|
// avoid rehashing the same string over for each scope
|
|
const ctx = bun.StringHashMapContext.pre(name);
|
|
|
|
if (this.name_counts.getAdapted(name, ctx)) |count| {
|
|
return .{ .same_scope = count };
|
|
}
|
|
|
|
var s: ?*NumberScope = this.parent;
|
|
|
|
while (s) |scope| : (s = scope.parent) {
|
|
if (scope.name_counts.containsAdapted(name, ctx)) {
|
|
return .{ .used = {} };
|
|
}
|
|
}
|
|
|
|
return .{ .unused = {} };
|
|
}
|
|
};
|
|
|
|
const UnusedName = union(enum) {
|
|
no_collision: void,
|
|
renamed: string,
|
|
};
|
|
|
|
/// Caller must use an arena allocator
|
|
pub fn findUnusedName(this: *NumberScope, allocator: std.mem.Allocator, temp_allocator: std.mem.Allocator, input_name: []const u8) UnusedName {
|
|
var name = bun.MutableString.ensureValidIdentifier(input_name, temp_allocator) catch unreachable;
|
|
|
|
switch (NameUse.find(this, name)) {
|
|
.unused => {},
|
|
else => |use| {
|
|
var tries: u32 = if (use == .used)
|
|
1
|
|
else
|
|
// To avoid O(n^2) behavior, the number must start off being the number
|
|
// that we used last time there was a collision with this name. Otherwise
|
|
// if there are many collisions with the same name, each name collision
|
|
// would have to increment the counter past all previous name collisions
|
|
// which is a O(n^2) time algorithm. Only do this if this symbol comes
|
|
// from the same scope as the previous one since sibling scopes can reuse
|
|
// the same name without problems.
|
|
use.same_scope;
|
|
|
|
const prefix = name;
|
|
|
|
tries += 1;
|
|
|
|
var mutable_name = MutableString.initEmpty(temp_allocator);
|
|
mutable_name.growIfNeeded(prefix.len + 4) catch unreachable;
|
|
mutable_name.appendSlice(prefix) catch unreachable;
|
|
mutable_name.appendInt(tries) catch unreachable;
|
|
|
|
switch (NameUse.find(this, mutable_name.toOwnedSliceLeaky())) {
|
|
.unused => {
|
|
name = mutable_name.toOwnedSliceLeaky();
|
|
|
|
if (use == .same_scope) {
|
|
var existing = this.name_counts.getOrPut(allocator, prefix) catch unreachable;
|
|
if (!existing.found_existing) {
|
|
if (strings.eqlLong(input_name, prefix, true)) {
|
|
existing.key_ptr.* = input_name;
|
|
} else {
|
|
existing.key_ptr.* = allocator.dupe(u8, prefix) catch unreachable;
|
|
}
|
|
}
|
|
|
|
existing.value_ptr.* = tries;
|
|
}
|
|
},
|
|
else => |cur_use| {
|
|
while (true) {
|
|
mutable_name.resetTo(prefix.len);
|
|
mutable_name.appendInt(tries) catch unreachable;
|
|
|
|
tries += 1;
|
|
|
|
switch (NameUse.find(this, mutable_name.toOwnedSliceLeaky())) {
|
|
.unused => {
|
|
if (cur_use == .same_scope) {
|
|
var existing = this.name_counts.getOrPut(allocator, prefix) catch unreachable;
|
|
if (!existing.found_existing) {
|
|
if (strings.eqlLong(input_name, prefix, true)) {
|
|
existing.key_ptr.* = input_name;
|
|
} else {
|
|
existing.key_ptr.* = allocator.dupe(u8, prefix) catch unreachable;
|
|
}
|
|
}
|
|
|
|
existing.value_ptr.* = tries;
|
|
}
|
|
|
|
name = mutable_name.toOwnedSliceLeaky();
|
|
break;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
// Each name starts off with a count of 1 so that the first collision with
|
|
// "name" is called "name2"
|
|
if (strings.eqlLong(name, input_name, true)) {
|
|
this.name_counts.putNoClobber(allocator, input_name, 1) catch unreachable;
|
|
return .{ .no_collision = {} };
|
|
}
|
|
|
|
name = allocator.dupe(u8, name) catch unreachable;
|
|
|
|
this.name_counts.putNoClobber(allocator, name, 1) catch unreachable;
|
|
return .{ .renamed = name };
|
|
}
|
|
};
|
|
};
|
|
|
|
pub const ExportRenamer = struct {
|
|
string_buffer: bun.MutableString,
|
|
used: bun.StringHashMap(u32),
|
|
|
|
pub fn init(allocator: std.mem.Allocator) ExportRenamer {
|
|
return ExportRenamer{
|
|
.string_buffer = MutableString.initEmpty(allocator),
|
|
.used = bun.StringHashMap(u32).init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn clearRetainingCapacity(this: *ExportRenamer) void {
|
|
this.used.clearRetainingCapacity();
|
|
this.string_buffer.reset();
|
|
}
|
|
|
|
pub fn deinit(this: *ExportRenamer) void {
|
|
this.used.deinit();
|
|
this.string_buffer.deinit();
|
|
}
|
|
|
|
pub fn nextRenamedName(this: *ExportRenamer, input: []const u8) string {
|
|
var entry = this.used.getOrPut(input) catch unreachable;
|
|
var tries: u32 = 1;
|
|
if (entry.found_existing) {
|
|
while (true) {
|
|
this.string_buffer.reset();
|
|
var writer = this.string_buffer.writer();
|
|
writer.print("{s}{d}", .{ input, tries }) catch unreachable;
|
|
tries += 1;
|
|
var attempt = this.string_buffer.toOwnedSliceLeaky();
|
|
entry = this.used.getOrPut(attempt) catch unreachable;
|
|
if (!entry.found_existing) {
|
|
const to_use = this.string_buffer.allocator.dupe(u8, attempt) catch unreachable;
|
|
entry.key_ptr.* = to_use;
|
|
entry.value_ptr.* = tries;
|
|
|
|
entry = this.used.getOrPut(input) catch unreachable;
|
|
entry.value_ptr.* = tries;
|
|
return to_use;
|
|
}
|
|
}
|
|
} else {
|
|
entry.value_ptr.* = tries;
|
|
}
|
|
|
|
return entry.key_ptr.*;
|
|
}
|
|
};
|
|
|
|
pub fn computeInitialReservedNames(
|
|
allocator: std.mem.Allocator,
|
|
) !bun.StringHashMapUnmanaged(u32) {
|
|
var names = bun.StringHashMapUnmanaged(u32){};
|
|
|
|
const extras = .{
|
|
"Promise",
|
|
"Require",
|
|
};
|
|
|
|
try names.ensureTotalCapacityContext(
|
|
allocator,
|
|
@truncate(u32, JSLexer.Keywords.keys().len + JSLexer.StrictModeReservedWords.keys().len + 1 + extras.len),
|
|
bun.StringHashMapContext{},
|
|
);
|
|
|
|
for (JSLexer.Keywords.keys()) |keyword| {
|
|
names.putAssumeCapacity(keyword, 1);
|
|
}
|
|
|
|
for (JSLexer.StrictModeReservedWords.keys()) |keyword| {
|
|
names.putAssumeCapacity(keyword, 1);
|
|
}
|
|
|
|
inline for (comptime extras) |extra| {
|
|
names.putAssumeCapacity(extra, 1);
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
pub fn computeReservedNamesForScope(
|
|
scope: *js_ast.Scope,
|
|
symbols: *const js_ast.Symbol.Map,
|
|
names_: *bun.StringHashMapUnmanaged(u32),
|
|
allocator: std.mem.Allocator,
|
|
) void {
|
|
var names = names_.*;
|
|
defer names_.* = names;
|
|
|
|
var member_iter = scope.members.valueIterator();
|
|
while (member_iter.next()) |member| {
|
|
const symbol = symbols.get(member.ref).?;
|
|
if (symbol.kind == .unbound or symbol.must_not_be_renamed) {
|
|
names.put(allocator, symbol.original_name, 1) catch unreachable;
|
|
}
|
|
}
|
|
|
|
for (scope.generated.slice()) |ref| {
|
|
const symbol = symbols.get(ref).?;
|
|
if (symbol.kind == .unbound or symbol.must_not_be_renamed) {
|
|
names.put(allocator, symbol.original_name, 1) catch unreachable;
|
|
}
|
|
}
|
|
|
|
// If there's a direct "eval" somewhere inside the current scope, continue
|
|
// traversing down the scope tree until we find it to get all reserved names
|
|
if (scope.contains_direct_eval) {
|
|
for (scope.children.slice()) |child| {
|
|
if (child.contains_direct_eval) {
|
|
names_.* = names;
|
|
computeReservedNamesForScope(child, symbols, &names, allocator);
|
|
}
|
|
}
|
|
}
|
|
}
|