Treat aliases of an accessor class as unreadable in the effect gate

Reading a class with a static accessor is refused because the accessor
can run import-time effects, but `const B = A` gave the alias the kind
"value", so a later read through `B` was admitted as pure and the
accessor's effects could move with the deferred module. Propagate the
"accessors" kind through alias chains to a fixed point and cover the
alias in the refusal test.

Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-27 19:14:09 -07:00
co-authored by Claude 5.5
parent 603fec49b9
commit 1d2ab2e874
2 changed files with 28 additions and 0 deletions
@@ -79,6 +79,12 @@ const v = C.x;`,
"a read through a class expression's static getter",
`const C = class { static get x() { return 1; } };
const v = C.x;`,
],
[
"a read through an alias of a class with a static getter",
`class C { static get x() { return require("fs").readFileSync("/etc/hostname"); } }
const D = (C);
const v = D.x;`,
],
[
"a computed key that converts an object",
@@ -280,6 +280,28 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void
else if (nb) for (const el of nb.elements) locals.set(el.name.text, "module");
}
}
// Aliasing a class (`const B = A`) gives another name for the same
// static members, so a read through the alias runs the same accessor.
// Propagate to a fixed point: an alias chain can be any length, and a
// class binding is initialized before any alias of it (its TDZ).
const aliases: [string, string][] = [];
for (const stmt of sf.statements) {
if (!parser.isVariableStatement(stmt)) continue;
for (const d of stmt.declarationList.declarations) {
if (!parser.isIdentifier(d.name) || d.initializer === undefined) continue;
let init: ts.Expression = d.initializer;
while (parser.isParenthesizedExpression(init)) init = init.expression;
if (parser.isIdentifier(init)) aliases.push([d.name.text, init.text]);
}
}
for (let changed = true; changed; ) {
changed = false;
for (const [name, source] of aliases)
if (locals.get(source) === "accessors" && locals.get(name) !== "accessors") {
locals.set(name, "accessors");
changed = true;
}
}
function bindNames(name: ts.BindingName, kind: "module" | "value"): void {
if (parser.isIdentifier(name)) locals.set(name.text, kind);
else