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.
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user