Accessor Uses Expected Field
ID |
javascript.accessor_uses_expected_field |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
JavaScript |
Tags |
code-style, copy-paste, field |
Description
Reports a class get x() whose single return does not read the field that backs x, or a set x(v) whose single assignment does not write it — while that field exists in the class.
The backing field is looked for under the two spellings JavaScript actually uses: the private field #x and the underscore convention _x. The property’s own name cannot back it — a class cannot hold both a get x() and a field x, and a getter that read this.x would recurse forever. If neither spelling is declared in the class, nothing is reported: an accessor with no same-named field is a computed property, not a mistake.
The body must be exactly one statement — a return for a getter, an assignment for a setter. Anywhere in that one statement counts as touching the field, so return this.#x * this.scale; and return this.#x ?? this.#fallback(); are both fine.
Object-literal accessors are out of scope: an object literal has no private fields, so there is no declared backing field to compare against.
Rationale
// Bad
class Point {
#x = 0;
#y = 0;
get x() { return this.#x; }
get y() { return this.#x; } // reports the wrong coordinate
set y(value) { this.#x = value; }
}
-
An accessor pair is written by copying the one above it and editing the name. Editing the name and forgetting the field is the single most common way this goes wrong, and the result reads correctly at every call site:
point.yis a valid expression that returns a number. -
Nothing else catches it. The class compiles, the property exists, and the type of the value is right — only its meaning is wrong.
#yquietly becomes write-only, or never written at all, so even a test that round-trips the property can pass. -
getandsetare syntax in JavaScript, not a naming convention, so the rule is not guessing that this method was meant as an accessor. That is what lets it be strict: an accessor forythat never mentions#y, in a class that declares#y, has no innocent reading.
The two guards are what keep it quiet elsewhere. Requiring the field to exist excludes every computed property — get fullName() { return this.#first + " " + this.#last; } has no #fullName to miss. Requiring a single statement excludes the accessors that validate, cache or convert, where the field may legitimately be touched on a line other than the one being read.
Remediation
Point the accessor at its own field.
// Good
class Point {
#x = 0;
#y = 0;
get x() { return this.#x; }
get y() { return this.#y; }
set y(value) { this.#y = value; }
}
If the property really is derived from another field, drop the same-named field or rename it, so the class no longer claims a backing store it does not use.