2026-07-22 20:38:26 +08:00
|
|
|
import * as t from "@babel/types";
|
|
|
|
|
|
2026-10-09 20:03:19 +08:00
|
|
|
import {
|
|
|
|
|
semanticContainer,
|
|
|
|
|
semanticPropertyPointer,
|
|
|
|
|
} from "./javascriptSemanticSlots.js";
|
|
|
|
|
|
2026-07-22 20:38:26 +08:00
|
|
|
import type {
|
|
|
|
|
JavaScriptSemanticCallable,
|
|
|
|
|
JavaScriptSemanticModuleLink,
|
|
|
|
|
JavaScriptSemanticReturnSite,
|
|
|
|
|
} from "./javascriptSemanticIr.js";
|
2026-10-09 22:45:18 +08:00
|
|
|
import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js";
|
2026-08-10 14:57:44 +08:00
|
|
|
import type {
|
|
|
|
|
ProjectedPropertyCoverage,
|
|
|
|
|
ProjectedReturnField,
|
|
|
|
|
} from "./javascriptExportShapeComparisonSchemas.js";
|
2026-10-09 23:17:17 +08:00
|
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
2026-09-29 13:57:24 +08:00
|
|
|
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
|
2026-10-10 01:39:28 +08:00
|
|
|
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
|
2026-07-22 20:38:26 +08:00
|
|
|
import {
|
|
|
|
|
resolveSemanticBindingState,
|
|
|
|
|
type JavaScriptSemanticAnalysisState,
|
|
|
|
|
type JavaScriptSemanticBindingState,
|
|
|
|
|
} from "./javascriptSemanticState.js";
|
|
|
|
|
import { evaluateSemanticExpression } from "./javascriptSemanticValues.js";
|
2026-10-06 04:55:26 +08:00
|
|
|
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
2026-08-10 14:57:44 +08:00
|
|
|
import { semanticReturnCoverage } from "./javascriptSemanticCoverage.js";
|
2026-10-10 15:46:24 +08:00
|
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
|
|
|
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
|
2026-07-22 20:38:26 +08:00
|
|
|
|
|
|
|
|
interface ReturnExpression {
|
|
|
|
|
readonly node: t.Node | null;
|
|
|
|
|
readonly location: JavaScriptSemanticReturnSite["location"];
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 13:57:24 +08:00
|
|
|
/** Evaluate direct returns while excluding every nested callable. */
|
2026-07-22 20:38:26 +08:00
|
|
|
export const collectSemanticReturns = (
|
|
|
|
|
program: t.Program,
|
|
|
|
|
state: JavaScriptSemanticAnalysisState,
|
|
|
|
|
parserPartial: boolean,
|
|
|
|
|
): JavaScriptSemanticCallable[] => {
|
|
|
|
|
const nodes = callableNodes(program);
|
|
|
|
|
return state.callables.map((callable) => {
|
|
|
|
|
const node = nodes.get(callable.callableId);
|
|
|
|
|
if (node === undefined)
|
|
|
|
|
return {
|
|
|
|
|
...callable,
|
|
|
|
|
returnCoverage: {
|
|
|
|
|
status: "partial" as const,
|
|
|
|
|
retainedCount: 0,
|
|
|
|
|
omittedCount: null,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const expressions = directReturnExpressions(node);
|
|
|
|
|
const sites: JavaScriptSemanticReturnSite[] = [];
|
|
|
|
|
for (const expression of expressions) {
|
|
|
|
|
sites.push({
|
2026-07-22 21:40:23 +00:00
|
|
|
returnSiteId: semanticReturnSiteId(
|
|
|
|
|
callable.callableId,
|
|
|
|
|
expression.location,
|
|
|
|
|
),
|
2026-07-22 20:38:26 +08:00
|
|
|
location: expression.location,
|
2026-07-22 23:07:51 +00:00
|
|
|
identityReferenceLocation:
|
|
|
|
|
expression.node !== null && t.isIdentifier(expression.node)
|
|
|
|
|
? range(expression.node)
|
|
|
|
|
: null,
|
2026-07-22 20:38:26 +08:00
|
|
|
value:
|
|
|
|
|
expression.node === null
|
|
|
|
|
? { status: "unknown", reason: "Return has no value." }
|
|
|
|
|
: evaluateSemanticExpression(expression.node, state),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...callable,
|
|
|
|
|
returnSites: sites,
|
2026-09-29 13:57:24 +08:00
|
|
|
returnCoverage: semanticReturnCoverage(sites.length, parserPartial),
|
2026-07-22 20:38:26 +08:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-22 21:40:23 +00:00
|
|
|
const semanticReturnSiteId = (
|
|
|
|
|
callableId: string,
|
|
|
|
|
location: JavaScriptSemanticReturnSite["location"],
|
|
|
|
|
): string =>
|
|
|
|
|
`${callableId}:return:${String(location.start.line)}:${String(location.start.column)}:${String(location.end.line)}:${String(location.end.column)}`;
|
|
|
|
|
|
2026-07-22 20:38:26 +08:00
|
|
|
/** Link exports to callables only when lexical resolution is unique and exact. */
|
|
|
|
|
export const resolveSemanticModuleCallables = (
|
|
|
|
|
state: JavaScriptSemanticAnalysisState,
|
|
|
|
|
callables: readonly JavaScriptSemanticCallable[],
|
|
|
|
|
): JavaScriptSemanticModuleLink[] => {
|
|
|
|
|
const callableIds = new Set(callables.map(({ callableId }) => callableId));
|
|
|
|
|
return state.moduleLinks.map((link) => {
|
2026-10-09 18:39:56 -04:00
|
|
|
// Named exports follow their lexical binding, including every assignment.
|
|
|
|
|
// A declaration's ID or a callable's display name cannot prove that link.
|
|
|
|
|
if (link.localName === null)
|
|
|
|
|
return link.callableId !== null && callableIds.has(link.callableId)
|
|
|
|
|
? link
|
|
|
|
|
: { ...link, callableId: null };
|
|
|
|
|
const binding = state.moduleLinkBindings.get(link);
|
2026-07-22 20:38:26 +08:00
|
|
|
const resolved =
|
|
|
|
|
binding === undefined
|
|
|
|
|
? []
|
|
|
|
|
: callableIdsForBinding(binding, state, callableIds, new Set());
|
|
|
|
|
return {
|
|
|
|
|
...link,
|
|
|
|
|
callableId: resolved.length === 1 ? (resolved[0] ?? null) : null,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const callableNodes = (program: t.Program): Map<string, t.Node> => {
|
|
|
|
|
const output = new Map<string, t.Node>();
|
|
|
|
|
traverseJavaScriptAst(program, {
|
|
|
|
|
enter: (node) => {
|
|
|
|
|
const id = semanticCallableIdForNode(node);
|
|
|
|
|
if (id !== null) output.set(id, node);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return output;
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-10 15:46:24 +08:00
|
|
|
const directReturnExpressions = (callable: t.Node): ReturnExpression[] =>
|
|
|
|
|
semanticCallableResultExpressions(callable)
|
|
|
|
|
.filter(({ kind }) => kind === "return")
|
|
|
|
|
.map(({ node, site }) => ({ node, location: range(site) }));
|
2026-07-22 20:38:26 +08:00
|
|
|
|
|
|
|
|
const callableIdsForBinding = (
|
|
|
|
|
binding: JavaScriptSemanticBindingState,
|
|
|
|
|
state: JavaScriptSemanticAnalysisState,
|
|
|
|
|
admitted: ReadonlySet<string>,
|
|
|
|
|
seen: ReadonlySet<string>,
|
|
|
|
|
): string[] => {
|
2026-10-09 18:39:56 -04:00
|
|
|
// A single initializer does not prove an assignment executes or replace
|
|
|
|
|
// the unknown incoming value of a parameter/catch binding.
|
|
|
|
|
if (
|
|
|
|
|
seen.has(binding.bindingId) ||
|
|
|
|
|
binding.initializers.length !== 1 ||
|
|
|
|
|
binding.definitions.some(
|
|
|
|
|
({ kind }) =>
|
|
|
|
|
kind === "assignment" || kind === "parameter" || kind === "catch",
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-07-22 20:38:26 +08:00
|
|
|
return [];
|
|
|
|
|
const initializer = binding.initializers[0];
|
2026-10-09 18:39:56 -04:00
|
|
|
if (
|
|
|
|
|
initializer === undefined ||
|
|
|
|
|
initializer.projection.length > 0 ||
|
|
|
|
|
state.conditionalInitializers.has(initializer.node)
|
|
|
|
|
)
|
|
|
|
|
return [];
|
2026-07-22 20:38:26 +08:00
|
|
|
return callableIdsForNode(
|
|
|
|
|
initializer.node,
|
|
|
|
|
state,
|
|
|
|
|
admitted,
|
|
|
|
|
new Set([...seen, binding.bindingId]),
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const callableIdsForNode = (
|
|
|
|
|
node: t.Node,
|
|
|
|
|
state: JavaScriptSemanticAnalysisState,
|
|
|
|
|
admitted: ReadonlySet<string>,
|
|
|
|
|
seen: ReadonlySet<string>,
|
|
|
|
|
): string[] => {
|
2026-10-10 01:39:28 +08:00
|
|
|
const unwrapped = unwrapJavaScriptExpression(node).node;
|
|
|
|
|
if (unwrapped !== node)
|
|
|
|
|
return callableIdsForNode(unwrapped, state, admitted, seen);
|
2026-07-22 20:38:26 +08:00
|
|
|
const direct = semanticCallableIdForNode(node);
|
|
|
|
|
if (direct !== null && admitted.has(direct)) return [direct];
|
|
|
|
|
if (t.isIdentifier(node)) {
|
|
|
|
|
const binding = resolveSemanticBindingState(state, node, node.name);
|
|
|
|
|
return binding === undefined
|
|
|
|
|
? []
|
|
|
|
|
: callableIdsForBinding(binding, state, admitted, seen);
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Flatten a semantic value to bounded JSON-pointer leaves for graph projection. */
|
|
|
|
|
export const flattenSemanticReturnValue = (
|
|
|
|
|
value: JavaScriptSemanticValue,
|
|
|
|
|
): {
|
2026-08-10 14:57:44 +08:00
|
|
|
readonly fields: readonly ProjectedReturnField[];
|
|
|
|
|
readonly propertyCoverage: readonly ProjectedPropertyCoverage[];
|
2026-07-22 20:38:26 +08:00
|
|
|
} => {
|
2026-08-10 14:57:44 +08:00
|
|
|
const fields: ProjectedReturnField[] = [];
|
|
|
|
|
const propertyCoverage: ProjectedPropertyCoverage[] = [];
|
2026-07-22 20:38:26 +08:00
|
|
|
flattenValue(value, "", fields, propertyCoverage);
|
2026-10-09 23:17:17 +08:00
|
|
|
fields.sort((left, right) => compareUnicodeCodePoints(left.path, right.path));
|
2026-07-22 20:38:26 +08:00
|
|
|
propertyCoverage.sort((left, right) =>
|
2026-10-09 23:17:17 +08:00
|
|
|
compareUnicodeCodePoints(left.path, right.path),
|
2026-07-22 20:38:26 +08:00
|
|
|
);
|
|
|
|
|
return { fields, propertyCoverage };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const flattenValue = (
|
|
|
|
|
value: JavaScriptSemanticValue,
|
|
|
|
|
path: string,
|
2026-08-10 14:57:44 +08:00
|
|
|
fields: ProjectedReturnField[],
|
|
|
|
|
coverage: ProjectedPropertyCoverage[],
|
2026-10-09 03:30:31 -07:00
|
|
|
presence: "present" | "absent" | "unknown-coverage" = "present",
|
2026-07-22 20:38:26 +08:00
|
|
|
): void => {
|
2026-10-09 03:30:31 -07:00
|
|
|
if (presence !== "present") {
|
|
|
|
|
fields.push({
|
|
|
|
|
path,
|
|
|
|
|
presence,
|
|
|
|
|
state: "unknown",
|
|
|
|
|
value: null,
|
|
|
|
|
reason:
|
|
|
|
|
"reason" in value ? value.reason : "Property presence is uncertain.",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-22 20:38:26 +08:00
|
|
|
if (value.status === "literal") {
|
2026-10-09 03:30:31 -07:00
|
|
|
fields.push({
|
|
|
|
|
path,
|
|
|
|
|
presence,
|
|
|
|
|
state: "literal",
|
|
|
|
|
value: value.value,
|
|
|
|
|
reason: null,
|
|
|
|
|
});
|
2026-07-22 20:38:26 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (value.status === "union") {
|
|
|
|
|
fields.push({
|
|
|
|
|
path,
|
2026-10-09 03:30:31 -07:00
|
|
|
presence,
|
2026-07-22 20:38:26 +08:00
|
|
|
state: "union",
|
|
|
|
|
value: [...value.values],
|
|
|
|
|
reason: null,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-10-09 20:03:19 +08:00
|
|
|
if ("reason" in value) {
|
|
|
|
|
fields.push({
|
|
|
|
|
path,
|
|
|
|
|
presence,
|
|
|
|
|
state: "unknown",
|
|
|
|
|
value: null,
|
|
|
|
|
reason: value.reason,
|
|
|
|
|
});
|
2026-07-22 20:38:26 +08:00
|
|
|
return;
|
|
|
|
|
}
|
2026-10-09 20:03:19 +08:00
|
|
|
const container = semanticContainer(value);
|
|
|
|
|
coverage.push({ path, ...container.coverage });
|
|
|
|
|
for (const slot of container.slots)
|
|
|
|
|
flattenValue(
|
|
|
|
|
slot.value,
|
|
|
|
|
`${path}${semanticPropertyPointer([slot.name])}`,
|
|
|
|
|
fields,
|
|
|
|
|
coverage,
|
|
|
|
|
slot.presence,
|
2026-07-22 20:38:26 +08:00
|
|
|
);
|
|
|
|
|
};
|