Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1341,19 +1341,17 @@ impl Tag {
}

pub fn typeof_(tag: Tag) -> Option<&'static [u8]> {
// This must only return `Some` when the operand is guaranteed to have
// no side effects. Array/object/class literals are omitted because
// their elements, properties, and static initializers can run code.
Some(match tag {
Tag::EArray
| Tag::EObject
| Tag::EArrayJSON
| Tag::EObjectJSON
| Tag::ENull
| Tag::ERegExp => b"object",
Tag::EArrayJSON | Tag::EObjectJSON | Tag::ENull | Tag::ERegExp => b"object",
Tag::EUndefined => b"undefined",
Tag::EBoolean | Tag::EBranchBoolean => b"boolean",
Tag::ENumber => b"number",
Tag::EBigInt => b"bigint",
Tag::EString => b"string",
Tag::EClass | Tag::EFunction | Tag::EArrow => b"function",
Tag::EFunction | Tag::EArrow => b"function",
_ => return None,
})
}
Expand Down
32 changes: 18 additions & 14 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,19 +1044,21 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// like .e_import_identifier tracking works
// Reminder that this can only be done after
// `target` is visited.
if let Some(rewrite) = p.maybe_rewrite_property_access(
expr.loc,
e_.target,
s.data.slice(),
unwrapped.loc,
IdentifierOpts::default()
.with_is_call_target(is_call_target)
// .is_template_tag = is_template_tag,
.with_is_delete_target(is_delete_target)
.with_assign_target(in_.assign_target),
) {
*e = rewrite;
return;
if e_.optional_chain.is_none() {
if let Some(rewrite) = p.maybe_rewrite_property_access(
expr.loc,
e_.target,
s.data.slice(),
unwrapped.loc,
IdentifierOpts::default()
.with_is_call_target(is_call_target)
// .is_template_tag = is_template_tag,
.with_is_delete_target(is_delete_target)
.with_assign_target(in_.assign_target),
) {
*e = rewrite;
return;
}
}
}
}
Expand Down Expand Up @@ -1256,7 +1258,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

