mirror of
https://github.com/morluto/rea.git
synced 2026-11-02 21:06:04 +00:00
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:
1 parent
48b92e1be1
commit
dea770d5d9
7 files changed
+942
-113
No files matched your search
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user