@studnicky/v8/prototype-modification
Disallows whole-prototype and prototype-property assignments, __proto__ assignments, and calls that pass a prototype to Object.assign, Object.defineProperty, Object.defineProperties, Object.setPrototypeOf, Reflect.set, or Reflect.setPrototypeOf. All six calls resolve through CallIdentity to their standard-library declaration, so computed forms (Object['assign'](...), Reflect['set'](...)) are covered, and a locally declared object or class named Object or Reflect cannot trigger a false positive.
A module-top-level mutation outside every function and loop is exempt because that shape is provably one-shot before an instance exists. Mutations nested in a function or a loop, including a per-element iteration callback, are reported. The hazard is proven with node --allow-natives-syntax: once %OptimizeFunctionOnNextCall compiles a hot method call against an instance's prototype, %GetOptimizationStatus(hot).toString(2) reports 1010001 (optimized bit set); assigning a further member onto that prototype afterward drops the status to 1 (optimized bit cleared) — the mutation alone deoptimizes hot. At 5,000,000 subsequent calls the amortized cost washes out, since V8 re-optimizes hot again within the loop, so the measured hazard is the one-time recompilation stall, not a sustained throughput loss — the stall matters most on latency- or startup-sensitive paths. See scratchpad/bench_prototypeModification.js. define-property applies the same "not proven to run exactly once" reasoning to a plain object's own properties instead of a prototype.
Fixable: No · Options: No · Suggested severity: error
✗ Incorrect
function addMethod(): void {
Object.assign(Worker.prototype, {
describe(): string {
return 'worker';
}
});
}for (const target of targets) {
target.__proto__ = replacement;
}✓ Correct
class Worker {
public describe(): string {
return 'worker';
}
}class Worker {
public value = 1;
}
Object.assign(Worker.prototype, {
describe(): string {
return String(this.value);
}
});