Skip to content

Commit 74c9ddd

Browse files
Dunqingclaude
andcommitted
fix(ecmascript): treat collection constructor with variable arg as side-effectful
`new Map(x)`, `new Set(x)`, `new WeakMap(x)`, `new WeakSet(x)` were incorrectly considered side-effect-free when the argument is a variable reference. These constructors iterate their argument via `Symbol.iterator`, which can execute arbitrary user code. Now only provably safe arguments are considered pure: no arguments, `null`, `undefined`, or array literals (for Map/WeakMap, entries must also be array literals). This matches esbuild and Rollup behavior. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 618a598 commit 74c9ddd

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

crates/oxc_ecmascript/src/side_effects/expressions.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,14 +295,50 @@ fn is_pure_call(name: &str) -> bool {
295295

296296
#[rustfmt::skip]
297297
fn is_pure_constructor(name: &str) -> bool {
298-
matches!(name, "Set" | "Map" | "WeakSet" | "WeakMap" | "ArrayBuffer" | "Date"
298+
matches!(name, "ArrayBuffer" | "Date"
299299
| "Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError"
300300
| "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String"
301301
| "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array"
302302
| "Int32Array" | "Uint32Array" | "Float32Array" | "Float64Array"
303303
| "BigInt64Array" | "BigUint64Array")
304304
}
305305

306+
/// Whether a collection constructor (`Map`, `Set`, `WeakMap`, `WeakSet`) call is pure.
307+
///
308+
/// These constructors iterate their argument via `Symbol.iterator`, which can have side effects
309+
/// when the argument is a variable reference (custom iterators, proxies, etc.).
310+
/// Only provably safe arguments are considered pure:
311+
/// - No arguments: `new Set()`, `new Map()`
312+
/// - `null` or `undefined`: `new Set(null)`, `new Map(undefined)`
313+
/// - Array literals: `new Set([1,2,3])`, `new Map([[k,v]])`
314+
///
315+
/// Following esbuild and Rollup behavior.
316+
/// See <https://github.com/oxc-project/oxc/issues/XXXX>
317+
fn is_pure_collection_constructor(name: &str, args: &[Argument<'_>]) -> bool {
318+
if !matches!(name, "Set" | "Map" | "WeakSet" | "WeakMap") {
319+
return false;
320+
}
321+
match args.first() {
322+
// No arguments: always pure
323+
None => true,
324+
Some(arg) => match arg.as_expression() {
325+
Some(Expression::NullLiteral(_)) => true,
326+
Some(Expression::Identifier(id)) if id.name == "undefined" => true,
327+
Some(Expression::ArrayExpression(arr)) => {
328+
// For Map/WeakMap, each element must also be an array literal (key-value pair)
329+
if matches!(name, "Map" | "WeakMap") {
330+
arr.elements
331+
.iter()
332+
.all(|el| matches!(el, ArrayExpressionElement::ArrayExpression(_)))
333+
} else {
334+
true
335+
}
336+
}
337+
_ => false,
338+
},
339+
}
340+
}
341+
306342
/// Whether the name matches any known global constructors.
307343
///
308344
/// <https://tc39.es/ecma262/multipage/global-object.html#sec-constructor-properties-of-the-global-object>
@@ -998,7 +1034,9 @@ impl<'a> MayHaveSideEffects<'a> for NewExpression<'a> {
9981034
if let Expression::Identifier(ident) = &self.callee
9991035
&& ctx.is_global_reference(ident)
10001036
&& let name = ident.name.as_str()
1001-
&& (is_pure_constructor(name) || (name == "RegExp" && is_valid_regexp(&self.arguments)))
1037+
&& (is_pure_constructor(name)
1038+
|| (name == "RegExp" && is_valid_regexp(&self.arguments))
1039+
|| is_pure_collection_constructor(name, &self.arguments))
10021040
{
10031041
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
10041042
}

crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,36 @@ fn test_new_expressions() {
957957
test("new BigUint64Array", false);
958958
// DataView requires an ArrayBuffer argument; calling without one throws
959959
test("new DataView", true);
960+
961+
// Collection constructors iterate their argument via Symbol.iterator,
962+
// so they are only pure with no args, null, undefined, or array literals.
963+
// null/undefined — pure
964+
test("new Set(null)", false);
965+
test("new Map(null)", false);
966+
test("new WeakSet(null)", false);
967+
test("new WeakMap(null)", false);
968+
test("new Set(undefined)", false);
969+
test("new Map(undefined)", false);
970+
// Array literal — pure
971+
test("new Set([])", false);
972+
test("new Set([1, 2, 3])", false);
973+
test("new Map([])", false);
974+
test("new Map([[1, 2], [3, 4]])", false);
975+
test("new WeakSet([])", false);
976+
test("new WeakMap([])", false);
977+
test("new WeakMap([[{}, 1]])", false);
978+
// Variable reference — NOT pure (could have custom Symbol.iterator)
979+
test("new Set(x)", true);
980+
test("new Map(x)", true);
981+
test("new WeakSet(x)", true);
982+
test("new WeakMap(x)", true);
983+
// Non-array literals — NOT pure
984+
test("new Set(false)", true);
985+
test("new Map({})", true);
986+
// Map with non-array entries — NOT pure
987+
test("new Map([x])", true);
988+
test("new Map([x, []])", true);
989+
test("new Map([[], x])", true);
960990
}
961991

962992
// `PF` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>

0 commit comments

Comments
 (0)