From c7a890d12987d9b2d496119c1fbf1a88c0fc298e Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:08:41 +0800 Subject: [PATCH] fix(javascript): preserve unknown module-alias provenance Replace the implicit local-provenance fallback with explicit locally constructed values. Keep opaque results, parameters and caught values unknown; follow assignment and sequence results while retaining exact module aliases. Clarify that alias provenance does not represent transitive data taint. Validation: 26 focused provenance/module-link tests, 135 semantic and graph-builder tests, six application module-relationship boundary tests, and the fast typecheck/lint gate. --- src/domain/javascript/javascriptSemanticIr.ts | 5 +- .../javascriptSemanticProvenance.test.ts | 94 +++++++++++++++++++ .../javascript/javascriptSemanticValues.ts | 33 ++++++- 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 src/domain/javascript/javascriptSemanticProvenance.test.ts diff --git a/src/domain/javascript/javascriptSemanticIr.ts b/src/domain/javascript/javascriptSemanticIr.ts index 0054717752..d8349e895d 100644 --- a/src/domain/javascript/javascriptSemanticIr.ts +++ b/src/domain/javascript/javascriptSemanticIr.ts @@ -13,7 +13,10 @@ export interface JavaScriptModuleOrigin { readonly importedPath: readonly string[]; } -/** Fail-closed provenance classification for one binding. */ +/** + * Fail-closed module-alias provenance for one binding, not transitive data flow. + * Locally constructed values may contain or derive from imported values. + */ export type JavaScriptBindingProvenance = | { readonly status: "module"; diff --git a/src/domain/javascript/javascriptSemanticProvenance.test.ts b/src/domain/javascript/javascriptSemanticProvenance.test.ts new file mode 100644 index 0000000000..82a77a4499 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticProvenance.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; + +import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { + onlyBinding, + topLevelBinding, +} from "./javascriptSemanticAnalysis.fixture.js"; + +describe("JavaScript semantic binding provenance", () => { + it("keeps opaque expression results unknown instead of declaring them local", () => { + const ir = analyzeJavaScriptSemantics(` + const called = loadFeature(); + const constructed = new Feature(); + const tagged = tag\`feature\`; + const dynamicRequired = require(specifier); + const sequence = (0, loadFeature()); + const assignment = (slot = loadFeature()); + async function readLater(parameter = require("fallback")) { + const awaited = await loadFeature(); + return awaited; + } + try { loadFeature(); } catch (caught) { void caught; } + function* yieldLater() { + const yielded = yield loadFeature(); + return yielded; + } + `); + + for (const name of [ + "called", + "constructed", + "tagged", + "dynamicRequired", + "sequence", + "assignment", + "awaited", + "yielded", + "parameter", + "caught", + ]) { + expect(onlyBinding(ir, name).provenance).toMatchObject({ + status: "unknown", + origins: [], + }); + } + }); + + it("recognizes explicit local values and preserves exact module aliases", () => { + const ir = analyzeJavaScriptSemantics(` + const object = { value: 1 }; + const array = [1, 2]; + const template = \`feature\`; + const primitive = -1 + 2; + const derived = loadFeature() + 1; + const derivedText = \`value: \${loadFeature()}\`; + let uninitialized; + const callable = () => 1; + const Constructed = class {}; + const imported = require("feature"); + const alias = imported; + const sequence = (0, require("sequence-feature")); + const assignment = (slot = require("assignment-feature")); + `); + + for (const name of [ + "object", + "array", + "template", + "primitive", + "derived", + "derivedText", + "uninitialized", + "callable", + "Constructed", + ]) + expect(topLevelBinding(ir, name).provenance).toMatchObject({ + status: "local", + origins: [], + }); + + expect(topLevelBinding(ir, "alias").provenance).toMatchObject({ + status: "module", + origins: [{ specifier: "feature" }], + }); + expect(topLevelBinding(ir, "sequence").provenance).toMatchObject({ + status: "module", + origins: [{ specifier: "sequence-feature" }], + }); + expect(topLevelBinding(ir, "assignment").provenance).toMatchObject({ + status: "module", + origins: [{ specifier: "assignment-feature" }], + }); + }); +}); diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index cf31e0575f..d15c5edfc3 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -817,6 +817,13 @@ const provenanceForBindingValue = ( binding: JavaScriptSemanticBindingState, context: EvaluationContext, ): JavaScriptBindingProvenance => { + if (binding.kind === "parameter" || binding.kind === "catch") + return semanticUnresolvedProvenance( + "unknown", + binding.kind === "parameter" + ? "Parameter provenance depends on caller arguments." + : "Catch binding provenance depends on the thrown value.", + ); if (binding.directOrigins.length > 0) return semanticOriginsProvenance(binding.directOrigins); if (binding.initializers.length === 0) return semanticLocalProvenance(); @@ -890,6 +897,14 @@ const provenanceForExpression = ( const required = semanticRequireOrigin(node, context.state); if (required !== undefined) return semanticOriginsProvenance([required.origin]); + if (t.isSequenceExpression(node)) { + const finalExpression = node.expressions.at(-1); + return finalExpression === undefined + ? semanticUnresolvedProvenance("unknown", "Empty sequence expression.") + : provenanceForExpression(finalExpression, nestedContext(context)); + } + if (t.isAssignmentExpression(node) && node.operator === "=") + return provenanceForExpression(node.right, nestedContext(context)); if (t.isIdentifier(node)) { const binding = resolveSemanticBindingState(context.state, node, node.name); return binding === undefined @@ -961,9 +976,25 @@ const provenanceForExpression = ( "Conditional provenance is unresolved.", ); } - return semanticLocalProvenance(); + if (isLocallyCreatedValue(node)) return semanticLocalProvenance(); + return semanticUnresolvedProvenance( + "unknown", + `Unsupported ${node.type} provenance.`, + ); }; +const isLocallyCreatedValue = (node: t.Node): boolean => + readExactJavaScriptLiteral(node).found || + t.isObjectExpression(node) || + t.isArrayExpression(node) || + t.isFunctionExpression(node) || + t.isArrowFunctionExpression(node) || + t.isClassExpression(node) || + t.isTemplateLiteral(node) || + t.isUnaryExpression(node) || + t.isBinaryExpression(node) || + t.isUpdateExpression(node); + const mergeValues = ( values: readonly JavaScriptSemanticValue[], ): JavaScriptSemanticValue => {