mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
539 lines
17 KiB
TypeScript
539 lines
17 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
|
import {
|
|
onlyCallable,
|
|
topLevelBinding,
|
|
} from "./javascriptSemanticAnalysis.fixture.js";
|
|
|
|
describe("JavaScript semantic analysis: dataflow 1", () => {
|
|
it("recovers static object reads, writes, spreads, and destructuring", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const source = { token: "TOKEN", count: 1 };
|
|
const { token } = source;
|
|
const copy = { ...source };
|
|
source.count = 2;
|
|
const read = source.token;
|
|
`);
|
|
|
|
expect(
|
|
ir.objectOperations.map(({ kind, propertyPath, resolution }) => ({
|
|
kind,
|
|
propertyPath,
|
|
resolution,
|
|
})),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
kind: "destructure",
|
|
propertyPath: ["token"],
|
|
resolution: "complete",
|
|
},
|
|
{ kind: "spread", propertyPath: [], resolution: "complete" },
|
|
{ kind: "write", propertyPath: ["count"], resolution: "complete" },
|
|
{ kind: "read", propertyPath: ["token"], resolution: "complete" },
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("does not invent property names for dynamic destructuring keys", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const source = { token: "TOKEN" };
|
|
const key = getKey();
|
|
const { [key]: dynamic } = source;
|
|
const { ["token"]: literal } = source;
|
|
const { token: renamed } = source;
|
|
const { token } = source;
|
|
const { [1]: numeric } = source;
|
|
`);
|
|
const destructures = ir.objectOperations.filter(
|
|
({ kind }) => kind === "destructure",
|
|
);
|
|
|
|
expect(destructures.map(({ propertyPath }) => propertyPath)).toEqual([
|
|
["token"],
|
|
["token"],
|
|
["token"],
|
|
["1"],
|
|
]);
|
|
expect(
|
|
destructures.every(({ resolution }) => resolution === "complete"),
|
|
).toBe(true);
|
|
expect(topLevelBinding(ir, "dynamic").value.status).toBe("unknown");
|
|
});
|
|
|
|
it("does not project positional argument flow after a spread", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
function target(first, second, third) { return third; }
|
|
target(one, ...rest, three);
|
|
`);
|
|
|
|
expect(ir.argumentFlows.map(({ argumentIndex }) => argumentIndex)).toEqual([
|
|
0,
|
|
]);
|
|
});
|
|
|
|
it("retains all statically recovered call, flow, capture, and frontier facts", () => {
|
|
const ir = analyzeJavaScriptSemantics(
|
|
`
|
|
const captured = 1;
|
|
function target(value, other) { return value + other + captured; }
|
|
target(1, 2);
|
|
target(3, 4);
|
|
object[first];
|
|
object[second];
|
|
`,
|
|
);
|
|
|
|
expect(ir.callSites).toHaveLength(2);
|
|
expect(ir.callSites.map(({ arguments: args }) => args)).toEqual([
|
|
[
|
|
expect.objectContaining({ index: 0, spread: false }),
|
|
expect.objectContaining({ index: 1, spread: false }),
|
|
],
|
|
[
|
|
expect.objectContaining({ index: 0, spread: false }),
|
|
expect.objectContaining({ index: 1, spread: false }),
|
|
],
|
|
]);
|
|
expect(ir.argumentFlows).toHaveLength(4);
|
|
expect(ir.callReturnFlows).toHaveLength(2);
|
|
expect(ir.closureCaptures).toHaveLength(1);
|
|
expect(ir.frontiers).toHaveLength(2);
|
|
expect(ir.coverage.status).toBe("complete");
|
|
});
|
|
|
|
it("recovers only direct return sites with literal and unknown object fields", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
export default function parseMarkdown(value) {
|
|
if (value.startsWith("# "))
|
|
return { type: "heading", depth: 1, text: value.slice(2) };
|
|
function nested() { return { type: "nested" }; }
|
|
const arrow = () => ({ type: "arrow" });
|
|
void nested; void arrow;
|
|
return { type: "paragraph", text: value.replaceAll("x", "y") };
|
|
}
|
|
`);
|
|
|
|
const parse = onlyCallable(ir, "parseMarkdown");
|
|
expect(parse.returnCoverage).toEqual({
|
|
status: "complete",
|
|
retainedCount: 2,
|
|
omittedCount: 0,
|
|
});
|
|
expect(parse.returnSites).toHaveLength(2);
|
|
expect(parse.returnSites[0]?.value).toMatchObject({
|
|
status: "object",
|
|
unknownProperties: false,
|
|
omittedProperties: 0,
|
|
properties: expect.arrayContaining([
|
|
{
|
|
name: "depth",
|
|
presence: "present",
|
|
value: { status: "literal", value: 1 },
|
|
},
|
|
{
|
|
name: "type",
|
|
presence: "present",
|
|
value: { status: "literal", value: "heading" },
|
|
},
|
|
{
|
|
name: "text",
|
|
presence: "present",
|
|
value: {
|
|
status: "unknown",
|
|
reason: "Unsupported CallExpression value.",
|
|
},
|
|
},
|
|
]),
|
|
});
|
|
expect(onlyCallable(ir, "nested").returnSites).toHaveLength(1);
|
|
expect(onlyCallable(ir, "arrow").returnSites).toHaveLength(1);
|
|
expect(ir.moduleLinks).toContainEqual(
|
|
expect.objectContaining({
|
|
exportedName: "default",
|
|
callableId: parse.callableId,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("JavaScript semantic call result flows", () => {
|
|
it("links only directly assigned scalar call results", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
function run() {
|
|
const direct = produce();
|
|
const { field } = unpack();
|
|
const nested = outer(inner());
|
|
return direct;
|
|
}
|
|
`);
|
|
const bindingNames = new Map(
|
|
ir.bindings.map(({ bindingId, name }) => [bindingId, name]),
|
|
);
|
|
|
|
expect(
|
|
ir.callResultFlows.map(({ bindingId }) => bindingNames.get(bindingId)),
|
|
).toEqual(["direct", "nested"]);
|
|
expect(ir.callSites).toHaveLength(4);
|
|
expect(ir.callResultFlows.map(({ callSiteId }) => callSiteId)).toEqual([
|
|
ir.callSites[0]?.callSiteId,
|
|
ir.callSites[2]?.callSiteId,
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("JavaScript semantic analysis: read-modify-write", () => {
|
|
it.each(["+= 1", "++", "--", "||= 1", "&&= 1", "??= 1"])(
|
|
"retains the property read in a read-modify-write operation: %s",
|
|
(operator) => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const source = { count: 1 };
|
|
source.count ${operator};
|
|
`);
|
|
expect(ir.objectOperations.map(({ kind }) => kind)).toEqual([
|
|
"read",
|
|
"write",
|
|
]);
|
|
},
|
|
);
|
|
|
|
it("keeps plain member assignment write-only and member access read-only", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const source = { count: 1 };
|
|
source.count = 2;
|
|
const value = source.count;
|
|
`);
|
|
expect(ir.objectOperations.map(({ kind }) => kind)).toEqual([
|
|
"write",
|
|
"read",
|
|
]);
|
|
});
|
|
|
|
it("does not model a compound binding write as a competing replacement", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
let count = 1;
|
|
count += 2;
|
|
`);
|
|
|
|
expect(topLevelBinding(ir, "count").value).toMatchObject({
|
|
status: "unknown",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("JavaScript semantic analysis: dataflow 2", () => {
|
|
it("retains partial property coverage and empty returns", () => {
|
|
const partial = analyzeJavaScriptSemantics(`
|
|
const spread = () => ({ type: "spread", ...dynamic });
|
|
const computed = () => ({ type: "computed", [key]: 1 });
|
|
function noReturn() { throw new Error("stop"); }
|
|
function emptyReturn() { return; }
|
|
`);
|
|
expect(onlyCallable(partial, "spread").returnSites[0]?.value).toMatchObject(
|
|
{
|
|
status: "object",
|
|
unknownProperties: true,
|
|
omittedProperties: null,
|
|
},
|
|
);
|
|
expect(
|
|
onlyCallable(partial, "computed").returnSites[0]?.value,
|
|
).toMatchObject({
|
|
status: "object",
|
|
unknownProperties: true,
|
|
omittedProperties: 1,
|
|
});
|
|
expect(onlyCallable(partial, "noReturn").returnSites).toEqual([]);
|
|
expect(onlyCallable(partial, "emptyReturn").returnSites[0]?.value).toEqual({
|
|
status: "unknown",
|
|
reason: "Return has no value.",
|
|
});
|
|
});
|
|
|
|
it("fails closed on assignment ambiguity and alias cycles", () => {
|
|
const ambiguous = analyzeJavaScriptSemantics(`
|
|
let current = "first";
|
|
current = "second";
|
|
const left = right;
|
|
const right = left;
|
|
`);
|
|
expect(topLevelBinding(ambiguous, "current").value).toMatchObject({
|
|
status: "ambiguous",
|
|
});
|
|
expect(topLevelBinding(ambiguous, "left").value).toMatchObject({
|
|
status: "cycle",
|
|
});
|
|
expect(topLevelBinding(ambiguous, "right").provenance).toMatchObject({
|
|
status: "cycle",
|
|
});
|
|
});
|
|
|
|
it("does not invent exact module paths for dynamic property access", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const key = getKey();
|
|
const dynamicMember = require("electron")[key];
|
|
const { [key]: dynamicBinding } = require("electron");
|
|
`);
|
|
|
|
expect(topLevelBinding(ir, "dynamicMember").provenance).toMatchObject({
|
|
status: "unknown",
|
|
origins: [],
|
|
});
|
|
expect(topLevelBinding(ir, "dynamicBinding").provenance).toMatchObject({
|
|
status: "unknown",
|
|
origins: [],
|
|
});
|
|
expect(
|
|
ir.moduleLinks.filter(({ localName }) =>
|
|
["dynamicMember", "dynamicBinding"].includes(localName ?? ""),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("retains all scopes, bindings, and references without truncation", () => {
|
|
const ir = analyzeJavaScriptSemantics(
|
|
`
|
|
const retained = "yes";
|
|
const omitted = "no";
|
|
retained;
|
|
function nested(value) { return retained + value; }
|
|
`,
|
|
);
|
|
|
|
expect(ir.bindings.map(({ name }) => name)).toEqual(
|
|
expect.arrayContaining(["retained", "omitted", "value"]),
|
|
);
|
|
expect(ir.callables.map(({ name }) => name)).toContain("nested");
|
|
expect(ir.coverage.status).toBe("complete");
|
|
expect(ir.references.filter(({ name }) => name === "retained")).toEqual([
|
|
expect.objectContaining({
|
|
resolution: "resolved",
|
|
bindingId: topLevelBinding(ir, "retained").bindingId,
|
|
}),
|
|
expect.objectContaining({
|
|
resolution: "resolved",
|
|
bindingId: topLevelBinding(ir, "retained").bindingId,
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("primitive addition values", () => {
|
|
it.each([
|
|
["true + 1", 2],
|
|
["null + 2", 2],
|
|
["false + true", 1],
|
|
['"x" + false', "xfalse"],
|
|
['null + "x"', "nullx"],
|
|
["3 + 4", 7],
|
|
['1 + 2 + "3"', "33"],
|
|
['1 + (2 + "3")', "123"],
|
|
['(1 + 2) + ("3" + 4)', "334"],
|
|
])("recovers %s using primitive coercion", (expression, expected) => {
|
|
const ir = analyzeJavaScriptSemantics(`const answer = ${expression};`);
|
|
expect(topLevelBinding(ir, "answer").value).toEqual({
|
|
status: "literal",
|
|
value: expected,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("array positional value recovery", () => {
|
|
it("keeps holes at their actual destructuring indices", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const list = ["zero", , "two"];
|
|
const [first, second, third] = list;
|
|
`);
|
|
expect(topLevelBinding(ir, "first").value).toEqual({
|
|
status: "literal",
|
|
value: "zero",
|
|
});
|
|
expect(topLevelBinding(ir, "second").value.status).toBe("unknown");
|
|
expect(topLevelBinding(ir, "third").value).toEqual({
|
|
status: "literal",
|
|
value: "two",
|
|
});
|
|
});
|
|
|
|
it("projects canonical array indices without treating arbitrary strings as indices", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const list = ["zero", "one"];
|
|
const numeric = list[1];
|
|
const quoted = list["1"];
|
|
const leadingZero = list["01"];
|
|
const negative = list[-1];
|
|
`);
|
|
for (const name of ["numeric", "quoted"])
|
|
expect(topLevelBinding(ir, name).value).toEqual({
|
|
status: "literal",
|
|
value: "one",
|
|
});
|
|
for (const name of ["leadingZero", "negative"])
|
|
expect(topLevelBinding(ir, name).value.status).toBe("unknown");
|
|
});
|
|
|
|
it("does not shift trailing values across an unknown spread", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const values = ["first", ...unknownValues, "last"];
|
|
const [first, second] = values;
|
|
`);
|
|
expect(topLevelBinding(ir, "first").value).toEqual({
|
|
status: "literal",
|
|
value: "first",
|
|
});
|
|
expect(topLevelBinding(ir, "second").value.status).toBe("unknown");
|
|
});
|
|
});
|
|
|
|
describe("object value overwrite boundaries", () => {
|
|
it("uses the last named property and keeps later exact overwrites", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const duplicate = { key: "old", key: "new" };
|
|
const selected = duplicate.key;
|
|
const later = { key: "old", ...unknownObject, key: "final" };
|
|
const final = later.key;
|
|
`);
|
|
expect(topLevelBinding(ir, "selected").value).toEqual({
|
|
status: "literal",
|
|
value: "new",
|
|
});
|
|
expect(topLevelBinding(ir, "final").value).toEqual({
|
|
status: "literal",
|
|
value: "final",
|
|
});
|
|
});
|
|
it("does not retain an exact earlier value after an unknown overwrite", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const spread = { key: "old", ...unknownObject };
|
|
const dynamic = { key: "old", [unknownKey]: "new" };
|
|
const method = { key: "old", key() { return "new"; } };
|
|
const spreadValue = spread.key;
|
|
const dynamicValue = dynamic.key;
|
|
const methodValue = method.key;
|
|
`);
|
|
for (const name of ["spreadValue", "dynamicValue", "methodValue"])
|
|
expect(topLevelBinding(ir, name).value.status).toBe("unknown");
|
|
});
|
|
it("admits literal computed keys while excluding prototype setters", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const object = { ["key"]: "value", __proto__: { prototype: true } };
|
|
const selected = object.key;
|
|
const prototype = object.__proto__;
|
|
`);
|
|
expect(topLevelBinding(ir, "selected").value).toEqual({
|
|
status: "literal",
|
|
value: "value",
|
|
});
|
|
expect(topLevelBinding(ir, "prototype").value.status).toBe("unknown");
|
|
});
|
|
});
|
|
|
|
it("retains shorthand __proto__ as an own data property", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const __proto__ = "own";
|
|
const object = { __proto__ };
|
|
const selected = object.__proto__;
|
|
`);
|
|
expect(topLevelBinding(ir, "selected").value).toEqual({
|
|
status: "literal",
|
|
value: "own",
|
|
});
|
|
});
|
|
|
|
describe("nonfinite static values", () => {
|
|
it.each(['+"not-a-number"', '-"not-a-number"', "1e308 + 1e308", "1e309"])(
|
|
"keeps %s unknown instead of publishing a non-JSON number",
|
|
(expression) => {
|
|
const ir = analyzeJavaScriptSemantics(`const answer = ${expression};`);
|
|
expect(topLevelBinding(ir, "answer").value.status).toBe("unknown");
|
|
},
|
|
);
|
|
it("does not collapse an uncertain nonfinite branch to its finite alternative", () => {
|
|
const ir = analyzeJavaScriptSemantics(
|
|
'const answer = condition ? 1 : +"invalid";',
|
|
);
|
|
expect(topLevelBinding(ir, "answer").value.status).toBe("ambiguous");
|
|
});
|
|
});
|
|
|
|
describe("binding write invalidation", () => {
|
|
it.each([
|
|
["let value = 1; value++; const observed = value;", "unknown"],
|
|
["let value = 1; --value; const observed = value;", "unknown"],
|
|
[
|
|
'let value = "old"; [value] = ["new"]; const observed = value;',
|
|
"ambiguous",
|
|
],
|
|
[
|
|
'let value = "old"; ({key: value} = {key: "new"}); const observed = value;',
|
|
"ambiguous",
|
|
],
|
|
[
|
|
'let value = "old"; for (value of ["new"]) {} const observed = value;',
|
|
"ambiguous",
|
|
],
|
|
[
|
|
'let value = "old"; for (value in {new: true}) {} const observed = value;',
|
|
"ambiguous",
|
|
],
|
|
] as const)(
|
|
"does not retain an exact initializer across %s",
|
|
(source, status) => {
|
|
const ir = analyzeJavaScriptSemantics(source);
|
|
expect(topLevelBinding(ir, "observed").value.status).toBe(status);
|
|
expect(
|
|
topLevelBinding(ir, "value").definitions.some(
|
|
({ kind }) => kind === "assignment",
|
|
),
|
|
).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("does not treat destructuring keys or member owners as assigned bindings", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const key = "stable";
|
|
const owner = "stable";
|
|
let target = "old";
|
|
({key: target} = {key: "new"});
|
|
({owner: object.slot} = {owner: "new"});
|
|
const preservedKey = key;
|
|
const preservedOwner = owner;
|
|
`);
|
|
for (const name of ["preservedKey", "preservedOwner"])
|
|
expect(topLevelBinding(ir, name).value).toEqual({
|
|
status: "literal",
|
|
value: "stable",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("JavaScript semantic analysis: unknown default inputs", () => {
|
|
it.each([
|
|
'function launch(mode = "safe") { return mode; }',
|
|
'function launch({ mode } = { mode: "safe" }) { return mode; }',
|
|
'function launch([mode] = ["safe"]) { return mode; }',
|
|
])("does not assume an optional argument is omitted: %s", (source) => {
|
|
const ir = analyzeJavaScriptSemantics(source);
|
|
expect(onlyCallable(ir, "launch").returnSites[0]?.value.status).toBe(
|
|
"unknown",
|
|
);
|
|
});
|
|
|
|
it("preserves known declaration inputs and unknown catch bindings", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const { mode: retained = "fallback" } = { mode: "actual" };
|
|
function caught() {
|
|
try { risky(); } catch ({ message = "fallback" }) { return message; }
|
|
}
|
|
`);
|
|
expect(topLevelBinding(ir, "retained").value).toMatchObject({
|
|
status: "literal",
|
|
value: "actual",
|
|
});
|
|
expect(onlyCallable(ir, "caught").returnSites[0]?.value.status).toBe(
|
|
"unknown",
|
|
);
|
|
});
|
|
});
|