diff --git a/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs b/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs index f98a8ffacca7f..75a96739c2ec2 100644 --- a/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs +++ b/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs @@ -242,6 +242,36 @@ impl<'a> MayHaveSideEffects<'a> for BinaryExpression<'a> { } } +fn is_pure_regexp(name: &str, args: &[Argument<'_>]) -> bool { + name == "RegExp" + && match args.len() { + 0 | 1 => true, + 2 => args[1].as_expression().is_some_and(|e| { + matches!(e, Expression::Identifier(_) | Expression::StringLiteral(_)) + }), + _ => false, + } +} + +#[rustfmt::skip] +fn is_pure_global_function(name: &str) -> bool { + matches!(name, "decodeURI" | "decodeURIComponent" | "encodeURI" | "encodeURIComponent" + | "escape" | "isFinite" | "isNaN" | "parseFloat" | "parseInt") +} + +#[rustfmt::skip] +fn is_pure_call(name: &str) -> bool { + matches!(name, "Date" | "Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError" + | "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String" | "Symbol") +} + +#[rustfmt::skip] +fn is_pure_constructor(name: &str) -> bool { + matches!(name, "Set" | "Map" | "WeakSet" | "WeakMap" | "ArrayBuffer" | "Date" + | "Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError" + | "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String" | "Symbol") +} + /// Whether the name matches any known global constructors. /// /// @@ -530,15 +560,68 @@ fn get_array_minimum_length(arr: &ArrayExpression) -> usize { .sum() } +// `PF` in impl<'a> MayHaveSideEffects<'a> for CallExpression<'a> { fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool { if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) { return self.arguments.iter().any(|e| e.may_have_side_effects(ctx)); } + + if let Expression::Identifier(ident) = &self.callee + && ctx.is_global_reference(ident) + && let name = ident.name.as_str() + && (is_pure_global_function(name) + || is_pure_call(name) + || is_pure_regexp(name, &self.arguments)) + { + return self.arguments.iter().any(|e| e.may_have_side_effects(ctx)); + } + + let (ident, name) = match &self.callee { + Expression::StaticMemberExpression(member) if !member.optional => { + (member.object.get_identifier_reference(), member.property.name.as_str()) + } + Expression::ComputedMemberExpression(member) if !member.optional => { + match &member.expression { + Expression::StringLiteral(s) => { + (member.object.get_identifier_reference(), s.value.as_str()) + } + _ => return true, + } + } + _ => return true, + }; + + let Some(object) = ident.map(|ident| ident.name.as_str()) else { return true }; + + #[rustfmt::skip] + let is_global = match object { + "Array" => matches!(name, "isArray" | "of"), + "ArrayBuffer" => name == "isView", + "Date" => matches!(name, "now" | "parse" | "UTC"), + "Math" => matches!(name, "abs" | "acos" | "acosh" | "asin" | "asinh" | "atan" | "atan2" | "atanh" + | "cbrt" | "ceil" | "clz32" | "cos" | "cosh" | "exp" | "expm1" | "floor" | "fround" | "hypot" + | "imul" | "log" | "log10" | "log1p" | "log2" | "max" | "min" | "pow" | "random" | "round" + | "sign" | "sin" | "sinh" | "sqrt" | "tan" | "tanh" | "trunc"), + "Number" => matches!(name, "isFinite" | "isInteger" | "isNaN" | "isSafeInteger" | "parseFloat" | "parseInt"), + "Object" => matches!(name, "create" | "getOwnPropertyDescriptor" | "getOwnPropertyDescriptors" | "getOwnPropertyNames" + | "getOwnPropertySymbols" | "getPrototypeOf" | "hasOwn" | "is" | "isExtensible" | "isFrozen" | "isSealed" | "keys"), + "String" => matches!(name, "fromCharCode" | "fromCodePoint" | "raw"), + "Symbol" => matches!(name, "for" | "keyFor"), + "URL" => name == "canParse", + "Float32Array" | "Float64Array" | "Int16Array" | "Int32Array" | "Int8Array" | "Uint16Array" | "Uint32Array" | "Uint8Array" | "Uint8ClampedArray" => name == "of", + _ => false, + }; + + if is_global { + return self.arguments.iter().any(|e| e.may_have_side_effects(ctx)); + } + true } } +// `[ValueProperties]: PURE` in impl<'a> MayHaveSideEffects<'a> for NewExpression<'a> { fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool { if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) { @@ -546,27 +629,8 @@ impl<'a> MayHaveSideEffects<'a> for NewExpression<'a> { } if let Expression::Identifier(ident) = &self.callee && ctx.is_global_reference(ident) - && matches!( - ident.name.as_str(), - "Set" - | "Map" - | "WeakSet" - | "WeakMap" - | "ArrayBuffer" - | "Date" - | "Boolean" - | "Error" - | "EvalError" - | "RangeError" - | "ReferenceError" - | "RegExp" - | "SyntaxError" - | "TypeError" - | "URIError" - | "Number" - | "Object" - | "String" - ) + && let name = ident.name.as_str() + && (is_pure_constructor(name) || is_pure_regexp(name, &self.arguments)) { return self.arguments.iter().any(|e| e.may_have_side_effects(ctx)); } diff --git a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs index 1097a0e63986f..a3ca609c2bb2e 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs @@ -577,7 +577,7 @@ impl<'a> PeepholeOptimizations { } } - false + !call_expr.may_have_side_effects(ctx) } fn fold_arguments_into_needed_expressions( diff --git a/crates/oxc_minifier/src/peephole/replace_known_methods.rs b/crates/oxc_minifier/src/peephole/replace_known_methods.rs index c0c620e5e29b7..c6e7d74d19272 100644 --- a/crates/oxc_minifier/src/peephole/replace_known_methods.rs +++ b/crates/oxc_minifier/src/peephole/replace_known_methods.rs @@ -1431,7 +1431,7 @@ mod test { test("x = String.fromCharCode(0)", "x = '\\0'"); test("x = String.fromCharCode(120)", "x = 'x'"); test("x = String.fromCharCode(120, 121)", "x = 'xy'"); - test_same("String.fromCharCode(55358, 56768)"); + test_same("x = String.fromCharCode(55358, 56768)"); test("x = String.fromCharCode(0x10000)", "x = '\\0'"); test("x = String.fromCharCode(0x10078, 0x10079)", "x = 'xy'"); test("x = String.fromCharCode(0x1_0000_FFFF)", "x = '\u{ffff}'"); @@ -1635,8 +1635,8 @@ mod test { test("x = encodeURI('café')", "x = 'caf%C3%A9'"); // spellchecker:disable-line test("x = encodeURI('测试')", "x = '%E6%B5%8B%E8%AF%95'"); - test_same("encodeURI('a', 'b')"); - test_same("encodeURI(x)"); + test_same("x = encodeURI('a', 'b')"); + test_same("x = encodeURI(x)"); } #[test] @@ -1654,8 +1654,8 @@ mod test { test("x = encodeURIComponent('café')", "x = 'caf%C3%A9'"); // spellchecker:disable-line test("x = encodeURIComponent('测试')", "x = '%E6%B5%8B%E8%AF%95'"); - test_same("encodeURIComponent('a', 'b')"); - test_same("encodeURIComponent(x)"); + test_same("x = encodeURIComponent('a', 'b')"); + test_same("x = encodeURIComponent(x)"); } #[test] @@ -1675,11 +1675,11 @@ mod test { test("x = decodeURI('caf%C3%A9')", "x = 'café'"); // spellchecker:disable-line test("x = decodeURI('%E6%B5%8B%E8%AF%95')", "x = '测试'"); - test_same("decodeURI('%ZZ')"); // URIError - test_same("decodeURI('%A')"); // URIError + test_same("x = decodeURI('%ZZ')"); // URIError + test_same("x = decodeURI('%A')"); // URIError - test_same("decodeURI('a', 'b')"); - test_same("decodeURI(x)"); + test_same("x = decodeURI('a', 'b')"); + test_same("x = decodeURI(x)"); } #[test] @@ -1698,11 +1698,11 @@ mod test { test("x = decodeURIComponent('caf%C3%A9')", "x = 'café'"); // spellchecker:disable-line test("x = decodeURIComponent('%E6%B5%8B%E8%AF%95')", "x = '测试'"); - test_same("decodeURIComponent('%ZZ')"); // URIError - test_same("decodeURIComponent('%A')"); // URIError + test_same("x = decodeURIComponent('%ZZ')"); // URIError + test_same("x = decodeURIComponent('%A')"); // URIError - test_same("decodeURIComponent('a', 'b')"); - test_same("decodeURIComponent(x)"); + test_same("x = decodeURIComponent('a', 'b')"); + test_same("x = decodeURIComponent(x)"); } #[test] diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 60550b87e03b3..604fbb0e3b471 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -1699,7 +1699,7 @@ mod test { test("new RegExp('a')", ""); test("new RegExp(0)", ""); test("new RegExp(null)", ""); - test("new RegExp('a', 'g')", "RegExp('a', 'g')"); + test("x = new RegExp('a', 'g')", "x = RegExp('a', 'g')"); test_same("new RegExp(foo)"); test("new RegExp(/foo/)", ""); } diff --git a/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs b/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs index eaebcd5e2bf28..85520eaf578bb 100644 --- a/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs +++ b/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs @@ -180,7 +180,8 @@ fn closure_compiler_tests() { test("templateFunction`template`", true); test("st = `${name}template`", true); test("tempFunc = templateFunction`template`", true); - // test("new RegExp('foobar', 'i')", false); + test("new RegExp('foobar', 'i')", false); + test("new RegExp('foobar', 2)", true); test("new RegExp(SomethingWacky(), 'i')", true); // test("new Array()", false); // test("new Array", false); @@ -205,8 +206,8 @@ fn closure_compiler_tests() { // test("(true ? {} : []).foo = 2;", false); // test("({},[]).foo = 2;", false); test("delete a.b", true); - // test("Math.random();", false); - test("Math.random(seed);", true); + test("Math.random();", false); + test("Math.random(Math);", true); // test("[1, 1].foo;", false); // test("export var x = 0;", true); // test("export let x = 0;", true); @@ -751,6 +752,7 @@ fn test_property_access() { test("[...a, 1][0]", true); // "...a" may have a sideeffect } +// `[ValueProperties]: PURE` in #[test] fn test_new_expressions() { test("new AggregateError", true); @@ -773,6 +775,127 @@ fn test_new_expressions() { test("new Number", false); test("new Object", false); test("new String", false); + test("new Symbol", false); +} + +// `PF` in +#[test] +fn test_call_expressions() { + test("AggregateError()", true); + test("DataView()", true); + test("Set()", true); + test("Map()", true); + test("WeakSet()", true); + test("WeakMap()", true); + test("ArrayBuffer()", true); + test("Date()", false); + test("Boolean()", false); + test("Error()", false); + test("EvalError()", false); + test("RangeError()", false); + test("ReferenceError()", false); + test("RegExp()", false); + test("SyntaxError()", false); + test("TypeError()", false); + test("URIError()", false); + test("Number()", false); + test("Object()", false); + test("String()", false); + test("Symbol()", false); + + test("decodeURI()", false); + test("decodeURIComponent()", false); + test("encodeURI()", false); + test("encodeURIComponent()", false); + test("escape()", false); + test("isFinite()", false); + test("isNaN()", false); + test("parseFloat()", false); + test("parseInt()", false); + + test("Array.isArray()", false); + test("Array.of()", false); + + test("ArrayBuffer.isView()", false); + + test("Date.now()", false); + test("Date.parse()", false); + test("Date.UTC()", false); + + test("Math.abs()", false); + test("Math.acos()", false); + test("Math.acosh()", false); + test("Math.asin()", false); + test("Math.asinh()", false); + test("Math.atan()", false); + test("Math.atan2()", false); + test("Math.atanh()", false); + test("Math.cbrt()", false); + test("Math.ceil()", false); + test("Math.clz32()", false); + test("Math.cos()", false); + test("Math.cosh()", false); + test("Math.exp()", false); + test("Math.expm1()", false); + test("Math.floor()", false); + test("Math.fround()", false); + test("Math.hypot()", false); + test("Math.imul()", false); + test("Math.log()", false); + test("Math.log10()", false); + test("Math.log1p()", false); + test("Math.log2()", false); + test("Math.max()", false); + test("Math.min()", false); + test("Math.pow()", false); + test("Math.random()", false); + test("Math.round()", false); + test("Math.sign()", false); + test("Math.sin()", false); + test("Math.sinh()", false); + test("Math.sqrt()", false); + test("Math.tan()", false); + test("Math.tanh()", false); + test("Math.trunc()", false); + + test("Number.isFinite()", false); + test("Number.isInteger()", false); + test("Number.isNaN()", false); + test("Number.isSafeInteger()", false); + test("Number.parseFloat()", false); + test("Number.parseInt()", false); + + test("Object.create()", false); + test("Object.getOwnPropertyDescriptor()", false); + test("Object.getOwnPropertyDescriptors()", false); + test("Object.getOwnPropertyNames()", false); + test("Object.getOwnPropertySymbols()", false); + test("Object.getPrototypeOf()", false); + test("Object.hasOwn()", false); + test("Object.is()", false); + test("Object.isExtensible()", false); + test("Object.isFrozen()", false); + test("Object.isSealed()", false); + test("Object.keys()", false); + + test("String.fromCharCode()", false); + test("String.fromCodePoint()", false); + test("String.raw()", false); + + test("Symbol.for()", false); + test("Symbol.keyFor()", false); + + test("URL.canParse()", false); + + test("Float32Array.of()", false); + test("Float64Array.of()", false); + test("Int16Array.of()", false); + test("Int32Array.of()", false); + test("Int8Array.of()", false); + test("Uint16Array.of()", false); + test("Uint32Array.of()", false); + test("Uint8Array.of()", false); + test("Uint8ClampedArray.of()", false); } #[test] diff --git a/crates/oxc_minifier/tests/peephole/obscure_edge_cases.rs b/crates/oxc_minifier/tests/peephole/obscure_edge_cases.rs index df55596276d1f..805d9e12d46b6 100644 --- a/crates/oxc_minifier/tests/peephole/obscure_edge_cases.rs +++ b/crates/oxc_minifier/tests/peephole/obscure_edge_cases.rs @@ -46,7 +46,7 @@ fn test_string_concatenation_edge_cases() { // Test cases that should NOT be optimized test_same("return obj['123invalid']"); // starts with number - test_same("return obj['key-with-dash']"); // contains dash + test_same("return obj['key-with-dash']"); // contains dash test_same("return obj['key with space']"); // contains space test_same("return obj[dynamicKey]"); // dynamic key test_same("return obj['']"); // empty string @@ -125,7 +125,7 @@ fn test_mathematical_expression_edge_cases() { // Test cases that are eliminated as dead code (unused expressions) test("NaN + 1", ""); // eliminated as unused expression - test("NaN * 0", ""); // eliminated as unused expression + test("NaN * 0", ""); // eliminated as unused expression test("NaN / NaN", ""); // eliminated as unused expression test("Infinity + 1", ""); // eliminated as unused expression test("Infinity - Infinity", ""); // eliminated as unused expression @@ -147,10 +147,11 @@ fn test_function_call_optimization_edge_cases() { // Test cases that should NOT be optimized due to side effects test_same("console.log('test')"); - test_same("Math.random()"); - test_same("Date.now()"); test_same("Object.keys(obj)"); // depends on obj - test_same("Object(null)"); // object constructor edge case + + test("Math.random()", ""); + test("Date.now()", ""); + test("Object(null)", ""); // Test method calls on literals that get optimized test("return 'hello'.length", "return 5"); // string length optimization @@ -224,7 +225,7 @@ fn test_assignment_optimization_edge_cases() { // Test cases that demonstrate current conservative behavior with property/array access test_same("obj.prop = obj.prop + 1"); // conservative with property access (getters/setters) - test_same("arr[i] = arr[i] + 1"); // conservative with array access + test_same("arr[i] = arr[i] + 1"); // conservative with array access test("this.prop = this.prop + 1", "this.prop += 1"); // this context gets optimized } @@ -232,7 +233,7 @@ fn test_assignment_optimization_edge_cases() { fn test_side_effect_analysis_edge_cases() { // Test expressions that are pure but not eliminated in all contexts test("42;", ""); // literal expressions get eliminated - test("'hello';", "'hello';"); // string literal statement not eliminated + test("'hello';", "'hello';"); // string literal statement not eliminated test("true;", ""); // boolean literals get eliminated test("1 + 2 + 3;", ""); // pure arithmetic gets eliminated diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index e68e70c4be828..e7a5c8551d9c6 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -11,7 +11,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi 544.10 kB | 71.38 kB | 72.48 kB | 25.85 kB | 26.20 kB | 1 | lodash.js -555.77 kB | 270.87 kB | 270.13 kB | 88.23 kB | 90.80 kB | 2 | d3.js +555.77 kB | 270.86 kB | 270.13 kB | 88.23 kB | 90.80 kB | 2 | d3.js 1.01 MB | 440.04 kB | 458.89 kB | 122.28 kB | 126.71 kB | 2 | bundle.min.js