Skip to content

Commit fbe6663

Browse files
committed
feat(minifier): mark more known global methods as side-effect free (#13086)
closes #13067
1 parent a36c3ce commit fbe6663

File tree

7 files changed

+235
-47
lines changed

7 files changed

+235
-47
lines changed

crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,36 @@ impl<'a> MayHaveSideEffects<'a> for BinaryExpression<'a> {
242242
}
243243
}
244244

245+
fn is_pure_regexp(name: &str, args: &[Argument<'_>]) -> bool {
246+
name == "RegExp"
247+
&& match args.len() {
248+
0 | 1 => true,
249+
2 => args[1].as_expression().is_some_and(|e| {
250+
matches!(e, Expression::Identifier(_) | Expression::StringLiteral(_))
251+
}),
252+
_ => false,
253+
}
254+
}
255+
256+
#[rustfmt::skip]
257+
fn is_pure_global_function(name: &str) -> bool {
258+
matches!(name, "decodeURI" | "decodeURIComponent" | "encodeURI" | "encodeURIComponent"
259+
| "escape" | "isFinite" | "isNaN" | "parseFloat" | "parseInt")
260+
}
261+
262+
#[rustfmt::skip]
263+
fn is_pure_call(name: &str) -> bool {
264+
matches!(name, "Date" | "Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError"
265+
| "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String" | "Symbol")
266+
}
267+
268+
#[rustfmt::skip]
269+
fn is_pure_constructor(name: &str) -> bool {
270+
matches!(name, "Set" | "Map" | "WeakSet" | "WeakMap" | "ArrayBuffer" | "Date"
271+
| "Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError"
272+
| "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String" | "Symbol")
273+
}
274+
245275
/// Whether the name matches any known global constructors.
246276
///
247277
/// <https://tc39.es/ecma262/multipage/global-object.html#sec-constructor-properties-of-the-global-object>
@@ -530,43 +560,77 @@ fn get_array_minimum_length(arr: &ArrayExpression) -> usize {
530560
.sum()
531561
}
532562

563+
// `PF` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
533564
impl<'a> MayHaveSideEffects<'a> for CallExpression<'a> {
534565
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
535566
if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) {
536567
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
537568
}
569+
570+
if let Expression::Identifier(ident) = &self.callee
571+
&& ctx.is_global_reference(ident)
572+
&& let name = ident.name.as_str()
573+
&& (is_pure_global_function(name)
574+
|| is_pure_call(name)
575+
|| is_pure_regexp(name, &self.arguments))
576+
{
577+
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
578+
}
579+
580+
let (ident, name) = match &self.callee {
581+
Expression::StaticMemberExpression(member) if !member.optional => {
582+
(member.object.get_identifier_reference(), member.property.name.as_str())
583+
}
584+
Expression::ComputedMemberExpression(member) if !member.optional => {
585+
match &member.expression {
586+
Expression::StringLiteral(s) => {
587+
(member.object.get_identifier_reference(), s.value.as_str())
588+
}
589+
_ => return true,
590+
}
591+
}
592+
_ => return true,
593+
};
594+
595+
let Some(object) = ident.map(|ident| ident.name.as_str()) else { return true };
596+
597+
#[rustfmt::skip]
598+
let is_global = match object {
599+
"Array" => matches!(name, "isArray" | "of"),
600+
"ArrayBuffer" => name == "isView",
601+
"Date" => matches!(name, "now" | "parse" | "UTC"),
602+
"Math" => matches!(name, "abs" | "acos" | "acosh" | "asin" | "asinh" | "atan" | "atan2" | "atanh"
603+
| "cbrt" | "ceil" | "clz32" | "cos" | "cosh" | "exp" | "expm1" | "floor" | "fround" | "hypot"
604+
| "imul" | "log" | "log10" | "log1p" | "log2" | "max" | "min" | "pow" | "random" | "round"
605+
| "sign" | "sin" | "sinh" | "sqrt" | "tan" | "tanh" | "trunc"),
606+
"Number" => matches!(name, "isFinite" | "isInteger" | "isNaN" | "isSafeInteger" | "parseFloat" | "parseInt"),
607+
"Object" => matches!(name, "create" | "getOwnPropertyDescriptor" | "getOwnPropertyDescriptors" | "getOwnPropertyNames"
608+
| "getOwnPropertySymbols" | "getPrototypeOf" | "hasOwn" | "is" | "isExtensible" | "isFrozen" | "isSealed" | "keys"),
609+
"String" => matches!(name, "fromCharCode" | "fromCodePoint" | "raw"),
610+
"Symbol" => matches!(name, "for" | "keyFor"),
611+
"URL" => name == "canParse",
612+
"Float32Array" | "Float64Array" | "Int16Array" | "Int32Array" | "Int8Array" | "Uint16Array" | "Uint32Array" | "Uint8Array" | "Uint8ClampedArray" => name == "of",
613+
_ => false,
614+
};
615+
616+
if is_global {
617+
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
618+
}
619+
538620
true
539621
}
540622
}
541623

