Accessor Recursion
ID |
javascript.accessor_recursion |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
classes, control-flow, reliability |
Description
Reports a getter that reads its own property on this, or a setter that writes it. The access does not reach a field — it re-enters the accessor.
Which direction recurses depends on which accessor is written, and the rule follows that exactly. A read inside a setter (set x(v) { if (this.x !== v) … }) calls the getter, and a write inside a getter calls the setter; both are ordinary code. A compound assignment or a ++ counts as both a read and a write, so it recurses in either accessor.
The private and public spellings are kept apart: get x() { return this.#x; } is the standard backing-field form and is not reported, because #x and x are compared as different names.
An access inside a nested function is not reported — that function rebinds this to its own call site. An arrow does not rebind it and is walked through. Object-literal accessors recurse in exactly the same way and are reported too.
Rationale
// Bad
class Point {
get x() {
return this.x;
}
set x(value) {
this.x = value;
}
}
-
Reading
this.xinsideget x()invokesget x(), which readsthis.xagain. Nothing terminates the loop. -
The failure is
RangeError: Maximum call stack size exceeded, and it names no property. The trace is a few thousand identical frames. -
The mistake is one rename away at all times:
return this._x;is correct, and dropping the underscore while cleaning up, while converting a field to an accessor, or while merging turns a working class into one that cannot be constructed.
Remediation
Read and write the backing field, not the property the accessor is named after.
// Good
class Point {
#x = 0;
get x() {
return this.#x;
}
set x(value) {
this.#x = value;
}
}