mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
e9fc8fb30c
* fix(javascript): preserve literal percent runtime paths * fix(javascript): ignore resolve prefixes before the final anchor * fix(electron): retain application roots for loadFile declarations * fix(electron): resolve loadFile from enclosing application package * fix(electron): retain outer application root across package scopes * fix(electron): derive application ownership from inventoried packages * fix(electron): retain resolved package entrypoint ownership * fix(electron): preserve competing application ownership * fix(electron): retain invalid and omitted runtime ownership * fix(electron): preserve omitted module ownership and runtime paths * fix(electron): preserve proven roots before confining page paths * fix(electron): retain possible owners behind missing imports * fix(electron): distinguish absent imports and owned page candidates * fix(electron): keep unavailable JSON out of owner gaps * fix(electron): retain exact loader formats and default entry status * fix(electron): retain unavailable entrypoint ownership candidates * test(ghidra): preserve Windows Node probe prerequisites * test(ghidra): assert cancellation during profile shutdown * fix(electron): separate examined imports from frontend ownership * fix(electron): retain unavailable directory entrypoint owners * test(jeb): retain Linux ownership prerequisite in isolation * fix(javascript): retain unchecked loader ownership and literal main paths * fix(javascript): preserve runtime paths and call-site module aliases * fix(javascript): bound module alias traversal and preserve projections * fix(javascript): retain host module and nested artifact path boundaries * fix(javascript): preserve ESM and archive module loader provenance * test(javascript): distinguish unmapped containers from missing exports * fix: retain default module aliases and Electron builtin imports * fix: preserve host file URL and inward ASAR ownership * fix: preserve CommonJS file URL classification in CI fixtures * fix: retain bound module loaders and Windows entrypoint uncertainty * fix: retain ASAR uncertainty and invalidate changed Electron app roots * fix(javascript): retain ownership uncertainty across loader boundaries * fix(javascript): preserve loader sources and bound alias queries * fix(javascript): retain destructured loader sources * fix(javascript): preserve loader uncertainty at resolution boundaries * fix(javascript): preserve callable and package launch ownership * fix(javascript): preserve captured values across member reads * test(process): assert tagged filesystem cancellation failures * fix(javascript): preserve inventory bases and invocation read timing * fix(javascript): preserve ownership across indirect and unread loads * test(javascript): order fixture directory creation on Windows * test(javascript): share ordered filesystem fixture writer * fix(javascript): unify loader provenance and package validation * fix(javascript): preserve captured Node require loader values * fix(javascript): preserve loader invocation authority * fix(javascript): verify generic loader invocation authority * fix(javascript): preserve conditional loader method alternatives * fix(javascript): require proven factories for source association * fix(javascript): honor replaced Electron app path setters * fix(javascript): prove path and assignment source ownership * fix(javascript): retain native delegate eligibility through shared writes * fix(process): settle stale baseline errors after confirmed absence * fix(javascript): preserve assignment and path boundary evidence * fix(javascript): prove source-directory resolve anchors * fix: preserve application path provenance and portable schemas * fix(javascript): retain CommonJS exported setter effects * fix: isolate Linux identity observations and retain native module exports * fix(javascript): preserve published app and helper provenance * fix(javascript): normalize typed stores and native path fixtures
855 lines
28 KiB
TypeScript
855 lines
28 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import type {
|
|
JavaScriptSemanticBinding,
|
|
JavaScriptSemanticModuleLink,
|
|
JavaScriptSemanticReference,
|
|
JavaScriptSemanticScope,
|
|
} from "./javascriptSemanticIr.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
semanticResolutionBlocked,
|
|
type JavaScriptSemanticAnalysisState,
|
|
type JavaScriptSemanticScopeState,
|
|
} from "./javascriptSemanticState.js";
|
|
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|
import {
|
|
completeSemanticSteps,
|
|
traverseJavaScriptAstSteps,
|
|
} from "./javascriptSemanticTraversal.js";
|
|
import {
|
|
evaluateSemanticBinding,
|
|
evaluateSemanticProvenance,
|
|
} from "./javascriptSemanticValues.js";
|
|
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
|
import {
|
|
propertyName,
|
|
semanticStaticPropertyKey,
|
|
semanticWrappedWriteSite,
|
|
semanticWriteSiteReadsFirst,
|
|
semanticWriteSiteTarget,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import {
|
|
isTypeOnlyExportDeclaration,
|
|
isTypeOnlyExportSpecifier,
|
|
isTypeOnlyImportDeclaration,
|
|
isTypeOnlyImportSpecifier,
|
|
semanticImportEqualsSpecifier,
|
|
} from "./javascriptModuleDeclaration.js";
|
|
import {
|
|
commonJsRuntimeExportValues,
|
|
type CommonJsExportReceiverEffect,
|
|
type CommonJsExportSite,
|
|
} from "./javascriptCommonJsExportReceivers.js";
|
|
import type { JavaScriptSemanticPropertyPath } from "./javascriptSemanticPropertyPaths.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;
|
|
const callableId =
|
|
semanticCallableIdForNode(node) ?? "callable:unknown:-1:-1";
|
|
state.callables.push({
|
|
callableId,
|
|
kind,
|
|
name: callableName(node, parent),
|
|
containerScopeId: containerScope.scopeId,
|
|
bodyScopeId:
|
|
bodyScope === undefined || !bodyScope.bindingsComplete
|
|
? null
|
|
: bodyScope.scopeId,
|
|
location: range(node),
|
|
returnSites: [],
|
|
returnCoverage: {
|
|
status: "partial",
|
|
retainedCount: 0,
|
|
omittedCount: null,
|
|
},
|
|
});
|
|
state.callableNodesById.set(callableId, node);
|
|
};
|
|
|
|
/** Collect one static import/export relationship for later composition. */
|
|
export const collectSemanticModuleLink = (
|
|
node: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
commonJsReceivers: ReadonlyMap<
|
|
CommonJsExportSite,
|
|
CommonJsExportReceiverEffect
|
|
>,
|
|
): void => {
|
|
if (t.isTSImportEqualsDeclaration(node)) collectImportEquals(node, state);
|
|
else if (t.isImportDeclaration(node)) collectImports(node, state);
|
|
else if (t.isExportAllDeclaration(node))
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind: isTypeOnlyExportDeclaration(node) ? "type-import" : "re-export",
|
|
specifier: node.source.value,
|
|
importedName: "*",
|
|
localName: null,
|
|
exportedName: "*",
|
|
location: range(node),
|
|
},
|
|
node,
|
|
);
|
|
else if (t.isExportNamedDeclaration(node)) collectNamedExports(node, state);
|
|
else if (t.isExportDefaultDeclaration(node))
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind: "export",
|
|
specifier: null,
|
|
importedName: null,
|
|
localName: defaultExportLocalName(node.declaration),
|
|
exportedName: "default",
|
|
callableId: semanticCallableIdForNode(node.declaration),
|
|
location: range(node),
|
|
},
|
|
node,
|
|
);
|
|
else if (t.isVariableDeclarator(node)) collectRequireLink(node, state);
|
|
else if (t.isAssignmentExpression(node))
|
|
collectCommonJsExport(node, state, commonJsReceivers.get(node));
|
|
else if (t.isCallExpression(node)) {
|
|
collectCommonJsExport(node, state, commonJsReceivers.get(node));
|
|
collectUnboundRequireLink(node, state);
|
|
}
|
|
};
|
|
|
|
const collectImportEquals = (
|
|
node: t.TSImportEqualsDeclaration,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
const moduleReference = node.moduleReference;
|
|
const specifier =
|
|
semanticImportEqualsSpecifier(node) ??
|
|
(node.importKind === "type" &&
|
|
t.isTSExternalModuleReference(moduleReference) &&
|
|
t.isStringLiteral(moduleReference.expression)
|
|
? moduleReference.expression.value
|
|
: null);
|
|
if (specifier === null) return;
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind: node.importKind === "type" ? "type-import" : "require",
|
|
specifier,
|
|
importedName: null,
|
|
localName: node.id.name,
|
|
exportedName: null,
|
|
location: range(node),
|
|
},
|
|
node,
|
|
);
|
|
};
|
|
|
|
/** Collect lexical references after definitions exist, optionally for one name. */
|
|
export const collectSemanticReferences = (
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
name?: string,
|
|
): JavaScriptSemanticReference[] =>
|
|
completeSemanticSteps(collectSemanticReferencesSteps(program, state, name));
|
|
|
|
/** Collect lexical references in steps while preserving resolution and visit order. */
|
|
export function* collectSemanticReferencesSteps(
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
name?: string,
|
|
): Generator<void, JavaScriptSemanticReference[]> {
|
|
const output: JavaScriptSemanticReference[] = [];
|
|
const seen = new Set<string>();
|
|
const patterns: t.Node[] = [];
|
|
yield* traverseJavaScriptAstSteps(program, {
|
|
enter: (node, parent, readAncestors) => {
|
|
if (isPatternNode(node)) patterns.push(node);
|
|
if (
|
|
!t.isIdentifier(node) ||
|
|
parent === null ||
|
|
(name !== undefined && node.name !== name) ||
|
|
(isNonReferenceKey(node, parent) &&
|
|
!patterns.some((pattern) => bindsIdentifier(pattern, parent, node)))
|
|
)
|
|
return;
|
|
const ancestors = readAncestors();
|
|
const role = semanticIdentifierRole(node, parent, ancestors);
|
|
if (role === null) return;
|
|
// A compound assignment or update reads before it writes; emit both so
|
|
// reference consumers see the read they actually execute.
|
|
const roles =
|
|
role === "write" &&
|
|
semanticReadsBeforeWrite(
|
|
node,
|
|
semanticWrappedWriteSite(node, parent, ancestors) ?? parent,
|
|
)
|
|
? (["read", "write"] as const)
|
|
: ([role] as const);
|
|
for (const resolvedRole of roles) {
|
|
const key = `${String(node.start)}:${resolvedRole}:${node.name}`;
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
const binding = resolveSemanticBindingState(state, node, node.name);
|
|
const blocked =
|
|
binding === undefined &&
|
|
semanticResolutionBlocked(state, node, node.name);
|
|
output.push({
|
|
name: node.name,
|
|
role: resolvedRole,
|
|
location: range(node),
|
|
bindingId: binding?.bindingId ?? null,
|
|
resolution:
|
|
binding !== undefined
|
|
? "resolved"
|
|
: blocked
|
|
? "unknown"
|
|
: "unbound",
|
|
});
|
|
}
|
|
},
|
|
exit: (node) => {
|
|
if (isPatternNode(node)) patterns.pop();
|
|
},
|
|
});
|
|
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(compareUnicodeCodePoints),
|
|
}));
|
|
|
|
/** 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) =>
|
|
compareUnicodeCodePoints(left.bindingId, right.bindingId),
|
|
);
|
|
|
|
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}`;
|
|
// `{[key]() {}}` names nothing statically; reading `key` off the key node
|
|
// would report the variable name as the method name. `{[""]() {}}` names
|
|
// the exact empty key, which is retained rather than replaced.
|
|
return (
|
|
semanticStaticPropertyKey(node.key, node.computed) ??
|
|
`[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: isTypeOnlyImportDeclaration(node) ? "type-import" : "import",
|
|
specifier: node.source.value,
|
|
importedName: null,
|
|
localName: null,
|
|
exportedName: null,
|
|
location: range(node),
|
|
},
|
|
node,
|
|
);
|
|
for (const specifier of node.specifiers)
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind:
|
|
isTypeOnlyImportDeclaration(node) ||
|
|
(t.isImportSpecifier(specifier) &&
|
|
isTypeOnlyImportSpecifier(specifier))
|
|
? "type-import"
|
|
: "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),
|
|
},
|
|
node,
|
|
);
|
|
};
|
|
|
|
const collectNamedExports = (
|
|
node: t.ExportNamedDeclaration,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
const declarationTypeOnly = isTypeOnlyExportDeclaration(node);
|
|
if (node.declaration !== null && node.declaration !== undefined)
|
|
for (const localName of declarationNames(node.declaration))
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind: declarationTypeOnly ? "type-import" : "export",
|
|
specifier: null,
|
|
importedName: null,
|
|
localName,
|
|
exportedName: localName,
|
|
callableId: declarationCallableId(node.declaration, localName),
|
|
location: range(node.declaration),
|
|
},
|
|
node,
|
|
);
|
|
for (const specifier of node.specifiers)
|
|
if (
|
|
t.isExportSpecifier(specifier) ||
|
|
(t.isExportNamespaceSpecifier(specifier) && node.exportKind !== "type")
|
|
)
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind:
|
|
declarationTypeOnly ||
|
|
(t.isExportSpecifier(specifier) &&
|
|
isTypeOnlyExportSpecifier(specifier))
|
|
? "type-import"
|
|
: node.source == null
|
|
? "export"
|
|
: "re-export",
|
|
specifier: node.source?.value ?? null,
|
|
importedName: t.isExportNamespaceSpecifier(specifier)
|
|
? "*"
|
|
: propertyName(specifier.local),
|
|
localName:
|
|
node.source == null && t.isExportSpecifier(specifier)
|
|
? propertyName(specifier.local)
|
|
: null,
|
|
exportedName: propertyName(specifier.exported),
|
|
location: range(specifier),
|
|
},
|
|
node,
|
|
);
|
|
};
|
|
|
|
const collectRequireLink = (
|
|
node: t.VariableDeclarator,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
const required = semanticRequireOrigin(node.init, state);
|
|
if (required === undefined) return;
|
|
const { origin, call } = required;
|
|
const bindings = requirePatternBindings(node.id, origin.importedPath);
|
|
for (const binding of bindings)
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind: "require",
|
|
specifier: origin.specifier,
|
|
importedName: binding.importedName,
|
|
localName: binding.localName,
|
|
exportedName: null,
|
|
location: range(node),
|
|
},
|
|
node,
|
|
);
|
|
if (bindings.length > 0) state.linkedRequireCalls.add(call);
|
|
};
|
|
|
|
const collectUnboundRequireLink = (
|
|
node: t.CallExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
if (state.linkedRequireCalls.has(node)) return;
|
|
const required = semanticRequireOrigin(node, state);
|
|
if (required === undefined) return;
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind: "require",
|
|
specifier: required.origin.specifier,
|
|
importedName: null,
|
|
localName: null,
|
|
exportedName: null,
|
|
location: range(node),
|
|
},
|
|
node,
|
|
);
|
|
state.linkedRequireCalls.add(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 = semanticStaticPropertyKey(property.key, property.computed);
|
|
return name === null
|
|
? []
|
|
: 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: CommonJsExportSite,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
receiverEffect: CommonJsExportReceiverEffect | undefined,
|
|
): void => {
|
|
if (receiverEffect === "detached") return;
|
|
// Compound and logical writes also depend on the previous value, so remain
|
|
// unknown.
|
|
for (const exported of commonJsRuntimeExportValues(node, state))
|
|
addCommonJsExport(
|
|
node,
|
|
state,
|
|
exported.exportedName,
|
|
receiverEffect === "uncertain" ? null : exported.value,
|
|
exported.projection,
|
|
);
|
|
};
|
|
|
|
const addCommonJsExport = (
|
|
node: CommonJsExportSite,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
exportedName: string,
|
|
exported: t.Node | null,
|
|
projection?: JavaScriptSemanticPropertyPath,
|
|
): void => {
|
|
// Simple assignment chains yield their rightmost assigned value.
|
|
let value = exported;
|
|
while (t.isAssignmentExpression(value, { operator: "=" }))
|
|
value = value.right;
|
|
const required =
|
|
value === null ? undefined : semanticRequireOrigin(value, state);
|
|
// Copies retain their module provenance, but do not export the source object
|
|
// itself. A wildcard with exclusions cannot express one module link.
|
|
const origin = projection?.some(
|
|
(key) =>
|
|
typeof key === "object" && key !== null && key.excludedKeys.length > 0,
|
|
)
|
|
? undefined
|
|
: required?.origin;
|
|
const selectedName = projection?.at(-1);
|
|
addModuleLink(
|
|
state,
|
|
{
|
|
kind: "commonjs-export",
|
|
specifier: origin?.specifier ?? null,
|
|
importedName:
|
|
origin === undefined
|
|
? null
|
|
: typeof selectedName === "string" || typeof selectedName === "number"
|
|
? String(selectedName)
|
|
: (origin.importedPath.at(-1) ?? null),
|
|
localName:
|
|
projection === undefined && value !== null && t.isIdentifier(value)
|
|
? value.name
|
|
: null,
|
|
exportedName,
|
|
callableId:
|
|
projection !== undefined || value === null
|
|
? null
|
|
: semanticCallableIdForNode(value),
|
|
location: range(node),
|
|
},
|
|
node,
|
|
);
|
|
if (origin !== undefined && required !== undefined)
|
|
state.linkedRequireCalls.add(required.call);
|
|
};
|
|
|
|
const addModuleLink = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
link: Omit<JavaScriptSemanticModuleLink, "callableId"> & {
|
|
readonly callableId?: string | null;
|
|
},
|
|
referenceNode: t.Node,
|
|
): void => {
|
|
const resolutionMode =
|
|
link.resolutionMode ??
|
|
(link.kind === "import" ||
|
|
link.kind === "re-export" ||
|
|
link.kind === "type-import"
|
|
? state.staticImportResolutionMode
|
|
: link.kind === "require"
|
|
? "require"
|
|
: undefined);
|
|
const collected = {
|
|
...link,
|
|
...(resolutionMode === undefined ? {} : { resolutionMode }),
|
|
callableId: link.callableId ?? null,
|
|
};
|
|
state.moduleLinks.push(collected);
|
|
if (
|
|
t.isExportDefaultDeclaration(referenceNode) &&
|
|
t.isExpression(referenceNode.declaration)
|
|
)
|
|
state.defaultExportExpressions.set(collected, referenceNode.declaration);
|
|
if (link.localName !== null) {
|
|
const binding = resolveSemanticBindingState(
|
|
state,
|
|
referenceNode,
|
|
link.localName,
|
|
);
|
|
if (binding !== undefined) state.moduleLinkBindings.set(collected, binding);
|
|
}
|
|
};
|
|
|
|
/** Deterministic callable identity shared by collection and return recovery. */
|
|
export const semanticCallableIdForNode = (node: t.Node): string | null => {
|
|
const kind = callableKind(node);
|
|
return kind === undefined
|
|
? null
|
|
: `callable:${kind}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`;
|
|
};
|
|
|
|
const declarationCallableId = (
|
|
declaration: t.Declaration,
|
|
localName: string,
|
|
): string | null => {
|
|
if (
|
|
(t.isFunctionDeclaration(declaration) ||
|
|
t.isClassDeclaration(declaration)) &&
|
|
declaration.id?.name === localName
|
|
)
|
|
return semanticCallableIdForNode(declaration);
|
|
if (!t.isVariableDeclaration(declaration)) return null;
|
|
const declarator = declaration.declarations.find(({ id }) =>
|
|
t.isIdentifier(id, { name: localName }),
|
|
);
|
|
return declarator?.init === null || declarator?.init === undefined
|
|
? null
|
|
: semanticCallableIdForNode(declarator.init);
|
|
};
|
|
|
|
/**
|
|
* True when writing `node` also reads it first: a compound assignment
|
|
* (`+=`, `||=`, …) or an update (`++`/`--`). A plain `=` writes only.
|
|
*
|
|
* One predicate serves both member operations and identifier references so the
|
|
* two can never disagree about what counts as a read-modify-write.
|
|
*/
|
|
export const semanticReadsBeforeWrite = (
|
|
node: t.Node,
|
|
site: t.Node,
|
|
): boolean => {
|
|
const target = semanticWriteSiteTarget(site);
|
|
return (
|
|
target !== null &&
|
|
unwrapJavaScriptExpression(target).node === node &&
|
|
semanticWriteSiteReadsFirst(site)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Role of one identifier occurrence, given the node's ancestry.
|
|
*
|
|
* A binding inside a destructuring pattern has an ObjectProperty/ArrayPattern
|
|
* as its direct parent, so the immediate parent cannot tell a declaration
|
|
* (`const {a} = o`, `catch ({message})`) from an assignment target
|
|
* (`({a} = o)`, `for (a of list)`). Walking the ancestry to the nearest
|
|
* pattern-owning construct is what makes the distinction possible.
|
|
*/
|
|
export const semanticIdentifierRole = (
|
|
node: t.Identifier,
|
|
parent: t.Node,
|
|
ancestors: readonly t.Node[],
|
|
): JavaScriptSemanticReference["role"] | null => {
|
|
const patternAncestor = enclosingPatternAncestor(node, parent, ancestors);
|
|
if (patternAncestor !== null) {
|
|
if (isPatternOwnerDeclaration(patternAncestor)) return null;
|
|
return "write";
|
|
}
|
|
const site = semanticWrappedWriteSite(node, parent, ancestors);
|
|
if (site !== null && site !== parent && !isDeleteSite(site)) return "write";
|
|
return semanticReferenceRole(node, parent);
|
|
};
|
|
|
|
/**
|
|
* The nearest ancestor that owns this identifier as a *bound* pattern slot, or
|
|
* null when the identifier is not being bound. A binding inside a pattern has
|
|
* an ObjectProperty/ArrayPattern parent, so the immediate parent cannot say
|
|
* whether the identifier declares a binding or is assigned to.
|
|
*
|
|
* Only the bound side counts: `{a = compute()}` binds `a` but merely *reads*
|
|
* `compute`, so the default-value side must not be mistaken for a binding.
|
|
*/
|
|
const enclosingPatternAncestor = (
|
|
node: t.Identifier,
|
|
parent: t.Node,
|
|
ancestors: readonly t.Node[],
|
|
): t.Node | null => {
|
|
const chain = [...ancestors, parent];
|
|
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
const ancestor = chain[index];
|
|
if (ancestor === undefined || !isPatternNode(ancestor)) continue;
|
|
if (!bindsIdentifier(ancestor, parent, node)) continue;
|
|
// Found the binding slot; now find what owns the pattern.
|
|
for (let owner = index - 1; owner >= 0; owner -= 1) {
|
|
const candidate = chain[owner];
|
|
if (candidate === undefined) continue;
|
|
if (
|
|
t.isAssignmentExpression(candidate) ||
|
|
t.isForInStatement(candidate) ||
|
|
t.isForOfStatement(candidate)
|
|
)
|
|
return candidate;
|
|
if (isPatternOwnerDeclaration(candidate)) return candidate;
|
|
}
|
|
return null;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/** True when `parent` places `node` in the bound slot of `pattern`. */
|
|
const bindsIdentifier = (
|
|
pattern: t.Node,
|
|
parent: t.Node,
|
|
node: t.Identifier,
|
|
): boolean => {
|
|
// The identifier sits directly in the pattern: `[b]`, `({b})`, `{b = 1}`.
|
|
if (parent === pattern) return patternContains(pattern, node);
|
|
if (t.isObjectPattern(pattern))
|
|
return (
|
|
t.isObjectProperty(parent) &&
|
|
(parent.value === node || (parent.shorthand && parent.key === node))
|
|
);
|
|
if (t.isArrayPattern(pattern))
|
|
return pattern.elements.some((element) => element === parent);
|
|
if (t.isAssignmentPattern(pattern)) return pattern.left === parent;
|
|
return t.isRestElement(pattern) && pattern.argument === parent;
|
|
};
|
|
|
|
const isPatternNode = (node: t.Node): boolean =>
|
|
t.isObjectPattern(node) ||
|
|
t.isArrayPattern(node) ||
|
|
t.isAssignmentPattern(node) ||
|
|
t.isRestElement(node);
|
|
|
|
const isDeleteSite = (site: t.Node): boolean =>
|
|
t.isUnaryExpression(site, { operator: "delete" });
|
|
|
|
const isPatternOwnerDeclaration = (node: t.Node): boolean =>
|
|
t.isTSParameterProperty(node) ||
|
|
(t.isVariableDeclarator(node) && t.isPattern(node.id)) ||
|
|
(t.isFunction(node) &&
|
|
node.params.some((value): value is t.Pattern => t.isPattern(value))) ||
|
|
(t.isCatchClause(node) && node.param !== null && t.isPattern(node.param));
|
|
|
|
/** Classify one identifier occurrence consistently with semantic references. */
|
|
export const semanticReferenceRole = (
|
|
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) ||
|
|
((t.isForInStatement(parent) || t.isForOfStatement(parent)) &&
|
|
parent.left === node)
|
|
)
|
|
return "write";
|
|
if (t.isExportSpecifier(parent) && parent.local === node) return "export";
|
|
return "read";
|
|
};
|
|
|
|
const isDeclarationIdentifier = (
|
|
node: t.Identifier,
|
|
parent: t.Node,
|
|
): boolean => {
|
|
if (t.isTSImportEqualsDeclaration(parent)) return parent.id === node;
|
|
return (
|
|
(t.isTSParameterProperty(parent) &&
|
|
patternContains(parent.parameter, node)) ||
|
|
(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)) ||
|
|
isNoncomputedClassMemberKey(node, parent) ||
|
|
t.isPrivateName(parent) ||
|
|
(t.isExportSpecifier(parent) && parent.exported === node) ||
|
|
t.isLabeledStatement(parent) ||
|
|
t.isBreakStatement(parent) ||
|
|
t.isContinueStatement(parent);
|
|
|
|
/** A noncomputed class member name is a property key, never a lexical read. */
|
|
const isNoncomputedClassMemberKey = (
|
|
node: t.Identifier,
|
|
parent: t.Node,
|
|
): boolean =>
|
|
(t.isClassMethod(parent) ||
|
|
t.isClassProperty(parent) ||
|
|
t.isTSDeclareMethod(parent)) &&
|
|
parent.key === node &&
|
|
!parent.computed;
|
|
|
|
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.isTSParameterProperty(pattern) &&
|
|
patternContains(pattern.parameter, 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];
|
|
if (
|
|
(t.isTSInterfaceDeclaration(declaration) ||
|
|
t.isTSTypeAliasDeclaration(declaration) ||
|
|
t.isTSDeclareFunction(declaration)) &&
|
|
t.isIdentifier(declaration.id)
|
|
)
|
|
return [declaration.id.name];
|
|
if (
|
|
t.isTSImportEqualsDeclaration(declaration) &&
|
|
semanticImportEqualsSpecifier(declaration) !== null
|
|
)
|
|
return [declaration.id.name];
|
|
return [];
|
|
};
|
|
|
|
const defaultExportLocalName = (
|
|
declaration: t.ExportDefaultDeclaration["declaration"],
|
|
): string | null => {
|
|
const value = unwrapJavaScriptExpression(declaration).node;
|
|
if (t.isIdentifier(value)) return value.name;
|
|
return (t.isFunctionDeclaration(value) || t.isClassDeclaration(value)) &&
|
|
t.isIdentifier(value.id)
|
|
? value.id.name
|
|
: null;
|
|
};
|