biome style/useObjectSpread
labelled safe
Analysis Nemotron 3 Super
The rule flags calls to Object.assign() used to create a new object by merging properties where the first argument is an empty object literal.
Fix: Replace Object.assign({}, sources...) with an object literal using spread properties: {...sources}
10 hazards
- Rewriting Object.assign({}, foo) to {...foo} fails if foo is null or undefined because spreading null/undefined throws a TypeError, while Object.assign treats them as no-ops.
- If the first argument is not an empty object (e.g., Object.assign({a:1}, b)), the rule does not fire, but if mistakenly applied, spreading would lose the initial {a:1} properties.
- When Object.assign is used with a non-literal first argument like a variable (Object.assign(x, y)), spreading {...x, ...y} changes semantics if x is mutated later, as the spread captures x's value at call time vs ObjectAssign's reference.
- If sources contain getters, Object.assign invokes them during assignment, while spread evaluates them once during object creation—though both are eager, differences arise if getters have side effects or depend on mutable state.
- In non-object parameters (e.g., Object.assign({}, 'abc')), spreading a string creates properties for each character ('0':'a','1':'b',etc), matching Object.assign, but if the value is symbol-only, spread ignores symbols while Object.assign copies them—though both actually copy symbol properties, so this hazard is invalid; corrected: spread and Object.assign both copy symbol-enumerable properties identically.
- Comments attached to the Object.assign call or its arguments may be lost or misplaced during rewrite, as the autofix replaces the entire expression and may not preserve comment associations.
- If Object.assign is shadowed by a local variable (e.g., const ObjectAssign = Object.assign; ObjectAssign({}, x)), the rule does not trigger because it's not the global Object.assign, but if misapplied, the fix would break by introducing spread syntax unrelated to the shadowed binding.
- In strict mode, both behaviors are identical, but in sloppy mode, Object.assign may trigger __set__ on proxies differently than spread—though modern engines align them, historical differences could exist in edge cases with proxy traps.
- When the call is part of a larger expression (e.g., return Object.assign({}, x)), rewriting to return {...x} preserves value but may alter stack traces or debugging profiles due to different function origins.
- If the sources include expressions with side effects (e.g., Object.assign({}, f(), g())), the evaluation order of f() and g() is left-to-right in both Object.assign and spread, so no hazard—but if the rewrite incorrectly reordered arguments, it would break; however, Biome preserves order, so this is not a hazard.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent | Non-literal first argument mutation | When Object.assign is used with a non-literal first argument like a variable (Object.assign(x, y)), spreading {...x, ...y} changes semantics if x is mutated later, as the spread captures x's value at |
| r1-p002 | fix ok | Getter side effects difference | If sources contain getters, Object.assign invokes them during assignment, while spread evaluates them once during object creation—though both are eager, differences arise if getters have side effects |
| r1-p003 | silent prediction off | Strict vs sloppy mode proxy behavior | In strict mode, both behaviors are identical, but in sloppy mode, Object.assign may trigger __set__ on proxies differently than spread—though modern engines align them, historical differences could ex |
| r1-p004 | fix ok | Regexp lastIndex shared state | regular expressions: escapes, unicode flag, lastIndex with the g flag |
| r1-p005 | fix ok | Side effect evaluation order preservation | If the sources include expressions with side effects (e.g., Object.assign({}, f(), g())), the evaluation order of f() and g() is left-to-right in both Object.assign and spread, so no hazard—but if the |
| r1-p006 | fix ok | Return expression stack trace origin | When the call is part of a larger expression (e.g., return Object.assign({}, x)), rewriting to return {...x} preserves value but may alter stack traces or debugging profiles due to different function |
| r1-p007 | silent | Non-empty first argument near-miss | If the first argument is not an empty object (e.g., Object.assign({a:1}, b)), the rule does not fire, but if mistakenly applied, spreading would lose the initial {a:1} properties. |
| r1-p008 | fix ok | Var hoisting and redeclaration in loop | var hoisting and re-declaration inside loops or after use |
| r2-p001 | silent prediction off | ASI and semicolon-less code | Leading parenthesis on next line causes misparsing when combined with object spread rewrite |
| r2-p002 | silent prediction off | Non-literal first argument mutation | Spreading captures value at call time vs Object.assign maintaining reference via target |
| r2-p003 | invalid | Strict vs sloppy mode semantics | In sloppy mode, Object.assign may coerce primitives; spread rejects them directly |
| r2-p004 | silent | Function declarations in blocks/switch/labels | Function declaration scoping differs between statement and expression contexts after rewrite |
| r2-p005 | fix ok | Var hoisting and redeclaration in loop | Rebinding var across iterations interacts differently with captured values in spread vs Object.assign |
| r2-p006 | fix ok | Part of larger expression affecting stack traces | Returning object spread vs Object.assign changes function origin in stack traces |
| r2-p007 | invalid | Non-object parameters: string and symbol handling | String spreading creates indexed properties; symbol properties copied identically in both |
| r2-p008 | fix ok prediction off | This binding: arrow vs function, extracted methods | Method extraction loses this binding; neither Object.assign nor spread involves this in target/sources |
Sources
r1-p001 · Non-literal first argument mutation
1let x = { a: 1 };
2const y = { b: 2 };
3// Expected trigger: true? No - because arg is not {}, so expect_trigger=false
4const ref = Object.assign(x, y);
5x.a = 99;
6console.log(ref.a);
99
r1-p002 · Getter side effects difference
1let counter = 0;
2const src = {
3 get value() {
4 counter++;
5 return counter * 10;
6 }
7};
8const result = Object.assign({}, src);
9console.log('result.value:', result.value, 'counter:', counter);
--- orig.js 2026-10-17 15:38:52.903212603 -0400 +++ fixed.js 2026-10-17 15:38:53.180208894 -0400 @@ -5,5 +5,5 @@ return counter * 10; } }; -const result = Object.assign({}, src); +const result = ({...src,}); console.log('result.value:', result.value, 'counter:', counter);
result.value: 10 counter: 1
r1-p003 · Strict vs sloppy mode proxy behavior
1"use sloppy";
2let log = [];
3const target = {};
4const proxy = new Proxy(target, {
5 set(t, p, v) {
6 log.push(p + '=' + v);
7 return true;
8 }
9});
10Object.assign(proxy, { a: 1 });
11console.log(log.join(','));
a=1
r1-p004 · Regexp lastIndex shared state
1const regex = /a/g;
2regex.lastIndex = 0;
3const str = 'aa';
4const match1 = regex.exec(str);
5const obj = Object.assign({}, { pattern: regex });
6const match2 = regex.exec(str);
7console.log('match1:', match1 ? match1[0] : null, 'match2:', match2 ? match2[0] : null);
--- orig.js 2026-10-17 15:38:53.564203745 -0400 +++ fixed.js 2026-10-17 15:38:53.835200111 -0400 @@ -2,6 +2,6 @@ regex.lastIndex = 0; const str = 'aa'; const match1 = regex.exec(str); -const obj = Object.assign({}, { pattern: regex }); +const obj = ({pattern: regex ,}); const match2 = regex.exec(str); console.log('match1:', match1 ? match1[0] : null, 'match2:', match2 ? match2[0] : null);
match1: a match2: a
r1-p005 · Side effect evaluation order preservation
1let order = '';
2function f() { order += 'f'; return { x: 1 }; }
3function g() { order += 'g'; return { y: 2 }; }
4const result = Object.assign({}, f(), g());
5console.log('order:', order, 'result:', JSON.stringify(result));
--- orig.js 2026-10-17 15:38:54.001197886 -0400 +++ fixed.js 2026-10-17 15:38:54.279194144 -0400 @@ -1,5 +1,5 @@ let order = ''; function f() { order += 'f'; return { x: 1 }; } function g() { order += 'g'; return { y: 2 }; } -const result = Object.assign({}, f(), g()); +const result = ({...f(),...g(),}); console.log('order:', order, 'result:', JSON.stringify(result));
order: fg result: {"x":1,"y":2}r1-p006 · Return expression stack trace origin
1function getObj() {
2 return { a: 1 };
3}
4function wrapper() {
5 try {
6 throw new Error('trace');
7 } catch (e) {
8 // Stack trace differs based on whether we use Object.assign or spread
9 console.log(e.stack.split('\n')[1].trim());
10 }
11 return Object.assign({}, getObj());
12}
13wrapper();
--- orig.js 2026-10-17 15:38:54.446191896 -0400 +++ fixed.js 2026-10-17 15:38:54.724188155 -0400 @@ -8,6 +8,6 @@ // Stack trace differs based on whether we use Object.assign or spread console.log(e.stack.split('\n')[1].trim()); } - return Object.assign({}, getObj()); + return ({...getObj(),}); } wrapper();
at wrapper (file:///home/sun/dev/diaghunt/runs/2026-10-17T19-38-41-627Z-2c90-biome-style_useObjectSpread/r1-p006/orig.js:6:11)
r1-p007 · Non-empty first argument near-miss
1const base = { a: 1, b: 2 };
2const add = { c: 3 };
3const result = Object.assign(base, add);
4console.log('result.a:', result.a, 'result.b:', result.b, 'result.c:', result.c);
result.a: 1 result.b: 2 result.c: 3
r1-p008 · Var hoisting and redeclaration in loop
1for (var i = 0; i < 2; i++) {
2 var obj = { x: i };
3 if (i === 0) {
4 var copied = Object.assign({}, obj);
5 }
6 obj.x = 99;
7}
8console.log('copied.x:', copied.x);
--- orig.js 2026-10-17 15:38:55.102183062 -0400 +++ fixed.js 2026-10-17 15:38:55.368179468 -0400 @@ -1,7 +1,7 @@ for (var i = 0; i < 2; i++) { var obj = { x: i }; if (i === 0) { - var copied = Object.assign({}, obj); + var copied = ({...obj,}); } obj.x = 99; }
copied.x: 0
r2-p001 · ASI and semicolon-less code
1let x = {}
2;
3(() => console.log('IIFE ran'))();
4console.log(typeof x)
IIFE ran object
r2-p002 · Non-literal first argument mutation
1let obj = {a: 1}; let ref = obj;
2const res1 = Object.assign(ref, {b: 2});
3obj.a = 99;
4console.log(res1.a, ref === obj)
5obj = {a: 1}; ref = obj;
6const res2 = {...ref, b: 2};
7obj.a = 99;
8console.log(res2.a, ref === obj)
99 true 1 true
r2-p003 · Strict vs sloppy mode semantics (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-17T19-38-4)
1"use sloppy";
2try { Object.assign({}, undefined) } catch(e) { console.log(e.name) }
3"use strict";
4try { Object.assign({}, undefined) } catch(e) { console.log(e.name) }
5try { {} = { ...undefined } } catch(e) { console.log(e.name) }
r2-p004 · Function declarations in blocks/switch/labels
1switch(0) {
2 case 0:
3 function f(){ return 1 }
4 break;
5}
6console.log(typeof f)
undefined
r2-p005 · Var hoisting and redeclaration in loop
1for (var i = 0; i < 2; i++) {
2 var obj = {value: i};
3 const copy = Object.assign({}, obj);
4 obj.value += 10;
5 console.log(copy.value)
6}
--- orig.js 2026-10-17 15:39:01.053101892 -0400 +++ fixed.js 2026-10-17 15:39:01.315098274 -0400 @@ -1,6 +1,6 @@ for (var i = 0; i < 2; i++) { var obj = {value: i}; - const copy = Object.assign({}, obj); + const copy = ({...obj,}); obj.value += 10; console.log(copy.value) }
0 1
r2-p006 · Part of larger expression affecting stack traces
1function getObj() { return {x: 1} }
2function test() {
3 return Object.assign({}, getObj());
4}
5try { test() } catch(e) {
6 console.log(e.stack.split('\n')[1].trim())
7}
--- orig.js 2026-10-17 15:39:01.480095995 -0400 +++ fixed.js 2026-10-17 15:39:01.725092611 -0400 @@ -1,6 +1,6 @@ function getObj() { return {x: 1} } function test() { - return Object.assign({}, getObj()); + return ({...getObj(),}); } try { test() } catch(e) { console.log(e.stack.split('\n')[1].trim())
r2-p007 · Non-object parameters: string and symbol handling (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-17T19-38-4)
1const s = 'ab';
2console.log(Object.assign({}, s).0, Object.assign({}, s).length)
3console.log({...s}.0, {...s}.length)
4const symObj = {[Symbol.for('test')]: 42};
5console.log(Object.assign({}, symObj)[Symbol.for('test')])
6console.log({...symObj}[Symbol.for('test')])
r2-p008 · This binding: arrow vs function, extracted methods
1const obj = {val: 1, getVal() { return this.val }};
2const fn = obj.getVal;
3console.log(fn.call(obj))
4const copied = Object.assign({}, obj);
5console.log(copied.getVal?.call(obj))
--- orig.js 2026-10-17 15:39:01.931089766 -0400 +++ fixed.js 2026-10-17 15:39:02.198086068 -0400 @@ -1,5 +1,5 @@ const obj = {val: 1, getVal() { return this.val }}; const fn = obj.getVal; console.log(fn.call(obj)) -const copied = Object.assign({}, obj); +const copied = ({...obj,}); console.log(copied.getVal?.call(obj))
1 1