Skip to content
Merged
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
106 changes: 85 additions & 21 deletions crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
/// <https://tc39.es/ecma262/multipage/global-object.html#sec-constructor-properties-of-the-global-object>
Expand Down Expand Up @@ -530,43 +560,77 @@ fn get_array_minimum_length(arr: &ArrayExpression) -> usize {
.sum()
}

// `PF` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
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 <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
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) {
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
}
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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ impl<'a> PeepholeOptimizations {
}
}

false
!call_expr.may_have_side_effects(ctx)
}

fn fold_arguments_into_needed_expressions(
Expand Down
26 changes: 13 additions & 13 deletions crates/oxc_minifier/src/peephole/replace_known_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}'");
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/)", "");
}
Expand Down
129 changes: 126 additions & 3 deletions crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -751,6 +752,7 @@ fn test_property_access() {
test("[...a, 1][0]", true); // "...a" may have a sideeffect
}

// `[ValueProperties]: PURE` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
#[test]
fn test_new_expressions() {
test("new AggregateError", true);
Expand All @@ -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 <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
#[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]
Expand Down
Loading
Loading