222 lines
7.0 KiB
TypeScript
222 lines
7.0 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, propertyName, resolution }) => ({
|
|
kind,
|
|
propertyName,
|
|
resolution,
|
|
})),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
kind: "destructure",
|
|
propertyName: "token",
|
|
resolution: "complete",
|
|
},
|
|
{ kind: "spread", propertyName: null, resolution: "complete" },
|
|
{ kind: "write", propertyName: "count", resolution: "complete" },
|
|
{ kind: "read", propertyName: "token", resolution: "complete" },
|
|
]),
|
|
);
|
|
});
|
|
|
|
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", value: { status: "literal", value: 1 } },
|
|
{ name: "type", value: { status: "literal", value: "heading" } },
|
|
{
|
|
name: "text",
|
|
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 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,
|
|
}),
|
|
]);
|
|
});
|
|
});
|