* fix(comparisons)!: preserve unknowns across partial observations Share absence and digest semantics, keep unobserved managed bodies unknown, and match unique managed type/name/signatures before structural fallback. BREAKING CHANGE: Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback. * fix(javascript): preserve projected result binding semantics Share syntax-aware property reads and assigned-result resolution. Retain destructuring paths and downgrade downstream whole-result relationships instead of resolving nested or wrapped calls as aliases. * fix(native)!: admit complete parser values and preserve typed failures Decode Mach headers by named columns, validate complete slice keys and lipo alignment, and preserve codesign runtime flags and tagged parser errors. Rename the Ghidra test dossier fixture and connect fake-backed suites to real verification lanes. BREAKING CHANGE: Native load-command results no longer contain the unused, always-null file_offset field. * fix(process): preserve host identity and actionable boundary failures Thread selected platform and environment through process and Doctor seams, stamp observed producer identity in Evidence, and retain PTY, filesystem, lease and lazy-provider failure causes. Drain cancelled lazy loads and omit exited descendants from verified lineage. * fix(browser): preserve cleanup and document-transition evidence Make V8 and Playwright teardown cancellation-aware, expose primary failures alongside incomplete cleanup, share frame-tree and navigation rules, and clear stale WebMCP registrations without pruning unrelated child frames. * fix(adapters): align managed targets and typed CLI outcomes Inspect managed PE/CLI targets independently of native provider selection, reuse successful managed selections, and keep evidence bound to the actual inspected target. Classify canonical CLI error envelopes and exhaustive setup/update/uninstall outcomes, audit registered CLI coverage, and assert platform-specific runtime results. * docs: refresh boundary contracts and shipped roadmap behavior
431 lines
13 KiB
TypeScript
431 lines
13 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import type {
|
|
JavaScriptSemanticArgumentFlow,
|
|
JavaScriptSemanticCallable,
|
|
JavaScriptSemanticCallResultFlow,
|
|
JavaScriptSemanticCallReturnFlow,
|
|
JavaScriptSemanticCallSite,
|
|
JavaScriptSemanticClosureCapture,
|
|
JavaScriptSemanticFrontier,
|
|
} from "./javascriptSemanticIr.js";
|
|
import {
|
|
semanticCallableIdForNode,
|
|
semanticReferenceRole,
|
|
semanticStaticPropertyName,
|
|
} from "./javascriptSemanticProjection.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
isUnshadowedGlobal,
|
|
type JavaScriptSemanticAnalysisState,
|
|
type JavaScriptSemanticBindingState,
|
|
} from "./javascriptSemanticState.js";
|
|
import {
|
|
type LocalCallableResolution,
|
|
parameterBindings,
|
|
resolveLocalCallables,
|
|
} from "./javascriptSemanticCallResolution.js";
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
|
import { assignedSemanticResultBindings } from "./javascriptSemanticDataEffectHelpers.js";
|
|
|
|
/** Local call, flow, capture, and unsupported-frontier facts. */
|
|
export interface JavaScriptSemanticCallAnalysis {
|
|
readonly callSites: readonly JavaScriptSemanticCallSite[];
|
|
readonly argumentFlows: readonly JavaScriptSemanticArgumentFlow[];
|
|
readonly callReturnFlows: readonly JavaScriptSemanticCallReturnFlow[];
|
|
readonly callResultFlows: readonly JavaScriptSemanticCallResultFlow[];
|
|
readonly closureCaptures: readonly JavaScriptSemanticClosureCapture[];
|
|
readonly frontiers: readonly JavaScriptSemanticFrontier[];
|
|
}
|
|
|
|
interface MutableCallAnalysis {
|
|
readonly callSites: JavaScriptSemanticCallSite[];
|
|
readonly argumentFlows: JavaScriptSemanticArgumentFlow[];
|
|
readonly callReturnFlows: JavaScriptSemanticCallReturnFlow[];
|
|
readonly callResultFlows: JavaScriptSemanticCallResultFlow[];
|
|
readonly closureCaptures: JavaScriptSemanticClosureCapture[];
|
|
readonly frontiers: JavaScriptSemanticFrontier[];
|
|
}
|
|
|
|
interface CallCollectionContext {
|
|
readonly state: JavaScriptSemanticAnalysisState;
|
|
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
|
readonly output: MutableCallAnalysis;
|
|
readonly bindingResolutionCache: Map<string, LocalCallableResolution>;
|
|
readonly parameterBindingCache: Map<
|
|
string,
|
|
readonly JavaScriptSemanticBindingState[]
|
|
>;
|
|
readonly ancestors: t.Node[];
|
|
}
|
|
|
|
/** Recover direct-call and lexical-capture candidates from inert syntax. */
|
|
export const collectJavaScriptSemanticCalls = (
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
callables: readonly JavaScriptSemanticCallable[],
|
|
): JavaScriptSemanticCallAnalysis => {
|
|
const output: MutableCallAnalysis = {
|
|
callSites: [],
|
|
argumentFlows: [],
|
|
callReturnFlows: [],
|
|
callResultFlows: [],
|
|
closureCaptures: [],
|
|
frontiers: [],
|
|
};
|
|
const callableById = new Map(
|
|
callables.map((callable) => [callable.callableId, callable]),
|
|
);
|
|
const context: CallCollectionContext = {
|
|
state,
|
|
callableById,
|
|
output,
|
|
bindingResolutionCache: new Map(),
|
|
parameterBindingCache: new Map(),
|
|
ancestors: [],
|
|
};
|
|
const callableStack: JavaScriptSemanticCallable[] = [];
|
|
traverseJavaScriptAst(program, {
|
|
enter: (node, parent) => {
|
|
const callableId = semanticCallableIdForNode(node);
|
|
const callable =
|
|
callableId === null ? undefined : callableById.get(callableId);
|
|
if (callable !== undefined) callableStack.push(callable);
|
|
const owner = enclosingFunction(callableStack);
|
|
collectCapture(node, parent, owner, context);
|
|
collectDynamicProperty(node, owner, state, output);
|
|
collectDynamicScope(node, owner, state, output);
|
|
if (
|
|
t.isCallExpression(node) ||
|
|
t.isOptionalCallExpression(node) ||
|
|
t.isNewExpression(node)
|
|
)
|
|
collectCallSite(node, parent, owner, context);
|
|
context.ancestors.push(node);
|
|
},
|
|
exit: (node) => {
|
|
context.ancestors.pop();
|
|
const callableId = semanticCallableIdForNode(node);
|
|
if (
|
|
callableId !== null &&
|
|
callableStack.at(-1)?.callableId === callableId
|
|
)
|
|
callableStack.pop();
|
|
},
|
|
});
|
|
return {
|
|
callSites: output.callSites,
|
|
argumentFlows: output.argumentFlows,
|
|
callReturnFlows: output.callReturnFlows,
|
|
callResultFlows: output.callResultFlows,
|
|
closureCaptures: output.closureCaptures,
|
|
frontiers: output.frontiers,
|
|
};
|
|
};
|
|
|
|
const collectCallSite = (
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
parent: t.Node | null,
|
|
owner: JavaScriptSemanticCallable | undefined,
|
|
context: CallCollectionContext,
|
|
): void => {
|
|
const { state, callableById, output } = context;
|
|
const resolution = resolveLocalCallables({
|
|
node: node.callee,
|
|
state,
|
|
callableById,
|
|
seenBindings: new Set(),
|
|
bindingCache: context.bindingResolutionCache,
|
|
});
|
|
const callSiteId = semanticCallSiteId(node);
|
|
const argumentsValue = retainedArguments(node.arguments);
|
|
const site: JavaScriptSemanticCallSite = {
|
|
callSiteId,
|
|
kind: t.isNewExpression(node) ? "construct" : "call",
|
|
callerCallableId: owner?.callableId ?? null,
|
|
location: range(node),
|
|
calleeLocation: range(node.callee),
|
|
resolution:
|
|
resolution.callableIds.length === 1 && resolution.complete
|
|
? "exact"
|
|
: resolution.callableIds.length > 0
|
|
? "ambiguous"
|
|
: "unresolved",
|
|
calleeCallableIds: resolution.callableIds,
|
|
arguments: argumentsValue,
|
|
};
|
|
output.callSites.push(site);
|
|
collectCallResultFlow(site, node, context);
|
|
if (site.resolution !== "exact")
|
|
addFrontier(
|
|
{
|
|
kind: "dynamic-call",
|
|
callableId: owner?.callableId ?? null,
|
|
location: range(node.callee),
|
|
reason: resolution.reason,
|
|
},
|
|
state,
|
|
output,
|
|
);
|
|
collectArgumentFlows(site, node, context);
|
|
if (site.kind === "call")
|
|
collectReturnFlows(site, callableById, state, output);
|
|
};
|
|
|
|
const collectCallResultFlow = (
|
|
site: JavaScriptSemanticCallSite,
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
context: CallCollectionContext,
|
|
): void => {
|
|
for (const assigned of assignedSemanticResultBindings(
|
|
node,
|
|
context.ancestors,
|
|
context.state,
|
|
).filter(({ projectionPath }) => projectionPath.length === 0)) {
|
|
const binding = [...context.state.bindingsById.values()].find(
|
|
({ bindingId }) => bindingId === assigned.bindingId,
|
|
);
|
|
if (binding === undefined) continue;
|
|
const identifierRange = range(assigned.identifier);
|
|
const definition = binding.definitions.find(
|
|
({ location }) =>
|
|
location.start.line === identifierRange.start.line &&
|
|
location.start.column === identifierRange.start.column &&
|
|
location.end.line === identifierRange.end.line &&
|
|
location.end.column === identifierRange.end.column,
|
|
);
|
|
if (definition === undefined) continue;
|
|
context.output.callResultFlows.push({
|
|
callSiteId: site.callSiteId,
|
|
bindingId: binding.bindingId,
|
|
definitionLocation: definition.location,
|
|
});
|
|
}
|
|
};
|
|
|
|
const retainedArguments = (
|
|
nodes: readonly (
|
|
| t.Expression
|
|
| t.SpreadElement
|
|
| t.JSXNamespacedName
|
|
| t.ArgumentPlaceholder
|
|
)[],
|
|
): JavaScriptSemanticCallSite["arguments"] => {
|
|
const retained: JavaScriptSemanticCallSite["arguments"][number][] = [];
|
|
for (const [index, node] of nodes.entries()) {
|
|
retained.push({
|
|
index,
|
|
location: range(node),
|
|
spread: t.isSpreadElement(node),
|
|
});
|
|
}
|
|
return retained;
|
|
};
|
|
|
|
const collectArgumentFlows = (
|
|
site: JavaScriptSemanticCallSite,
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
context: CallCollectionContext,
|
|
): void => {
|
|
const { callableById, output } = context;
|
|
const retainedIndexes = new Set(site.arguments.map(({ index }) => index));
|
|
let positionIsExact = true;
|
|
for (const [index, argument] of node.arguments.entries()) {
|
|
if (t.isSpreadElement(argument)) {
|
|
positionIsExact = false;
|
|
continue;
|
|
}
|
|
if (
|
|
!positionIsExact ||
|
|
!retainedIndexes.has(index) ||
|
|
!t.isExpression(argument)
|
|
)
|
|
continue;
|
|
for (const callableId of site.calleeCallableIds) {
|
|
const callable = callableById.get(callableId);
|
|
if (callable === undefined) continue;
|
|
for (const parameter of cachedParameterBindings(
|
|
callable,
|
|
index,
|
|
context,
|
|
)) {
|
|
const definition = parameter.definitions.find(
|
|
({ kind }) => kind === "parameter",
|
|
);
|
|
if (definition === undefined) continue;
|
|
output.argumentFlows.push({
|
|
callSiteId: site.callSiteId,
|
|
argumentIndex: index,
|
|
argumentLocation: range(argument),
|
|
callableId,
|
|
parameterBindingId: parameter.bindingId,
|
|
parameterLocation: definition.location,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const cachedParameterBindings = (
|
|
callable: JavaScriptSemanticCallable,
|
|
index: number,
|
|
context: CallCollectionContext,
|
|
): readonly JavaScriptSemanticBindingState[] => {
|
|
const key = `${callable.callableId}:${String(index)}`;
|
|
const cached = context.parameterBindingCache.get(key);
|
|
if (cached !== undefined) return cached;
|
|
const bindings = parameterBindings(callable, index, context.state);
|
|
context.parameterBindingCache.set(key, bindings);
|
|
return bindings;
|
|
};
|
|
|
|
const collectReturnFlows = (
|
|
site: JavaScriptSemanticCallSite,
|
|
callableById: ReadonlyMap<string, JavaScriptSemanticCallable>,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
output: MutableCallAnalysis,
|
|
): void => {
|
|
for (const callableId of site.calleeCallableIds) {
|
|
const callable = callableById.get(callableId);
|
|
if (callable === undefined) continue;
|
|
for (const returnSite of callable.returnSites) {
|
|
output.callReturnFlows.push({
|
|
callSiteId: site.callSiteId,
|
|
callableId,
|
|
returnSiteId: returnSite.returnSiteId,
|
|
returnLocation: returnSite.location,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const collectCapture = (
|
|
node: t.Node,
|
|
parent: t.Node | null,
|
|
owner: JavaScriptSemanticCallable | undefined,
|
|
context: CallCollectionContext,
|
|
): void => {
|
|
const { state, output } = context;
|
|
if (
|
|
!t.isIdentifier(node) ||
|
|
parent === null ||
|
|
owner === undefined ||
|
|
owner.bodyScopeId === null
|
|
)
|
|
return;
|
|
if (semanticReferenceRole(node, parent) === null) return;
|
|
const binding = resolveSemanticBindingState(state, node, node.name);
|
|
if (
|
|
binding === undefined ||
|
|
bindingIsWithinCallable(binding, owner.bodyScopeId, state)
|
|
)
|
|
return;
|
|
output.closureCaptures.push({
|
|
callableId: owner.callableId,
|
|
bindingId: binding.bindingId,
|
|
referenceLocation: range(node),
|
|
});
|
|
};
|
|
|
|
const collectDynamicProperty = (
|
|
node: t.Node,
|
|
owner: JavaScriptSemanticCallable | undefined,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
output: MutableCallAnalysis,
|
|
): void => {
|
|
if (
|
|
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) &&
|
|
semanticStaticPropertyName(node.property, node.computed) === ""
|
|
)
|
|
addFrontier(
|
|
{
|
|
kind: "dynamic-property",
|
|
callableId: owner?.callableId ?? null,
|
|
location: range(node.property),
|
|
reason: "Computed member property is not a static string or number.",
|
|
},
|
|
state,
|
|
output,
|
|
);
|
|
else if (
|
|
(t.isObjectProperty(node) || t.isObjectMethod(node)) &&
|
|
node.computed &&
|
|
semanticStaticPropertyName(node.key, true) === ""
|
|
)
|
|
addFrontier(
|
|
{
|
|
kind: "dynamic-property",
|
|
callableId: owner?.callableId ?? null,
|
|
location: range(node.key),
|
|
reason: "Computed object property is not a static string or number.",
|
|
},
|
|
state,
|
|
output,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* `with (o) { … }` and `eval("…")` decide at runtime which bindings exist, so
|
|
* every name inside them is unresolvable statically. Recording the frontier
|
|
* is what keeps the analyzer from presenting an empty or partial resolution as
|
|
* an observed fact — silence here would read as "nothing to resolve".
|
|
*/
|
|
const collectDynamicScope = (
|
|
node: t.Node,
|
|
owner: JavaScriptSemanticCallable | undefined,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
output: MutableCallAnalysis,
|
|
): void => {
|
|
const reason = t.isWithStatement(node)
|
|
? "`with` introduces a runtime binding environment; names inside are not statically resolvable."
|
|
: t.isCallExpression(node) && isUnshadowedGlobal(node.callee, state, "eval")
|
|
? "`eval` can declare bindings at runtime; names inside the evaluated source are not statically resolvable."
|
|
: null;
|
|
if (reason === null) return;
|
|
addFrontier(
|
|
{
|
|
kind: "dynamic-scope",
|
|
callableId: owner?.callableId ?? null,
|
|
location: range(node),
|
|
reason,
|
|
},
|
|
state,
|
|
output,
|
|
);
|
|
};
|
|
|
|
const addFrontier = (
|
|
frontier: JavaScriptSemanticFrontier,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
output: MutableCallAnalysis,
|
|
): void => {
|
|
output.frontiers.push(frontier);
|
|
};
|
|
|
|
const bindingIsWithinCallable = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
bodyScopeId: string,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean => {
|
|
let scope = state.scopesById.get(binding.scopeId);
|
|
while (scope !== undefined) {
|
|
if (scope.scopeId === bodyScopeId) return true;
|
|
scope =
|
|
scope.parentScopeId === null
|
|
? undefined
|
|
: state.scopesById.get(scope.parentScopeId);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const enclosingFunction = (
|
|
stack: readonly JavaScriptSemanticCallable[],
|
|
): JavaScriptSemanticCallable | undefined =>
|
|
stack.findLast(({ kind }) => kind !== "class");
|
|
|
|
const semanticCallSiteId = (
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
): string =>
|
|
`call:${t.isNewExpression(node) ? "construct" : "call"}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`;
|