From 4896adcd0f9316034e83129685ed3807ade6f3ea Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Fri, 9 Oct 2026 12:09:15 -0400 Subject: [PATCH] fix(javascript): parse declaration artifacts in ambient mode --- .../JavaScriptApplicationDeclarations.test.ts | 54 +++++++++++++++++++ .../javascript/javascriptSourceParser.ts | 15 +++--- 2 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 src/application/javascript/JavaScriptApplicationDeclarations.test.ts diff --git a/src/application/javascript/JavaScriptApplicationDeclarations.test.ts b/src/application/javascript/JavaScriptApplicationDeclarations.test.ts new file mode 100644 index 0000000000..30a80bb625 --- /dev/null +++ b/src/application/javascript/JavaScriptApplicationDeclarations.test.ts @@ -0,0 +1,54 @@ +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 = 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([]); +}); diff --git a/src/domain/javascript/javascriptSourceParser.ts b/src/domain/javascript/javascriptSourceParser.ts index e86b1bca3f..86d030bc3e 100644 --- a/src/domain/javascript/javascriptSourceParser.ts +++ b/src/domain/javascript/javascriptSourceParser.ts @@ -1,4 +1,4 @@ -import { parse } from "@babel/parser"; +import { parse, type ParserPlugin } from "@babel/parser"; /** Babel AST produced by REA's inert JavaScript parser boundary. */ export type ParsedJavaScriptSource = ReturnType; @@ -8,6 +8,10 @@ export const parseJavaScriptSource = ( source: string, sourcePath?: string, ): ParsedJavaScriptSource | null => { + const typescript: ParserPlugin = [ + "typescript", + { dts: /\.d\.(?:ts|mts|cts)$/iu.test(sourcePath ?? "") }, + ]; try { return parse(source, { sourceType: "unambiguous", @@ -17,12 +21,11 @@ export const parseJavaScriptSource = ( // forms, including parameter decorators. Without it a decorated // TypeScript source fails to parse at all rather than reporting // recovered syntax, which loses every fact derived from that source. - // Plain .ts artifacts admit angle-bracket type assertions instead of JSX. + // Plain .ts and .cts artifacts admit angle-bracket type assertions instead of JSX. // Unknown paths retain the existing JSX-capable parser contract. - plugins: - sourcePath?.toLowerCase().endsWith(".ts") === true - ? ["decorators-legacy", "typescript"] - : ["decorators-legacy", "jsx", "typescript"], + plugins: /\.(?:ts|cts)$/iu.test(sourcePath ?? "") + ? ["decorators-legacy", typescript] + : ["decorators-legacy", "jsx", typescript], }); } catch (cause: unknown) { // Unparseable source is represented by the null return.