mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
refactor(javascript): group domain semantics and contracts (#907)
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticArgumentFlow,
|
||||
JavaScriptSemanticCallable,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import { compareCodePoints } from "../canonicalOrdering.js";
|
||||
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
|
||||
/** Resolution of one call target within the currently analyzed source. */
|
||||
export interface LocalCallableResolution {
|
||||
readonly callableIds: readonly string[];
|
||||
readonly complete: boolean;
|
||||
readonly reason: string;
|
||||
}
|
||||
|
||||
interface CallResolutionContext {
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
||||
readonly seenBindings: ReadonlySet<string>;
|
||||
readonly bindingCache: Map<string, LocalCallableResolution>;
|
||||
}
|
||||
|
||||
/** Inputs needed to resolve one local call target. */
|
||||
export interface ResolveLocalCallablesInput {
|
||||
readonly node: t.Node;
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
||||
readonly seenBindings: ReadonlySet<string>;
|
||||
readonly bindingCache: Map<string, LocalCallableResolution>;
|
||||
}
|
||||
|
||||
/** Resolve local callable candidates without following dynamic properties. */
|
||||
export const resolveLocalCallables = (
|
||||
input: ResolveLocalCallablesInput,
|
||||
): LocalCallableResolution =>
|
||||
resolveCallables(input.node, {
|
||||
state: input.state,
|
||||
callableById: input.callableById,
|
||||
seenBindings: input.seenBindings,
|
||||
bindingCache: input.bindingCache,
|
||||
});
|
||||
|
||||
const resolveCallables = (
|
||||
node: t.Node,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const direct = semanticCallableIdForNode(node);
|
||||
if (direct !== null && context.callableById.has(direct))
|
||||
return {
|
||||
callableIds: [direct],
|
||||
complete: true,
|
||||
reason: "Direct local callable.",
|
||||
};
|
||||
if (t.isIdentifier(node)) return resolveIdentifier(node, context);
|
||||
if (
|
||||
t.isTSAsExpression(node) ||
|
||||
t.isTSTypeAssertion(node) ||
|
||||
t.isTSNonNullExpression(node)
|
||||
)
|
||||
return resolveCallables(node.expression, context);
|
||||
if (t.isConditionalExpression(node) || t.isLogicalExpression(node))
|
||||
return resolveAlternatives(
|
||||
t.isConditionalExpression(node) ? node.consequent : node.left,
|
||||
t.isConditionalExpression(node) ? node.alternate : node.right,
|
||||
context,
|
||||
);
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Unsupported ${node.type} call target.`,
|
||||
};
|
||||
};
|
||||
|
||||
const resolveIdentifier = (
|
||||
node: t.Identifier,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
||||
return binding === undefined
|
||||
? {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Unresolved call target ${node.name}.`,
|
||||
}
|
||||
: resolveBindingCallables(binding, context);
|
||||
};
|
||||
|
||||
const resolveAlternatives = (
|
||||
left: t.Node,
|
||||
right: t.Node,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const candidates = [left, right].map((candidate) =>
|
||||
resolveCallables(candidate, context),
|
||||
);
|
||||
return {
|
||||
callableIds: uniqueCallableIds(
|
||||
candidates.flatMap(({ callableIds }) => callableIds),
|
||||
),
|
||||
complete: candidates.every(({ complete }) => complete),
|
||||
reason: "Conditional or logical call target has multiple candidates.",
|
||||
};
|
||||
};
|
||||
|
||||
const resolveBindingCallables = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const cached = context.bindingCache.get(binding.bindingId);
|
||||
if (cached !== undefined) return cached;
|
||||
if (context.seenBindings.has(binding.bindingId))
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Call target alias cycle at ${binding.name}.`,
|
||||
};
|
||||
if (binding.initializers.length === 0)
|
||||
return missingBindingInitializer(binding);
|
||||
const candidateContext = {
|
||||
...context,
|
||||
seenBindings: new Set([...context.seenBindings, binding.bindingId]),
|
||||
};
|
||||
const candidates = binding.initializers.map(({ node, projection }) =>
|
||||
projection.length === 0
|
||||
? resolveCallables(node, candidateContext)
|
||||
: {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Projected call target ${binding.name} is unresolved.`,
|
||||
},
|
||||
);
|
||||
const callableIds = uniqueCallableIds(
|
||||
candidates.flatMap((candidate) => candidate.callableIds),
|
||||
);
|
||||
const resolution = {
|
||||
callableIds,
|
||||
complete: candidates.every(({ complete }) => complete),
|
||||
reason:
|
||||
callableIds.length === 1 && candidates.every(({ complete }) => complete)
|
||||
? "Unique local callable target."
|
||||
: `Call target ${binding.name} has ambiguous or unsupported assignments.`,
|
||||
};
|
||||
if (resolution.complete)
|
||||
context.bindingCache.set(binding.bindingId, resolution);
|
||||
return resolution;
|
||||
};
|
||||
|
||||
const missingBindingInitializer = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
): LocalCallableResolution => ({
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason:
|
||||
binding.kind === "import"
|
||||
? `Call target ${binding.name} is imported from outside this source.`
|
||||
: `Call target ${binding.name} has no local initializer.`,
|
||||
});
|
||||
|
||||
/** Find all bindings introduced by one positional parameter pattern. */
|
||||
export const parameterBindings = (
|
||||
callable: JavaScriptSemanticCallable,
|
||||
index: number,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticBindingState[] => {
|
||||
if (callable.bodyScopeId === null) return [];
|
||||
const scope = state.scopesById.get(callable.bodyScopeId);
|
||||
if (scope === undefined) return [];
|
||||
return [...scope.bindings.values()]
|
||||
.filter(
|
||||
(binding) =>
|
||||
binding.kind === "parameter" &&
|
||||
binding.definitions.some(
|
||||
({ kind, location }) =>
|
||||
kind === "parameter" &&
|
||||
parameterAcceptsArgument(callable, location, index, state),
|
||||
),
|
||||
)
|
||||
.sort((left, right) => compareCodePoints(left.bindingId, right.bindingId));
|
||||
};
|
||||
|
||||
const parameterAcceptsArgument = (
|
||||
callable: JavaScriptSemanticCallable,
|
||||
location: JavaScriptSemanticArgumentFlow["parameterLocation"],
|
||||
argumentIndex: number,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
const node = state.callableNodesById.get(callable.callableId);
|
||||
if (!t.isFunction(node)) return false;
|
||||
const parameterIndex = node.params.findIndex((parameter) =>
|
||||
rangeContains(range(parameter), location),
|
||||
);
|
||||
if (parameterIndex === argumentIndex) return true;
|
||||
return (
|
||||
parameterIndex >= 0 &&
|
||||
parameterIndex < argumentIndex &&
|
||||
t.isRestElement(node.params[parameterIndex])
|
||||
);
|
||||
};
|
||||
|
||||
const rangeContains = (
|
||||
outer: JavaScriptSemanticArgumentFlow["parameterLocation"],
|
||||
inner: JavaScriptSemanticArgumentFlow["parameterLocation"],
|
||||
): boolean =>
|
||||
(outer.start.line < inner.start.line ||
|
||||
(outer.start.line === inner.start.line &&
|
||||
outer.start.column <= inner.start.column)) &&
|
||||
(outer.end.line > inner.end.line ||
|
||||
(outer.end.line === inner.end.line &&
|
||||
outer.end.column >= inner.end.column));
|
||||
|
||||
const uniqueCallableIds = (values: readonly string[]): string[] =>
|
||||
[...new Set(values)].sort(compareCodePoints);
|
||||
Reference in New Issue
Block a user