Skip to content

Commit f561e47

Browse files
Claude Botclaude
andcommitted
Restrict feature() to if/ternary conditions only
Adds e_branch_boolean expr type that behaves like e_boolean but: - Is only valid in if statement or ternary conditions - Panics at print time if used outside those contexts This prevents patterns like `const x = feature("FLAG")` which would defeat the purpose of compile-time dead code elimination. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 81dc0ef commit f561e47

File tree

6 files changed

+67
-42
lines changed

6 files changed

+67
-42
lines changed

src/ast/Expr.zig

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,9 @@ pub fn getObject(expr: *const Expr, name: string) ?Expr {
312312

313313
pub fn getBoolean(expr: *const Expr, name: string) ?bool {
314314
if (expr.asProperty(name)) |query| {
315-
if (query.expr.data == .e_boolean) {
316-
return query.expr.data.e_boolean.value;
315+
switch (query.expr.data) {
316+
.e_boolean, .e_branch_boolean => |b| return b.value,
317+
else => {},
317318
}
318319
}
319320
return null;
@@ -510,9 +511,10 @@ pub inline fn asStringZ(expr: *const Expr, allocator: std.mem.Allocator) OOM!?st
510511
pub fn asBool(
511512
expr: *const Expr,
512513
) ?bool {
513-
if (expr.data != .e_boolean) return null;
514-
515-
return expr.data.e_boolean.value;
514+
return switch (expr.data) {
515+
.e_boolean, .e_branch_boolean => |b| b.value,
516+
else => null,
517+
};
516518
}
517519

518520
pub fn asNumber(expr: *const Expr) ?f64 {
@@ -1490,6 +1492,7 @@ pub const Tag = enum {
14901492
e_private_identifier,
14911493
e_commonjs_export_identifier,
14921494
e_boolean,
1495+
e_branch_boolean,
14931496
e_number,
14941497
e_big_int,
14951498
e_string,
@@ -1513,7 +1516,7 @@ pub const Tag = enum {
15131516
// object, regex and array may have had side effects
15141517
pub fn isPrimitiveLiteral(tag: Tag) bool {
15151518
return switch (tag) {
1516-
.e_null, .e_undefined, .e_string, .e_boolean, .e_number, .e_big_int => true,
1519+
.e_null, .e_undefined, .e_string, .e_boolean, .e_branch_boolean, .e_number, .e_big_int => true,
15171520
else => false,
15181521
};
15191522
}
@@ -1522,7 +1525,7 @@ pub const Tag = enum {
15221525
return switch (tag) {
15231526
.e_array, .e_object, .e_null, .e_reg_exp => "object",
15241527
.e_undefined => "undefined",
1525-
.e_boolean => "boolean",
1528+
.e_boolean, .e_branch_boolean => "boolean",
15261529
.e_number => "number",
15271530
.e_big_int => "bigint",
15281531
.e_string => "string",
@@ -1537,7 +1540,7 @@ pub const Tag = enum {
15371540
.e_array => writer.writeAll("array"),
15381541
.e_unary => writer.writeAll("unary"),
15391542
.e_binary => writer.writeAll("binary"),
1540-
.e_boolean => writer.writeAll("boolean"),
1543+
.e_boolean, .e_branch_boolean => writer.writeAll("boolean"),
15411544
.e_super => writer.writeAll("super"),
15421545
.e_null => writer.writeAll("null"),
15431546
.e_undefined => writer.writeAll("undefined"),
@@ -1627,14 +1630,7 @@ pub const Tag = enum {
16271630
}
16281631
}
16291632
pub fn isBoolean(self: Tag) bool {
1630-
switch (self) {
1631-
.e_boolean => {
1632-
return true;
1633-
},
1634-
else => {
1635-
return false;
1636-
},
1637-
}
1633+
return self == .e_boolean or self == .e_branch_boolean;
16381634
}
16391635
pub fn isSuper(self: Tag) bool {
16401636
switch (self) {
@@ -1921,7 +1917,7 @@ pub const Tag = enum {
19211917

19221918
pub fn isBoolean(a: *const Expr) bool {
19231919
return switch (a.data) {
1924-
.e_boolean => true,
1920+
.e_boolean, .e_branch_boolean => true,
19251921
.e_if => |ex| ex.yes.isBoolean() and ex.no.isBoolean(),
19261922
.e_unary => |ex| ex.op == .un_not or ex.op == .un_delete,
19271923
.e_binary => |ex| switch (ex.op) {
@@ -1978,7 +1974,7 @@ pub fn maybeSimplifyNot(expr: *const Expr, allocator: std.mem.Allocator) ?Expr {
19781974
.e_null, .e_undefined => {
19791975
return expr.at(E.Boolean, E.Boolean{ .value = true }, allocator);
19801976
},
1981-
.e_boolean => |b| {
1977+
.e_boolean, .e_branch_boolean => |b| {
19821978
return expr.at(E.Boolean, E.Boolean{ .value = b.value }, allocator);
19831979
},
19841980
.e_number => |n| {
@@ -2049,7 +2045,7 @@ pub fn toStringExprWithoutSideEffects(expr: *const Expr, allocator: std.mem.Allo
20492045
.e_null => "null",
20502046
.e_string => return expr.*,
20512047
.e_undefined => "undefined",
2052-
.e_boolean => |data| if (data.value) "true" else "false",
2048+
.e_boolean, .e_branch_boolean => |data| if (data.value) "true" else "false",
20532049
.e_big_int => |bigint| bigint.value,
20542050
.e_number => |num| if (num.toString(allocator)) |str|
20552051
str
@@ -2151,6 +2147,7 @@ pub const Data = union(Tag) {
21512147
e_commonjs_export_identifier: E.CommonJSExportIdentifier,
21522148

21532149
e_boolean: E.Boolean,
2150+
e_branch_boolean: E.Boolean,
21542151
e_number: E.Number,
21552152
e_big_int: *E.BigInt,
21562153
e_string: *E.String,
@@ -2589,7 +2586,7 @@ pub const Data = union(Tag) {
25892586
const symbol = e.ref.getSymbol(symbol_table);
25902587
hasher.update(symbol.original_name);
25912588
},
2592-
inline .e_boolean, .e_number => |e| {
2589+
inline .e_boolean, .e_branch_boolean, .e_number => |e| {
25932590
writeAnyToHasher(hasher, e.value);
25942591
},
25952592
inline .e_big_int, .e_reg_exp => |e| {
@@ -2643,6 +2640,7 @@ pub const Data = union(Tag) {
26432640
return switch (this) {
26442641
.e_number,
26452642
.e_boolean,
2643+
.e_branch_boolean,
26462644
.e_null,
26472645
.e_undefined,
26482646
.e_inlined_enum,
@@ -2671,6 +2669,7 @@ pub const Data = union(Tag) {
26712669

26722670
.e_number,
26732671
.e_boolean,
2672+
.e_branch_boolean,
26742673
.e_null,
26752674
.e_undefined,
26762675
// .e_reg_exp,
@@ -2696,7 +2695,7 @@ pub const Data = union(Tag) {
26962695
// rope strings can throw when toString is called.
26972696
.e_string => |str| str.next == null,
26982697

2699-
.e_number, .e_boolean, .e_undefined, .e_null => true,
2698+
.e_number, .e_boolean, .e_branch_boolean, .e_undefined, .e_null => true,
27002699
// BigInt is deliberately excluded as a large enough BigInt could throw an out of memory error.
27012700
//
27022701

@@ -2707,7 +2706,7 @@ pub const Data = union(Tag) {
27072706
pub fn knownPrimitive(data: Expr.Data) PrimitiveType {
27082707
return switch (data) {
27092708
.e_big_int => .bigint,
2710-
.e_boolean => .boolean,
2709+
.e_boolean, .e_branch_boolean => .boolean,
27112710
.e_null => .null,
27122711
.e_number => .number,
27132712
.e_string => .string,
@@ -2843,7 +2842,7 @@ pub const Data = union(Tag) {
28432842
// +'1' => 1
28442843
return stringToEquivalentNumberValue(str.slice8());
28452844
},
2846-
.e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0),
2845+
.e_boolean, .e_branch_boolean => |b| @as(f64, if (b.value) 1.0 else 0.0),
28472846
.e_number => data.e_number.value,
28482847
.e_inlined_enum => |inlined| switch (inlined.value.data) {
28492848
.e_number => |num| num.value,
@@ -2862,7 +2861,7 @@ pub const Data = union(Tag) {
28622861

28632862
pub fn toFiniteNumber(data: Expr.Data) ?f64 {
28642863
return switch (data) {
2865-
.e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0),
2864+
.e_boolean, .e_branch_boolean => |b| @as(f64, if (b.value) 1.0 else 0.0),
28662865
.e_number => if (std.math.isFinite(data.e_number.value))
28672866
data.e_number.value
28682867
else
@@ -2953,12 +2952,12 @@ pub const Data = union(Tag) {
29532952
.ok = ok,
29542953
};
29552954
},
2956-
.e_boolean => |l| {
2955+
.e_boolean, .e_branch_boolean => |l| {
29572956
switch (right) {
2958-
.e_boolean => {
2957+
.e_boolean, .e_branch_boolean => |r| {
29592958
return .{
29602959
.ok = true,
2961-
.equal = l.value == right.e_boolean.value,
2960+
.equal = l.value == r.value,
29622961
};
29632962
},
29642963
.e_number => |num| {
@@ -2996,7 +2995,7 @@ pub const Data = union(Tag) {
29962995
.equal = l.value == r.value.data.e_number.value,
29972996
};
29982997
},
2999-
.e_boolean => |r| {
2998+
.e_boolean, .e_branch_boolean => |r| {
30002999
if (comptime kind == .loose) {
30013000
return .{
30023001
.ok = true,
@@ -3111,7 +3110,7 @@ pub const Data = union(Tag) {
31113110
.e_string => |e| e.toJS(allocator, globalObject),
31123111
.e_null => jsc.JSValue.null,
31133112
.e_undefined => .js_undefined,
3114-
.e_boolean => |boolean| if (boolean.value)
3113+
.e_boolean, .e_branch_boolean => |boolean| if (boolean.value)
31153114
.true
31163115
else
31173116
.false,

src/ast/P.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3965,6 +3965,7 @@ pub fn NewParser_(
39653965
.e_undefined,
39663966
.e_missing,
39673967
.e_boolean,
3968+
.e_branch_boolean,
39683969
.e_number,
39693970
.e_big_int,
39703971
.e_string,

src/ast/SideEffects.zig

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ pub const SideEffects = enum(u1) {
7272
.e_undefined,
7373
.e_string,
7474
.e_boolean,
75+
.e_branch_boolean,
7576
.e_number,
7677
.e_big_int,
7778
.e_inlined_enum,
@@ -88,6 +89,7 @@ pub const SideEffects = enum(u1) {
8889
.e_undefined,
8990
.e_missing,
9091
.e_boolean,
92+
.e_branch_boolean,
9193
.e_number,
9294
.e_big_int,
9395
.e_string,
@@ -545,6 +547,7 @@ pub const SideEffects = enum(u1) {
545547
.e_null,
546548
.e_undefined,
547549
.e_boolean,
550+
.e_branch_boolean,
548551
.e_number,
549552
.e_big_int,
550553
.e_string,
@@ -651,7 +654,7 @@ pub const SideEffects = enum(u1) {
651654
}
652655
switch (exp) {
653656
// Never null or undefined
654-
.e_boolean, .e_number, .e_string, .e_reg_exp, .e_function, .e_arrow, .e_big_int => {
657+
.e_boolean, .e_branch_boolean, .e_number, .e_string, .e_reg_exp, .e_function, .e_arrow, .e_big_int => {
655658
return Result{ .value = false, .side_effects = .no_side_effects, .ok = true };
656659
},
657660

@@ -770,7 +773,7 @@ pub const SideEffects = enum(u1) {
770773
.e_null, .e_undefined => {
771774
return Result{ .ok = true, .value = false, .side_effects = .no_side_effects };
772775
},
773-
.e_boolean => |e| {
776+
.e_boolean, .e_branch_boolean => |e| {
774777
return Result{ .ok = true, .value = e.value, .side_effects = .no_side_effects };
775778
},
776779
.e_number => |e| {

src/ast/visitExpr.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1691,7 +1691,7 @@ pub fn VisitExpr(
16911691
return p.newExpr(E.Boolean{ .value = false }, loc);
16921692
}
16931693
const is_enabled = p.options.features.bundler_feature_flags.map.contains(flag_string.data);
1694-
return p.newExpr(E.Boolean{ .value = is_enabled }, loc);
1694+
return .{ .data = .{ .e_branch_boolean = .{ .value = is_enabled } }, .loc = loc };
16951695
}
16961696
};
16971697
};

src/js_printer.zig

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ fn NewPrinter(
632632
binary_expression_stack: std.array_list.Managed(BinaryExpressionVisitor) = undefined,
633633

634634
was_lazy_export: bool = false,
635+
in_branch_condition: bool = false,
635636

636637
const Printer = @This();
637638

@@ -735,7 +736,7 @@ fn NewPrinter(
735736
.e_await, .e_undefined, .e_number => {
736737
left_level.* = .call;
737738
},
738-
.e_boolean => {
739+
.e_boolean, .e_branch_boolean => {
739740
// When minifying, booleans are printed as "!0 and "!1"
740741
if (p.options.minify_syntax) {
741742
left_level.* = .call;
@@ -2477,7 +2478,9 @@ fn NewPrinter(
24772478
p.print("(");
24782479
flags.remove(.forbid_in);
24792480
}
2481+
p.in_branch_condition = true;
24802482
p.printExpr(e.test_, .conditional, flags);
2483+
p.in_branch_condition = false;
24812484
p.printSpace();
24822485
p.print("?");
24832486
p.printSpace();
@@ -2690,6 +2693,24 @@ fn NewPrinter(
26902693
p.print(if (e.value) "true" else "false");
26912694
}
26922695
},
2696+
.e_branch_boolean => |e| {
2697+
// e_branch_boolean is produced by feature() from bun:bundle.
2698+
// It can only be used directly in an if statement or ternary condition.
2699+
if (!p.in_branch_condition) {
2700+
Output.panic("feature() from \"bun:bundle\" can only be used directly in an if statement or ternary condition", .{});
2701+
}
2702+
p.addSourceMapping(expr.loc);
2703+
if (p.options.minify_syntax) {
2704+
if (level.gte(Level.prefix)) {
2705+
p.print(if (e.value) "(!0)" else "(!1)");
2706+
} else {
2707+
p.print(if (e.value) "!0" else "!1");
2708+
}
2709+
} else {
2710+
p.printSpaceBeforeIdentifier();
2711+
p.print(if (e.value) "true" else "false");
2712+
}
2713+
},
26932714
.e_string => |e| {
26942715
e.resolveRopeIfNeeded(p.options.allocator);
26952716
p.addSourceMapping(expr.loc);
@@ -4790,7 +4811,9 @@ fn NewPrinter(
47904811
p.print("if");
47914812
p.printSpace();
47924813
p.print("(");
4814+
p.in_branch_condition = true;
47934815
p.printExpr(s.test_, .lowest, ExprFlag.None());
4816+
p.in_branch_condition = false;
47944817
p.print(")");
47954818

47964819
switch (s.yes.data) {

test/bundler/bundler_feature_flag.test.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,16 @@ if (feature("SUPER_SECRET")) {
5353
files: {
5454
"/a.js": `
5555
import { feature } from "bun:bundle";
56-
const a = feature("FLAG_A");
57-
const b = feature("FLAG_B");
58-
const c = feature("FLAG_C");
59-
console.log(a, b, c);
56+
if (feature("FLAG_A")) console.log("FLAG_A");
57+
if (feature("FLAG_B")) console.log("FLAG_B");
58+
if (feature("FLAG_C")) console.log("FLAG_C");
6059
`,
6160
},
6261
features: ["FLAG_A", "FLAG_C"],
6362
onAfterBundle(api) {
6463
// FLAG_A and FLAG_C are enabled, FLAG_B is not
65-
api.expectFile("out.js").toInclude("a = true");
66-
api.expectFile("out.js").toInclude("b = false");
67-
api.expectFile("out.js").toInclude("c = true");
64+
api.expectFile("out.js").toInclude("true");
65+
api.expectFile("out.js").toInclude("false");
6866
},
6967
});
7068

@@ -95,8 +93,9 @@ if (feature("DISABLED_FEATURE")) {
9593
files: {
9694
"/a.js": `
9795
import { feature } from "bun:bundle";
98-
const x = feature("TEST");
99-
console.log(x);
96+
if (feature("TEST")) {
97+
console.log("test enabled");
98+
}
10099
`,
101100
},
102101
onAfterBundle(api) {

0 commit comments

Comments
 (0)