fix(transformer/class-properties): apply a compound assignment to a static private field once (#28606)

If the target is `es2021` or lower, a compound assignment to a static
private field applies the operator twice, unless the left side of
`.#prop` is the class name. For example, `this.#count += k` is affected,
but `Counter.#count += k` is not.

```js
class Counter {
  static #count = 10;
  static add(k) {
    this.#count += k;
    return this.#count;
  }
}
console.log(Counter.add(1)); // 11
```

The transformed code prints `21`, because it keeps `+=` and also adds
the old value on the right side:

```js
// before
_count._ += _assertClassBrand(Counter, this, _assertClassBrand(Counter, this, _count)._ + k);

// after
_count._ = _assertClassBrand(Counter, this, _assertClassBrand(Counter, this, _count)._ + k);
```

`Counter.#count += k` uses a separate branch that sets the operator to
`=`. The branch for `this.#count += k` did not set the operator. This PR
adds that line.

The new fixture `oxc/private-static-compound-assignment` covers `this`,
an object from a function call, and an object without the private field.
Its `exec.js` fails on main with `expected 21 to be 11`.

---

AI assistance was used for this change. I reviewed and tested it.
This commit is contained in:
YT authored and GitHub committed 2026-11-04 09:52:54 +08:00
1 parent 07627f7a9b
commit 8432de6b27
6 files changed
+48 -3

No files matched your search

@@ -708,6 +708,7 @@ impl<'a> ClassProperties<'a> {
let value =
Expression::new_binary_expression(SPAN, get_expr, operator, value, ctx);
// `_assertClassBrand(Class, object, _assertClassBrand(Class, object, _prop)._ + value)`
assign_expr.operator = AssignmentOperator::Assign;
assign_expr.right =
self.create_assert_class_brand(class_ident2, object1, value, SPAN, ctx);
} else if let Some(operator) = operator.to_logical_operator() {
@@ -1,6 +1,6 @@
commit: b835f0c6
Passed: 307/436
Passed: 308/437
# All Passed:
* babel-plugin-transform-class-static-block
@@ -34,7 +34,7 @@ after transform: SymbolId(1) "C"
rebuilt : SymbolId(3) "C"
# babel-plugin-transform-class-properties (31/35)
# babel-plugin-transform-class-properties (32/36)
* private-field-resolve-to-method/input.js
x Output mismatch
@@ -2,7 +2,7 @@ commit: b835f0c6
node: v26.12.1
Passed: 42 of 44 (95.45%)
Passed: 43 of 45 (95.56%)
Failures:
@@ -0,0 +1,28 @@
class Counter {
static #count = 10;
static add(k) {
return (this.#count += k);
}
static subtract(getObject, k) {
getObject().#count -= k;
return this.#count;
}
static addTo(obj) {
obj.#count += 1;
}
}
let calls = 0;
const getCounter = () => {
calls++;
return Counter;
};
expect(Counter.add(1)).toBe(11);
expect(Counter.subtract(getCounter, 2)).toBe(9);
expect(calls).toBe(1);
expect(() => Counter.addTo({})).toThrow(TypeError);
expect(Counter.add(0)).toBe(9);
@@ -0,0 +1,8 @@
class Foo {
static #prop = 1;
static test() {
this.#prop += 1;
getObj().#prop -= 2;
}
}
@@ -0,0 +1,8 @@
class Foo {
static test() {
var _getObj;
_prop._ = babelHelpers.assertClassBrand(Foo, this, babelHelpers.assertClassBrand(Foo, this, _prop)._ + 1);
_prop._ = babelHelpers.assertClassBrand(Foo, _getObj = getObj(), babelHelpers.assertClassBrand(Foo, _getObj, _prop)._ - 2);
}
}
var _prop = { _: 1 };