mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
fix(minifier): model uninitialized module vars as undefined (#25497)
An uninitialized module-level `var` is `undefined` until it is assigned. After Rolldown removed an unreachable setter, Oxc did not keep this fact, so a dead optional call could remain in the output.
This PR records safe uninitialized module `var` bindings as known `undefined`. Existing minifier passes can then remove optional chains and simplify other reads.
The optimization is limited to module scope. It is disabled for resolved writes, redeclarations, and direct `eval`. Script globals and function-local variables are not changed.
Direct aliases keep the same known value without replacing every read with `void 0`. This avoids larger output and lets the existing alias cleanup work normally.
Generic optimizer tests now use function parameters when they need an unknown value. Focused tests cover the new behavior for uninitialized module variables.
## Example
Input:
```js
var slot
function make(value) {
slot?.(value)
return value
}
console.log(make(1))
```
Before:
```js
var slot;
function make(value) {
slot?.(value);
return value;
}
console.log(make(1));
```
After:
```js
function make(value) {
return value;
}
console.log(make(1));
```
Verified with the full `oxc_minifier` test suite (649 passed, 34 ignored), Clippy, formatting, minsize, and allocation checks. Minsize and allocation snapshots are unchanged.
Fixes rolldown/rolldown#10659
AI-assisted by OpenAI Codex.
This commit is contained in:
1 parent
63ff8ef0a1
commit
b13fd48471
11 files changed
+290
-164
No files matched your search
@@ -32,16 +32,19 @@ impl<'a> PeepholeOptimizations {
|
||||
// for-statement initializers have their value set by the for statement itself.
|
||||
None
|
||||
} else if declaration_kind.is_var()
|
||||
&& !Self::is_hoisted_var_inlineable(
|
||||
decl,
|
||||
symbol_id,
|
||||
declaration_in_body_statement_list,
|
||||
ctx,
|
||||
)
|
||||
&& decl.init.is_some()
|
||||
&& !Self::is_hoisted_var_inlineable(symbol_id, declaration_in_body_statement_list, ctx)
|
||||
{
|
||||
// `var` is hoisted: reads before the initializer line see `undefined`.
|
||||
// Skip unless the safety predicate proves no such read exists.
|
||||
None
|
||||
} else if declaration_kind.is_var()
|
||||
&& decl.init.is_none()
|
||||
&& !Self::is_module_scope_symbol(symbol_id, ctx)
|
||||
{
|
||||
// Keep the new fact module-scoped. Script globals alias the global
|
||||
// object, and function-scoped vars have additional runtime cases.
|
||||
None
|
||||
} else {
|
||||
// No initializer hoists to `undefined`; otherwise reuse the constant.
|
||||
decl.init.as_ref().map_or(Some(ConstantValue::Undefined), |_| init_constant)
|
||||
@@ -53,7 +56,21 @@ impl<'a> PeepholeOptimizations {
|
||||
} else {
|
||||
FreshValueKind::None
|
||||
};
|
||||
ctx.init_value(symbol_id, value, kind, falsy_init, decl.init.is_none());
|
||||
let implicit_undefined = decl
|
||||
.init
|
||||
.as_ref()
|
||||
.is_none_or(|init| Self::is_direct_implicit_undefined_alias(init, ctx));
|
||||
ctx.init_value(symbol_id, value, kind, falsy_init, implicit_undefined);
|
||||
}
|
||||
|
||||
/// Preserve the provenance of an implicit `undefined` through direct
|
||||
/// aliases. Materializing the value as `void 0` can be larger than keeping
|
||||
/// the alias and can preempt statement-level single-use substitution.
|
||||
fn is_direct_implicit_undefined_alias(init: &Expression<'a>, ctx: &TraverseCtx<'a>) -> bool {
|
||||
init.get_identifier_reference()
|
||||
.and_then(|ident| ctx.scoping().get_reference(ident.reference_id()).symbol_id())
|
||||
.and_then(|symbol_id| ctx.state.symbols.value(symbol_id))
|
||||
.is_some_and(|value| value.implicit_undefined)
|
||||
}
|
||||
|
||||
/// A `ConstantValue` that coerces to `false` (`false`, `0`/`-0`/`NaN`, `""`,
|
||||
@@ -90,8 +107,8 @@ impl<'a> PeepholeOptimizations {
|
||||
/// still in its declarative prelude;
|
||||
/// - the declaration is a direct body statement-list item rather than a
|
||||
/// conditional, loop, or other nested statement position;
|
||||
/// - it has an initializer (uninitialized `var foo;` would inline to
|
||||
/// `undefined`, which churns existing tests for marginal benefit);
|
||||
/// - it has an initializer (initializer-less vars use the simpler predicate
|
||||
/// below because they are `undefined` both before and after the declaration);
|
||||
/// - script-mode top-level vars are excluded (they alias the global object);
|
||||
/// - at program scope, if the module loads any other module (`import`,
|
||||
/// `export … from`, `export * from`), skip: a cyclic importer can call
|
||||
@@ -110,15 +127,11 @@ impl<'a> PeepholeOptimizations {
|
||||
/// already been visited and won't be inlined. Safe but suboptimal; the
|
||||
/// common "flag declared at the top" pattern is unaffected.
|
||||
fn is_hoisted_var_inlineable(
|
||||
decl: &VariableDeclarator<'a>,
|
||||
symbol_id: SymbolId,
|
||||
declaration_in_body_statement_list: bool,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> bool {
|
||||
if decl.init.is_none()
|
||||
|| !declaration_in_body_statement_list
|
||||
|| Self::is_script_root_scope(ctx)
|
||||
{
|
||||
if !declaration_in_body_statement_list || Self::is_script_root_scope(ctx) {
|
||||
return false;
|
||||
}
|
||||
// `hoisted_var_inlining_unsafe` is set by a preceding non-declarative
|
||||
@@ -137,6 +150,12 @@ impl<'a> PeepholeOptimizations {
|
||||
reads.all(|read| Self::read_crosses_function_boundary(read.scope_id(), frame.scope_id, ctx))
|
||||
}
|
||||
|
||||
/// Whether a symbol is declared in the root scope of a module.
|
||||
fn is_module_scope_symbol(symbol_id: SymbolId, ctx: &TraverseCtx<'a>) -> bool {
|
||||
ctx.source_type().is_module()
|
||||
&& ctx.scoping().symbol_scope_id(symbol_id) == ctx.scoping().root_scope_id()
|
||||
}
|
||||
|
||||
/// Classify the fresh value an expression creates (a value that cannot alias
|
||||
/// another binding and has no setters/getters that could trigger side effects
|
||||
/// on property writes), or `FreshValueKind::None` when it is not fresh.
|
||||
|
||||
@@ -85,13 +85,13 @@ pub struct SymbolValue<'a> {
|
||||
/// `None` when the value is not a constant evaluated value.
|
||||
pub initialized_constant: Option<ConstantValue<'a>>,
|
||||
|
||||
/// The `initialized_constant` is the implicit `undefined` of a declaration
|
||||
/// with no initializer (`let x;`), not an evaluated initializer. Textually
|
||||
/// inlining such a read prints `void 0` — longer than a mangled identifier
|
||||
/// read — and there is no initializer whose elimination pays for it, so
|
||||
/// `inline_identifier_reference` skips it (rolldown#10174). Constant-driven
|
||||
/// folds (`if (x)`, `x === void 0`, `return x`) are unaffected: they
|
||||
/// resolve the value through `initialized_constant`.
|
||||
/// The `initialized_constant` originates from the implicit `undefined` of
|
||||
/// a declaration with no initializer, possibly through direct aliases.
|
||||
/// Textually inlining such a read prints `void 0` — longer than a mangled
|
||||
/// identifier read — and can also preempt more profitable alias collapsing,
|
||||
/// so `inline_identifier_reference` skips it (rolldown#10174).
|
||||
/// Constant-driven folds (`if (x)`, `x === void 0`, `return x`) are
|
||||
/// unaffected: they resolve the value through `initialized_constant`.
|
||||
pub implicit_undefined: bool,
|
||||
|
||||
pub references: ReferenceCounts,
|
||||
|
||||
@@ -312,7 +312,7 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
constant: Option<ConstantValue<'a>>,
|
||||
kind: FreshValueKind,
|
||||
falsy_init: bool,
|
||||
init_absent: bool,
|
||||
implicit_undefined_source: bool,
|
||||
) {
|
||||
let mut references = ReferenceCounts::default();
|
||||
for reference in self.scoping().get_resolved_references(symbol_id) {
|
||||
@@ -358,9 +358,10 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
};
|
||||
|
||||
// See `SymbolValue::implicit_undefined` — only meaningful when the
|
||||
// recorded constant is the hoist-produced `undefined` of `let x;`.
|
||||
let implicit_undefined =
|
||||
init_absent && initialized_constant.as_ref().is_some_and(ConstantValue::is_undefined);
|
||||
// recorded constant is the implicit `undefined` of an uninitialized
|
||||
// binding or a direct alias of one.
|
||||
let implicit_undefined = implicit_undefined_source
|
||||
&& initialized_constant.as_ref().is_some_and(ConstantValue::is_undefined);
|
||||
|
||||
let symbol_value = SymbolValue {
|
||||
initialized_constant,
|
||||
|
||||
@@ -816,6 +816,42 @@ fn fold_optional_chain_on_undefined_let_binding() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_optional_chain_on_undefined_var_binding() {
|
||||
// https://github.com/rolldown/rolldown/issues/10659
|
||||
// An uninitialized module-scoped `var` with no writes is always `undefined`,
|
||||
// including before its declaration is evaluated.
|
||||
test(
|
||||
"var slot; function setSlot(cb) { slot = cb } function make(v) { slot?.(v); return v } console.log(make(1))",
|
||||
"function make(v) { return v } console.log(make(1))",
|
||||
);
|
||||
test(
|
||||
"var slot; function make(v) { slot?.(v); return v } console.log(make(1))",
|
||||
"function make(v) { return v } console.log(make(1))",
|
||||
);
|
||||
test("var slot; export function call() { slot?.foo }", "export function call() {}");
|
||||
|
||||
// Constant-driven folds consume the same general `undefined` value. The
|
||||
// provenance flag only prevents textual substitution with `void 0`.
|
||||
test("var slot; export function read() { return slot }", "export function read() {}");
|
||||
|
||||
// A resolved write means the binding is not statically undefined.
|
||||
test_same(
|
||||
"var slot; export function setSlot(v) { slot = v } export function call() { slot?.() }",
|
||||
);
|
||||
// Script-root `var`s are observable and mutable through the global object.
|
||||
test_same_source_type("var slot; function call() { slot?.() } call()", SourceType::script());
|
||||
// Function-scoped vars are outside this optimization.
|
||||
test_same("export function call() { var slot; slot?.() }");
|
||||
// A name inside `with` may resolve to a property instead of a local binding.
|
||||
test_same_source_type(
|
||||
"function call(obj) { var slot; with (obj) slot?.() } call(obj)",
|
||||
SourceType::script(),
|
||||
);
|
||||
// Direct `eval` can assign to the local without a resolved write reference.
|
||||
test_same("var slot; eval('slot = fn'); export function call() { slot?.() }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_optional_chain_on_null_const_binding() {
|
||||
// A `const` initialized to `null` resolves to `ValueType::Null`, so the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::test;
|
||||
use crate::{test, test_same};
|
||||
|
||||
/// Esbuild minfication tests
|
||||
///
|
||||
@@ -233,7 +233,10 @@ fn js_parser_test() {
|
||||
// "for (; x; ) { let y = function() { }; var y = y; }",
|
||||
// );
|
||||
test("while (x) { if (y) continue; let y }", "for (; x; ) { if (y) continue; let y; }");
|
||||
test("while (x) { if (y) continue; var y }", "for (; x; ) if (!y) var y; ");
|
||||
test(
|
||||
"v = function(x, y) { while (x) { if (y) continue; var y } }",
|
||||
"v = function(x, y) { for (; x; ) if (!y) var y }",
|
||||
);
|
||||
test("console.log(undefined)", "console.log(void 0);");
|
||||
test("console.log(+undefined)", "console.log(NaN);");
|
||||
test("console.log(undefined + undefined)", "console.log(NaN);");
|
||||
@@ -985,20 +988,20 @@ fn constant_evaluation_test() {
|
||||
|
||||
#[test]
|
||||
fn test_remove_dead_expr_nullish_related() {
|
||||
test("var a; a != null && a.b()", "var a; a?.b();");
|
||||
test("var a; a == null || a.b()", "var a; a?.b();");
|
||||
test("var a; null != a && a.b()", "var a; a?.b();");
|
||||
test("var a; null == a || a.b()", "var a; a?.b();");
|
||||
test("var a; a == null && a.b()", "var a; a ?? a.b();");
|
||||
test("var a; a != null || a.b()", "var a; a ?? a.b();");
|
||||
test("var a; null == a && a.b()", "var a; a ?? a.b();");
|
||||
test("var a; null != a || a.b()", "var a; a ?? a.b();");
|
||||
test("v = function(a) { a != null && a.b() }", "v = function(a) { a?.b() }");
|
||||
test("v = function(a) { a == null || a.b() }", "v = function(a) { a?.b() }");
|
||||
test("v = function(a) { null != a && a.b() }", "v = function(a) { a?.b() }");
|
||||
test("v = function(a) { null == a || a.b() }", "v = function(a) { a?.b() }");
|
||||
test("v = function(a) { a == null && a.b() }", "v = function(a) { a ?? a.b() }");
|
||||
test("v = function(a) { a != null || a.b() }", "v = function(a) { a ?? a.b() }");
|
||||
test("v = function(a) { null == a && a.b() }", "v = function(a) { a ?? a.b() }");
|
||||
test("v = function(a) { null != a || a.b() }", "v = function(a) { a ?? a.b() }");
|
||||
test("x = a != null && a.b()", "x = a != null && a.b();");
|
||||
test("x = a == null || a.b()", "x = a == null || a.b();");
|
||||
test("var a; if (a != null) a.b()", "var a; a?.b();");
|
||||
test("var a; if (a == null) ; else a.b()", "var a; a?.b();");
|
||||
test("var a; if (a == null) a.b()", "var a; a ?? a.b();");
|
||||
test("var a; if (a != null) ; else a.b()", "var a; a ?? a.b();");
|
||||
test("v = function(a) { if (a != null) a.b() }", "v = function(a) { a?.b() }");
|
||||
test("v = function(a) { if (a == null); else a.b() }", "v = function(a) { a?.b() }");
|
||||
test("v = function(a) { if (a == null) a.b() }", "v = function(a) { a ?? a.b() }");
|
||||
test("v = function(a) { if (a != null); else a.b() }", "v = function(a) { a ?? a.b() }");
|
||||
test("x(y ?? 1)", "x(y ?? 1);");
|
||||
test("x(y.z ?? 1)", "x(y.z ?? 1);");
|
||||
test("x(y[z] ?? 1)", "x(y[z] ?? 1);");
|
||||
@@ -1541,12 +1544,12 @@ fn test_remove_dead_expr() {
|
||||
"var bound; unbound, {...unbound}, unbound;",
|
||||
);
|
||||
test(
|
||||
"var bound; ({x: 123, bound, ...bound, [bound]: null, y: 234})",
|
||||
"var bound; ({...bound});",
|
||||
"v = function(bound) { ({x: 123, bound, ...bound, [bound]: null, y: 234}) }",
|
||||
"v = function(bound) { ({...bound}) }",
|
||||
);
|
||||
test(
|
||||
"var bound; ({x: 123, bound, ...bound, [bound]: foo(), y: 234})",
|
||||
"var bound; ({...bound}), foo();",
|
||||
"v = function(bound) { ({x: 123, bound, ...bound, [bound]: foo(), y: 234}) }",
|
||||
"v = function(bound) { ({...bound}), foo() }",
|
||||
);
|
||||
test("console.log(1, foo(), bar())", "console.log(1, foo(), bar());");
|
||||
test("/* @__PURE__ */ console.log(1, foo(), bar())", "foo(), bar();");
|
||||
@@ -1604,17 +1607,17 @@ fn test_remove_dead_expr() {
|
||||
test("foo ? bar : 2", "foo && bar;");
|
||||
test("foo ? bar : baz", "foo ? bar : baz;");
|
||||
test("foo && bar", "foo && bar;");
|
||||
test("var foo; foo && bar", "var foo;foo && bar;");
|
||||
test("var bar; foo && bar", "var bar;foo;");
|
||||
test("var foo, bar; foo && bar", "var foo, bar;");
|
||||
test_same("v = function(foo) { foo && bar }");
|
||||
test("v = function(bar) { foo && bar }", "v = function(bar) { foo }");
|
||||
test("v = function(foo, bar) { foo && bar }", "v = function(foo, bar) {}");
|
||||
test("foo || bar", "foo || bar;");
|
||||
test("var foo; foo || bar", "var foo;foo || bar;");
|
||||
test("var bar; foo || bar", "var bar;foo;");
|
||||
test("var foo, bar; foo || bar", "var foo, bar;");
|
||||
test_same("v = function(foo) { foo || bar }");
|
||||
test("v = function(bar) { foo || bar }", "v = function(bar) { foo }");
|
||||
test("v = function(foo, bar) { foo || bar }", "v = function(foo, bar) {}");
|
||||
test("foo ?? bar", "foo ?? bar;");
|
||||
test("var foo; foo ?? bar", "var foo;foo ?? bar;");
|
||||
test("var bar; foo ?? bar", "var bar;foo;");
|
||||
test("var foo, bar; foo ?? bar", "var foo, bar;");
|
||||
test_same("v = function(foo) { foo ?? bar }");
|
||||
test("v = function(bar) { foo ?? bar }", "v = function(bar) { foo }");
|
||||
test("v = function(foo, bar) { foo ?? bar }", "v = function(foo, bar) {}");
|
||||
test("tag`a${b}c${d}e`", "tag`a${b}c${d}e`;");
|
||||
test("`a${b}c${d}e`", "`${b}${d}`;");
|
||||
test("`stuff ${x} ${1}`", "`${x}`;");
|
||||
@@ -1635,64 +1638,64 @@ fn test_remove_dead_expr() {
|
||||
#[test]
|
||||
fn test_inline_single_use_variable() {
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) {var x = foo; return x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return foo;}",
|
||||
"function wrapper(foo, arg0, arg1) {var x = foo; return x}",
|
||||
"function wrapper(foo, arg0, arg1) { return foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return foo;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return x}",
|
||||
"function wrapper(foo, arg0, arg1) { return foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) {const x = foo; return x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return foo;}",
|
||||
"function wrapper(foo, arg0, arg1) {const x = foo; return x}",
|
||||
"function wrapper(foo, arg0, arg1) { return foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; if (false) x++; return x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return foo;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; if (false) x++; return x}",
|
||||
"function wrapper(foo, arg0, arg1) { return foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; if (true) x++; return x}",
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return x++, x;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return x + x}",
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return x + x;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return x + x}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return x + x;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return +x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return +foo;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return +x}",
|
||||
"function wrapper(foo, arg0, arg1) { return +foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return -x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return -foo;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return -x}",
|
||||
"function wrapper(foo, arg0, arg1) { return -foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return !x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return !foo;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return !x}",
|
||||
"function wrapper(foo, arg0, arg1) { return !foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return ~x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return ~foo;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return ~x}",
|
||||
"function wrapper(foo, arg0, arg1) { return ~foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return void x}",
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return typeof x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return typeof foo;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return typeof x}",
|
||||
"function wrapper(foo, arg0, arg1) { return typeof foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return `<${x}>`}",
|
||||
"var foo; function wrapper(arg0, arg1) { return `<${foo}>`;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return x + 2}",
|
||||
"var foo; function wrapper(arg0, arg1) { return foo + 2;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return x + 2}",
|
||||
"function wrapper(foo, arg0, arg1) { return foo + 2;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return 2 + x}",
|
||||
"var foo; function wrapper(arg0, arg1) { return 2 + foo;}",
|
||||
"function wrapper(foo, arg0, arg1) { let x = foo; return 2 + x}",
|
||||
"function wrapper(foo, arg0, arg1) { return 2 + foo;}",
|
||||
);
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return x + arg0}",
|
||||
@@ -1966,6 +1969,9 @@ fn test_inline_single_use_variable() {
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; let y = ++x; return y}",
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return ++x;}",
|
||||
);
|
||||
// Keep implicit-undefined provenance through direct aliases. Expanding both
|
||||
// reads to `void 0` is larger and prevents the single-use `y` alias from
|
||||
// collapsing back into `x`.
|
||||
test(
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; let y = x; return [x, y]}",
|
||||
"var foo; function wrapper(arg0, arg1) { let x = foo; return [x, x];}",
|
||||
|
||||
@@ -671,7 +671,7 @@ fn test_fold_nullish_coalesce() {
|
||||
fold("a() ?? (1 ?? b())", "a() ?? 1");
|
||||
fold("(a() ?? 1) ?? b()", "a() ?? 1 ?? b()");
|
||||
|
||||
test_same("var y; x = (y ?? 1)()"); // can compress to "var y; x = y()" if y is not null or undefined
|
||||
test_same("v = function(y) { x = (y ?? 1)() }");
|
||||
test_same("var y; x = (y.z ?? 1)()"); // "var y; x = (0, y.z)()" if y is not null or undefined
|
||||
test("var y; x = (null ?? y)()", "var y; x = y()");
|
||||
test("var y; x = (null ?? y.z)()", "var y; x = (0, y.z)()");
|
||||
@@ -1279,7 +1279,7 @@ fn test_fold_instance_of() {
|
||||
|
||||
// An unknown value should never be folded.
|
||||
fold_same("x instanceof Foo");
|
||||
test_same("var x; foo(x instanceof Object)");
|
||||
test_same("v = function(x) { foo(x instanceof Object) }");
|
||||
fold_same("x instanceof Object");
|
||||
fold_same("0 instanceof Foo");
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ fn test_minimize_expr_condition() {
|
||||
test("(x ? false : true) && y()", "!x && y()");
|
||||
test("(x ? true : y) && y()", "(x || y) && y();");
|
||||
test("(x ? y : false) && y()", "(x && y) && y()");
|
||||
test("var x; (x && true) && y()", "var x; x && y()");
|
||||
test("v = function(x) { (x && true) && y() }", "v = function(x) { x && y() }");
|
||||
test("var x; (x && false) && y()", "var x");
|
||||
test("(x && true) && y()", "x && y()");
|
||||
test("(x && false) && y()", "x");
|
||||
test("var x; (x || true) && y()", "var x; y()");
|
||||
test("var x; (x || false) && y()", "var x; x && y()");
|
||||
test("v = function(x) { (x || false) && y() }", "v = function(x) { x && y() }");
|
||||
|
||||
test("(x || true) && y()", "x, y()");
|
||||
test("(x || false) && y()", "x && y()");
|
||||
@@ -48,29 +48,48 @@ fn minimize_conditional_exprs() {
|
||||
test("a ? (b, c) : c", "(a && b), c");
|
||||
test("a ? b || c : c", "(a && b) || c");
|
||||
test("a ? c : b && c", "(a || b) && c");
|
||||
test("var a, b; a ? b(c, d) : b(e, d)", "var a, b; b(a ? c : e, d)");
|
||||
test("var a, b; a ? b(...c) : b(...e)", "var a, b; b(...a ? c : e)");
|
||||
test("var a, b; a ? b(c) : b(e)", "var a, b; b(a ? c : e)");
|
||||
test("var a, b; a ? b() : b()", "var a, b; b()");
|
||||
test("var a, b; a === 0 ? b(c) : b(e)", "var a, b; b(a === 0 ? c : e)");
|
||||
test_same("var a; a === 0 ? b(c) : b(e)"); // accessing global `b` may assign a different value to `a`
|
||||
test_same("var b; a === 0 ? b(c) : b(e)"); // accessing global `a` may assign a different value to `b`
|
||||
test(
|
||||
"v = function(a, b) { return a ? b(c, d) : b(e, d) }",
|
||||
"v = function(a, b) { return b(a ? c : e, d) }",
|
||||
);
|
||||
test(
|
||||
"v = function(a, b) { return a ? b(...c) : b(...e) }",
|
||||
"v = function(a, b) { return b(...a ? c : e) }",
|
||||
);
|
||||
test(
|
||||
"v = function(a, b) { return a ? b(c) : b(e) }",
|
||||
"v = function(a, b) { return b(a ? c : e) }",
|
||||
);
|
||||
test("v = function(a, b) { return a ? b() : b() }", "v = function(a, b) { return b() }");
|
||||
test(
|
||||
"v = function(a, b) { return a === 0 ? b(c) : b(e) }",
|
||||
"v = function(a, b) { return b(a === 0 ? c : e) }",
|
||||
);
|
||||
test_same("v = function(a) { return a === 0 ? b(c) : b(e) }"); // accessing global `b` may assign a different value to `a`
|
||||
test_same("v = function(b) { return a === 0 ? b(c) : b(e) }"); // accessing global `a` may assign a different value to `b`
|
||||
test_same("a === 0 ? b(c) : b(e)"); // accessing global `a`, `b` may have a side effect
|
||||
test("a() != null ? a() : b", "a() == null ? b : a()");
|
||||
test("var a; a != null ? a : b", "var a; a ?? b");
|
||||
test("v = function(a) { return a != null ? a : b }", "v = function(a) { return a ?? b }");
|
||||
test("var a; (a = _a) != null ? a : b", "var a; (a = _a) ?? b");
|
||||
test("v = a != null ? a : b", "v = a == null ? b : a"); // accessing global `a` may have a getter with side effects
|
||||
test_target("var a; v = a != null ? a : b", "var a; v = a == null ? b : a", "chrome79");
|
||||
test("var a; v = a != null ? a.b.c[d](e) : undefined", "var a; v = a?.b.c[d](e)");
|
||||
test_target(
|
||||
"v = function(a) { return a != null ? a : b }",
|
||||
"v = function(a) { return a == null ? b : a }",
|
||||
"chrome79",
|
||||
);
|
||||
test(
|
||||
"v = function(a) { return a != null ? a.b.c[d](e) : undefined }",
|
||||
"v = function(a) { return a?.b.c[d](e) }",
|
||||
);
|
||||
test("var a; v = (a = _a) != null ? a.b.c[d](e) : undefined", "var a; v = (a = _a)?.b.c[d](e)");
|
||||
test("v = a != null ? a.b.c[d](e) : undefined", "v = a == null ? void 0 : a.b.c[d](e)"); // accessing global `a` may have a getter with side effects
|
||||
test(
|
||||
"var a, undefined = 1; v = a != null ? a.b.c[d](e) : undefined",
|
||||
"var a; v = a == null ? 1 : a.b.c[d](e)",
|
||||
"v = function(a) { var undefined = 1; return a != null ? a.b.c[d](e) : undefined }",
|
||||
"v = function(a) { return a == null ? 1 : a.b.c[d](e) }",
|
||||
);
|
||||
test_target(
|
||||
"var a; v = a != null ? a.b.c[d](e) : undefined",
|
||||
"var a; v = a == null ? void 0 : a.b.c[d](e)",
|
||||
"v = function(a) { return a != null ? a.b.c[d](e) : undefined }",
|
||||
"v = function(a) { return a == null ? void 0 : a.b.c[d](e) }",
|
||||
"chrome79",
|
||||
);
|
||||
test("v = cmp !== 0 ? cmp : (bar, cmp);", "v = (cmp === 0 && bar, cmp);");
|
||||
|
||||
@@ -53,16 +53,22 @@ fn test_fold_one_child_blocks() {
|
||||
test("if(e1){with(e2){if(e3){foo()}}}else{bar()}", "if(e1)with(e2)e3&&foo();else bar()");
|
||||
|
||||
// test("if(a||b){if(c||d){var x;}}", "if(a||b)if(c||d)var x");
|
||||
test("if(x){ if(y){var x;}else{var z;} }", "if(x){if(y)var x;else var z}");
|
||||
test(
|
||||
"v = function(x, y) { if(x) { if(y) { var x } else { var z } } }",
|
||||
"v = function(x, y) { if(x) { if(y) var x; else var z } }",
|
||||
);
|
||||
|
||||
// NOTE - technically we can remove the blocks since both the parent
|
||||
// and child have elses. But we don't since it causes ambiguities in
|
||||
// some cases where not all descendent ifs having elses
|
||||
test(
|
||||
"if(x){ if(y){var x;}else{var z;} }else{var w}",
|
||||
"if(x){if(y)var x;else var z;}else var w",
|
||||
"v = function(x, y) { if(x) { if(y) { var x } else { var z } } else { var w } }",
|
||||
"v = function(x, y) { if(x) { if(y) var x; else var z } else var w }",
|
||||
);
|
||||
test(
|
||||
"v = function(x, y) { if(x) { var x } else { if(y) { var y } } }",
|
||||
"v = function(x, y) { if(x) var x; else if(y) var y }",
|
||||
);
|
||||
test("if (x) {var x;}else { if (y) { var y;} }", "if(x)var x;else if(y)var y");
|
||||
|
||||
// Here's some of the ambiguous cases
|
||||
test(
|
||||
|
||||
@@ -240,7 +240,7 @@ fn remove_unused_expressions_in_sequence() {
|
||||
test("typeof (0, foo);", "foo");
|
||||
test_same("v = typeof (0, foo);");
|
||||
test("var foo; typeof (0, foo);", "var foo;");
|
||||
test("var foo; v = typeof (0, foo);", "var foo; v = typeof foo");
|
||||
test("v = function(foo) { return typeof (0, foo) }", "v = function(foo) { return typeof foo }");
|
||||
test("typeof 0", "");
|
||||
|
||||
test_same("delete (0, foo);");
|
||||
|
||||
@@ -279,12 +279,12 @@ fn test_fold_sequence_expr() {
|
||||
|
||||
#[test]
|
||||
fn test_logical_expression() {
|
||||
test("var a; a != null && a.b()", "var a; a?.b()");
|
||||
test("var a; a == null || a.b()", "var a; a?.b()");
|
||||
test("v = function(a) { a != null && a.b() }", "v = function(a) { a?.b() }");
|
||||
test("v = function(a) { a == null || a.b() }", "v = function(a) { a?.b() }");
|
||||
test_same("a != null && a.b()"); // a may have a getter
|
||||
test_same("a == null || a.b()"); // a may have a getter
|
||||
test("var a; null != a && a.b()", "var a; a?.b()");
|
||||
test("var a; null == a || a.b()", "var a; a?.b()");
|
||||
test("v = function(a) { null != a && a.b() }", "v = function(a) { a?.b() }");
|
||||
test("v = function(a) { null == a || a.b() }", "v = function(a) { a?.b() }");
|
||||
|
||||
test("x == null && y", "x ?? y");
|
||||
test("x != null || y", "x ?? y");
|
||||
@@ -416,26 +416,32 @@ fn test_fold_conditional_expression() {
|
||||
|
||||
#[test]
|
||||
fn test_fold_binary_expression() {
|
||||
test("var a, b; a === b", "var a, b;");
|
||||
test("var a, b; a() === b", "var a, b; a()");
|
||||
test("var a, b; a === b()", "var a, b; b()");
|
||||
test("var a, b; a() === b()", "var a, b; a(), b()");
|
||||
test("v = function(a, b) { a === b }", "v = function(a, b) {}");
|
||||
test("v = function(a, b) { a() === b }", "v = function(a, b) { a() }");
|
||||
test("v = function(a, b) { a === b() }", "v = function(a, b) { b() }");
|
||||
test("v = function(a, b) { a() === b() }", "v = function(a, b) { a(), b() }");
|
||||
|
||||
test("var a, b; a !== b", "var a, b;");
|
||||
test("var a, b; a == b", "var a, b;");
|
||||
test("var a, b; a != b", "var a, b;");
|
||||
test("var a, b; a < b", "var a, b;");
|
||||
test("var a, b; a > b", "var a, b;");
|
||||
test("var a, b; a <= b", "var a, b;");
|
||||
test("var a, b; a >= b", "var a, b;");
|
||||
test("v = function(a, b) { a !== b }", "v = function(a, b) {}");
|
||||
test("v = function(a, b) { a == b }", "v = function(a, b) {}");
|
||||
test("v = function(a, b) { a != b }", "v = function(a, b) {}");
|
||||
test("v = function(a, b) { a < b }", "v = function(a, b) {}");
|
||||
test("v = function(a, b) { a > b }", "v = function(a, b) {}");
|
||||
test("v = function(a, b) { a <= b }", "v = function(a, b) {}");
|
||||
test("v = function(a, b) { a >= b }", "v = function(a, b) {}");
|
||||
|
||||
test_same("var a, b; a + b");
|
||||
test("var a, b; 'a' + b", "var a, b; '' + b");
|
||||
test_same("var a, b; a + '' + b");
|
||||
test("var a, b, c; 'a' + (b === c)", "var a, b, c;");
|
||||
test("var a, b; 'a' + +b", "var a, b; '' + +b"); // can be improved to "var a, b; +b"
|
||||
test_same("var a, b; a + ('' + b)");
|
||||
test("var a, b, c; a + ('' + (b === c))", "var a, b, c; a + ''");
|
||||
test_same("v = function(a, b) { a + b }");
|
||||
test("v = function(a, b) { 'a' + b }", "v = function(a, b) { '' + b }");
|
||||
test_same("v = function(a, b) { a + '' + b }");
|
||||
test("v = function(a, b, c) { 'a' + (b === c) }", "v = function(a, b, c) {}");
|
||||
test("v = function(a, b) { 'a' + +b }", "v = function(a, b) { '' + +b }"); // can be improved to `+b`
|
||||
test_same("v = function(a, b) { a + ('' + b) }");
|
||||
test("v = function(a, b, c) { a + ('' + (b === c)) }", "v = function(a, b, c) { a + '' }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_unused_uninitialized_module_var_expression() {
|
||||
test("var a; a != null && a.b()", "var a");
|
||||
test("var a, b; a + b", "var a, b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -432,19 +432,43 @@ fn test_fold_arrow_function_return() {
|
||||
|
||||
#[test]
|
||||
fn test_fold_is_typeof_equals_undefined_resolved() {
|
||||
test("var x; v = typeof x !== 'undefined'", "var x; v = x !== void 0");
|
||||
test("var x; v = typeof x != 'undefined'", "var x; v = x !== void 0");
|
||||
test("var x; v = 'undefined' !== typeof x", "var x; v = x !== void 0");
|
||||
test("var x; v = 'undefined' != typeof x", "var x; v = x !== void 0");
|
||||
|
||||
test("var x; v = typeof x === 'undefined'", "var x; v = x === void 0");
|
||||
test("var x; v = typeof x == 'undefined'", "var x; v = x === void 0");
|
||||
test("var x; v = 'undefined' === typeof x", "var x; v = x === void 0");
|
||||
test("var x; v = 'undefined' == typeof x", "var x; v = x === void 0");
|
||||
test(
|
||||
"v = function(x) { return typeof x !== 'undefined' }",
|
||||
"v = function(x) { return x !== void 0 }",
|
||||
);
|
||||
test(
|
||||
"v = function(x) { return typeof x != 'undefined' }",
|
||||
"v = function(x) { return x !== void 0 }",
|
||||
);
|
||||
test(
|
||||
"v = function(x) { return 'undefined' !== typeof x }",
|
||||
"v = function(x) { return x !== void 0 }",
|
||||
);
|
||||
test(
|
||||
"v = function(x) { return 'undefined' != typeof x }",
|
||||
"v = function(x) { return x !== void 0 }",
|
||||
);
|
||||
|
||||
test(
|
||||
"var x; function foo() { v = typeof x !== 'undefined' }",
|
||||
"var x; function foo() { v = x !== void 0 }",
|
||||
"v = function(x) { return typeof x === 'undefined' }",
|
||||
"v = function(x) { return x === void 0 }",
|
||||
);
|
||||
test(
|
||||
"v = function(x) { return typeof x == 'undefined' }",
|
||||
"v = function(x) { return x === void 0 }",
|
||||
);
|
||||
test(
|
||||
"v = function(x) { return 'undefined' === typeof x }",
|
||||
"v = function(x) { return x === void 0 }",
|
||||
);
|
||||
test(
|
||||
"v = function(x) { return 'undefined' == typeof x }",
|
||||
"v = function(x) { return x === void 0 }",
|
||||
);
|
||||
|
||||
test(
|
||||
"v = function(x) { return function() { return typeof x !== 'undefined' } }",
|
||||
"v = function(x) { return function() { return x !== void 0 } }",
|
||||
);
|
||||
test(
|
||||
"v = typeof x !== 'undefined'; function foo() { var x }",
|
||||
@@ -459,6 +483,9 @@ fn test_fold_is_typeof_equals_undefined_resolved() {
|
||||
test("v = typeof x.y === 'undefined'", "v = x.y === void 0");
|
||||
test("v = typeof x.y !== 'undefined'", "v = x.y !== void 0");
|
||||
test("v = typeof (x + '') === 'undefined'", "v = x + '' === void 0");
|
||||
|
||||
test("var x; v = typeof x !== 'undefined'", "var x; v = !1");
|
||||
test("var x; v = typeof x === 'undefined'", "var x; v = !0");
|
||||
}
|
||||
|
||||
/// Port from <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_parser/js_parser_test.go#L4658>
|
||||
@@ -478,68 +505,74 @@ fn test_fold_is_typeof_equals_undefined() {
|
||||
#[test]
|
||||
fn test_fold_is_object_and_not_null() {
|
||||
test(
|
||||
"var foo; v = typeof foo === 'object' && foo !== null",
|
||||
"var foo; v = typeof foo == 'object' && !!foo",
|
||||
"v = function(foo) { return typeof foo === 'object' && foo !== null }",
|
||||
"v = function(foo) { return typeof foo == 'object' && !!foo }",
|
||||
);
|
||||
test(
|
||||
"var foo; v = typeof foo == 'object' && foo !== null",
|
||||
"var foo; v = typeof foo == 'object' && !!foo",
|
||||
"v = function(foo) { return typeof foo == 'object' && foo !== null }",
|
||||
"v = function(foo) { return typeof foo == 'object' && !!foo }",
|
||||
);
|
||||
test(
|
||||
"var foo; v = typeof foo === 'object' && foo != null",
|
||||
"var foo; v = typeof foo == 'object' && !!foo",
|
||||
"v = function(foo) { return typeof foo === 'object' && foo != null }",
|
||||
"v = function(foo) { return typeof foo == 'object' && !!foo }",
|
||||
);
|
||||
test(
|
||||
"var foo; v = typeof foo == 'object' && foo != null",
|
||||
"var foo; v = typeof foo == 'object' && !!foo",
|
||||
"v = function(foo) { return typeof foo == 'object' && foo != null }",
|
||||
"v = function(foo) { return typeof foo == 'object' && !!foo }",
|
||||
);
|
||||
test(
|
||||
"var foo; v = typeof foo !== 'object' || foo === null",
|
||||
"var foo; v = typeof foo != 'object' || !foo",
|
||||
"v = function(foo) { return typeof foo !== 'object' || foo === null }",
|
||||
"v = function(foo) { return typeof foo != 'object' || !foo }",
|
||||
);
|
||||
test(
|
||||
"var foo; v = typeof foo != 'object' || foo === null",
|
||||
"var foo; v = typeof foo != 'object' || !foo",
|
||||
"v = function(foo) { return typeof foo != 'object' || foo === null }",
|
||||
"v = function(foo) { return typeof foo != 'object' || !foo }",
|
||||
);
|
||||
test(
|
||||
"var foo; v = typeof foo !== 'object' || foo == null",
|
||||
"var foo; v = typeof foo != 'object' || !foo",
|
||||
"v = function(foo) { return typeof foo !== 'object' || foo == null }",
|
||||
"v = function(foo) { return typeof foo != 'object' || !foo }",
|
||||
);
|
||||
test(
|
||||
"var foo; v = typeof foo != 'object' || foo == null",
|
||||
"var foo; v = typeof foo != 'object' || !foo",
|
||||
"v = function(foo) { return typeof foo != 'object' || foo == null }",
|
||||
"v = function(foo) { return typeof foo != 'object' || !foo }",
|
||||
);
|
||||
test(
|
||||
"var foo, bar; v = typeof foo === 'object' && foo !== null && bar !== 1",
|
||||
"var foo, bar; v = typeof foo == 'object' && !!foo && bar !== 1",
|
||||
"v = function(foo, bar) { return typeof foo === 'object' && foo !== null && bar !== 1 }",
|
||||
"v = function(foo, bar) { return typeof foo == 'object' && !!foo && bar !== 1 }",
|
||||
);
|
||||
test(
|
||||
"var foo, bar; v = bar !== 1 && typeof foo === 'object' && foo !== null",
|
||||
"var foo, bar; v = bar !== 1 && typeof foo == 'object' && !!foo",
|
||||
"v = function(foo, bar) { return bar !== 1 && typeof foo === 'object' && foo !== null }",
|
||||
"v = function(foo, bar) { return bar !== 1 && typeof foo == 'object' && !!foo }",
|
||||
);
|
||||
test(
|
||||
"var foo, bar; v = typeof foo === 'object' && foo !== null || bar !== 1",
|
||||
"var foo, bar; v = typeof foo == 'object' && !!foo || bar !== 1",
|
||||
"v = function(foo, bar) { return typeof foo === 'object' && foo !== null || bar !== 1 }",
|
||||
"v = function(foo, bar) { return typeof foo == 'object' && !!foo || bar !== 1 }",
|
||||
);
|
||||
test(
|
||||
"var foo, bar; v = bar !== 1 || typeof foo === 'object' && foo !== null",
|
||||
"var foo, bar; v = bar !== 1 || typeof foo == 'object' && !!foo",
|
||||
"v = function(foo, bar) { return bar !== 1 || typeof foo === 'object' && foo !== null }",
|
||||
"v = function(foo, bar) { return bar !== 1 || typeof foo == 'object' && !!foo }",
|
||||
);
|
||||
test(
|
||||
"var foo, bar; v = (typeof foo !== 'object' || foo === null) && bar !== 1",
|
||||
"var foo, bar; v = (typeof foo != 'object' || !foo) && bar !== 1",
|
||||
"v = function(foo, bar) { return (typeof foo !== 'object' || foo === null) && bar !== 1 }",
|
||||
"v = function(foo, bar) { return (typeof foo != 'object' || !foo) && bar !== 1 }",
|
||||
);
|
||||
test(
|
||||
"var foo, bar; v = bar !== 1 && (typeof foo !== 'object' || foo === null)",
|
||||
"var foo, bar; v = bar !== 1 && (typeof foo != 'object' || !foo)",
|
||||
"v = function(foo, bar) { return bar !== 1 && (typeof foo !== 'object' || foo === null) }",
|
||||
"v = function(foo, bar) { return bar !== 1 && (typeof foo != 'object' || !foo) }",
|
||||
);
|
||||
test_same(
|
||||
"v = function(foo, bar) { return bar !== 1 && typeof foo != 'object' || foo === null }",
|
||||
);
|
||||
test_same(
|
||||
"v = function(foo, bar) { return typeof foo != 'object' || foo === null && bar !== 1 }",
|
||||
);
|
||||
test_same("var foo, bar; v = bar !== 1 && typeof foo != 'object' || foo === null");
|
||||
test_same("var foo, bar; v = typeof foo != 'object' || foo === null && bar !== 1");
|
||||
test_same("var foo; v = typeof foo.a == 'object' && foo.a !== null"); // cannot be folded because accessing foo.a might have a side effect
|
||||
test_same("v = foo !== null && typeof foo == 'object'"); // cannot be folded because accessing foo might have a side effect
|
||||
test_same("v = typeof foo == 'object' && foo !== null"); // cannot be folded because accessing foo might have a side effect
|
||||
test_same("var foo, bar; v = typeof foo == 'object' && bar !== null");
|
||||
test_same("var foo; v = typeof foo == 'string' && foo !== null");
|
||||
test_same("v = function(foo, bar) { return typeof foo == 'object' && bar !== null }");
|
||||
test_same("v = function(foo) { return typeof foo == 'string' && foo !== null }");
|
||||
|
||||
test("var foo; v = typeof foo === 'object' && foo !== null", "var foo; v = !1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user