481 lines
16 KiB
TypeScript
481 lines
16 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import type {
|
|
JavaScriptModuleOrigin,
|
|
JavaScriptSemanticBinding,
|
|
JavaScriptSemanticModuleLink,
|
|
JavaScriptSemanticReference,
|
|
JavaScriptSemanticScope,
|
|
} from "./javascriptSemanticIr.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
semanticResolutionBlocked,
|
|
type JavaScriptSemanticAnalysisState,
|
|
type JavaScriptSemanticScopeState,
|
|
} from "./javascriptSemanticState.js";
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
import {
|
|
evaluateSemanticBinding,
|
|
evaluateSemanticProvenance,
|
|
} from "./javascriptSemanticValues.js";
|
|
import {
|
|
compareCodePoints,
|
|
propertyName,
|
|
range,
|
|
stringValue,
|
|
} from "./javascriptStaticAnalysisHelpers.js";
|
|
|
|
interface CollectCallableInput {
|
|
readonly node: t.Node;
|
|
readonly parent: t.Node | null;
|
|
readonly containerScope: JavaScriptSemanticScopeState;
|
|
readonly bodyScope: JavaScriptSemanticScopeState | undefined;
|
|
readonly state: JavaScriptSemanticAnalysisState;
|
|
}
|
|
|
|
/** Retain callable identity without polluting lexical bindings. */
|
|
export const collectSemanticCallable = (input: CollectCallableInput): void => {
|
|
const { node, parent, containerScope, bodyScope, state } = input;
|
|
const kind = callableKind(node);
|
|
if (kind === undefined) return;
|
|
if (state.callables.length >= state.limits.maxCallables) {
|
|
reachSemanticLimit(state, "maxCallables");
|
|
return;
|
|
}
|
|
state.callables.push({
|
|
callableId: `callable:${kind}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
|
|
kind,
|
|
name: callableName(node, parent),
|
|
containerScopeId: containerScope.scopeId,
|
|
bodyScopeId:
|
|
bodyScope === undefined || !bodyScope.bindingsComplete
|
|
? null
|
|
: bodyScope.scopeId,
|
|
location: range(node),
|
|
});
|
|
};
|
|
|
|
/** Collect one static import/export relationship for later composition. */
|
|
export const collectSemanticModuleLink = (
|
|
node: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
if (t.isImportDeclaration(node)) collectImports(node, state);
|
|
else if (t.isExportAllDeclaration(node))
|
|
addModuleLink(state, {
|
|
kind: "re-export",
|
|
specifier: node.source.value,
|
|
importedName: "*",
|
|
localName: null,
|
|
exportedName: "*",
|
|
location: range(node),
|
|
});
|
|
else if (t.isExportNamedDeclaration(node)) collectNamedExports(node, state);
|
|
else if (t.isExportDefaultDeclaration(node))
|
|
addModuleLink(state, {
|
|
kind: "export",
|
|
specifier: null,
|
|
importedName: null,
|
|
localName: defaultDeclarationName(node.declaration),
|
|
exportedName: "default",
|
|
location: range(node),
|
|
});
|
|
else if (t.isVariableDeclarator(node)) collectRequireLink(node, state);
|
|
else if (t.isAssignmentExpression(node)) collectCommonJsExport(node, state);
|
|
};
|
|
|
|
/** Collect bounded lexical identifier references after definitions exist. */
|
|
export const collectSemanticReferences = (
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptSemanticReference[] => {
|
|
const output: JavaScriptSemanticReference[] = [];
|
|
const seen = new Set<string>();
|
|
traverseJavaScriptAst(program, {
|
|
enter: (node, parent) => {
|
|
if (!t.isIdentifier(node) || parent === null) return;
|
|
const role = referenceRole(node, parent);
|
|
if (role === null) return;
|
|
const key = `${String(node.start)}:${role}:${node.name}`;
|
|
if (seen.has(key)) return;
|
|
seen.add(key);
|
|
if (output.length >= state.limits.maxReferences) {
|
|
reachSemanticLimit(state, "maxReferences");
|
|
return;
|
|
}
|
|
const binding = resolveSemanticBindingState(state, node, node.name);
|
|
const blocked =
|
|
binding === undefined &&
|
|
semanticResolutionBlocked(state, node, node.name);
|
|
output.push({
|
|
name: node.name,
|
|
role,
|
|
location: range(node),
|
|
bindingId: binding?.bindingId ?? null,
|
|
resolution:
|
|
binding !== undefined ? "resolved" : blocked ? "unknown" : "unbound",
|
|
});
|
|
},
|
|
});
|
|
return output;
|
|
};
|
|
|
|
/** Freeze collected scopes into deterministic IR order. */
|
|
export const immutableSemanticScopes = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptSemanticScope[] =>
|
|
state.scopes.map((scope) => ({
|
|
scopeId: scope.scopeId,
|
|
parentScopeId: scope.parentScopeId,
|
|
kind: scope.kind,
|
|
location: scope.location,
|
|
bindingsComplete: scope.bindingsComplete,
|
|
bindingIds: [...scope.bindings.values()]
|
|
.map(({ bindingId }) => bindingId)
|
|
.sort(compareCodePoints),
|
|
}));
|
|
|
|
/** Evaluate and freeze bindings into deterministic IR order. */
|
|
export const immutableSemanticBindings = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptSemanticBinding[] =>
|
|
[...state.bindingsById.values()]
|
|
.map((binding) => ({
|
|
bindingId: binding.bindingId,
|
|
scopeId: binding.scopeId,
|
|
name: binding.name,
|
|
kind: binding.kind,
|
|
mutable: binding.mutable,
|
|
definitions: binding.definitions,
|
|
value: evaluateSemanticBinding(binding, state),
|
|
provenance: evaluateSemanticProvenance(binding, state),
|
|
}))
|
|
.sort((left, right) => compareCodePoints(left.bindingId, right.bindingId));
|
|
|
|
/** Recover a literal require origin plus an optional member chain. */
|
|
export const semanticModuleOrigin = (
|
|
node: t.Node | null | undefined,
|
|
projection: readonly (string | number | null)[],
|
|
): JavaScriptModuleOrigin | undefined => {
|
|
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
|
const nested = t.isNode(node.object)
|
|
? semanticModuleOrigin(node.object, projection)
|
|
: undefined;
|
|
const member = semanticStaticPropertyName(node.property, node.computed);
|
|
return nested === undefined || member === ""
|
|
? undefined
|
|
: { ...nested, importedPath: [...nested.importedPath, member] };
|
|
}
|
|
if (
|
|
!t.isCallExpression(node) ||
|
|
!t.isIdentifier(node.callee, { name: "require" })
|
|
)
|
|
return undefined;
|
|
const specifier = stringValue(node.arguments[0]);
|
|
return specifier === undefined || projection.includes(null)
|
|
? undefined
|
|
: {
|
|
specifier,
|
|
importedPath: projection.map((segment) => String(segment)),
|
|
};
|
|
};
|
|
|
|
/** Read a property only when its syntax commits an exact name. */
|
|
export const semanticStaticPropertyName = (
|
|
property: t.Node,
|
|
computed: boolean,
|
|
): string =>
|
|
computed && !t.isStringLiteral(property) && !t.isNumericLiteral(property)
|
|
? ""
|
|
: propertyName(property);
|
|
|
|
/** Record one exact limit hit without inventing the total omitted population. */
|
|
export const reachSemanticLimit = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
limit: keyof JavaScriptSemanticAnalysisState["limits"],
|
|
): void => {
|
|
state.limitsReached.add(limit);
|
|
state.omittedCount += 1;
|
|
};
|
|
|
|
const callableKind = (
|
|
node: t.Node,
|
|
): "function" | "class" | "method" | undefined => {
|
|
if (
|
|
t.isClassMethod(node) ||
|
|
t.isClassPrivateMethod(node) ||
|
|
t.isObjectMethod(node)
|
|
)
|
|
return "method";
|
|
if (t.isFunction(node)) return "function";
|
|
if (t.isClass(node)) return "class";
|
|
return undefined;
|
|
};
|
|
|
|
const callableName = (node: t.Node, parent: t.Node | null): string | null => {
|
|
if (
|
|
(t.isFunctionDeclaration(node) ||
|
|
t.isFunctionExpression(node) ||
|
|
t.isClassDeclaration(node) ||
|
|
t.isClassExpression(node)) &&
|
|
t.isIdentifier(node.id)
|
|
)
|
|
return node.id.name;
|
|
if (
|
|
t.isClassMethod(node) ||
|
|
t.isClassPrivateMethod(node) ||
|
|
t.isObjectMethod(node)
|
|
) {
|
|
if (t.isPrivateName(node.key)) return `#${node.key.id.name}`;
|
|
return propertyName(node.key) || `[computed@${String(node.start ?? -1)}]`;
|
|
}
|
|
if (
|
|
parent !== null &&
|
|
t.isVariableDeclarator(parent) &&
|
|
t.isIdentifier(parent.id)
|
|
)
|
|
return parent.id.name;
|
|
return null;
|
|
};
|
|
|
|
const collectImports = (
|
|
node: t.ImportDeclaration,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
if (node.specifiers.length === 0)
|
|
addModuleLink(state, {
|
|
kind: "import",
|
|
specifier: node.source.value,
|
|
importedName: null,
|
|
localName: null,
|
|
exportedName: null,
|
|
location: range(node),
|
|
});
|
|
for (const specifier of node.specifiers)
|
|
addModuleLink(state, {
|
|
kind: "import",
|
|
specifier: node.source.value,
|
|
importedName: t.isImportDefaultSpecifier(specifier)
|
|
? "default"
|
|
: t.isImportNamespaceSpecifier(specifier)
|
|
? "*"
|
|
: propertyName(specifier.imported),
|
|
localName: specifier.local.name,
|
|
exportedName: null,
|
|
location: range(specifier),
|
|
});
|
|
};
|
|
|
|
const collectNamedExports = (
|
|
node: t.ExportNamedDeclaration,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
if (node.declaration !== null && node.declaration !== undefined)
|
|
for (const localName of declarationNames(node.declaration))
|
|
addModuleLink(state, {
|
|
kind: "export",
|
|
specifier: null,
|
|
importedName: null,
|
|
localName,
|
|
exportedName: localName,
|
|
location: range(node.declaration),
|
|
});
|
|
for (const specifier of node.specifiers)
|
|
if (t.isExportSpecifier(specifier))
|
|
addModuleLink(state, {
|
|
kind: node.source == null ? "export" : "re-export",
|
|
specifier: node.source?.value ?? null,
|
|
importedName: propertyName(specifier.local),
|
|
localName: node.source == null ? propertyName(specifier.local) : null,
|
|
exportedName: propertyName(specifier.exported),
|
|
location: range(specifier),
|
|
});
|
|
};
|
|
|
|
const collectRequireLink = (
|
|
node: t.VariableDeclarator,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
const origin = semanticModuleOrigin(node.init, []);
|
|
if (origin === undefined) return;
|
|
for (const binding of requirePatternBindings(node.id, origin.importedPath))
|
|
addModuleLink(state, {
|
|
kind: "require",
|
|
specifier: origin.specifier,
|
|
importedName: binding.importedName,
|
|
localName: binding.localName,
|
|
exportedName: null,
|
|
location: range(node),
|
|
});
|
|
};
|
|
|
|
const requirePatternBindings = (
|
|
pattern: t.Node,
|
|
path: readonly string[],
|
|
): { readonly importedName: string; readonly localName: string }[] => {
|
|
if (t.isTSParameterProperty(pattern))
|
|
return requirePatternBindings(pattern.parameter, path);
|
|
if (t.isIdentifier(pattern))
|
|
return [{ importedName: path.at(-1) ?? "*", localName: pattern.name }];
|
|
if (t.isAssignmentPattern(pattern))
|
|
return requirePatternBindings(pattern.left, path);
|
|
if (t.isObjectPattern(pattern))
|
|
return pattern.properties.flatMap((property) => {
|
|
if (t.isRestElement(property)) return [];
|
|
const name = semanticStaticPropertyName(property.key, property.computed);
|
|
return name === ""
|
|
? []
|
|
: requirePatternBindings(property.value, [...path, name]);
|
|
});
|
|
if (t.isArrayPattern(pattern))
|
|
return pattern.elements.flatMap((element, index) =>
|
|
element === null
|
|
? []
|
|
: requirePatternBindings(element, [...path, String(index)]),
|
|
);
|
|
return [];
|
|
};
|
|
|
|
const collectCommonJsExport = (
|
|
node: t.AssignmentExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
const exportedName = commonJsExportName(node.left);
|
|
if (exportedName === undefined) return;
|
|
const origin = semanticModuleOrigin(node.right, []);
|
|
addModuleLink(state, {
|
|
kind: "commonjs-export",
|
|
specifier: origin?.specifier ?? null,
|
|
importedName: origin?.importedPath.at(-1) ?? null,
|
|
localName: t.isIdentifier(node.right) ? node.right.name : null,
|
|
exportedName,
|
|
location: range(node),
|
|
});
|
|
};
|
|
|
|
const addModuleLink = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
link: JavaScriptSemanticModuleLink,
|
|
): void => {
|
|
if (state.moduleLinks.length >= state.limits.maxModuleLinks) {
|
|
reachSemanticLimit(state, "maxModuleLinks");
|
|
return;
|
|
}
|
|
state.moduleLinks.push(link);
|
|
};
|
|
|
|
const referenceRole = (
|
|
node: t.Identifier,
|
|
parent: t.Node,
|
|
): JavaScriptSemanticReference["role"] | null => {
|
|
if (isDeclarationIdentifier(node, parent) || isNonReferenceKey(node, parent))
|
|
return null;
|
|
if (
|
|
(t.isAssignmentExpression(parent) && parent.left === node) ||
|
|
t.isUpdateExpression(parent)
|
|
)
|
|
return "write";
|
|
if (t.isExportSpecifier(parent) && parent.local === node) return "export";
|
|
return "read";
|
|
};
|
|
|
|
const isDeclarationIdentifier = (node: t.Identifier, parent: t.Node): boolean =>
|
|
(t.isVariableDeclarator(parent) && patternContains(parent.id, node)) ||
|
|
((t.isFunctionDeclaration(parent) || t.isFunctionExpression(parent)) &&
|
|
parent.id === node) ||
|
|
(t.isFunction(parent) &&
|
|
parent.params.some((value) => patternContains(value, node))) ||
|
|
((t.isClassDeclaration(parent) || t.isClassExpression(parent)) &&
|
|
parent.id === node) ||
|
|
(t.isImportSpecifier(parent) &&
|
|
(parent.local === node || parent.imported === node)) ||
|
|
(t.isImportDefaultSpecifier(parent) && parent.local === node) ||
|
|
(t.isImportNamespaceSpecifier(parent) && parent.local === node) ||
|
|
(t.isCatchClause(parent) && parent.param === node);
|
|
|
|
const isNonReferenceKey = (node: t.Identifier, parent: t.Node): boolean =>
|
|
((t.isMemberExpression(parent) || t.isOptionalMemberExpression(parent)) &&
|
|
parent.property === node &&
|
|
!parent.computed) ||
|
|
((t.isObjectProperty(parent) || t.isObjectMethod(parent)) &&
|
|
parent.key === node &&
|
|
!parent.computed &&
|
|
!(t.isObjectProperty(parent) && parent.shorthand)) ||
|
|
(t.isExportSpecifier(parent) && parent.exported === node) ||
|
|
t.isLabeledStatement(parent) ||
|
|
t.isBreakStatement(parent) ||
|
|
t.isContinueStatement(parent);
|
|
|
|
const patternNames = (pattern: t.Node): string[] => {
|
|
if (t.isTSParameterProperty(pattern)) return patternNames(pattern.parameter);
|
|
if (t.isIdentifier(pattern)) return [pattern.name];
|
|
if (t.isAssignmentPattern(pattern)) return patternNames(pattern.left);
|
|
if (t.isRestElement(pattern)) return patternNames(pattern.argument);
|
|
if (t.isObjectPattern(pattern))
|
|
return pattern.properties.flatMap((property) =>
|
|
t.isRestElement(property)
|
|
? patternNames(property.argument)
|
|
: patternNames(property.value),
|
|
);
|
|
if (t.isArrayPattern(pattern))
|
|
return pattern.elements.flatMap((element) =>
|
|
element === null ? [] : patternNames(element),
|
|
);
|
|
return [];
|
|
};
|
|
|
|
const patternContains = (pattern: t.Node, target: t.Identifier): boolean =>
|
|
pattern === target ||
|
|
(t.isAssignmentPattern(pattern) && patternContains(pattern.left, target)) ||
|
|
(t.isRestElement(pattern) && patternContains(pattern.argument, target)) ||
|
|
(t.isObjectPattern(pattern) &&
|
|
pattern.properties.some((property) =>
|
|
t.isRestElement(property)
|
|
? patternContains(property.argument, target)
|
|
: patternContains(property.value, target),
|
|
)) ||
|
|
(t.isArrayPattern(pattern) &&
|
|
pattern.elements.some(
|
|
(element) => element !== null && patternContains(element, target),
|
|
));
|
|
|
|
const declarationNames = (declaration: t.Declaration): string[] => {
|
|
if (t.isVariableDeclaration(declaration))
|
|
return declaration.declarations.flatMap(({ id }) => patternNames(id));
|
|
if (
|
|
(t.isFunctionDeclaration(declaration) ||
|
|
t.isClassDeclaration(declaration)) &&
|
|
t.isIdentifier(declaration.id)
|
|
)
|
|
return [declaration.id.name];
|
|
return [];
|
|
};
|
|
|
|
const defaultDeclarationName = (
|
|
declaration: t.ExportDefaultDeclaration["declaration"],
|
|
): string | null =>
|
|
(t.isFunctionDeclaration(declaration) || t.isClassDeclaration(declaration)) &&
|
|
t.isIdentifier(declaration.id)
|
|
? declaration.id.name
|
|
: null;
|
|
|
|
const commonJsExportName = (node: t.Node): string | undefined => {
|
|
if (t.isIdentifier(node, { name: "exports" })) return "default";
|
|
if (!t.isMemberExpression(node) && !t.isOptionalMemberExpression(node))
|
|
return undefined;
|
|
const name = propertyName(node.property);
|
|
if (t.isIdentifier(node.object, { name: "exports" })) return name || "*";
|
|
if (
|
|
t.isMemberExpression(node.object) &&
|
|
t.isIdentifier(node.object.object, { name: "module" }) &&
|
|
propertyName(node.object.property) === "exports"
|
|
)
|
|
return name || "default";
|
|
if (
|
|
t.isIdentifier(node.object, { name: "module" }) &&
|
|
propertyName(node.property) === "exports"
|
|
)
|
|
return "default";
|
|
return undefined;
|
|
};
|