Add typeof Bun DCE support for --target=bun

- Handle typeof Bun !== 'undefined' and typeof Bun === 'undefined' patterns
- Support both direct identifier (typeof Bun) and dot expressions (typeof globalThis.Bun)
- Works with strict (===, !==) and loose (==, !=) equality operators
- Handles reverse comparisons ('undefined' === typeof Bun)
- Dead code elimination now works for all common Bun detection patterns

Test coverage includes:
- Direct Bun identifier checks
- globalThis.Bun property checks
- process.versions.bun property checks
- typeof Bun patterns (all variations)

All patterns properly eliminate dead code while preserving runtime values.
This commit is contained in:
Claude Bot
2025-09-09 14:14:11 +00:00
parent e5bb815302
commit 2b9276f599
2 changed files with 171 additions and 0 deletions

View File

@@ -764,6 +764,110 @@ pub const SideEffects = enum(u1) {
}
}
}
// Check for typeof Bun patterns in binary expressions
if (exp == .e_binary) {
const e_ = exp.e_binary;
switch (e_.op) {
.bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne => {
// Check for typeof Bun !== "undefined" or typeof Bun === "undefined"
if (e_.left.data == .e_unary) {
const unary = e_.left.data.e_unary;
if (unary.op == .un_typeof) {
// Handle typeof identifier (e.g., typeof Bun)
if (unary.value.data == .e_identifier) {
const ident = unary.value.data.e_identifier;
if (p.define.forIdentifier(p.loadNameFromRef(ident.ref))) |define| {
if (define.is_truthy()) {
// Bun is truthy, so typeof Bun is NOT "undefined"
if (e_.right.data == .e_string) {
const str = e_.right.data.e_string;
if (str.eqlComptime("undefined")) {
// typeof Bun === "undefined" -> false
// typeof Bun !== "undefined" -> true
const is_not_equal = e_.op == .bin_strict_ne or e_.op == .bin_loose_ne;
return Result{ .ok = true, .value = is_not_equal, .side_effects = .could_have_side_effects };
}
}
}
}
}
// Handle typeof dot expression (e.g., typeof globalThis.Bun)
if (unary.value.data == .e_dot) {
const dot = unary.value.data.e_dot;
if (p.define.dots.get(dot.name)) |parts| {
for (parts) |*define| {
if (p.isDotDefineMatch(unary.value, define.parts)) {
if (define.data.is_truthy()) {
// globalThis.Bun is truthy, so typeof globalThis.Bun is NOT "undefined"
if (e_.right.data == .e_string) {
const str = e_.right.data.e_string;
if (str.eqlComptime("undefined")) {
// typeof globalThis.Bun === "undefined" -> false
// typeof globalThis.Bun !== "undefined" -> true
const is_not_equal = e_.op == .bin_strict_ne or e_.op == .bin_loose_ne;
return Result{ .ok = true, .value = is_not_equal, .side_effects = .could_have_side_effects };
}
}
}
break;
}
}
}
}
}
}
// Also check the reverse: "undefined" === typeof Bun
if (e_.right.data == .e_unary) {
const unary = e_.right.data.e_unary;
if (unary.op == .un_typeof) {
// Handle typeof identifier (e.g., typeof Bun)
if (unary.value.data == .e_identifier) {
const ident = unary.value.data.e_identifier;
if (p.define.forIdentifier(p.loadNameFromRef(ident.ref))) |define| {
if (define.is_truthy()) {
// Bun is truthy, so typeof Bun is NOT "undefined"
if (e_.left.data == .e_string) {
const str = e_.left.data.e_string;
if (str.eqlComptime("undefined")) {
// "undefined" === typeof Bun -> false
// "undefined" !== typeof Bun -> true
const is_not_equal = e_.op == .bin_strict_ne or e_.op == .bin_loose_ne;
return Result{ .ok = true, .value = is_not_equal, .side_effects = .could_have_side_effects };
}
}
}
}
}
// Handle typeof dot expression (e.g., typeof globalThis.Bun)
if (unary.value.data == .e_dot) {
const dot = unary.value.data.e_dot;
if (p.define.dots.get(dot.name)) |parts| {
for (parts) |*define| {
if (p.isDotDefineMatch(unary.value, define.parts)) {
if (define.data.is_truthy()) {
// globalThis.Bun is truthy, so typeof globalThis.Bun is NOT "undefined"
if (e_.left.data == .e_string) {
const str = e_.left.data.e_string;
if (str.eqlComptime("undefined")) {
// "undefined" === typeof globalThis.Bun -> false
// "undefined" !== typeof globalThis.Bun -> true
const is_not_equal = e_.op == .bin_strict_ne or e_.op == .bin_loose_ne;
return Result{ .ok = true, .value = is_not_equal, .side_effects = .could_have_side_effects };
}
}
}
break;
}
}
}
}
}
}
},
else => {},
}
}
return toBooleanWithoutDCECheck(exp);
}

View File

@@ -187,6 +187,73 @@ test("compare dead code elimination: --target=bun vs --target=node", async () =>
expect(nodeBundle).toContain("process.versions.node");
});
test("dead code elimination for typeof Bun checks with --target=bun", async () => {
using dir = tempDir("bun-typeof-dce", {
"index.js": `
// Test typeof Bun checks
if (typeof Bun !== "undefined") {
exports.test1 = "bun-typeof";
} else {
exports.test1 = "not-bun-typeof";
require("fs").writeFileSync("typeof-fail.txt", "should not exist");
}
// Test typeof globalThis.Bun checks
if (typeof globalThis.Bun !== "undefined") {
exports.test2 = "bun-typeof-global";
} else {
exports.test2 = "not-bun-typeof-global";
require("fs").writeFileSync("typeof-global-fail.txt", "should not exist");
}
// Test reverse order
if ("undefined" === typeof Bun) {
exports.test3 = "not-bun-reverse";
require("fs").writeFileSync("reverse-fail.txt", "should not exist");
} else {
exports.test3 = "bun-reverse";
}
// Test != instead of !==
if (typeof Bun != "undefined") {
exports.test4 = "bun-loose-ne";
} else {
exports.test4 = "not-bun-loose-ne";
}
`,
});
// Build with --target=bun
await using bundleProc = Bun.spawn({
cmd: [bunExe(), "build", "index.js", "--target=bun", "--outfile=bundle.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const bundleCode = await bundleProc.exited;
expect(bundleCode).toBe(0);
const bundled = await Bun.file(String(dir) + "/bundle.js").text();
// All "not-bun" branches should be eliminated
expect(bundled).not.toContain("not-bun-typeof");
expect(bundled).not.toContain("not-bun-typeof-global");
expect(bundled).not.toContain("not-bun-reverse");
expect(bundled).not.toContain("not-bun-loose-ne");
// None of the fail files should be referenced
expect(bundled).not.toContain("typeof-fail.txt");
expect(bundled).not.toContain("typeof-global-fail.txt");
expect(bundled).not.toContain("reverse-fail.txt");
// The "bun" branches should remain
expect(bundled).toContain("bun-typeof");
expect(bundled).toContain("bun-typeof-global");
expect(bundled).toContain("bun-reverse");
expect(bundled).toContain("bun-loose-ne");
});
test("--target=bun does not hardcode runtime values", async () => {
using dir = tempDir("bun-no-hardcode", {
"index.js": `