let side_effects = SideEffects::to_boolean(p, &e_.value.data);
if side_effects.ok {
if side_effects.ok
&& side_effects.side_effects == SideEffects::NoSideEffects
{
*e = p.new_expr(
E::Boolean {
value: !side_effects.value,
Expand Down
107 changes: 107 additions & 0 deletions test/bundler/transpiler/simplifier-side-effects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

// The simplifier used to fold `![array]`, `!{obj}`, `typeof [array]`, etc. to a
// constant while discarding side effects inside the literal. It also inlined
// `Enum?.["A"]` to the member's constant value, dropping the optional chain.

describe("simplifier preserves side effects inside literals", () => {
function transform(code: string): string {
return new Bun.Transpiler({ loader: "js" }).transformSync(code);
}

function transformMin(code: string): string {
return new Bun.Transpiler({ loader: "js", minify: { syntax: true } }).transformSync(code);
}

for (const min of [false, true]) {
const label = min ? " (minify)" : "";
const tx = min ? transformMin : transform;

test(`![sideEffect()] keeps the call${label}`, () => {
expect(tx("const r = ![sideEffect()];")).toContain("sideEffect()");
});

test(`!{x: sideEffect()} keeps the call${label}`, () => {
expect(tx("const r = !{ x: sideEffect() };")).toContain("sideEffect()");
});

test(`!(class { static x = sideEffect(); }) keeps the call${label}`, () => {
expect(tx("const r = !(class { static x = sideEffect(); });")).toContain("sideEffect()");
});

test(`!void sideEffect() keeps the call${label}`, () => {
expect(tx("const r = !void sideEffect();")).toContain("sideEffect()");
});

test(`!![sideEffect()] keeps the call${label}`, () => {
expect(tx("const r = !![sideEffect()];")).toContain("sideEffect()");
});

test(`typeof [sideEffect()] keeps the call${label}`, () => {
expect(tx("const r = typeof [sideEffect()];")).toContain("sideEffect()");
});

test(`typeof {x: sideEffect()} keeps the call${label}`, () => {
expect(tx("const r = typeof { x: sideEffect() };")).toContain("sideEffect()");
});

test(`typeof class { static x = sideEffect(); } keeps the call${label}`, () => {
expect(tx("const r = typeof class { static x = sideEffect(); };")).toContain("sideEffect()");
});

test(`typeof [sideEffect()] === 'object' keeps the call${label}`, () => {
expect(tx("const r = typeof [sideEffect()] === 'object';")).toContain("sideEffect()");
});
}

test("pure literals still fold under !", () => {
expect(transform("export const r = !null;").trim()).toBe("export const r = true;");
expect(transform("export const r = !undefined;").trim()).toBe("export const r = true;");
expect(transform("export const r = !0;").trim()).toBe("export const r = true;");
expect(transform('export const r = !"x";').trim()).toBe("export const r = false;");
expect(transform("export const r = !function(){};").trim()).toBe("export const r = false;");
});

test("typeof pure primitives still fold", () => {
expect(transform("export const r = typeof null;").trim()).toBe('export const r = "object";');
expect(transform("export const r = typeof 123;").trim()).toBe('export const r = "number";');
expect(transform("export const r = typeof (() => {});").trim()).toBe('export const r = "function";');
});

test("side effects inside ![...] run at runtime", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`let n = 0; const fx = () => { n++; return 1 }; ` +
`const a = ![fx()]; const b = !{ x: fx() }; const c = typeof [fx()]; ` +
`console.log(n, a, b, c);`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("3 false false object\n");
expect(exitCode).toBe(0);
});
});

describe("enum optional-chain index is not inlined", () => {
const transpiler = new Bun.Transpiler({ loader: "ts" });

test("Foo?.['A'] keeps the optional-chain access", () => {
const out = transpiler.transformSync(`enum Foo { A }\nexport let y = Foo?.["A"];`);
expect(out).toContain(`Foo?.["A"]`);
});

test("Foo?.['A']() keeps the optional-chain call", () => {
const out = transpiler.transformSync(`enum Foo { A }\nexport let y = Foo?.["A"]();`);
expect(out).toContain(`let y = Foo?.["A"]();`);
});

test("Foo['A'] without optional chain is still inlined", () => {
const out = transpiler.transformSync(`enum Foo { A }\nexport let y = Foo["A"];`);
expect(out).toMatch(/let y = 0\b/);
});
});
24 changes: 14 additions & 10 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3544,16 +3544,20 @@ console.log(foo, array);
expectPrinted("typeof 'abc'", '"string"');
expectPrinted("typeof function() {}", '"function"');
expectPrinted("typeof (() => {})", '"function"');
expectPrinted("typeof {}", '"object"');
expectPrinted("typeof {foo: 123}", '"object"');
expectPrinted("typeof []", '"object"');
expectPrinted("typeof [0]", '"object"');
expectPrinted("typeof [null]", '"object"');
expectPrinted("typeof ['boolean']", '"object"');

expectPrinted('typeof [] === "object"', "!0");
expectPrinted("typeof {foo: 123} === typeof {bar: 123}", "!0");
expectPrinted("typeof {foo: 123} !== typeof 123", "!0");
// Array/object/class literals may contain side effects in their
// elements/properties, so `typeof` is not folded for them.
expectPrinted("typeof {}", "typeof {}");
expectPrinted("typeof {foo: 123}", "typeof { foo: 123 }");
expectPrinted("typeof []", "typeof []");
expectPrinted("typeof [0]", "typeof [0]");
expectPrinted("typeof [null]", "typeof [null]");
expectPrinted("typeof ['boolean']", 'typeof ["boolean"]');
expectPrinted("typeof [sideEffect()]", "typeof [sideEffect()]");
expectPrinted("typeof {x: sideEffect()}", "typeof { x: sideEffect() }");

expectPrinted('typeof [] === "object"', 'typeof [] === "object"');
expectPrinted("typeof {foo: 123} === typeof {bar: 123}", "typeof { foo: 123 } === typeof { bar: 123 }");
expectPrinted("typeof {foo: 123} !== typeof 123", 'typeof { foo: 123 } !== "number"');

expectPrinted("undefined === undefined", "!0");
expectPrinted("undefined !== undefined", "!1");
Expand Down
Loading