mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
refactor(javascript): normalize erased expression syntax once
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
|
||||
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
|
||||
|
||||
it.each(["types.d.ts", "types.d.mts", "types.d.cts"])(
|
||||
"keeps valid ambient declarations complete in %s",
|
||||
async (path) => {
|
||||
const root = await createTestTempDirectory("rea-declaration-file-");
|
||||
const source = "export const value: number;\n";
|
||||
await writeFile(join(root, path), source);
|
||||
const result = await analyzeJavaScriptApplication({
|
||||
input_path: root,
|
||||
format: "directory",
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const analysis = javascriptApplicationAnalysisResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
expect(analysis.graph.coverage.status).toBe("complete");
|
||||
expect(parseJavaScriptSource(source, path)?.errors).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains the missing-initializer diagnostic for ordinary TypeScript", () => {
|
||||
const errors = parseJavaScriptSource(
|
||||
"export const value: number;",
|
||||
"types.ts",
|
||||
)?.errors;
|
||||
expect(errors?.map(({ message }) => message)).toContain(
|
||||
"Missing initializer in const declaration. (1:26)",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps CommonJS TypeScript angle-bracket assertions complete", async () => {
|
||||
const root = await createTestTempDirectory("rea-cts-assertion-");
|
||||
const source = "export const value = <number>42;\n";
|
||||
await writeFile(join(root, "app.cts"), source);
|
||||
const result = await analyzeJavaScriptApplication({
|
||||
input_path: root,
|
||||
format: "directory",
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const analysis = javascriptApplicationAnalysisResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
expect(analysis.graph.coverage.status).toBe("complete");
|
||||
expect(parseJavaScriptSource(source, "app.cts")?.errors).toEqual([]);
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
|
||||
import { topLevelBinding } from "../../domain/javascript/javascriptSemanticAnalysis.fixture.js";
|
||||
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
|
||||
|
||||
it.each([
|
||||
'"ready" satisfies string',
|
||||
'("ready" satisfies string) as string',
|
||||
'("ready" satisfies string)!',
|
||||
])("retains the runtime primitive through %s", (expression) => {
|
||||
const ir = analyzeJavaScriptSemantics(`const value = ${expression};`);
|
||||
expect(topLevelBinding(ir, "value").value).toEqual({
|
||||
status: "literal",
|
||||
value: "ready",
|
||||
});
|
||||
});
|
||||
|
||||
it("retains exported constant evidence through a type-only satisfies expression", async () => {
|
||||
const root = await createTestTempDirectory("rea-ts-satisfies-");
|
||||
await writeFile(
|
||||
join(root, "app.ts"),
|
||||
'export const value = "ready" satisfies string;\n',
|
||||
);
|
||||
const result = await analyzeJavaScriptApplication({
|
||||
input_path: root,
|
||||
format: "directory",
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const analysis = javascriptApplicationAnalysisResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
expect(
|
||||
analysis.semantic_graph.nodes.filter(
|
||||
({ kind, properties }) =>
|
||||
kind === "literal" && properties.value === "ready",
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("retains local call targets and exported callable links through satisfies", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
'export const render = (() => "ready") satisfies (() => string); render();',
|
||||
);
|
||||
const callable = ir.callables[0];
|
||||
if (callable === undefined) throw new Error("Missing callable");
|
||||
expect(
|
||||
ir.moduleLinks.find(({ localName }) => localName === "render")?.callableId,
|
||||
).toBe(callable.callableId);
|
||||
expect(ir.callSites[0]?.calleeCallableIds).toEqual([callable.callableId]);
|
||||
expect(ir.callSites[0]?.resolution).toBe("exact");
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { canonicalJson } from "../../domain/comparisonSemantics.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
|
||||
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
|
||||
|
||||
type ApplicationAnalysis = ReturnType<
|
||||
typeof javascriptApplicationAnalysisResultSchema.parse
|
||||
>;
|
||||
|
||||
const analyzeFiles = async (
|
||||
files: ReadonlyMap<string, string>,
|
||||
): Promise<ApplicationAnalysis> => {
|
||||
const root = await createTestTempDirectory("rea-typescript-application-");
|
||||
await Promise.all(
|
||||
[...files].map(([path, source]) => writeFile(join(root, path), source)),
|
||||
);
|
||||
const result = await analyzeJavaScriptApplication({
|
||||
input_path: root,
|
||||
format: "directory",
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const analysis = javascriptApplicationAnalysisResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
return javascriptApplicationAnalysisResultSchema.parse(
|
||||
JSON.parse(canonicalJson(analysis, "TypeScript application analysis")),
|
||||
);
|
||||
};
|
||||
|
||||
it.each(["types.d.ts", "types.d.mts", "types.d.cts"])(
|
||||
"keeps valid ambient declarations complete in %s",
|
||||
async (path) => {
|
||||
const source = "export const value: number;\n";
|
||||
const analysis = await analyzeFiles(new Map([[path, source]]));
|
||||
|
||||
expect(analysis.graph.coverage.status).toBe("complete");
|
||||
expect(parseJavaScriptSource(source, path)?.errors).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains the missing-initializer diagnostic for ordinary TypeScript", () => {
|
||||
const errors = parseJavaScriptSource(
|
||||
"export const value: number;",
|
||||
"types.ts",
|
||||
)?.errors;
|
||||
expect(errors?.map(({ message }) => message)).toContain(
|
||||
"Missing initializer in const declaration. (1:26)",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps CommonJS TypeScript angle-bracket assertions complete", async () => {
|
||||
const source = "export const value = <number>42;\n";
|
||||
const analysis = await analyzeFiles(new Map([["app.cts", source]]));
|
||||
|
||||
expect(analysis.graph.coverage.status).toBe("complete");
|
||||
expect(parseJavaScriptSource(source, "app.cts")?.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("retains exported constant evidence through a type-only satisfies expression", async () => {
|
||||
const analysis = await analyzeFiles(
|
||||
new Map([["app.ts", 'export const value = "ready" satisfies string;\n']]),
|
||||
);
|
||||
|
||||
expect(
|
||||
analysis.semantic_graph.nodes.filter(
|
||||
({ kind, properties }) =>
|
||||
kind === "literal" && properties.value === "ready",
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("resolves erased wrappers through the composed application graph", async () => {
|
||||
const source = `
|
||||
const dependency = require("./dependency.ts")["value" as const] satisfies number;
|
||||
export { dependency };
|
||||
function render<T>() { return "ready"; }
|
||||
export const specialized = render<string> satisfies () => string;
|
||||
specialized();
|
||||
`;
|
||||
const analysis = await analyzeFiles(
|
||||
new Map([
|
||||
["app.mts", source],
|
||||
["dependency.ts", "export const value = 1;\n"],
|
||||
]),
|
||||
);
|
||||
|
||||
expect(analysis.graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "imports",
|
||||
properties: expect.objectContaining({
|
||||
specifier: "./dependency.ts",
|
||||
imported_path: ["value"],
|
||||
resolved_path: "dependency.ts",
|
||||
resolution_status: "resolved",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(analysis.semantic_graph.relations).toContainEqual(
|
||||
expect.objectContaining({ relation: "calls", resolution: "resolved" }),
|
||||
);
|
||||
expect(
|
||||
analysis.graph.nodes.flatMap(({ observations }) => observations),
|
||||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
semantic_role: "export-return-shapes",
|
||||
exported_name: "specialized",
|
||||
static_return_shapes: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value_status: "literal",
|
||||
fields: expect.arrayContaining([
|
||||
expect.objectContaining({ path: "", value: "ready" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(parseJavaScriptSource(source, "app.mts")?.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("applies TypeScript .mts ambiguity rules while retaining unambiguous generics", () => {
|
||||
expect(
|
||||
parseJavaScriptSource(
|
||||
"export const identity = <T,>(value: T) => value;",
|
||||
"app.mts",
|
||||
)?.errors,
|
||||
).toEqual([]);
|
||||
expect(
|
||||
parseJavaScriptSource(
|
||||
"export const identity = <T>(value: T) => value;",
|
||||
"app.mts",
|
||||
)?.errors.map(({ reasonCode }) => reasonCode),
|
||||
).toContain("ReservedArrowTypeParam");
|
||||
expect(parseJavaScriptSource("const value = <Widget />;", "app.mts")).toBe(
|
||||
null,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user