mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 14:56:32 +00:00
refactor(javascript): group domain semantics and contracts (#907)
This commit is contained in:
1 parent
d5f28fb29c
commit
f6a58f1062
253 files changed
+432
-410
No files matched your search
@@ -0,0 +1,430 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticArgumentFlow,
|
||||
JavaScriptSemanticCallable,
|
||||
JavaScriptSemanticCallResultFlow,
|
||||
JavaScriptSemanticCallReturnFlow,
|
||||
JavaScriptSemanticCallSite,
|
||||
JavaScriptSemanticClosureCapture,
|
||||
JavaScriptSemanticFrontier,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
semanticCallableIdForNode,
|
||||
semanticReferenceRole,
|
||||
semanticStaticPropertyKey,
|
||||
} 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)) &&
|
||||
semanticStaticPropertyKey(node.property, node.computed) === null
|
||||
)
|
||||
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 &&
|
||||
semanticStaticPropertyKey(node.key, true) === null
|
||||
)
|
||||
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)}`;
|
||||
Reference in new issue
Block a user