Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 18 additions & 11 deletions src/js_parser/scan/scan_side_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,25 @@ impl SideEffects {
}

pub(crate) fn is_primitive_to_reorder(data: &ExprData) -> bool {
matches!(
data,
match data {
ExprData::ENull(_)
| ExprData::EUndefined(_)
| ExprData::EString(_)
| ExprData::EBoolean(_)
| ExprData::EBranchBoolean(_)
| ExprData::ENumber(_)
| ExprData::EBigInt(_)
| ExprData::EInlinedEnum(_)
| ExprData::ERequireMain
)
| ExprData::EUndefined(_)
| ExprData::EString(_)
| ExprData::EBoolean(_)
| ExprData::EBranchBoolean(_)
| ExprData::ENumber(_)
| ExprData::EBigInt(_)
| ExprData::EInlinedEnum(_)
| ExprData::ERequireMain => true,
// `print_number` spells an infinite number as `1 / 0` (`Infinity` may be shadowed) and
// only minify_syntax folds that back, so it has to count as the literal it stands for:
// otherwise `"a" != Infinity` transpiles to `"a" != 1 / 0`, and that to `1 / 0 != "a"`.
ExprData::EBinary(e) if e.op == Op::Code::BinDiv => {
Expr::extract_numeric_values(&e.left.data, &e.right.data)
.is_some_and(|[dividend, divisor]| (dividend / divisor).is_infinite())
}
_ => false,
}
}

pub(crate) fn simplify_unused_expr<'a, const TS: bool, const SCAN: bool>(
Expand Down
68 changes: 68 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5399,6 +5399,74 @@ describe("numeric property keys that overflow to Infinity", () => {
});
});

// The parser moves literal operands of ==, !=, === and !== to the right-hand side. Since an
// infinite number is printed as "1 / 0" (see above), that division has to count as a literal too,
// or transpiling the output a second time swaps the operands again:
// `"a" != Infinity` -> `"a" != 1 / 0` -> `1 / 0 != "a"`.
describe("equality comparisons against an infinite number", () => {
const plain = new Bun.Transpiler({ loader: "ts" });
const minifier = new Bun.Transpiler({ loader: "ts", minifyWhitespace: true });

function expectStablePrint(transpiler, input, output) {
const once = transpiler.transformSync(input);
expect(once).toBe(output);
expect(transpiler.transformSync(once)).toBe(output);
}
hideFromStackTrace(expectStablePrint);

it.each([
['"a" != Infinity', '"a" != 1 / 0', '"a"!=1/0'],
['"a" == -Infinity', '"a" == -1 / 0', '"a"==-1/0'],
['"a" == 1e999', '"a" == 1 / 0', '"a"==1/0'],
['"a" != -1e999', '"a" != -1 / 0', '"a"!=-1/0'],
["1n === Infinity", "1n === 1 / 0", "1n===1/0"],
["1n !== -1e999", "1n !== -1 / 0", "1n!==-1/0"],
["require.main === Infinity", "require.main === 1 / 0", "require.main===1/0"],
["require.main !== -Infinity", "require.main !== -1 / 0", "require.main!==-1/0"],
// Non-literal operands still move to the left, no matter how the infinity is spelled.
["Infinity != x", "x != 1 / 0", "x!=1/0"],
["-1e999 === x", "x === -1 / 0", "x===-1/0"],
["1 / 0 != x", "x != 1 / 0", "x!=1/0"],
["-1 / 0 === x", "x === -1 / 0", "x===-1/0"],
['"a" != 1 / 0', '"a" != 1 / 0', '"a"!=1/0'],
['"a" == -1 / 0', '"a" == -1 / 0', '"a"==-1/0'],
// Only a division that evaluates to an infinity stands for a number literal.
["1 / 2 == x", "1 / 2 == x", "1/2==x"],
["0 / 0 == x", "0 / 0 == x", "0/0==x"],
["x / 0 == y", "x / 0 == y", "x/0==y"],
['"a" == 1 / 2', '1 / 2 == "a"', '1/2=="a"'],
['"a" == x / 0', 'x / 0 == "a"', 'x/0=="a"'],
])("%s prints the same way on every pass", (input, output, minified) => {
expectStablePrint(plain, `y = ${input};`, `y = ${output};\n`);
expectStablePrint(minifier, `y = ${input};`, `y=${minified};`);
});

it("holds in a file that declares its own Infinity", () => {
expectStablePrint(
plain,
'function f(Infinity) {}\ny = "a" != 1e999;',
'function f(Infinity) {}\ny = "a" != 1 / 0;\n',
);
});

it("an inlined enum member whose value is infinite stays on the right", () => {
const enumObject = 'var F;\n((F) => {\n F[F["A"] = 1 / 0] = "A";\n})(F ||= {});\n';
const once = plain.transformSync('const enum F { A = 1e999 }\ny = "a" == F.A;');
expect(once).toBe(`${enumObject}y = "a" == 1 / 0 /* A */;\n`);
// Only the inlining comment goes away on the second pass, not the operand order.
expect(plain.transformSync(once)).toBe(`${enumObject}y = "a" == 1 / 0;\n`);
});

it("reordering a division keeps its value", () => {
const out = plain.transformSync(`
var x = 2;
var result = [1 / 0 != x, -1 / 0 === x, 1 / 0 != Infinity, -1 / 0 === -Infinity];
`);
expect(out).toBe("var x = 2;\nvar result = [x != 1 / 0, x === -1 / 0, 1 / 0 != 1 / 0, -1 / 0 === -1 / 0];\n");
expect(new Function(`${out}; return result;`)()).toEqual([true, false, false, true]);
});
});

describe("parse error flood", () => {
it("reports duplicate-binding floods in linear time", async () => {
await using proc = Bun.spawn({
Expand Down
Loading