624+
// `[ValueProperties]: PURE` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
542625
impl<'a> MayHaveSideEffects<'a> for NewExpression<'a> {
543626
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
544627
if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) {
545628
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
546629
}
547630
if let Expression::Identifier(ident) = &self.callee
548631
&& ctx.is_global_reference(ident)
549-
&& matches!(
550-
ident.name.as_str(),
551-
"Set"
552-
| "Map"
553-
| "WeakSet"
554-
| "WeakMap"
555-
| "ArrayBuffer"
556-
| "Date"
557-
| "Boolean"
558-
| "Error"
559-
| "EvalError"
560-
| "RangeError"
561-
| "ReferenceError"
562-
| "RegExp"
563-
| "SyntaxError"
564-
| "TypeError"
565-
| "URIError"
566-
| "Number"
567-
| "Object"
568-
| "String"
569-
)
632+
&& let name = ident.name.as_str()
633+
&& (is_pure_constructor(name) || is_pure_regexp(name, &self.arguments))
570634
{
571635
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
572636
}

crates/oxc_minifier/src/peephole/remove_unused_expression.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ impl<'a> PeepholeOptimizations {
577577
}
578578
}
579579

580-
false
580+
!call_expr.may_have_side_effects(ctx)
581581
}
582582

583583
fn fold_arguments_into_needed_expressions(

crates/oxc_minifier/src/peephole/replace_known_methods.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,7 +1431,7 @@ mod test {
14311431
test("x = String.fromCharCode(0)", "x = '\\0'");
14321432
test("x = String.fromCharCode(120)", "x = 'x'");
14331433
test("x = String.fromCharCode(120, 121)", "x = 'xy'");
1434-
test_same("String.fromCharCode(55358, 56768)");
1434+
test_same("x = String.fromCharCode(55358, 56768)");
14351435
test("x = String.fromCharCode(0x10000)", "x = '\\0'");
14361436
test("x = String.fromCharCode(0x10078, 0x10079)", "x = 'xy'");
14371437
test("x = String.fromCharCode(0x1_0000_FFFF)", "x = '\u{ffff}'");
@@ -1635,8 +1635,8 @@ mod test {
16351635
test("x = encodeURI('café')", "x = 'caf%C3%A9'"); // spellchecker:disable-line
16361636
test("x = encodeURI('测试')", "x = '%E6%B5%8B%E8%AF%95'");
16371637

1638-
test_same("encodeURI('a', 'b')");
1639-
test_same("encodeURI(x)");
1638+
test_same("x = encodeURI('a', 'b')");
1639+
test_same("x = encodeURI(x)");
16401640
}
16411641

16421642
#[test]
@@ -1654,8 +1654,8 @@ mod test {
16541654
test("x = encodeURIComponent('café')", "x = 'caf%C3%A9'"); // spellchecker:disable-line
16551655
test("x = encodeURIComponent('测试')", "x = '%E6%B5%8B%E8%AF%95'");
16561656

1657-
test_same("encodeURIComponent('a', 'b')");
1658-
test_same("encodeURIComponent(x)");
1657+
test_same("x = encodeURIComponent('a', 'b')");
1658+
test_same("x = encodeURIComponent(x)");
16591659
}
16601660

16611661
#[test]
@@ -1675,11 +1675,11 @@ mod test {
16751675
test("x = decodeURI('caf%C3%A9')", "x = 'café'"); // spellchecker:disable-line
16761676
test("x = decodeURI('%E6%B5%8B%E8%AF%95')", "x = '测试'");
16771677

1678-
test_same("decodeURI('%ZZ')"); // URIError
1679-
test_same("decodeURI('%A')"); // URIError
1678+
test_same("x = decodeURI('%ZZ')"); // URIError
1679+
test_same("x = decodeURI('%A')"); // URIError
16801680

1681-
test_same("decodeURI('a', 'b')");
1682-
test_same("decodeURI(x)");
1681+
test_same("x = decodeURI('a', 'b')");
1682+
test_same("x = decodeURI(x)");
16831683
}
16841684

16851685
#[test]
@@ -1698,11 +1698,11 @@ mod test {
16981698
test("x = decodeURIComponent('caf%C3%A9')", "x = 'café'"); // spellchecker:disable-line
16991699
test("x = decodeURIComponent('%E6%B5%8B%E8%AF%95')", "x = '测试'");
17001700

1701-
test_same("decodeURIComponent('%ZZ')"); // URIError
1702-
test_same("decodeURIComponent('%A')"); // URIError
1701+
test_same("x = decodeURIComponent('%ZZ')"); // URIError
1702+
test_same("x = decodeURIComponent('%A')"); // URIError
17031703

1704-
test_same("decodeURIComponent('a', 'b')");
1705-
test_same("decodeURIComponent(x)");
1704+
test_same("x = decodeURIComponent('a', 'b')");
1705+
test_same("x = decodeURIComponent(x)");
17061706
}
17071707

17081708
#[test]

crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1699,7 +1699,7 @@ mod test {
16991699
test("new RegExp('a')", "");
17001700
test("new RegExp(0)", "");
17011701
test("new RegExp(null)", "");
1702-
test("new RegExp('a', 'g')", "RegExp('a', 'g')");
1702+
test("x = new RegExp('a', 'g')", "x = RegExp('a', 'g')");
17031703
test_same("new RegExp(foo)");
17041704
test("new RegExp(/foo/)", "");
17051705
}

crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ fn closure_compiler_tests() {
180180
test("templateFunction`template`", true);
181181
test("st = `${name}template`", true);
182182
test("tempFunc = templateFunction`template`", true);
183-
// test("new RegExp('foobar', 'i')", false);
183+
test("new RegExp('foobar', 'i')", false);
184+
test("new RegExp('foobar', 2)", true);
184185
test("new RegExp(SomethingWacky(), 'i')", true);
185186
// test("new Array()", false);
186187
// test("new Array", false);
@@ -205,8 +206,8 @@ fn closure_compiler_tests() {
205206
// test("(true ? {} : []).foo = 2;", false);
206207
// test("({},[]).foo = 2;", false);
207208
test("delete a.b", true);
208-
// test("Math.random();", false);
209-
test("Math.random(seed);", true);
209+
test("Math.random();", false);
210+
test("Math.random(Math);", true);
210211
// test("[1, 1].foo;", false);
211212
// test("export var x = 0;", true);
212213
// test("export let x = 0;", true);
@@ -751,6 +752,7 @@ fn test_property_access() {
751752
test("[...a, 1][0]", true); // "...a" may have a sideeffect
752753
}
753754

755+
// `[ValueProperties]: PURE` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
754756
#[test]
755757
fn test_new_expressions() {
756758
test("new AggregateError", true);
@@ -773,6 +775,127 @@ fn test_new_expressions() {
773775
test("new Number", false);
774776
test("new Object", false);
775777
test("new String", false);
778+
test("new Symbol", false);
779+
}
780+
781+
// `PF` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
782+
#[test]
783+
fn test_call_expressions() {
784+
test("AggregateError()", true);
785+
test("DataView()", true);
786+
test("Set()", true);
787+
test("Map()", true);
788+
test("WeakSet()", true);
789+
test("WeakMap()", true);
790+
test("ArrayBuffer()", true);
791+
test("Date()", false);
792+
test("Boolean()", false);
793+
test("Error()", false);
794+
test("EvalError()", false);
795+
test("RangeError()", false);
796+
test("ReferenceError()", false);
797+
test("RegExp()", false);
798+
test("SyntaxError()", false);
799+
test("TypeError()", false);
800+
test("URIError()", false);
801+
test("Number()", false);
802+
test("Object()", false);
803+
test("String()", false);
804+
test("Symbol()", false);
805+
806+
test("decodeURI()", false);
807+
test("decodeURIComponent()", false);
808+
test("encodeURI()", false);
809+
test("encodeURIComponent()", false);
810+
test("escape()", false);
811+
test("isFinite()", false);
812+
test("isNaN()", false);
813+
test("parseFloat()", false);
814+
test("parseInt()", false);
815+
816+
test("Array.isArray()", false);
817+
test("Array.of()", false);
818+
819+
test("ArrayBuffer.isView()", false);
820+
821+
test("Date.now()", false);
822+
test("Date.parse()", false);
823+
test("Date.UTC()", false);
824+
825+
test("Math.abs()", false);
826+
test("Math.acos()", false);
827+
test("Math.acosh()", false);
828+
test("Math.asin()", false);
829+
test("Math.asinh()", false);
830+
test("Math.atan()", false);
831+
test("Math.atan2()", false);
832+
test("Math.atanh()", false);
833+
test("Math.cbrt()", false);
834+
test("Math.ceil()", false);
835+
test("Math.clz32()", false);
836+
test("Math.cos()", false);
837+
test("Math.cosh()", false);
838+
test("Math.exp()", false);
839+
test("Math.expm1()", false);
840+
test("Math.floor()", false);
841+
test("Math.fround()", false);
842+
test("Math.hypot()", false);
843+
test("Math.imul()", false);
844+
test("Math.log()", false);
845+
test("Math.log10()", false);
846+
test("Math.log1p()", false);
847+
test("Math.log2()", false);
848+
test("Math.max()", false);
849+
test("Math.min()", false);
850+
test("Math.pow()", false);
851+
test("Math.random()", false);
852+
test("Math.round()", false);
853+
test("Math.sign()", false);
854+
test("Math.sin()", false);
855+
test("Math.sinh()", false);
856+
test("Math.sqrt()", false);
857+
test("Math.tan()", false);
858+
test("Math.tanh()", false);
859+
test("Math.trunc()", false);
860+
861+
test("Number.isFinite()", false);
862+
test("Number.isInteger()", false);
863+
test("Number.isNaN()", false);
864+
test("Number.isSafeInteger()", false);
865+
test("Number.parseFloat()", false);
866+
test("Number.parseInt()", false);
867+
868+
test("Object.create()", false);
869+
test("Object.getOwnPropertyDescriptor()", false);
870+
test("Object.getOwnPropertyDescriptors()", false);
871+
test("Object.getOwnPropertyNames()", false);
872+
test("Object.getOwnPropertySymbols()", false);
873+
test("Object.getPrototypeOf()", false);
874+
test("Object.hasOwn()", false);
875+
test("Object.is()", false);
876+
test("Object.isExtensible()", false);
877+
test("Object.isFrozen()", false);
878+
test("Object.isSealed()", false);
879+
test("Object.keys()", false);
880+
881+
test("String.fromCharCode()", false);
882+
test("String.fromCodePoint()", false);
883+
test("String.raw()", false);
884+
885+
test("Symbol.for()", false);
886+
test("Symbol.keyFor()", false);
887+
888+
test("URL.canParse()", false);
889+
890+
test("Float32Array.of()", false);
891+
test("Float64Array.of()", false);
892+
test("Int16Array.of()", false);
893+
test("Int32Array.of()", false);
894+
test("Int8Array.of()", false);
895+
test("Uint16Array.of()", false);
896+
test("Uint32Array.of()", false);
897+
test("Uint8Array.of()", false);
898+
test("Uint8ClampedArray.of()", false);
776899
}
777900

778901
#[test]

0 commit comments

Comments
 (0)