mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(ecmascript): treat collection constructor with variable arg as side-effectful (#20383)
## Summary `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 (custom iterators, proxies, etc.). **Before:** `is_pure_constructor` unconditionally included `Map`/`Set`/`WeakMap`/`WeakSet`, so `new Map(variable)` was treated as pure. **After:** Collection constructors are only pure with provably safe arguments: - No arguments: `new Set()` - `null` or `undefined`: `new Set(null)` - Array literals: `new Set([1, 2, 3])`, `new Map([[k, v]])` - Variable references or other expressions: **NOT pure** This matches the behavior of **esbuild** and **Rollup**: - esbuild: only marks pure with no args, null, undefined, or array literals ([test cases](https://github.com/evanw/esbuild/blob/main/internal/js_parser/js_parser_test.go)) - Rollup: uses `PURE_WITH_ARRAY` — only array literals are trusted ([knownGlobals.ts](https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts)) **Found via:** Rolldown's `tree-shake-iterable` test failing after delegating `NewExpression` side-effect analysis to Oxc. ## Test plan - [x] Added tests for all cases: null, undefined, array literals, variable refs, Map with non-array entries - [x] All existing `oxc_minifier` tests pass (473 passed) - [x] Verified against Rolldown's `tree-shake-iterable` and `manual-pure-functions` test suites 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
1 parent
ce650993b0
commit
edb8677e26
2 files changed
+70
-2
No files matched your search
@@ -295,7 +295,7 @@ fn is_pure_call(name: &str) -> bool {
|
||||
|
||||
#[rustfmt::skip]
|
||||
fn is_pure_constructor(name: &str) -> bool {
|
||||
matches!(name, "Set" | "Map" | "WeakSet" | "WeakMap" | "ArrayBuffer" | "Date"
|
||||
matches!(name, "ArrayBuffer" | "Date"
|
||||
| "Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError"
|
||||
| "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String"
|
||||
| "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array"
|
||||
@@ -303,6 +303,42 @@ fn is_pure_constructor(name: &str) -> bool {
|
||||
| "BigInt64Array" | "BigUint64Array")
|
||||
}
|
||||
|
||||
/// Whether a collection constructor (`Map`, `Set`, `WeakMap`, `WeakSet`) call is pure.
|
||||
///
|
||||
/// These constructors iterate their argument via `Symbol.iterator`, which can have side effects
|
||||
/// when the argument is a variable reference (custom iterators, proxies, etc.).
|
||||
/// Only provably safe arguments are considered pure:
|
||||
/// - No arguments: `new Set()`, `new Map()`
|
||||
/// - `null` or `undefined`: `new Set(null)`, `new Map(undefined)`
|
||||
/// - Array literals: `new Set([1,2,3])`, `new Map([[k,v]])`
|
||||
///
|
||||
/// Following esbuild and Rollup behavior.
|
||||
/// See <https://github.com/oxc-project/oxc/issues/XXXX>
|
||||
fn is_pure_collection_constructor(name: &str, args: &[Argument<'_>]) -> bool {
|
||||
if !matches!(name, "Set" | "Map" | "WeakSet" | "WeakMap") {
|
||||
return false;
|
||||
}
|
||||
match args.first() {
|
||||
// No arguments: always pure
|
||||
None => true,
|
||||
Some(arg) => match arg.as_expression() {
|
||||
Some(Expression::NullLiteral(_)) => true,
|
||||
Some(Expression::Identifier(id)) if id.name == "undefined" => true,
|
||||
Some(Expression::ArrayExpression(arr)) => {
|
||||
// For Map/WeakMap, each element must also be an array literal (key-value pair)
|
||||
if matches!(name, "Map" | "WeakMap") {
|
||||
arr.elements
|
||||
.iter()
|
||||
.all(|el| matches!(el, ArrayExpressionElement::ArrayExpression(_)))
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the name matches any known global constructors.
|
||||
///
|
||||
/// <https://tc39.es/ecma262/multipage/global-object.html#sec-constructor-properties-of-the-global-object>
|
||||
@@ -1004,7 +1040,9 @@ impl<'a> MayHaveSideEffects<'a> for NewExpression<'a> {
|
||||
if let Expression::Identifier(ident) = &self.callee
|
||||
&& ctx.is_global_reference(ident)
|
||||
&& let name = ident.name.as_str()
|
||||
&& (is_pure_constructor(name) || (name == "RegExp" && is_valid_regexp(&self.arguments)))
|
||||
&& (is_pure_constructor(name)
|
||||
|| (name == "RegExp" && is_valid_regexp(&self.arguments))
|
||||
|| is_pure_collection_constructor(name, &self.arguments))
|
||||
{
|
||||
return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
|
||||
}
|
||||
|
||||
@@ -962,6 +962,36 @@ fn test_new_expressions() {
|
||||
test("new BigUint64Array", false);
|
||||
// DataView requires an ArrayBuffer argument; calling without one throws
|
||||
test("new DataView", true);
|
||||
|
||||
// Collection constructors iterate their argument via Symbol.iterator,
|
||||
// so they are only pure with no args, null, undefined, or array literals.
|
||||
// null/undefined — pure
|
||||
test("new Set(null)", false);
|
||||
test("new Map(null)", false);
|
||||
test("new WeakSet(null)", false);
|
||||
test("new WeakMap(null)", false);
|
||||
test("new Set(undefined)", false);
|
||||
test("new Map(undefined)", false);
|
||||
// Array literal — pure
|
||||
test("new Set([])", false);
|
||||
test("new Set([1, 2, 3])", false);
|
||||
test("new Map([])", false);
|
||||
test("new Map([[1, 2], [3, 4]])", false);
|
||||
test("new WeakSet([])", false);
|
||||
test("new WeakMap([])", false);
|
||||
test("new WeakMap([[{}, 1]])", false);
|
||||
// Variable reference — NOT pure (could have custom Symbol.iterator)
|
||||
test("new Set(x)", true);
|
||||
test("new Map(x)", true);
|
||||
test("new WeakSet(x)", true);
|
||||
test("new WeakMap(x)", true);
|
||||
// Non-array literals — NOT pure
|
||||
test("new Set(false)", true);
|
||||
test("new Map({})", true);
|
||||
// Map with non-array entries — NOT pure
|
||||
test("new Map([x])", true);
|
||||
test("new Map([x, []])", true);
|
||||
test("new Map([[], x])", true);
|
||||
}
|
||||
|
||||
// `PF` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
|
||||
|
||||
Reference in new issue
Block a user