mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): preserve per-field publication and provenance coverage
This commit is contained in:
@@ -392,7 +392,10 @@ name on each authenticated graph. Missing or duplicate exact selectors return
|
||||
candidate inventory and an unknown result; the tool never chooses a fuzzy
|
||||
match. An export is linked to a callable only through exact lexical/module
|
||||
relationships recovered by the AST analysis. A later named CommonJS deletion
|
||||
invalidates the earlier publication at the receiver owner. Conditional or
|
||||
invalidates the earlier publication per exported field at the receiver owner;
|
||||
surviving fields from the same assignment stay available. Descriptor-defined
|
||||
fields retain unknown deletion success unless configurability is established.
|
||||
Conditional or
|
||||
dynamic deletions retain uncertainty. Unresolved invocation spreads remain
|
||||
unknown source sequences and cannot publish a phantom array index.
|
||||
|
||||
@@ -401,8 +404,10 @@ through an execution-free value lattice. Literal object fields and direct
|
||||
return sites are represented in the result. Calls, dynamic spreads, computed
|
||||
keys, and parser recovery remain partial or unknown. Known conditional and
|
||||
logical tests select only the established branch; uncertain tests retain
|
||||
alternatives. Object copies preserve known own data fields at copy time while
|
||||
unknown sources retain incomplete coverage.
|
||||
alternatives. Object copies preserve known own primitive data fields at copy time while
|
||||
unknown sources retain incomplete coverage. Array holes do not overwrite own
|
||||
fields during a copy. Copied reference fields keep explicit uncertainty; shared
|
||||
reference projection remains responsible for their later member reads.
|
||||
Declaration initializers and later assignments share the binding-at-read owner;
|
||||
a declaration-only `var` read before its initializer does not inherit the future
|
||||
value. Earlier `break`/`continue` paths can leave a var initializer unexecuted.
|
||||
@@ -483,7 +488,9 @@ escape uncertainty. External prototype changes and unsupported runtime protocols
|
||||
remain unknown rather than supplying exact post-invocation values.
|
||||
Identifier references and closure captures share Babel syntax-role facts plus
|
||||
explicit binding/write roles; noncomputed declaration keys and import/meta names
|
||||
are not lexical reads. Direct generator and async calls produce iterators and
|
||||
are not lexical reads. Provenance-only expression-depth omissions enter the same typed source coverage
|
||||
as value-evaluation bounds and remain visible in retained application Evidence.
|
||||
Direct generator and async calls produce iterators and
|
||||
Promises; body returns do not become direct call-result flows. Their unresolved
|
||||
consumption remains an explicit frontier. Replaced emitter bindings select their
|
||||
initializer lifetime at each operation. A lexical `this` owner remains partial
|
||||
|
||||
@@ -51,10 +51,18 @@ const commonJsExportInvocation = (
|
||||
return interop.intrinsicInvocation(node);
|
||||
};
|
||||
|
||||
export type CommonJsExportReceiverEffect =
|
||||
| "connected"
|
||||
| "detached"
|
||||
| "uncertain";
|
||||
type ReceiverStatus = "connected" | "detached" | "uncertain";
|
||||
export interface CommonJsExportReceiverEffect {
|
||||
readonly receiver: ReceiverStatus;
|
||||
readonly fields?: ReadonlyMap<string, ReceiverStatus>;
|
||||
}
|
||||
|
||||
/** Publication is selected per field; one assign site can publish many fields. */
|
||||
export const commonJsExportApplicability = (
|
||||
effect: CommonJsExportReceiverEffect | undefined,
|
||||
name: string,
|
||||
): ReceiverStatus =>
|
||||
effect?.fields?.get(name) ?? effect?.receiver ?? "connected";
|
||||
|
||||
/** A CommonJS export assignment or `Object.defineProperty` export call. */
|
||||
export type CommonJsExportSite = t.AssignmentExpression | t.CallExpression;
|
||||
@@ -366,6 +374,7 @@ export function* indexCommonJsExportReceiversSteps(
|
||||
const fresh = new WeakSet<t.Node>([initial]);
|
||||
const values = new WeakMap<t.Node, Identity>();
|
||||
const captures = new Map<CommonJsExportSite, Identity>();
|
||||
const describedFields = new Map<Identity, Set<string>>();
|
||||
const deletions = new Map<
|
||||
Identity,
|
||||
Map<string, { readonly node: t.Node; readonly mode: Execution }>
|
||||
@@ -463,9 +472,20 @@ export function* indexCommonJsExportReceiversSteps(
|
||||
: publicIdentity,
|
||||
);
|
||||
}
|
||||
const defined =
|
||||
commonJsDefinedExport(node, state) ??
|
||||
commonJsAssignedExports(node, state);
|
||||
const descriptor = commonJsDefinedExport(node, state);
|
||||
if (descriptor !== undefined) {
|
||||
const receiver = isUnshadowedGlobal(
|
||||
descriptor.receiver,
|
||||
state,
|
||||
"exports",
|
||||
)
|
||||
? exportsIdentity
|
||||
: publicIdentity;
|
||||
const names = describedFields.get(receiver) ?? new Set<string>();
|
||||
names.add(descriptor.exportedName);
|
||||
describedFields.set(receiver, names);
|
||||
}
|
||||
const defined = descriptor ?? commonJsAssignedExports(node, state);
|
||||
if (defined !== undefined && t.isCallExpression(node))
|
||||
captures.set(
|
||||
node,
|
||||
@@ -513,7 +533,7 @@ export function* indexCommonJsExportReceiversSteps(
|
||||
const name = commonJsExportName(node.argument, state);
|
||||
if (
|
||||
name !== undefined &&
|
||||
name !== "default" &&
|
||||
!isCommonJsModuleExports(node.argument, state) &&
|
||||
(t.isMemberExpression(node.argument) ||
|
||||
t.isOptionalMemberExpression(node.argument))
|
||||
) {
|
||||
@@ -554,37 +574,46 @@ export function* indexCommonJsExportReceiversSteps(
|
||||
});
|
||||
const effects = new Map<CommonJsExportSite, CommonJsExportReceiverEffect>();
|
||||
for (const [node, receiver] of captures) {
|
||||
const removed = commonJsRuntimeExportValues(node, state).flatMap(
|
||||
({ exportedName }) => {
|
||||
const fields = deletions.get(receiver);
|
||||
return [fields?.get(exportedName), fields?.get("*")].filter(
|
||||
(deletion) =>
|
||||
deletion !== undefined &&
|
||||
(deletion.mode === "deferred" ||
|
||||
(deletion.node.start ?? -1) > (node.start ?? -1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (removed.length > 0) {
|
||||
effects.set(
|
||||
node,
|
||||
removed.some((deletion) => deletion?.mode !== "ordered") ||
|
||||
receiver === null
|
||||
? "uncertain"
|
||||
: "detached",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
effects.set(
|
||||
node,
|
||||
const status: ReceiverStatus =
|
||||
deferredReplacement || receiver === null || publicIdentity === null
|
||||
? "uncertain"
|
||||
: receiver === publicIdentity
|
||||
? "connected"
|
||||
: fresh.has(receiver) && fresh.has(publicIdentity)
|
||||
? "detached"
|
||||
: "uncertain",
|
||||
);
|
||||
: "uncertain";
|
||||
const fields = new Map<string, ReceiverStatus>();
|
||||
for (const { exportedName } of commonJsRuntimeExportValues(node, state)) {
|
||||
const removed = [
|
||||
deletions.get(receiver)?.get(exportedName),
|
||||
deletions.get(receiver)?.get("*"),
|
||||
].filter(
|
||||
(deletion) =>
|
||||
deletion !== undefined &&
|
||||
(deletion.mode === "deferred" ||
|
||||
(deletion.node.start ?? -1) > (node.start ?? -1)),
|
||||
);
|
||||
if (removed.length === 0) continue;
|
||||
// An ordinary publication creates configurable data slots. Descriptor
|
||||
// definitions do not establish deletion success without their flags.
|
||||
const configurable =
|
||||
(t.isAssignmentExpression(node) ||
|
||||
commonJsAssignedExports(node, state) !== undefined) &&
|
||||
!describedFields.get(receiver)?.has(exportedName) &&
|
||||
!describedFields.get(receiver)?.has("*");
|
||||
fields.set(
|
||||
exportedName,
|
||||
configurable &&
|
||||
status !== "uncertain" &&
|
||||
removed.every((deletion) => deletion?.mode === "ordered")
|
||||
? "detached"
|
||||
: "uncertain",
|
||||
);
|
||||
}
|
||||
effects.set(node, {
|
||||
receiver: status,
|
||||
...(fields.size === 0 ? {} : { fields }),
|
||||
});
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { assignmentValueSources } from "./javascriptSemanticRequireOrigin.js";
|
||||
import { semanticRuntimeExportValueSources } from "./javascriptModuleDeclaration.js";
|
||||
import {
|
||||
commonJsRuntimeExportValues,
|
||||
commonJsExportApplicability,
|
||||
indexCommonJsExportReceiversSteps,
|
||||
type CommonJsExportReceiverEffect,
|
||||
type CommonJsExportSite,
|
||||
@@ -366,13 +367,14 @@ export const createSemanticElectronAppPathChangeQuery = (
|
||||
commonJsReceivers ??= completeSemanticSteps(
|
||||
indexCommonJsExportReceiversSteps(state),
|
||||
);
|
||||
return (
|
||||
commonJsReceivers.get(node) !== "detached" &&
|
||||
exported.some(
|
||||
({ value, projection, ownEnumerable }) =>
|
||||
value !== null &&
|
||||
possiblePath(value, true, projection, ownEnumerable),
|
||||
)
|
||||
return exported.some(
|
||||
({ exportedName, value, projection, ownEnumerable }) =>
|
||||
commonJsExportApplicability(
|
||||
commonJsReceivers.get(node),
|
||||
exportedName,
|
||||
) !== "detached" &&
|
||||
value !== null &&
|
||||
possiblePath(value, true, projection, ownEnumerable),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -60,3 +60,32 @@ it("retains captured object-copy literals and known logical/conditional values",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("selects deletion applicability per published field and preserves descriptor uncertainty", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
Object.assign(exports,{deleted:()=>1,intact:()=>2}); delete exports.deleted;
|
||||
exports.default=()=>3; delete exports.default;
|
||||
Object.defineProperty(exports,"protected",{value:()=>4,enumerable:true}); delete exports.protected;
|
||||
`);
|
||||
const exports = ir.moduleLinks.filter(
|
||||
({ kind }) => kind === "commonjs-export",
|
||||
);
|
||||
expect(exports.map(({ exportedName }) => exportedName).sort()).toEqual([
|
||||
"intact",
|
||||
"protected",
|
||||
]);
|
||||
expect(
|
||||
exports.find(({ exportedName }) => exportedName === "protected")
|
||||
?.callableId,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("does not copy array holes over retained own object properties", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`export function observe(){ const copied={0:1,...[,]}; return copied[0]; }`,
|
||||
);
|
||||
expect(onlyCallable(ir, "observe").returnSites[0]?.value).toEqual({
|
||||
status: "literal",
|
||||
value: 1,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
import { topLevelBinding } from "./javascriptSemanticAnalysis.fixture.js";
|
||||
|
||||
it("retains a provenance-only depth limit in canonical file coverage", () => {
|
||||
const analyze = (count: number) =>
|
||||
analyzeJavaScriptSemantics(
|
||||
`const deep=${"a=".repeat(count)}"x"; const stable=7;`,
|
||||
);
|
||||
const shallow = analyze(10);
|
||||
const deep = analyze(300);
|
||||
expect(shallow.coverage.status).toBe("complete");
|
||||
expect(deep.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
omittedCount: null,
|
||||
resourceLimits: ["expression-depth"],
|
||||
});
|
||||
expect(topLevelBinding(deep, "deep").provenance).toMatchObject({
|
||||
status: "unknown",
|
||||
reason: expect.stringContaining("depth budget"),
|
||||
});
|
||||
expect(topLevelBinding(deep, "stable").value).toEqual({
|
||||
status: "literal",
|
||||
value: 7,
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
} from "./javascriptModuleDeclaration.js";
|
||||
import {
|
||||
commonJsRuntimeExportValues,
|
||||
commonJsExportApplicability,
|
||||
type CommonJsExportReceiverEffect,
|
||||
type CommonJsExportSite,
|
||||
} from "./javascriptCommonJsExportReceivers.js";
|
||||
@@ -468,17 +469,21 @@ const collectCommonJsExport = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
receiverEffect: CommonJsExportReceiverEffect | undefined,
|
||||
): void => {
|
||||
if (receiverEffect === "detached") return;
|
||||
// Compound and logical writes also depend on the previous value, so remain
|
||||
// unknown.
|
||||
for (const exported of commonJsRuntimeExportValues(node, state))
|
||||
// Compound and logical writes also depend on the previous value, so remain unknown.
|
||||
for (const exported of commonJsRuntimeExportValues(node, state)) {
|
||||
const applicability = commonJsExportApplicability(
|
||||
receiverEffect,
|
||||
exported.exportedName,
|
||||
);
|
||||
if (applicability === "detached") continue;
|
||||
addCommonJsExport(
|
||||
node,
|
||||
state,
|
||||
exported.exportedName,
|
||||
receiverEffect === "uncertain" ? null : exported.value,
|
||||
applicability === "uncertain" ? null : exported.value,
|
||||
exported.projection,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const addCommonJsExport = (
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
semanticBindingAtRead,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
|
||||
import { semanticSlotAtPath } from "./javascriptSemanticSlots.js";
|
||||
import {
|
||||
semanticSlotAtPath,
|
||||
semanticContainer,
|
||||
} from "./javascriptSemanticSlots.js";
|
||||
|
||||
import {
|
||||
invalidateSemanticEscapedPath,
|
||||
@@ -810,9 +813,10 @@ const evaluateObject = (
|
||||
};
|
||||
for (const property of node.properties) {
|
||||
if (t.isSpreadElement(property)) {
|
||||
const copied = evaluateCapturedExpression(
|
||||
const copied = evaluateExpression(
|
||||
property.argument,
|
||||
nestedContext(context),
|
||||
captureContextAt(property.argument, nestedContext(context)) ??
|
||||
nestedContext(context),
|
||||
);
|
||||
if (copied.status === "object" || copied.status === "array") {
|
||||
const unknown =
|
||||
@@ -824,10 +828,19 @@ const evaluateObject = (
|
||||
omittedProperties = null;
|
||||
invalidateEarlierProperties();
|
||||
}
|
||||
for (const field of copied.status === "object"
|
||||
? copied.properties
|
||||
: copied.items)
|
||||
propertiesByName.set(field.name, field);
|
||||
for (const field of semanticContainer(copied).slots) {
|
||||
if (field.presence === "absent") continue;
|
||||
propertiesByName.set(field.name, {
|
||||
...field,
|
||||
value: isPrimitive(field.value)
|
||||
? field.value
|
||||
: {
|
||||
status: "unknown",
|
||||
reason:
|
||||
"Copied reference fields retain live identity rather than a scalar snapshot.",
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
copied.status !== "literal" ||
|
||||
typeof copied.value === "string"
|
||||
@@ -1205,6 +1218,7 @@ const provenanceForExpression = (
|
||||
): JavaScriptBindingProvenance => {
|
||||
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
|
||||
context.dependence.lowest = EVERY_CALLER;
|
||||
context.state.valueResourceLimits.add("expression-depth");
|
||||
return semanticUnresolvedProvenance(
|
||||
"unknown",
|
||||
semanticResourceLimitReason("expression-depth"),
|
||||
@@ -1217,6 +1231,7 @@ const provenanceForExpression = (
|
||||
SEMANTIC_EXPRESSION_DEPTH_LIMIT
|
||||
) {
|
||||
context.dependence.lowest = EVERY_CALLER;
|
||||
context.state.valueResourceLimits.add("expression-depth");
|
||||
return semanticUnresolvedProvenance(
|
||||
"unknown",
|
||||
semanticResourceLimitReason("expression-depth"),
|
||||
|
||||
@@ -1234,6 +1234,22 @@ it("preserves export lifetime and selected value facts through retained comparis
|
||||
});
|
||||
});
|
||||
|
||||
it("retains provenance-only depth omissions alongside unaffected export facts", async () => {
|
||||
const graphs = await analyzeSources({
|
||||
left: `const deep=${"a=".repeat(300)}"x"; export default()=>({type:"stable",value:7});`,
|
||||
right: `const deep=${"a=".repeat(10)}"x"; export default()=>({type:"stable",value:7});`,
|
||||
});
|
||||
expect(graphs[0].graph.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
omitted_count: null,
|
||||
limits: expect.arrayContaining([
|
||||
expect.objectContaining({ name: "javascript_semantic_expression_depth" }),
|
||||
]),
|
||||
});
|
||||
expect(graphs[1].graph.coverage.status).toBe("complete");
|
||||
expect(compare(...graphs).changes).toEqual([]);
|
||||
});
|
||||
|
||||
type GraphSource = Awaited<ReturnType<typeof analyzeGraph>>;
|
||||
|
||||
const compare = (
|
||||
|
||||
Reference in New Issue
Block a user