From db171be6e570fd1261a30d711e4da2d17de85a93 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:53:46 +0800 Subject: [PATCH] fix(javascript): preserve agreeing module provenance --- ...aScriptApplicationModuleProvenance.test.ts | 70 +++++++++++++++++++ .../javascript/javascriptSemanticValues.ts | 15 +++- 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 src/application/javascript/JavaScriptApplicationModuleProvenance.test.ts diff --git a/src/application/javascript/JavaScriptApplicationModuleProvenance.test.ts b/src/application/javascript/JavaScriptApplicationModuleProvenance.test.ts new file mode 100644 index 0000000000..ba8e168ebf --- /dev/null +++ b/src/application/javascript/JavaScriptApplicationModuleProvenance.test.ts @@ -0,0 +1,70 @@ +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 { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js"; + +it.each([ + { + expression: + 'flag ? require("./dependency.js").value : require("./dependency.js").value', + resolved: true, + }, + { + expression: + 'require("./dependency.js").value || require("./dependency.js").value', + resolved: true, + }, + { + expression: 'flag ? require("./dependency.js").value : localValue', + resolved: false, + }, + { + expression: 'require("./dependency.js").value ?? localValue', + resolved: false, + }, +])( + "preserves export provenance for $expression", + async ({ expression, resolved }) => { + const root = await createTestTempDirectory("rea-js-module-provenance-"); + await Promise.all([ + writeFile( + join(root, "app.js"), + `const selected = ${expression};\nexport { selected };\n`, + ), + writeFile(join(root, "dependency.js"), "exports.value = 1;\n"), + ]); + + const result = await analyzeJavaScriptApplication({ + input_path: root, + format: "directory", + }); + if (!result.ok) throw result.error; + const analysis = javascriptApplicationAnalysisResultSchema.parse( + JSON.parse(JSON.stringify(result.value.normalized_result)), + ); + + const exportImports = analysis.graph.edges.filter( + ({ relation, properties }) => + relation === "imports" && properties.exported_name === "selected", + ); + if (!resolved) { + expect(exportImports).toEqual([]); + return; + } + expect(exportImports).toContainEqual( + expect.objectContaining({ + relation: "imports", + properties: expect.objectContaining({ + specifier: "./dependency.js", + imported_path: ["value"], + resolved_path: "dependency.js", + resolution_status: "resolved", + }), + }), + ); + }, +); diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 39380a9c00..2c0b855a47 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -604,11 +604,20 @@ const provenanceForExpression = ( provenanceForExpression(left, nestedContext(context)), provenanceForExpression(right, nestedContext(context)), ]; - const origins = candidates.flatMap((candidate) => candidate.origins); + const origins = uniqueSemanticOrigins( + candidates.flatMap((candidate) => candidate.origins), + ); + const allModuleOrigins = candidates.every( + ({ status }) => status === "module", + ); + if (allModuleOrigins && origins.length === 1) + return semanticOriginsProvenance(origins); return origins.length > 0 ? semanticAmbiguousProvenance( - uniqueSemanticOrigins(origins), - "Multiple module origins.", + origins, + allModuleOrigins + ? "Multiple module origins." + : "Conditional provenance includes a non-module alternative.", ) : semanticUnresolvedProvenance( "unknown",