feat(minifier): annotate more pure constructors (#11555)

closes https://github.com/rolldown/rolldown/issues/2603

I used the following script to capture what argument throws.

```js
function x(F) {
    try {
    new F(null)
    } catch (e) {
        console.log(F.name)
    }
}
x(Set)
x(Map)
x(WeakSet)
x(WeakMap)
x(AggregateError)
x(Boolean)
x(DataView)
x(Error)
x(EvalError)
x(RangeError)
x(ReferenceError)
x(RegExp)
x(SyntaxError)
x(TypeError)
x(URIError)
x(ArrayBuffer)
x(Date)
x(Number)
x(Object)
x(String)
```
This commit is contained in:
Boshen committed 2025-06-09 08:25:43 +00:00
1 parent e13ed51788
commit 40ac1863b5
5 files changed
+316 -63

No files matched your search

@@ -1,6 +1,6 @@
use oxc_ast::ast::{
AssignmentExpression, AssignmentOperator, BinaryExpression, ConditionalExpression, Expression,
LogicalExpression, LogicalOperator, UnaryExpression,
LogicalExpression, LogicalOperator, StaticMemberExpression, UnaryExpression,
};
use oxc_syntax::operator::{BinaryOperator, UnaryOperator};
@@ -110,6 +110,7 @@ impl DetermineValueType for Expression<'_> {
Expression::ConditionalExpression(e) => e.value_type(is_global_reference),
Expression::LogicalExpression(e) => e.value_type(is_global_reference),
Expression::ParenthesizedExpression(e) => e.expression.value_type(is_global_reference),
Expression::StaticMemberExpression(e) => e.value_type(is_global_reference),
_ => ValueType::Undetermined,
}
}
@@ -272,3 +273,18 @@ impl DetermineValueType for LogicalExpression<'_> {
}
}
}
impl DetermineValueType for StaticMemberExpression<'_> {
fn value_type(&self, is_global_reference: &impl IsGlobalReference) -> ValueType {
if matches!(self.property.name.as_str(), "POSITIVE_INFINITY" | "NEGATIVE_INFINITY") {
if let Some(ident) = self.object.get_identifier_reference() {
if ident.name.as_str() == "Number"
&& is_global_reference.is_global_reference(ident) == Some(true)
{
return ValueType::Number;
}
}
}
ValueType::Undetermined
}
}
@@ -1250,14 +1250,14 @@ mod test {
#[test]
fn test_coercion_substitution_boxed_number_vs_zero() {
test_same("var x = new Number(0);\nif (x != 0) throw 'a';\n");
test_same("var x = /* @__PURE__ */ new Number(0);\nif (x != 0) throw 'a';\n");
}
#[test]
fn test_coercion_substitution_boxed_primitives() {
test_same("var x = new Number(); if (x != null) throw 'a';");
test_same("var x = new String(); if (x != null) throw 'a';");
test_same("var x = new Boolean();\nif (x != null) throw 'a';");
test_same("var x = /* @__PURE__ */ new Number(); if (x != null) throw 'a';");
test_same("var x = /* @__PURE__ */ new String(); if (x != null) throw 'a';");
test_same("var x = /* @__PURE__ */ new Boolean();\nif (x != null) throw 'a';");
}
#[test]
+273 -37
View File
@@ -1,9 +1,6 @@
use oxc_allocator::{TakeIn, Vec};
use oxc_ast::ast::*;
use oxc_ecmascript::{
constant_evaluation::{DetermineValueType, ValueType},
side_effects::MayHaveSideEffects,
};
use oxc_ecmascript::constant_evaluation::{DetermineValueType, ValueType};
use oxc_semantic::IsGlobalReference;
use oxc_span::GetSpan;
use oxc_syntax::scope::ScopeFlags;
@@ -276,6 +273,8 @@ impl<'a> Normalize {
}
}
/// Set `pure` on side effect free `new Expr()`s.
/// `PC` or `PC_WITH_ARRAY` in <https://github.com/rollup/rollup/blob/v4.42.0/src/ast/nodes/shared/knownGlobals.ts>
fn set_pure_or_no_side_effects_to_new_expr(
new_expr: &mut NewExpression<'a>,
ctx: &TraverseCtx<'a>,
@@ -296,40 +295,79 @@ impl<'a> Normalize {
// callee is a global reference.
let ctx = Ctx(ctx);
let len = new_expr.arguments.len();
if match ident.name.as_str() {
"WeakSet" | "WeakMap" if ctx.is_global_reference(ident) => match len {
0 => true,
1 => match new_expr.arguments[0].as_expression() {
Some(Expression::NullLiteral(_)) => true,
Some(Expression::ArrayExpression(e)) => e.elements.is_empty(),
Some(e) if ctx.is_expression_undefined(e) => true,
_ => false,
},
_ => false,
},
"Date" if ctx.is_global_reference(ident) => match len {
0 => true,
1 => {
let Some(arg) = new_expr.arguments[0].as_expression() else { return };
let ty = arg.value_type(&ctx);
matches!(
ty,
ValueType::Null
| ValueType::Undefined
| ValueType::Boolean
| ValueType::Number
| ValueType::String
) && !arg.may_have_side_effects(&ctx)
let (zero_arg_throws_error, one_arg_array_throws_error, one_arg_throws_error): (
bool,
bool,
&'static [ValueType],
) = match ident.name.as_str() {
"AggregateError" => (
true,
false,
&[
ValueType::Undefined,
ValueType::Null,
ValueType::Number,
ValueType::BigInt,
ValueType::Boolean,
ValueType::Object,
],
),
"DataView" => (
true,
true,
&[
ValueType::Undefined,
ValueType::Null,
ValueType::Number,
ValueType::Boolean,
ValueType::BigInt,
ValueType::String,
ValueType::Boolean,
ValueType::Object,
],
),
"Set" | "Map" | "WeakSet" | "WeakMap" => (
false,
false,
&[
ValueType::Number,
ValueType::Boolean,
ValueType::BigInt,
ValueType::Boolean,
ValueType::Object,
],
),
"ArrayBuffer" | "Date" => (false, false, &[ValueType::BigInt]),
"Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError" | "RegExp"
| "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String" => {
(false, false, &[])
}
_ => return,
};
if !Self::can_set_pure(ident, ctx) {
return;
}
if match len {
0 if !zero_arg_throws_error => true,
1 => match new_expr.arguments[0].as_expression() {
Some(Expression::ArrayExpression(array_expr)) => {
array_expr.elements.is_empty() && !one_arg_array_throws_error
}
Some(e) => {
if let Expression::NewExpression(new_expr) = e {
new_expr.pure
} else {
let value_type = e.value_type(&ctx);
if value_type.is_undetermined() {
false
} else {
!one_arg_throws_error.contains(&value_type)
}
}
}
_ => false,
},
"Set" | "Map" if ctx.is_global_reference(ident) => match len {
0 => true,
1 => match new_expr.arguments[0].as_expression() {
Some(Expression::NullLiteral(_)) => true,
Some(e) if ctx.is_expression_undefined(e) => true,
_ => false,
},
_ => false,
},
_ => false,
@@ -337,6 +375,12 @@ impl<'a> Normalize {
new_expr.pure = true;
}
}
fn can_set_pure(ident: &IdentifierReference<'a>, ctx: Ctx<'a, '_>) -> bool {
ctx.is_global_reference(ident)
// Throw is never pure.
&& !matches!(ctx.parent(), Ancestor::ThrowStatementArgument(_))
}
}
#[cfg(test)]
@@ -398,4 +442,196 @@ mod test {
test("foo(Number.NaN)", "foo(NaN)");
test_same("let Number; foo(Number.NaN)");
}
#[test]
fn pure_constructors() {
test("new AggregateError", "AggregateError()");
test("new ArrayBuffer", "");
test("new Boolean", "");
test("new DataView", "new DataView()");
test("new Date", "");
test("new Error", "");
test("new EvalError", "");
test("new Map", "");
test("new Number", "");
test("new Object", "");
test("new RangeError", "");
test("new ReferenceError", "");
test("new RegExp", "");
test("new Set", "");
test("new String", "");
test("new SyntaxError", "");
test("new TypeError", "");
test("new URIError", "");
test("new WeakMap", "");
test("new WeakSet", "");
test("new AggregateError(null)", "AggregateError(null)");
test("new ArrayBuffer(null)", "");
test("new Boolean(null)", "");
test_same("new DataView(null)");
test("new Date(null)", "");
test("new Error(null)", "");
test("new EvalError(null)", "");
test("new Map(null)", "");
test("new Number(null)", "");
test("new Object(null)", "");
test("new RangeError(null)", "");
test("new ReferenceError(null)", "");
test("new RegExp(null)", "");
test("new Set(null)", "");
test("new String(null)", "");
test("new SyntaxError(null)", "");
test("new TypeError(null)", "");
test("new URIError(null)", "");
test("new WeakMap(null)", "");
test("new WeakSet(null)", "");
test("new AggregateError(undefined)", "AggregateError(void 0)");
test("new ArrayBuffer(undefined)", "");
test("new Boolean(undefined)", "");
test_same("new DataView(void 0)");
test("new Date(undefined)", "");
test("new Error(undefined)", "");
test("new EvalError(undefined)", "");
test("new Map(undefined)", "");
test("new Number(undefined)", "");
test("new Object(undefined)", "");
test("new RangeError(undefined)", "");
test("new ReferenceError(undefined)", "");
test("new RegExp(undefined)", "");
test("new Set(undefined)", "");
test("new String(undefined)", "");
test("new SyntaxError(undefined)", "");
test("new TypeError(undefined)", "");
test("new URIError(undefined)", "");
test("new WeakMap(undefined)", "");
test("new WeakSet(undefined)", "");
test("new AggregateError(0)", "AggregateError(0)");
test("new ArrayBuffer(0)", "");
test("new Boolean(0)", "");
test_same("new DataView(0)");
test("new Date(0)", "");
test("new Error(0)", "");
test("new EvalError(0)", "");
test_same("new Map(0)");
test("new Number(0)", "");
test("new Object(0)", "");
test("new RangeError(0)", "");
test("new ReferenceError(0)", "");
test("new RegExp(0)", "");
test_same("new Set(0)");
test("new String(0)", "");
test("new SyntaxError(0)", "");
test("new TypeError(0)", "");
test("new URIError(0)", "");
test_same("new WeakMap(0)");
test_same("new WeakSet(0)");
test("new AggregateError(10n)", "AggregateError(10n)");
test_same("new ArrayBuffer(10n)");
test("new Boolean(10n)", "");
test_same("new DataView(10n)");
test_same("new Date(10n)");
test("new Error(10n)", "");
test("new EvalError(10n)", "");
test_same("new Map(10n)");
test("new Number(10n)", "");
test("new Object(10n)", "");
test("new RangeError(10n)", "");
test("new ReferenceError(10n)", "");
test("new RegExp(10n)", "");
test_same("new Set(10n)");
test("new String(10n)", "");
test("new SyntaxError(10n)", "");
test("new TypeError(10n)", "");
test("new URIError(10n)", "");
test_same("new WeakMap(10n)");
test_same("new WeakSet(10n)");
test("new AggregateError('')", "");
test("new ArrayBuffer('')", "");
test("new Boolean('')", "");
test_same("new DataView('')");
test("new Date('')", "");
test("new Error('')", "");
test("new EvalError('')", "");
test("new Map('')", "");
test("new Number('')", "");
test("new Object('')", "");
test("new RangeError('')", "");
test("new ReferenceError('')", "");
test("new RegExp('')", "");
test("new Set('')", "");
test("new String('')", "");
test("new SyntaxError('')", "");
test("new TypeError('')", "");
test("new URIError('')", "");
test("new WeakMap('')", "");
test("new WeakSet('')", "");
test("new AggregateError(!0)", "AggregateError(!0)");
test("new ArrayBuffer(!0)", "");
test("new Boolean(!0)", "");
test_same("new DataView(!0)");
test("new Date(!0)", "");
test("new Error(!0)", "");
test("new EvalError(!0)", "");
test_same("new Map(!0)");
test("new Number(!0)", "");
test("new Object(!0)", "");
test("new RangeError(!0)", "");
test("new ReferenceError(!0)", "");
test("new RegExp(!0)", "");
test_same("new Set(!0)");
test("new String(!0)", "");
test("new SyntaxError(!0)", "");
test("new TypeError(!0)", "");
test("new URIError(!0)", "");
test_same("new WeakMap(!0)");
test_same("new WeakSet(!0)");
test("new AggregateError([])", "");
test("new ArrayBuffer([])", "");
test("new Boolean([])", "");
test_same("new DataView([])");
test("new Date([])", "");
test("new Error([])", "");
test("new EvalError([])", "");
test("new Map([])", "");
test("new Number([])", "");
test("new Object([])", "");
test("new RangeError([])", "");
test("new ReferenceError([])", "");
test("new RegExp([])", "");
test("new Set([])", "");
test("new String([])", "");
test("new SyntaxError([])", "");
test("new TypeError([])", "");
test("new URIError([])", "");
test("new WeakMap([])", "");
test("new WeakSet([])", "");
test("new AggregateError(a)", "AggregateError(a)");
test_same("new ArrayBuffer(a)");
test_same("new Boolean(a)");
test_same("new DataView(a)");
test_same("new Date(a)");
test("new Error(a)", "Error(a)");
test("new EvalError(a)", "EvalError(a)");
test_same("new Map(a)");
test_same("new Number(a)");
test_same("new Object(a)");
test("new RangeError(a)", "RangeError(a)");
test("new ReferenceError(a)", "ReferenceError(a)");
test_same("new RegExp(a)");
test_same("new Set(a)");
test_same("new String(a)");
test("new SyntaxError(a)", "SyntaxError(a)");
test("new TypeError(a)", "TypeError(a)");
test("new URIError(a)", "URIError(a)");
test_same("new WeakMap(a)");
test_same("new WeakSet(a)");
}
}
@@ -689,6 +689,7 @@ mod test {
test("new WeakSet([])", "");
test_same("new WeakSet([x])");
test_same("new WeakSet(x)");
test_same("throw new WeakSet()");
test("new WeakMap()", "");
test("new WeakMap(null)", "");
test("new WeakMap(void 0)", "");
@@ -838,8 +838,6 @@ impl<'a> PeepholeOptimizations {
return None;
}
if match name {
"Error" | "AggregateError" | "Function" => true,
_ if Self::is_native_error_name(name) => true,
"RegExp" => {
let arguments_len = e.arguments.len();
arguments_len == 0
@@ -849,18 +847,20 @@ impl<'a> PeepholeOptimizations {
!ty.is_undetermined() && !ty.is_object()
}))
}
"Error" | "AggregateError" | "Function" => true,
_ if Self::is_native_error_name(name) => true,
_ => unreachable!(),
} {
Some(ctx.ast.expression_call(
return Some(ctx.ast.expression_call_with_pure(
e.span,
e.callee.take_in(ctx.ast),
NONE,
e.arguments.take_in(ctx.ast),
false,
))
} else {
None
e.pure,
));
}
None
}
/// Whether the name matches any native error name.
@@ -1477,17 +1477,17 @@ mod test {
#[test]
fn test_fold_new_expressions() {
test("new Error()", "Error()");
test("new Error('a')", "Error('a')");
test("new Error('a', { cause: b })", "Error('a', { cause: b })");
test("let _ = new Error()", "let _ = /* @__PURE__ */ Error()");
test("let _ = new Error('a')", "let _ = /* @__PURE__ */ Error('a')");
test("let _ = new Error('a', { cause: b })", "let _ = Error('a', { cause: b })");
test_same("var Error; new Error()");
test("new EvalError()", "EvalError()");
test("new RangeError()", "RangeError()");
test("new ReferenceError()", "ReferenceError()");
test("new SyntaxError()", "SyntaxError()");
test("new TypeError()", "TypeError()");
test("new URIError()", "URIError()");
test("new AggregateError()", "AggregateError()");
test("let _ = new EvalError()", "let _ = /* @__PURE__ */ EvalError()");
test("let _ = new RangeError()", "let _ = /* @__PURE__ */ RangeError()");
test("let _ = new ReferenceError()", "let _ = /* @__PURE__ */ ReferenceError()");
test("let _ = new SyntaxError()", "let _ = /* @__PURE__ */ SyntaxError()");
test("let _ = new TypeError()", "let _ = /* @__PURE__ */ TypeError()");
test("let _ = new URIError()", "let _ = /* @__PURE__ */ URIError()");
test("let _ = new AggregateError('a')", "let _ = /* @__PURE__ */ AggregateError('a')");
test("new Function()", "Function()");
test(
@@ -1496,13 +1496,13 @@ mod test {
);
test_same("var Function; new Function()");
test("new RegExp()", "RegExp()");
test("new RegExp('a')", "RegExp('a')");
test("new RegExp(0)", "RegExp(0)");
test("new RegExp(null)", "RegExp(null)");
test("new RegExp()", "");
test("new RegExp('a')", "");
test("new RegExp(0)", "");
test("new RegExp(null)", "");
test("new RegExp('a', 'g')", "RegExp('a', 'g')");
test_same("new RegExp(foo)");
test_same("new RegExp(/foo/)");
test("new RegExp(/foo/)", "");
}
#[test]