432 lines
13 KiB
TypeScript
432 lines
13 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import type {
|
|
JavaScriptSemanticArgumentFlow,
|
|
JavaScriptSemanticCallable,
|
|
JavaScriptSemanticCallResultFlow,
|
|
JavaScriptSemanticCallReturnFlow,
|
|
JavaScriptSemanticCallSite,
|
|
JavaScriptSemanticClosureCapture,
|
|
JavaScriptSemanticFrontier,
|
|
} from "./javascriptSemanticIr.js";
|
|
import {
|
|
reachSemanticLimit,
|
|
semanticCallableIdForNode,
|
|
semanticReferenceRole,
|
|
semanticStaticPropertyName,
|
|
} from "./javascriptSemanticProjection.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
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";
|
|
|
|
/** 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[];
|
|
retainedArguments: number;
|
|
}
|
|
|
|
interface CallCollectionContext {
|
|
readonly state: JavaScriptSemanticAnalysisState;
|
|
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
|
readonly output: MutableCallAnalysis;
|
|
readonly bindingResolutionCache: Map<string, LocalCallableResolution>;
|
|
readonly parameterBindingCache: Map<
|
|
string,
|
|
readonly JavaScriptSemanticBindingState[]
|
|
>;
|
|
}
|
|
|
|
/** Recover bounded 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: [],
|
|
retainedArguments: 0,
|
|
};
|
|
const callableById = new Map(
|
|
callables.map((callable) => [callable.callableId, callable]),
|
|
);
|
|
const context: CallCollectionContext = {
|
|
state,
|
|
callableById,
|
|
output,
|
|
bindingResolutionCache: new Map(),
|
|
parameterBindingCache: new Map(),
|
|
};
|
|
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);
|
|
if (
|
|
t.isCallExpression(node) ||
|
|
t.isOptionalCallExpression(node) ||
|
|
t.isNewExpression(node)
|
|
)
|
|
collectCallSite(node, parent, owner, context);
|
|
},
|
|
exit: (node) => {
|
|
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;
|
|
if (output.callSites.length >= state.limits.maxCallSites) {
|
|
reachSemanticLimit(state, "maxCallSites");
|
|
return;
|
|
}
|
|
const resolution = resolveLocalCallables({
|
|
node: node.callee,
|
|
state,
|
|
callableById,
|
|
seenBindings: new Set(),
|
|
depth: 0,
|
|
bindingCache: context.bindingResolutionCache,
|
|
});
|
|
const callSiteId = semanticCallSiteId(node);
|
|
const argumentsValue = retainedArguments(node.arguments, state, output);
|
|
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, parent, 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,
|
|
parent: t.Node | null,
|
|
context: CallCollectionContext,
|
|
): void => {
|
|
const identifier =
|
|
t.isVariableDeclarator(parent) &&
|
|
parent.init === node &&
|
|
t.isIdentifier(parent.id)
|
|
? parent.id
|
|
: t.isAssignmentExpression(parent) &&
|
|
parent.right === node &&
|
|
t.isIdentifier(parent.left)
|
|
? parent.left
|
|
: null;
|
|
if (identifier === null) return;
|
|
const binding = resolveSemanticBindingState(
|
|
context.state,
|
|
identifier,
|
|
identifier.name,
|
|
);
|
|
const identifierRange = range(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 (binding === undefined || definition === undefined) return;
|
|
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
|
|
)[],
|
|
state: JavaScriptSemanticAnalysisState,
|
|
output: MutableCallAnalysis,
|
|
): JavaScriptSemanticCallSite["arguments"] => {
|
|
const retained: JavaScriptSemanticCallSite["arguments"][number][] = [];
|
|
for (const [index, node] of nodes.entries()) {
|
|
if (output.retainedArguments >= state.limits.maxCallArguments) {
|
|
reachSemanticLimit(state, "maxCallArguments");
|
|
break;
|
|
}
|
|
output.retainedArguments += 1;
|
|
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 { state, 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,
|
|
)) {
|
|
if (output.argumentFlows.length >= state.limits.maxArgumentFlows) {
|
|
reachSemanticLimit(state, "maxArgumentFlows");
|
|
return;
|
|
}
|
|
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) {
|
|
if (output.callReturnFlows.length >= state.limits.maxCallReturnFlows) {
|
|
reachSemanticLimit(state, "maxCallReturnFlows");
|
|
return;
|
|
}
|
|
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;
|
|
if (output.closureCaptures.length >= state.limits.maxClosureCaptures) {
|
|
reachSemanticLimit(state, "maxClosureCaptures");
|
|
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,
|
|
);
|
|
};
|
|
|
|
const addFrontier = (
|
|
frontier: JavaScriptSemanticFrontier,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
output: MutableCallAnalysis,
|
|
): void => {
|
|
if (output.frontiers.length >= state.limits.maxFrontiers) {
|
|
reachSemanticLimit(state, "maxFrontiers");
|
|
return;
|
|
}
|
|
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)}`;
|