300 lines
9.5 KiB
TypeScript
300 lines
9.5 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import { semanticBuiltinNamespacePath } from "./javascriptSemanticProvenance.js";
|
|
|
|
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
semanticResolutionBlocked,
|
|
type JavaScriptSemanticAnalysisState,
|
|
type JavaScriptSemanticBindingState,
|
|
} from "./javascriptSemanticState.js";
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
import {
|
|
semanticStaticPropertyKey,
|
|
semanticStaticPropertyName,
|
|
} from "./javascriptAstValues.js";
|
|
|
|
export interface SemanticProjectedBinding {
|
|
readonly bindingId: string;
|
|
readonly identifier: t.Identifier;
|
|
readonly projectionPath: readonly (string | number | null)[];
|
|
readonly resolution: "complete" | "partial";
|
|
}
|
|
|
|
/** Resolve bindings written by the nearest variable or assignment owner. */
|
|
export const assignedSemanticResultBindings = (
|
|
node: t.Node,
|
|
ancestors: readonly t.Node[],
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): SemanticProjectedBinding[] => {
|
|
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
const owner = ancestors[index];
|
|
if (owner === undefined) continue;
|
|
let pattern: t.Node | undefined;
|
|
let source: t.Node | null = null;
|
|
if (t.isVariableDeclarator(owner)) {
|
|
pattern = owner.id;
|
|
source = owner.init ?? null;
|
|
} else if (t.isAssignmentExpression(owner)) {
|
|
pattern = owner.left;
|
|
source = owner.right;
|
|
}
|
|
if (
|
|
pattern !== undefined &&
|
|
source !== null &&
|
|
assignedSourceIs(node, source)
|
|
)
|
|
return projectedPatternBindings(pattern, state);
|
|
if (semanticCallableIdForNode(owner) !== null) return [];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
const assignedSourceIs = (node: t.Node, source: t.Node): boolean => {
|
|
let value = source;
|
|
while (true) {
|
|
if (t.isAwaitExpression(value)) value = value.argument;
|
|
else if (
|
|
t.isParenthesizedExpression(value) ||
|
|
t.isTSAsExpression(value) ||
|
|
t.isTSTypeAssertion(value) ||
|
|
t.isTypeCastExpression(value) ||
|
|
t.isTSSatisfiesExpression(value)
|
|
)
|
|
value = value.expression;
|
|
else break;
|
|
}
|
|
return value === node;
|
|
};
|
|
|
|
const projectedPatternBindings = (
|
|
pattern: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): SemanticProjectedBinding[] => {
|
|
const output: SemanticProjectedBinding[] = [];
|
|
const visit = (
|
|
current: t.Node,
|
|
path: readonly (string | number | null)[],
|
|
partial: boolean,
|
|
): void => {
|
|
if (t.isIdentifier(current)) {
|
|
const binding = resolveSemanticBindingState(state, current, current.name);
|
|
if (binding !== undefined)
|
|
output.push({
|
|
bindingId: binding.bindingId,
|
|
identifier: current,
|
|
projectionPath: path,
|
|
resolution: partial ? "partial" : "complete",
|
|
});
|
|
return;
|
|
}
|
|
if (t.isAssignmentPattern(current)) return visit(current.left, path, true);
|
|
if (t.isRestElement(current))
|
|
return visit(current.argument, [...path, null], true);
|
|
if (t.isObjectPattern(current)) {
|
|
for (const property of current.properties) {
|
|
if (t.isRestElement(property))
|
|
visit(property.argument, [...path, null], true);
|
|
else
|
|
visit(
|
|
property.value,
|
|
[
|
|
...path,
|
|
semanticStaticPropertyKey(property.key, property.computed),
|
|
],
|
|
partial ||
|
|
semanticStaticPropertyKey(property.key, property.computed) ===
|
|
null,
|
|
);
|
|
}
|
|
} else if (t.isArrayPattern(current)) {
|
|
current.elements.forEach((element, index) => {
|
|
if (element !== null) visit(element, [...path, index], partial);
|
|
});
|
|
}
|
|
};
|
|
visit(pattern, [], false);
|
|
return output;
|
|
};
|
|
|
|
export const semanticProjectedBindingFacts = (
|
|
bindings: readonly SemanticProjectedBinding[],
|
|
): Omit<SemanticProjectedBinding, "identifier">[] =>
|
|
bindings.map(({ identifier: _identifier, ...fact }) => fact);
|
|
|
|
/** Return a scalar owner only for an unprojected identifier result. */
|
|
export const scalarAssignedResultBinding = (
|
|
bindings: readonly SemanticProjectedBinding[],
|
|
): string | null =>
|
|
bindings.length === 1 && bindings[0]?.projectionPath.length === 0
|
|
? (bindings[0]?.bindingId ?? null)
|
|
: null;
|
|
|
|
/** Shared mutable stacks reset before each deterministic data-effect pass. */
|
|
export interface DataEffectTraversalContext {
|
|
readonly state: JavaScriptSemanticAnalysisState;
|
|
readonly callableIds: ReadonlySet<string>;
|
|
readonly callableStack: string[];
|
|
readonly ancestors: t.Node[];
|
|
}
|
|
|
|
/** Resolve a direct or namespace-imported built-in method. */
|
|
export const builtinDataEffectMethod = (
|
|
callee: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
specifiers: readonly string[],
|
|
): string | null => {
|
|
if (t.isIdentifier(callee)) {
|
|
const binding = resolveSemanticBindingState(state, callee, callee.name);
|
|
return moduleMethod(binding, specifiers);
|
|
}
|
|
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
|
|
return null;
|
|
if (!t.isIdentifier(callee.object)) return null;
|
|
const binding = resolveSemanticBindingState(
|
|
state,
|
|
callee.object,
|
|
callee.object.name,
|
|
);
|
|
const namespace = binding?.directOrigins.some(
|
|
({ specifier, importedPath }) =>
|
|
specifiers.includes(specifier) &&
|
|
semanticBuiltinNamespacePath(importedPath),
|
|
);
|
|
return namespace
|
|
? semanticStaticPropertyName(callee.property, callee.computed)
|
|
: null;
|
|
};
|
|
|
|
const moduleMethod = (
|
|
binding: JavaScriptSemanticBindingState | undefined,
|
|
specifiers: readonly string[],
|
|
): string | null => {
|
|
for (const { specifier, importedPath } of binding?.directOrigins ?? []) {
|
|
if (!specifiers.includes(specifier)) continue;
|
|
return importedPath.at(-1) ?? null;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/** Recognize global or node:process aliases without accepting shadowing. */
|
|
export const isSemanticProcessObject = (
|
|
node: t.Expression | t.Super,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean => {
|
|
if (!t.isIdentifier(node)) return false;
|
|
const binding = resolveSemanticBindingState(state, node, node.name);
|
|
if (binding === undefined)
|
|
return (
|
|
node.name === "process" &&
|
|
!semanticResolutionBlocked(state, node, node.name)
|
|
);
|
|
return binding.directOrigins.some(
|
|
({ specifier }) => specifier === "process" || specifier === "node:process",
|
|
);
|
|
};
|
|
|
|
/** Resolve the nearest direct variable/assignment owner before a callable. */
|
|
export const outerDataEffectBinding = (
|
|
node: t.Node,
|
|
context: DataEffectTraversalContext,
|
|
): string | null => {
|
|
return scalarAssignedResultBinding(
|
|
assignedSemanticResultBindings(node, context.ancestors, context.state),
|
|
);
|
|
};
|
|
|
|
/** Traverse with exact callable and ancestor stacks. */
|
|
export const traverseDataEffects = (
|
|
program: t.Program,
|
|
context: DataEffectTraversalContext,
|
|
visit: (node: t.Node, parent: t.Node | null) => void,
|
|
): void => {
|
|
context.callableStack.length = 0;
|
|
context.ancestors.length = 0;
|
|
traverseJavaScriptAst(program, {
|
|
enter: (node, parent) => {
|
|
const callableId = semanticCallableIdForNode(node);
|
|
if (callableId !== null && context.callableIds.has(callableId))
|
|
context.callableStack.push(callableId);
|
|
visit(node, parent);
|
|
context.ancestors.push(node);
|
|
},
|
|
exit: (node) => {
|
|
context.ancestors.pop();
|
|
const callableId = semanticCallableIdForNode(node);
|
|
if (callableId !== null && context.callableStack.at(-1) === callableId)
|
|
context.callableStack.pop();
|
|
},
|
|
});
|
|
};
|
|
|
|
/** Resolve one direct identifier argument to a lexical binding. */
|
|
export const dataEffectArgumentBindingId = (
|
|
node:
|
|
| t.Expression
|
|
| t.SpreadElement
|
|
| t.JSXNamespacedName
|
|
| t.ArgumentPlaceholder
|
|
| undefined,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): string | null =>
|
|
t.isIdentifier(node)
|
|
? (resolveSemanticBindingState(state, node, node.name)?.bindingId ?? null)
|
|
: null;
|
|
|
|
/** Read one JSON primitive default without evaluating code. */
|
|
export const dataEffectPrimitive = (
|
|
node: t.Node,
|
|
): string | number | boolean | null => {
|
|
if (t.isStringLiteral(node) || t.isNumericLiteral(node)) return node.value;
|
|
if (t.isBooleanLiteral(node)) return node.value;
|
|
return null;
|
|
};
|
|
|
|
/** Read one static string or expression-free template. */
|
|
export const dataEffectLiteralString = (
|
|
node:
|
|
| t.Expression
|
|
| t.SpreadElement
|
|
| t.JSXNamespacedName
|
|
| t.ArgumentPlaceholder
|
|
| undefined,
|
|
): string | null => {
|
|
if (t.isStringLiteral(node)) return node.value;
|
|
if (t.isTemplateLiteral(node) && node.expressions.length === 0)
|
|
return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw ?? null;
|
|
return null;
|
|
};
|
|
|
|
/** Read a member callee from call or construction syntax. */
|
|
export const dataEffectMemberCallee = (
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
): t.MemberExpression | t.OptionalMemberExpression | null =>
|
|
t.isMemberExpression(node.callee) || t.isOptionalMemberExpression(node.callee)
|
|
? node.callee
|
|
: null;
|
|
|
|
/** Read a parent member's object when present. */
|
|
export const dataEffectMemberObject = (
|
|
node: t.Node | null,
|
|
): t.Expression | t.Super | null =>
|
|
node !== null &&
|
|
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
|
|
? node.object
|
|
: null;
|
|
|
|
/** Compare Babel source offsets for exact containment. */
|
|
export const containsSemanticNode = (outer: t.Node, inner: t.Node): boolean =>
|
|
outer.start !== null &&
|
|
outer.start !== undefined &&
|
|
outer.end !== null &&
|
|
outer.end !== undefined &&
|
|
inner.start !== null &&
|
|
inner.start !== undefined &&
|
|
inner.end !== null &&
|
|
inner.end !== undefined &&
|
|
outer.start <= inner.start &&
|
|
outer.end >= inner.end;
|