fix(javascript): retain getter receivers through local aliases (#2778)
* fix(javascript): retain getter receivers through local aliases * fix(javascript): resolve projected private getter receivers --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
parent
48b92e1be1
commit
dea770d5d9
|
|
@ -208,6 +208,18 @@ arrays, and later explicit data properties and methods replace earlier object
|
|||
spread references. A known array prefix does not expose values from a following
|
||||
spread. Accessors, custom or uncertain iteration, unknown computed keys, and
|
||||
unknown spread lengths retain conservative uncertainty.
|
||||
Getter-local aliases of `this`, including direct destructuring and assignments,
|
||||
retain their receiver when a returned shared reference is written. These writes
|
||||
invalidate the earlier literal instead of claiming the getter executed or the
|
||||
new value is known. Straight-line alias reads retain their capture order;
|
||||
uncertain control flow remains conservative. A binding outside the getter or
|
||||
`this` in a nested ordinary function is not attributed to the getter's receiver.
|
||||
Private reads keep receiver projections separate from the private value's path.
|
||||
Receiver projections follow known data fields initialized from `this` or earlier
|
||||
self-referencing fields; static fields retain their declaring class identity.
|
||||
An alias cycle that repeatedly selects deeper children retains escape uncertainty
|
||||
at its recovered roots rather than claiming a fixed number of loop iterations.
|
||||
|
||||
Writes through an object getter's returned receiver retain that uncertainty
|
||||
when the object is an array element, nested inside mixed object/array
|
||||
containers, or assigned to a previously declared identifier. Saving the getter
|
||||
|
|
|
|||
|
|
@ -0,0 +1,351 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js";
|
||||
|
||||
const returnedValue = (getter: string, write = "box.v.x = 2;") =>
|
||||
onlyCallable(
|
||||
analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1, y: 1 };
|
||||
const box = { s: shared, get v() { ${getter} } };
|
||||
${write}
|
||||
return shared.x;
|
||||
}`),
|
||||
"result",
|
||||
).returnSites[0]?.value;
|
||||
|
||||
describe("projected private getter receivers", () => {
|
||||
it.each([
|
||||
[
|
||||
"destructured self field",
|
||||
"shared",
|
||||
"peer = this;",
|
||||
"const { peer: self } = this; return self.#s;",
|
||||
true,
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"unrelated private child",
|
||||
"{ peer: shared }",
|
||||
"peer = this;",
|
||||
"const { peer: self } = this; return self.#s;",
|
||||
true,
|
||||
"literal",
|
||||
],
|
||||
[
|
||||
"read without write",
|
||||
"shared",
|
||||
"peer = this;",
|
||||
"const { peer: self } = this; return self.#s;",
|
||||
false,
|
||||
"literal",
|
||||
],
|
||||
[
|
||||
"two self fields",
|
||||
"shared",
|
||||
"peer = this; next = this.peer;",
|
||||
"const { next: self } = this; return self.#s;",
|
||||
true,
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"nested receiver projection",
|
||||
"shared",
|
||||
"peer = this;",
|
||||
"const { peer: { peer: self } } = this; return self.#s;",
|
||||
true,
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"member receiver alias",
|
||||
"shared",
|
||||
"peer = this;",
|
||||
"const self = this.peer.peer; return self.#s;",
|
||||
true,
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"projected result suffix",
|
||||
"{ child: shared }",
|
||||
"peer = this;",
|
||||
"const { peer: self } = this; return self.#s.child;",
|
||||
true,
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"external receiver",
|
||||
"shared",
|
||||
"peer = external;",
|
||||
"const { peer: self } = this; return self.#s;",
|
||||
true,
|
||||
"literal",
|
||||
],
|
||||
[
|
||||
"field receiver cycle",
|
||||
"shared",
|
||||
"peer = this.other; other = this.peer;",
|
||||
"const { peer: self } = this; return self.#s;",
|
||||
true,
|
||||
"literal",
|
||||
],
|
||||
] as const)(
|
||||
"keeps receiver and result paths separate for %s",
|
||||
(_name, value, fields, getter, write, status) => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1 };
|
||||
class Box {
|
||||
#s = ${value}; ${fields}
|
||||
get v() { ${getter} }
|
||||
}
|
||||
const box = new Box();
|
||||
${write ? "box.v.x = 2;" : "const saved = box.v;"}
|
||||
return shared.x;
|
||||
}`);
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject(
|
||||
status === "literal" ? { status, value: 1 } : { status },
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("private receiver field ownership", () => {
|
||||
it("bounds growing receiver projections under cyclic class inheritance", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1 };
|
||||
class A extends B {
|
||||
#s = shared;
|
||||
peer = this.other.other;
|
||||
get v() { const { peer: self } = this; return self.#s; }
|
||||
}
|
||||
class B extends A { other = this.peer.peer; }
|
||||
new A().v.x = 2;
|
||||
return shared.x;
|
||||
}`);
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject({
|
||||
status: "literal",
|
||||
value: 1,
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"own static receiver",
|
||||
"class Box { static #s=shared; static peer=this; static get v(){const {peer:self}=this;return self.#s;} } Box.v.x=2;",
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"inherited instance receiver",
|
||||
"class Base {peer=this;} class Box extends Base {#s=shared;get v(){const {peer:self}=this;return self.#s;}} new Box().v.x=2;",
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"inherited static field retains its declaring class",
|
||||
"class Base {static peer=this;} class Box extends Base {static #s=shared;static get v(){const {peer:self}=this;return self.#s;}} Box.v.x=2;",
|
||||
"literal",
|
||||
],
|
||||
[
|
||||
"inherited static getter's own private field",
|
||||
"class Base {static #s=shared;static peer=this;static get v(){const {peer:self}=this;return self.#s;}} class Box extends Base {} Box.v.x=2;",
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"a field that is not initialized yet",
|
||||
"class Box {#s=shared;peer=this.later;later=this;get v(){const {peer:self}=this;return self.#s;}} new Box().v.x=2;",
|
||||
"literal",
|
||||
],
|
||||
[
|
||||
"remaining getter projection after a captured field",
|
||||
"class Box {#s=shared;peer=this;next=this.peer;later=this;get v(){const {next:{later:self}}=this;return self.#s;}} new Box().v.x=2;",
|
||||
"unknown",
|
||||
],
|
||||
[
|
||||
"a replaced self field",
|
||||
"class Box {#s=shared;peer=this;peer=external;get v(){const {peer:self}=this;return self.#s;}} new Box().v.x=2;",
|
||||
"literal",
|
||||
],
|
||||
] as const)("respects %s", (_name, body, status) => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1 };
|
||||
${body}
|
||||
return shared.x;
|
||||
}`);
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject(
|
||||
status === "literal" ? { status, value: 1 } : { status },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getter-local receiver aliases (#2653)", () => {
|
||||
it.each([
|
||||
["initialized alias", "const self = this; return self.s;"],
|
||||
["assigned alias", "let self; self = this; return self.s;"],
|
||||
[
|
||||
"chained assignment",
|
||||
"let self; let alias; self = alias = this; return self.s;",
|
||||
],
|
||||
["destructured receiver", "const { s: value } = this; return value;"],
|
||||
["alias chain", "const self = this; const saved = self; return saved.s;"],
|
||||
[
|
||||
"capture before reassignment",
|
||||
"let self = this; const saved = self; self = { s: { x: 1 } }; return saved.s;",
|
||||
],
|
||||
[
|
||||
"conditional receiver alias",
|
||||
"const self = flag ? this : { s: { x: 1 } }; return self.s;",
|
||||
],
|
||||
])("invalidates shared values through %s", (_name, getter) => {
|
||||
expect(returnedValue(getter)?.status).toBe("unknown");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["no write", "const self = this; return self.s;", "const saved = box.v;"],
|
||||
[
|
||||
"an unrelated property write",
|
||||
"const self = this; return self.s;",
|
||||
"box.v.y = 2;",
|
||||
],
|
||||
[
|
||||
"a cycle that does not grow the selected path",
|
||||
"let self = this; while (flag) self = self; return self.s;",
|
||||
"box.v.y = 2;",
|
||||
],
|
||||
[
|
||||
"an alias replaced before return",
|
||||
"let self = this; self = { s: { x: 1 } }; return self.s;",
|
||||
"box.v.x = 2;",
|
||||
],
|
||||
[
|
||||
"a shadowing block binding",
|
||||
"const self = this; { const self = { s: { x: 1 } }; return self.s; }",
|
||||
"box.v.x = 2;",
|
||||
],
|
||||
[
|
||||
"a nested function's receiver",
|
||||
"function other() { const self = this; return self.s; } return other();",
|
||||
"box.v.x = 2;",
|
||||
],
|
||||
[
|
||||
"an alias assigned in a different receiver scope",
|
||||
"let self; function assign() { self = this; } assign(); return self.s;",
|
||||
"box.v.x = 2;",
|
||||
],
|
||||
[
|
||||
"an excluded destructuring default",
|
||||
"const { value = shared } = { value: { x: 1 } }; return value;",
|
||||
"box.v.x = 2;",
|
||||
],
|
||||
])("preserves known values with %s", (_name, getter, write) => {
|
||||
expect(returnedValue(getter, write)).toMatchObject({
|
||||
status: "literal",
|
||||
value: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getter receiver alias boundaries", () => {
|
||||
it("does not reinterpret an outer this binding as the getter receiver", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1 };
|
||||
const outside = this;
|
||||
const box = { s: shared, get v() { return outside.s; } };
|
||||
box.v.x = 2;
|
||||
return shared.x;
|
||||
}`);
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject({
|
||||
status: "literal",
|
||||
value: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("retains a private field's lexical owner through a receiver alias", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1 };
|
||||
class Box {
|
||||
#s = shared;
|
||||
get v() { const self = this; return self.#s; }
|
||||
}
|
||||
new Box().v.x = 2;
|
||||
return shared.x;
|
||||
}`);
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value.status).toBe(
|
||||
"unknown",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not append a receiver projection to a private field's result path", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1 };
|
||||
class Box {
|
||||
#s = { peer: shared };
|
||||
peer = this;
|
||||
get v() { const { peer: self } = this; return self.#s; }
|
||||
}
|
||||
new Box().v.x = 2;
|
||||
return shared.x;
|
||||
}`);
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject({
|
||||
status: "literal",
|
||||
value: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves a long local alias chain without recursive expansion", () => {
|
||||
const aliases = ["const a0 = this;"];
|
||||
for (let index = 1; index <= 2000; index++)
|
||||
aliases.push(`const a${String(index)} = a${String(index - 1)};`);
|
||||
expect(returnedValue(`${aliases.join("\n")} return a2000.s;`)?.status).toBe(
|
||||
"unknown",
|
||||
);
|
||||
});
|
||||
|
||||
it("bounds a growing alias cycle without losing the original receiver", () => {
|
||||
expect(
|
||||
returnedValue(
|
||||
"let self = this; while (flag) self = self.s; return self.s;",
|
||||
)?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"retains deeper shared references through an alias cycle (write=%s)",
|
||||
(write) => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1 };
|
||||
const box = {
|
||||
start: { child: { child: shared } },
|
||||
get v() {
|
||||
let self = this.start;
|
||||
while (flag) self = self.child;
|
||||
return self;
|
||||
}
|
||||
};
|
||||
${write ? "box.v.x = 2;" : "const saved = box.v;"}
|
||||
return shared.x;
|
||||
}`);
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject(
|
||||
write ? { status: "unknown" } : { status: "literal", value: 1 },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not widen a growing cycle to unrelated receiver properties", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function result() {
|
||||
const shared = { x: 1 };
|
||||
const unrelated = { x: 1 };
|
||||
const box = {
|
||||
start: { child: { child: shared } },
|
||||
unrelated,
|
||||
get v() {
|
||||
let self = this.start;
|
||||
while (flag) self = self.child;
|
||||
return self;
|
||||
}
|
||||
};
|
||||
box.v.x = 2;
|
||||
return unrelated.x;
|
||||
}`);
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value).toMatchObject({
|
||||
status: "literal",
|
||||
value: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,10 +10,7 @@ import {
|
|||
evaluateSemanticBinding,
|
||||
evaluateSemanticExpression,
|
||||
} from "./javascriptSemanticValues.js";
|
||||
import {
|
||||
semanticContainer,
|
||||
semanticSlotAtPath,
|
||||
} from "./javascriptSemanticSlots.js";
|
||||
import { semanticContainer } from "./javascriptSemanticSlots.js";
|
||||
import {
|
||||
semanticClassCandidates,
|
||||
semanticClassPropertyReferences,
|
||||
|
|
@ -24,6 +21,10 @@ import { semanticIterationSources } from "./javascriptSemanticIterationSources.j
|
|||
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
|
||||
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
|
||||
import { semanticMutationInitializers } from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
semanticReferenceSourcesInactive,
|
||||
semanticSelectedReferenceSlot,
|
||||
} from "./javascriptSemanticReferenceSources.js";
|
||||
import {
|
||||
semanticArrayIndex,
|
||||
SemanticPropertyPathCoverage,
|
||||
|
|
@ -329,7 +330,17 @@ export function* collectSemanticMemberMutationsSteps(
|
|||
for (const value of effect === "escape"
|
||||
? escapedReferenceLeaves(expression, current.path)
|
||||
: expandedValues(expression, current.path, effect)) {
|
||||
if (value.iterableFallbackPath !== undefined)
|
||||
if (value.escapes && effect !== "escape")
|
||||
markValue(
|
||||
value.node,
|
||||
value.path,
|
||||
new Set(),
|
||||
"escape",
|
||||
mutation,
|
||||
current.originAt,
|
||||
value.ownerInitializer,
|
||||
);
|
||||
else if (value.iterableFallbackPath !== undefined)
|
||||
deferIterable({
|
||||
node: value.node,
|
||||
projection: [],
|
||||
|
|
@ -504,7 +515,7 @@ export function* collectSemanticMemberMutationsSteps(
|
|||
for (const [index, reference] of pendingReferences.splice(0).entries()) {
|
||||
if (index % 64 === 0) yield;
|
||||
const { initializer } = reference;
|
||||
if (referenceSourcesInactive(initializer, state))
|
||||
if (semanticReferenceSourcesInactive(initializer, state))
|
||||
pendingReferences.push(reference);
|
||||
else if (t.isForOfStatement(initializer.node))
|
||||
deferIterable({
|
||||
|
|
@ -549,7 +560,7 @@ export function* collectSemanticMemberMutationsSteps(
|
|||
}
|
||||
for (const [index, reference] of [...iterableReferences].entries()) {
|
||||
if (index % 64 === 0) yield;
|
||||
if (referenceSourcesInactive(reference, state)) continue;
|
||||
if (semanticReferenceSourcesInactive(reference, state)) continue;
|
||||
if (reference.iterationOnly) {
|
||||
const expanded = expandedIterationReferences(reference, state);
|
||||
if (expanded !== null) {
|
||||
|
|
@ -557,7 +568,7 @@ export function* collectSemanticMemberMutationsSteps(
|
|||
continue;
|
||||
}
|
||||
}
|
||||
const slot = selectedReferenceSlot(reference, state);
|
||||
const slot = semanticSelectedReferenceSlot(reference, state);
|
||||
// Custom iteration can yield the iterable itself as well as any child.
|
||||
// Treat its root as escaped, including writes through yielded references.
|
||||
const knownArray =
|
||||
|
|
@ -610,27 +621,6 @@ interface IterableReference extends ReferencedValue {
|
|||
}[];
|
||||
}
|
||||
|
||||
const referenceSourcesInactive = (
|
||||
reference: Pick<IterableReference, "requiredSources" | "fallbackSources">,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean =>
|
||||
reference.requiredSources?.some(
|
||||
(source) =>
|
||||
selectedReferenceSlot(source, state, source.node)?.presence === "absent",
|
||||
) === true ||
|
||||
reference.fallbackSources?.some((source) => {
|
||||
// Selection precedes effects through the chosen reference. A later escape
|
||||
// cannot make a fallback that was excluded at destructuring reachable.
|
||||
const slot = selectedReferenceSlot(source, state, source.node);
|
||||
return (
|
||||
slot?.presence === "present" &&
|
||||
(slot.value.status === "literal" ||
|
||||
slot.value.status === "union" ||
|
||||
slot.value.status === "object" ||
|
||||
slot.value.status === "array")
|
||||
);
|
||||
}) === true;
|
||||
|
||||
const expandedIterationReferences = (
|
||||
reference: IterableReference,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
|
|
@ -645,7 +635,10 @@ const expandedIterationReferences = (
|
|||
...(reference.fallbackSources ?? []),
|
||||
].find((source) => source.node === iterable && source.projection[0] === null);
|
||||
if (source === undefined) return null;
|
||||
const slot = selectedReferenceSlot({ node: iterable, projection: [] }, state);
|
||||
const slot = semanticSelectedReferenceSlot(
|
||||
{ node: iterable, projection: [] },
|
||||
state,
|
||||
);
|
||||
const container = slot === undefined ? null : semanticContainer(slot.value);
|
||||
if (container?.coverage.status !== "complete") return null;
|
||||
const select = (
|
||||
|
|
@ -1141,19 +1134,3 @@ const copiedReferencePath = (
|
|||
}
|
||||
return [...initializer.projection.slice(0, offset), key, ...remaining];
|
||||
};
|
||||
|
||||
const selectedReferenceSlot = (
|
||||
source: { readonly node: t.Node; readonly projection: PropertyPath },
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
capturePoint?: t.Node,
|
||||
) => {
|
||||
const path: string[] = [];
|
||||
for (const key of source.projection) {
|
||||
if (key === null || typeof key === "object") return undefined;
|
||||
path.push(String(key));
|
||||
}
|
||||
return semanticSlotAtPath(
|
||||
evaluateSemanticExpression(source.node, state, capturePoint),
|
||||
path,
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -149,7 +149,50 @@ export const semanticMutationInitializers = (
|
|||
const mutationPosition =
|
||||
directMutationPosition(mutation, parents) ??
|
||||
referenceInitializerPosition(mutation, parents);
|
||||
if (mutationPosition === null) return binding;
|
||||
return selectInitializers(binding, mutationPosition, parents);
|
||||
};
|
||||
|
||||
/** Select a local alias at its read, before any later straight-line reassignment. */
|
||||
export const semanticReadInitializers = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
read: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): Pick<
|
||||
JavaScriptSemanticBindingState,
|
||||
"initializers" | "referenceInitializers"
|
||||
> => {
|
||||
let position: StatementPosition | null = null;
|
||||
let current = read;
|
||||
for (
|
||||
let parent = parents.get(current);
|
||||
parent;
|
||||
parent = parents.get(current)
|
||||
) {
|
||||
if (t.isLoop(parent)) return binding;
|
||||
if (t.isFunction(parent)) break;
|
||||
if (position === null) {
|
||||
if (t.isVariableDeclarator(parent)) {
|
||||
const declaration = parents.get(parent);
|
||||
if (t.isVariableDeclaration(declaration))
|
||||
position = statementPosition(declaration, parents, parent);
|
||||
} else if (t.isStatement(parent))
|
||||
position = statementPosition(parent, parents);
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return selectInitializers(binding, position, parents, true);
|
||||
};
|
||||
|
||||
const selectInitializers = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
position: StatementPosition | null,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
atRead = false,
|
||||
): Pick<
|
||||
JavaScriptSemanticBindingState,
|
||||
"initializers" | "referenceInitializers"
|
||||
> => {
|
||||
if (position === null) return binding;
|
||||
const origins = [
|
||||
...binding.initializers.map((initializer) => ({
|
||||
initializer,
|
||||
|
|
@ -169,12 +212,20 @@ export const semanticMutationInitializers = (
|
|||
];
|
||||
if (
|
||||
origins.some(
|
||||
({ position }) =>
|
||||
position === null || !positionPrecedes(position, mutationPosition),
|
||||
({ position: origin }) =>
|
||||
origin === null ||
|
||||
origin.body !== position.body ||
|
||||
(!atRead && !positionPrecedes(origin, position)),
|
||||
)
|
||||
)
|
||||
return binding;
|
||||
const latest = origins.reduce<StatementPosition | null>(
|
||||
const preceding = atRead
|
||||
? origins.filter(
|
||||
({ position: origin }) =>
|
||||
origin !== null && positionPrecedes(origin, position),
|
||||
)
|
||||
: origins;
|
||||
const latest = preceding.reduce<StatementPosition | null>(
|
||||
(latest, { position }) =>
|
||||
position !== null &&
|
||||
(latest === null || positionPrecedes(latest, position))
|
||||
|
|
@ -183,7 +234,7 @@ export const semanticMutationInitializers = (
|
|||
null,
|
||||
);
|
||||
const retained = new Set(
|
||||
origins
|
||||
preceding
|
||||
.filter(
|
||||
({ position }) =>
|
||||
position?.index === latest?.index &&
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import {
|
|||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
|
||||
import { semanticReadInitializers } from "./javascriptSemanticMutationInitializers.js";
|
||||
import { semanticReferenceSourcesInactive } from "./javascriptSemanticReferenceSources.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
|
|
@ -17,6 +19,7 @@ import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|||
export interface JavaScriptSemanticObjectReference {
|
||||
readonly node: t.Node;
|
||||
readonly path: PropertyPath;
|
||||
readonly escapes?: true;
|
||||
/** The allocation owned by this binding, rather than a read of its prior value. */
|
||||
readonly ownerInitializer?: t.Node;
|
||||
}
|
||||
|
|
@ -73,6 +76,7 @@ const instanceFieldReferences = (
|
|||
propertyName: string | null,
|
||||
path: PropertyPath,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
initializedBefore?: t.ClassProperty,
|
||||
): {
|
||||
readonly references: readonly JavaScriptSemanticObjectReference[];
|
||||
readonly definiteNames: ReadonlySet<string>;
|
||||
|
|
@ -94,7 +98,15 @@ const instanceFieldReferences = (
|
|||
if (previous !== undefined && previous.size === excluded.size) continue;
|
||||
selections.set(current.klass, excluded);
|
||||
const shadowed = new Set(excluded);
|
||||
for (const member of [...current.klass.body.body].reverse()) {
|
||||
const before =
|
||||
initializedBefore === undefined
|
||||
? -1
|
||||
: current.klass.body.body.indexOf(initializedBefore);
|
||||
const members =
|
||||
before < 0
|
||||
? current.klass.body.body
|
||||
: current.klass.body.body.slice(0, before);
|
||||
for (const member of [...members].reverse()) {
|
||||
if (!t.isClassProperty(member) || member.static) continue;
|
||||
const key = semanticStaticPropertyKey(member.key, member.computed);
|
||||
if (key !== null && shadowed.has(key)) continue;
|
||||
|
|
@ -123,12 +135,15 @@ interface ClassPropertyLookup {
|
|||
readonly owner: t.ClassDeclaration | t.ClassExpression;
|
||||
readonly receiver?: t.ClassDeclaration | t.ClassExpression;
|
||||
readonly kind: "instance" | "static" | "prototype";
|
||||
readonly fieldsOnly?: true;
|
||||
readonly initializedBefore?: t.ClassProperty;
|
||||
}
|
||||
|
||||
const classPropertyDescriptors = (
|
||||
klass: t.ClassDeclaration | t.ClassExpression,
|
||||
propertyName: string | null,
|
||||
kind: ClassPropertyLookup["kind"],
|
||||
initializedBefore?: t.ClassProperty,
|
||||
) => {
|
||||
const isStatic = kind === "static";
|
||||
const selectedGetters: t.ClassMethod[] = [];
|
||||
|
|
@ -138,7 +153,12 @@ const classPropertyDescriptors = (
|
|||
readonly index: number;
|
||||
readonly key: string | null;
|
||||
}[] = [];
|
||||
const before =
|
||||
initializedBefore === undefined
|
||||
? -1
|
||||
: klass.body.body.indexOf(initializedBefore);
|
||||
klass.body.body.forEach((member, index) => {
|
||||
if (before >= 0 && index >= before && t.isClassProperty(member)) return;
|
||||
if (
|
||||
(t.isClassMethod(member) ||
|
||||
(kind === "static" && t.isClassProperty(member))) &&
|
||||
|
|
@ -223,7 +243,13 @@ export const semanticClassPropertyReferences = (
|
|||
// constructors. They take precedence over every prototype descriptor.
|
||||
let instanceFields: ReadonlySet<string> = new Set();
|
||||
if (lookup.kind === "instance") {
|
||||
const fields = instanceFieldReferences(root, propertyName, path, state);
|
||||
const fields = instanceFieldReferences(
|
||||
root,
|
||||
propertyName,
|
||||
path,
|
||||
state,
|
||||
lookup.initializedBefore,
|
||||
);
|
||||
possibleResults.push(...fields.references);
|
||||
instanceFields = fields.definiteNames;
|
||||
if (propertyName !== null && instanceFields.has(propertyName))
|
||||
|
|
@ -237,11 +263,12 @@ export const semanticClassPropertyReferences = (
|
|||
klass,
|
||||
propertyName,
|
||||
lookup.kind,
|
||||
lookup.initializedBefore,
|
||||
);
|
||||
for (const field of descriptors.fields)
|
||||
if (field.value != null)
|
||||
possibleResults.push({ node: field.value, path });
|
||||
for (const getter of descriptors.getters) {
|
||||
for (const getter of lookup.fieldsOnly ? [] : descriptors.getters) {
|
||||
const key = semanticStaticPropertyKey(getter.key, getter.computed);
|
||||
if (key !== null && instanceFields.has(key)) continue;
|
||||
possibleResults.push(
|
||||
|
|
@ -345,6 +372,102 @@ interface GetterInvocation {
|
|||
| t.ClassExpression;
|
||||
}
|
||||
|
||||
/** Follow only data fields whose initializers retain a class receiver. */
|
||||
const privateReceiverProjection = (
|
||||
receiver: t.ClassDeclaration | t.ClassExpression,
|
||||
owner: t.ClassDeclaration | t.ClassExpression,
|
||||
path: PropertyPath,
|
||||
isStatic: boolean,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
if (path.some((key) => key === null || typeof key === "object")) return false;
|
||||
type Selection = { readonly key: string; readonly before?: t.ClassProperty };
|
||||
const pending: {
|
||||
receiver: typeof receiver;
|
||||
path: readonly Selection[];
|
||||
visits: ReadonlyMap<string, number>;
|
||||
}[] = [
|
||||
{
|
||||
receiver,
|
||||
path: path.map((key) => ({ key: String(key) })),
|
||||
visits: new Map(),
|
||||
},
|
||||
];
|
||||
const seen = new Set<string>();
|
||||
const declaringClass = (field: t.ClassProperty) => {
|
||||
const body = state.parentsByNode.get(field);
|
||||
return body === undefined ? undefined : state.parentsByNode.get(body);
|
||||
};
|
||||
for (let cursor = 0; cursor < pending.length; cursor++) {
|
||||
const current = pending[cursor];
|
||||
if (current === undefined) break;
|
||||
const [selection, ...remaining] = current.path;
|
||||
if (selection === undefined) {
|
||||
if (!isStatic || current.receiver === owner) return true;
|
||||
continue;
|
||||
}
|
||||
const origin = JSON.stringify([
|
||||
current.receiver.start,
|
||||
selection.key,
|
||||
selection.before?.start,
|
||||
]);
|
||||
const previous = current.visits.get(origin);
|
||||
// Repeated fields may consume a finite getter path. An initializer cycle
|
||||
// that preserves or grows it cannot establish another receiver identity.
|
||||
if (previous !== undefined && previous <= current.path.length) continue;
|
||||
const visits = new Map(current.visits).set(origin, current.path.length);
|
||||
const identity = JSON.stringify([
|
||||
current.receiver.start,
|
||||
current.path.map(({ key, before }) => [key, before?.start]),
|
||||
]);
|
||||
if (seen.has(identity)) continue;
|
||||
seen.add(identity);
|
||||
const lookupOwner =
|
||||
selection.before === undefined
|
||||
? current.receiver
|
||||
: declaringClass(selection.before);
|
||||
if (!t.isClass(lookupOwner)) continue;
|
||||
for (const reference of semanticClassPropertyReferences(
|
||||
{
|
||||
owner: lookupOwner,
|
||||
receiver: current.receiver,
|
||||
kind: isStatic ? "static" : "instance",
|
||||
fieldsOnly: true,
|
||||
...(selection.before === undefined
|
||||
? {}
|
||||
: { initializedBefore: selection.before }),
|
||||
},
|
||||
[selection.key],
|
||||
state,
|
||||
)) {
|
||||
const field = state.parentsByNode.get(reference.node);
|
||||
if (!t.isClassProperty(field)) continue;
|
||||
const fieldOwner = declaringClass(field);
|
||||
if (!t.isClass(fieldOwner)) continue;
|
||||
const projection: Selection[] = [];
|
||||
let value = unwrapJavaScriptExpression(reference.node).node;
|
||||
while (
|
||||
t.isMemberExpression(value) ||
|
||||
t.isOptionalMemberExpression(value)
|
||||
) {
|
||||
const key = semanticStaticPropertyKey(value.property, value.computed);
|
||||
if (key === null || t.isPrivateName(value.property)) break;
|
||||
projection.unshift({ key, before: field });
|
||||
value = unwrapJavaScriptExpression(value.object).node;
|
||||
}
|
||||
if (!t.isThisExpression(value)) continue;
|
||||
// Instance fields retain the actual instance. Static field `this` belongs
|
||||
// to its declaring class, even when a subclass inherits the field.
|
||||
pending.push({
|
||||
receiver: isStatic ? fieldOwner : current.receiver,
|
||||
path: [...projection, ...remaining],
|
||||
visits,
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getterResultReferences = (
|
||||
invocation: GetterInvocation,
|
||||
path: PropertyPath,
|
||||
|
|
@ -358,40 +481,206 @@ const getterResultReferences = (
|
|||
// path; recursive getters that preserve or grow that path cannot progress.
|
||||
const active = new Map(activeGetters).set(getter, path.length);
|
||||
const isStatic = t.isClassMethod(getter) && getter.static;
|
||||
const resolve = (
|
||||
node: t.Node,
|
||||
remaining: PropertyPath,
|
||||
): readonly JavaScriptSemanticObjectReference[] => {
|
||||
const expression = unwrapJavaScriptExpression(node).node;
|
||||
if (t.isThisExpression(expression)) {
|
||||
if (t.isClass(receiver)) {
|
||||
return semanticClassPropertyReferences(
|
||||
{ owner: receiver, kind: isStatic ? "static" : "instance" },
|
||||
remaining,
|
||||
state,
|
||||
active,
|
||||
);
|
||||
interface AliasVisit {
|
||||
readonly identity: string;
|
||||
readonly pathLength: number;
|
||||
readonly parent: AliasVisit | undefined;
|
||||
}
|
||||
const pending: {
|
||||
readonly node: t.Node;
|
||||
readonly path: PropertyPath;
|
||||
readonly aliases: AliasVisit | undefined;
|
||||
readonly escapes?: true;
|
||||
readonly privateRead?: {
|
||||
readonly node: t.MemberExpression | t.OptionalMemberExpression;
|
||||
readonly name: string;
|
||||
readonly path: PropertyPath;
|
||||
};
|
||||
}[] = semanticCallableResultExpressions(getter).flatMap(({ node }) =>
|
||||
node === null ? [] : [{ node, path, aliases: undefined }],
|
||||
);
|
||||
const references: JavaScriptSemanticObjectReference[] = [];
|
||||
const expandedAliases = new Map<string, Set<string>>();
|
||||
const getterScope = state.scopeByNode.get(getter)?.scopeId;
|
||||
for (let cursor = 0; cursor < pending.length; cursor++) {
|
||||
const current = pending[cursor];
|
||||
if (current === undefined) break;
|
||||
const { path: remaining } = current;
|
||||
const expression = unwrapJavaScriptExpression(current.node).node;
|
||||
const retain = (reference: JavaScriptSemanticObjectReference): void => {
|
||||
references.push(
|
||||
current.escapes ? { ...reference, escapes: true } : reference,
|
||||
);
|
||||
};
|
||||
const retainPrivateRead = (): void => {
|
||||
if (current.privateRead !== undefined)
|
||||
retain({
|
||||
node: current.privateRead.node,
|
||||
path: current.escapes ? [] : current.privateRead.path,
|
||||
});
|
||||
};
|
||||
if (t.isIdentifier(expression)) {
|
||||
const binding = resolveSemanticBindingState(
|
||||
state,
|
||||
expression,
|
||||
expression.name,
|
||||
);
|
||||
let scope =
|
||||
binding === undefined
|
||||
? undefined
|
||||
: state.scopesById.get(binding.scopeId);
|
||||
while (scope !== undefined && scope.scopeId !== getterScope) {
|
||||
if (scope.kind === "function" || scope.parentScopeId === null) {
|
||||
scope = undefined;
|
||||
break;
|
||||
}
|
||||
scope = state.scopesById.get(scope.parentScopeId);
|
||||
}
|
||||
return objectReceiverReference(receiver, remaining, state);
|
||||
if (
|
||||
binding !== undefined &&
|
||||
scope !== undefined &&
|
||||
binding.referenceInitializers.every(
|
||||
(initializer) => initializer.copyKind === undefined,
|
||||
)
|
||||
) {
|
||||
const identity = JSON.stringify([
|
||||
binding.bindingId,
|
||||
expression.start,
|
||||
expression.end,
|
||||
]);
|
||||
const known = expandedAliases.get(identity);
|
||||
const key = JSON.stringify([
|
||||
remaining,
|
||||
current.privateRead?.name,
|
||||
current.privateRead?.path,
|
||||
current.escapes,
|
||||
]);
|
||||
let cyclic = false;
|
||||
let growing = false;
|
||||
if (known !== undefined)
|
||||
for (
|
||||
let visit = current.aliases;
|
||||
visit !== undefined;
|
||||
visit = visit.parent
|
||||
)
|
||||
if (visit.identity === identity) {
|
||||
cyclic = true;
|
||||
growing = remaining.length > visit.pathLength;
|
||||
break;
|
||||
}
|
||||
if (cyclic) {
|
||||
// A repeated alias can select arbitrarily deep children. Revisit its
|
||||
// roots as escapes rather than enumerating iterations or dropping them.
|
||||
if (growing && !current.escapes)
|
||||
pending.push({
|
||||
...current,
|
||||
path: [],
|
||||
aliases: undefined,
|
||||
escapes: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (known?.has(key)) continue;
|
||||
const selections = known ?? new Set<string>();
|
||||
selections.add(key);
|
||||
expandedAliases.set(identity, selections);
|
||||
const aliases = {
|
||||
identity,
|
||||
pathLength: remaining.length,
|
||||
parent: current.aliases,
|
||||
};
|
||||
const origins = semanticReadInitializers(
|
||||
binding,
|
||||
expression,
|
||||
state.parentsByNode,
|
||||
);
|
||||
for (const initializer of [
|
||||
...origins.initializers,
|
||||
...origins.referenceInitializers.filter(
|
||||
(initializer) =>
|
||||
!semanticReferenceSourcesInactive(initializer, state),
|
||||
),
|
||||
])
|
||||
pending.push({
|
||||
...current,
|
||||
node: initializer.node,
|
||||
path: [...initializer.projection, ...remaining],
|
||||
aliases,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (t.isThisExpression(expression)) {
|
||||
// An outer binding or a nested ordinary function has a different receiver.
|
||||
let ownerNode = state.parentsByNode.get(expression);
|
||||
while (
|
||||
ownerNode !== undefined &&
|
||||
!t.isClass(ownerNode) &&
|
||||
!(t.isFunction(ownerNode) && !t.isArrowFunctionExpression(ownerNode))
|
||||
)
|
||||
ownerNode = state.parentsByNode.get(ownerNode);
|
||||
if (ownerNode !== getter) continue;
|
||||
if (current.privateRead !== undefined && t.isClass(owner)) {
|
||||
if (
|
||||
!t.isClass(receiver) ||
|
||||
!privateReceiverProjection(
|
||||
receiver,
|
||||
owner,
|
||||
remaining,
|
||||
isStatic,
|
||||
state,
|
||||
)
|
||||
) {
|
||||
retainPrivateRead();
|
||||
continue;
|
||||
}
|
||||
for (const member of owner.body.body)
|
||||
if (
|
||||
t.isClassPrivateProperty(member) &&
|
||||
member.static === isStatic &&
|
||||
member.key.id.name === current.privateRead.name &&
|
||||
member.value != null
|
||||
)
|
||||
retain({
|
||||
node: member.value,
|
||||
path: current.escapes ? [] : current.privateRead.path,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const reference of t.isClass(receiver)
|
||||
? semanticClassPropertyReferences(
|
||||
{ owner: receiver, kind: isStatic ? "static" : "instance" },
|
||||
remaining,
|
||||
state,
|
||||
active,
|
||||
)
|
||||
: objectReceiverReference(receiver, remaining, state))
|
||||
retain(reference);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
t.isMemberExpression(expression) ||
|
||||
t.isOptionalMemberExpression(expression)
|
||||
) {
|
||||
if (
|
||||
t.isPrivateName(expression.property) &&
|
||||
t.isThisExpression(expression.object) &&
|
||||
t.isClass(owner)
|
||||
current.privateRead !== undefined &&
|
||||
t.isPrivateName(expression.property)
|
||||
) {
|
||||
const privateName = expression.property.id.name;
|
||||
return owner.body.body.flatMap((member) =>
|
||||
t.isClassPrivateProperty(member) &&
|
||||
member.static === isStatic &&
|
||||
member.key.id.name === privateName &&
|
||||
member.value != null
|
||||
? [{ node: member.value, path: remaining }]
|
||||
: [],
|
||||
);
|
||||
retainPrivateRead();
|
||||
continue;
|
||||
}
|
||||
if (t.isPrivateName(expression.property) && t.isClass(owner)) {
|
||||
pending.push({
|
||||
...current,
|
||||
node: expression.object,
|
||||
path: [],
|
||||
privateRead: {
|
||||
node: expression,
|
||||
name: expression.property.id.name,
|
||||
path: remaining,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const key = semanticStaticPropertyKey(
|
||||
expression.property,
|
||||
|
|
@ -402,41 +691,52 @@ const getterResultReferences = (
|
|||
t.isClass(owner) &&
|
||||
owner.superClass != null
|
||||
) {
|
||||
return semanticClassCandidates(owner.superClass, state).flatMap(
|
||||
(base) =>
|
||||
semanticClassPropertyReferences(
|
||||
{
|
||||
owner: base,
|
||||
receiver: t.isClass(receiver) ? receiver : owner,
|
||||
kind: isStatic ? "static" : "prototype",
|
||||
},
|
||||
[key, ...remaining],
|
||||
state,
|
||||
active,
|
||||
),
|
||||
);
|
||||
for (const base of semanticClassCandidates(owner.superClass, state))
|
||||
for (const reference of semanticClassPropertyReferences(
|
||||
{
|
||||
owner: base,
|
||||
receiver: t.isClass(receiver) ? receiver : owner,
|
||||
kind: isStatic ? "static" : "prototype",
|
||||
},
|
||||
[key, ...remaining],
|
||||
state,
|
||||
active,
|
||||
))
|
||||
retain(reference);
|
||||
continue;
|
||||
}
|
||||
return resolve(expression.object, [key, ...remaining]);
|
||||
}
|
||||
if (t.isConditionalExpression(expression))
|
||||
return [
|
||||
...resolve(expression.consequent, remaining),
|
||||
...resolve(expression.alternate, remaining),
|
||||
];
|
||||
if (t.isLogicalExpression(expression))
|
||||
return [
|
||||
...resolve(expression.left, remaining),
|
||||
...resolve(expression.right, remaining),
|
||||
];
|
||||
if (t.isSequenceExpression(expression)) {
|
||||
pending.push({
|
||||
...current,
|
||||
node: expression.object,
|
||||
path: [key, ...remaining],
|
||||
});
|
||||
} else if (t.isConditionalExpression(expression))
|
||||
pending.push(
|
||||
{ ...current, node: expression.consequent },
|
||||
{ ...current, node: expression.alternate },
|
||||
);
|
||||
else if (t.isLogicalExpression(expression))
|
||||
pending.push(
|
||||
{ ...current, node: expression.left },
|
||||
{ ...current, node: expression.right },
|
||||
);
|
||||
else if (t.isSequenceExpression(expression)) {
|
||||
const last = expression.expressions.at(-1);
|
||||
return last === undefined ? [] : resolve(last, remaining);
|
||||
}
|
||||
return [{ node: expression, path: remaining }];
|
||||
};
|
||||
return semanticCallableResultExpressions(getter).flatMap(({ node }) =>
|
||||
node === null ? [] : resolve(node, path),
|
||||
);
|
||||
if (last !== undefined) pending.push({ ...current, node: last });
|
||||
} else if (t.isAssignmentExpression(expression, { operator: "=" }))
|
||||
pending.push({ ...current, node: expression.right });
|
||||
else if (
|
||||
t.isAssignmentExpression(expression) &&
|
||||
["&&=", "||=", "??="].includes(expression.operator)
|
||||
)
|
||||
pending.push(
|
||||
{ ...current, node: expression.left },
|
||||
{ ...current, node: expression.right },
|
||||
);
|
||||
else if (current.privateRead !== undefined) retainPrivateRead();
|
||||
else retain({ node: expression, path: remaining });
|
||||
}
|
||||
return references;
|
||||
};
|
||||
|
||||
export const semanticObjectPropertyReferences = (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import type * as t from "@babel/types";
|
||||
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
|
||||
import type { JavaScriptSemanticPropertyPath as PropertyPath } from "./javascriptSemanticPropertyPaths.js";
|
||||
import { evaluateSemanticExpression } from "./javascriptSemanticValues.js";
|
||||
import { semanticSlotAtPath } from "./javascriptSemanticSlots.js";
|
||||
|
||||
interface ReferenceSource {
|
||||
readonly node: t.Node;
|
||||
readonly projection: PropertyPath;
|
||||
}
|
||||
|
||||
/** Exclude references whose source slot or destructuring fallback is absent. */
|
||||
export const semanticReferenceSourcesInactive = (
|
||||
reference: {
|
||||
readonly requiredSources?: readonly ReferenceSource[];
|
||||
readonly fallbackSources?: readonly ReferenceSource[];
|
||||
},
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean =>
|
||||
reference.requiredSources?.some(
|
||||
(source) =>
|
||||
semanticSelectedReferenceSlot(source, state, source.node)?.presence ===
|
||||
"absent",
|
||||
) === true ||
|
||||
reference.fallbackSources?.some((source) => {
|
||||
// Selection precedes effects through the chosen reference. A later escape
|
||||
// cannot make a fallback that was excluded at destructuring reachable.
|
||||
const slot = semanticSelectedReferenceSlot(source, state, source.node);
|
||||
return (
|
||||
slot?.presence === "present" &&
|
||||
(slot.value.status === "literal" ||
|
||||
slot.value.status === "union" ||
|
||||
slot.value.status === "object" ||
|
||||
slot.value.status === "array")
|
||||
);
|
||||
}) === true;
|
||||
|
||||
/** Select a retained source slot at its capture, before later effects. */
|
||||
export const semanticSelectedReferenceSlot = (
|
||||
source: ReferenceSource,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
capturePoint?: t.Node,
|
||||
) => {
|
||||
const path: string[] = [];
|
||||
for (const key of source.projection) {
|
||||
if (key === null || typeof key === "object") return undefined;
|
||||
path.push(String(key));
|
||||
}
|
||||
return semanticSlotAtPath(
|
||||
evaluateSemanticExpression(source.node, state, capturePoint),
|
||||
path,
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"retains getter-local alias mutations in exported Evidence through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-getter-local-aliases-");
|
||||
const cases = [
|
||||
["initialized", "const self = this; return self.s;", true],
|
||||
["assigned", "let self; self = this; return self.s;", true],
|
||||
["destructured", "const { s: value } = this; return value;", true],
|
||||
["readOnly", "const self = this; return self.s;", false],
|
||||
[
|
||||
"cyclic",
|
||||
"let self = this.start; while (flag) self = self.child; return self;",
|
||||
true,
|
||||
"start: { child: { child: shared } }",
|
||||
],
|
||||
] as const;
|
||||
const privateCases = [
|
||||
["privateProjection", "shared", true, "unknown"],
|
||||
["privateUnrelated", "{ peer: shared }", true, "literal"],
|
||||
["privateReadOnly", "shared", false, "literal"],
|
||||
] as const;
|
||||
await writeFile(
|
||||
join(root, "app.mjs"),
|
||||
cases
|
||||
.map(
|
||||
([
|
||||
name,
|
||||
getter,
|
||||
write,
|
||||
members = "s: shared",
|
||||
]) => `export function ${name}() {
|
||||
const shared = { x: 1 };
|
||||
const box = { ${members}, get v() { ${getter} } };
|
||||
${write ? "box.v.x = 2;" : "const saved = box.v;"}
|
||||
return shared.x;
|
||||
}`,
|
||||
)
|
||||
.join("\n") +
|
||||
privateCases
|
||||
.map(
|
||||
([name, value, write]) => `export function ${name}() {
|
||||
const shared = { x: 1 };
|
||||
class Box {
|
||||
#s = ${value};
|
||||
peer = this;
|
||||
get v() { const { peer: self } = this; return self.#s; }
|
||||
}
|
||||
const box = new Box();
|
||||
${write ? "box.v.x = 2;" : "const saved = box.v;"}
|
||||
return shared.x;
|
||||
}`,
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
for (const [name, status] of [
|
||||
...cases.map(([name, , write]) => [name, write ? "unknown" : "literal"]),
|
||||
...privateCases.map(([name, , , status]) => [name, status]),
|
||||
]) {
|
||||
const observation = graph.nodes
|
||||
.flatMap(({ observations }) => observations)
|
||||
.find(
|
||||
({ properties }) =>
|
||||
properties.semantic_role === "export-return-shapes" &&
|
||||
properties.exported_name === name,
|
||||
);
|
||||
expect(observation, name).toBeDefined();
|
||||
expect(observation?.properties.static_return_shapes).toMatchObject([
|
||||
{
|
||||
value_status: status,
|
||||
fields: [
|
||||
{ path: "", state: status, value: status === "unknown" ? null : 1 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue