mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
55f0267c40
* fix(javascript): bind named class expressions in their class scope Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> * test(javascript): isolate class expression lexical names regression cases Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> --------- Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
463 lines
13 KiB
TypeScript
463 lines
13 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import {
|
|
failedJavaScriptSemanticIr,
|
|
type JavaScriptModuleOrigin,
|
|
type JavaScriptSemanticDefinition,
|
|
type JavaScriptSemanticIr,
|
|
} from "./javascriptSemanticIr.js";
|
|
import {
|
|
collectSemanticModuleLink,
|
|
collectSemanticCallable,
|
|
collectSemanticReferences,
|
|
immutableSemanticBindings,
|
|
immutableSemanticScopes,
|
|
semanticStaticPropertyName,
|
|
} from "./javascriptSemanticProjection.js";
|
|
import type {
|
|
JavaScriptSemanticAnalysisState,
|
|
JavaScriptSemanticBindingState,
|
|
JavaScriptSemanticScopeState,
|
|
} from "./javascriptSemanticState.js";
|
|
import {
|
|
currentSemanticScope,
|
|
resolveSemanticBindingFromScope,
|
|
semanticScopeId,
|
|
semanticVariableScope,
|
|
} from "./javascriptSemanticState.js";
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
import {
|
|
collectSemanticReturns,
|
|
resolveSemanticModuleCallables,
|
|
} from "./javascriptSemanticReturns.js";
|
|
import { collectJavaScriptDerivedSemantics } from "./javascriptSemanticDerivedAnalysis.js";
|
|
import { propertyName, range } from "./javascriptStaticAnalysisHelpers.js";
|
|
import { semanticCoverage } from "./javascriptSemanticCoverage.js";
|
|
import {
|
|
parseJavaScriptSource,
|
|
type ParsedJavaScriptSource,
|
|
} from "./javascriptSourceParser.js";
|
|
|
|
interface BindPatternInput {
|
|
readonly pattern: t.Node;
|
|
readonly initializer: t.Node | null;
|
|
readonly scope: JavaScriptSemanticScopeState;
|
|
readonly state: JavaScriptSemanticAnalysisState;
|
|
readonly mutable: boolean;
|
|
readonly kind?: JavaScriptSemanticDefinition["kind"];
|
|
readonly projection?: readonly (string | number | null)[];
|
|
}
|
|
|
|
interface AddBindingInput {
|
|
readonly state: JavaScriptSemanticAnalysisState;
|
|
readonly scope: JavaScriptSemanticScopeState;
|
|
readonly name: string;
|
|
readonly kind: JavaScriptSemanticDefinition["kind"];
|
|
readonly mutable: boolean;
|
|
readonly definitionNode: t.Node;
|
|
readonly initializer: t.Node | null;
|
|
readonly directOrigin?: JavaScriptModuleOrigin;
|
|
readonly projection?: readonly (string | number | null)[];
|
|
}
|
|
|
|
/** Recover lexical bindings, aliases, constants, and module links. */
|
|
export const analyzeJavaScriptSemantics = (
|
|
source: string,
|
|
): JavaScriptSemanticIr => {
|
|
const file = parseJavaScriptSource(source);
|
|
return file === null
|
|
? failedJavaScriptSemanticIr()
|
|
: analyzeParsedJavaScriptSemantics(file);
|
|
};
|
|
|
|
/** Recover semantics from an already parsed JavaScript artifact. */
|
|
export const analyzeParsedJavaScriptSemantics = (
|
|
file: ParsedJavaScriptSource,
|
|
): JavaScriptSemanticIr => {
|
|
const state = createState(file.program);
|
|
collectDefinitions(file.program, state);
|
|
traverseJavaScriptAst(file.program, {
|
|
enter: (node) => collectSemanticModuleLink(node, state),
|
|
});
|
|
const references = collectSemanticReferences(file.program, state);
|
|
const parserPartial = file.errors.length > 0;
|
|
const bindings = immutableSemanticBindings(state);
|
|
const callables = collectSemanticReturns(file.program, state, parserPartial);
|
|
const moduleLinks = resolveSemanticModuleCallables(state, callables);
|
|
const derived = collectJavaScriptDerivedSemantics(
|
|
file.program,
|
|
state,
|
|
callables,
|
|
parserPartial,
|
|
);
|
|
return {
|
|
schema: "JavaScriptSemanticIR",
|
|
scopes: immutableSemanticScopes(state),
|
|
bindings,
|
|
callables,
|
|
references,
|
|
moduleLinks,
|
|
...derived,
|
|
coverage: semanticCoverage(parserPartial),
|
|
limitations: [
|
|
...(parserPartial
|
|
? [
|
|
"The parser recovered from syntax errors; affected bindings are partial.",
|
|
]
|
|
: []),
|
|
"Values and aliases were recovered from inert syntax only; no JavaScript was executed.",
|
|
"Return sites include only direct callable returns; nested callable returns remain separate.",
|
|
"Local call, argument, return, and closure relations are static candidates and do not prove runtime invocation.",
|
|
"Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.",
|
|
"Cross-function mutation and dynamic property resolution remain unknown.",
|
|
],
|
|
};
|
|
};
|
|
|
|
const createState = (program: t.Program): JavaScriptSemanticAnalysisState => {
|
|
const root: JavaScriptSemanticScopeState = {
|
|
scopeId: semanticScopeId("program", program),
|
|
parentScopeId: null,
|
|
kind: "program",
|
|
location: range(program),
|
|
bindingsComplete: true,
|
|
bindings: new Map(),
|
|
};
|
|
return {
|
|
scopes: [root],
|
|
scopesById: new Map([[root.scopeId, root]]),
|
|
scopeByNode: new WeakMap([[program, root]]),
|
|
bindingsById: new Map(),
|
|
callables: [],
|
|
callableNodesById: new Map(),
|
|
moduleLinks: [],
|
|
};
|
|
};
|
|
|
|
const collectDefinitions = (
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
const stack: JavaScriptSemanticScopeState[] = [
|
|
currentSemanticScope(state.scopes),
|
|
];
|
|
const openedScopes = new WeakSet<t.Node>();
|
|
traverseJavaScriptAst(program, {
|
|
enter: (node, parent) => {
|
|
const parentScope = currentSemanticScope(stack);
|
|
bindOuterDeclaration(node, parentScope, state);
|
|
const nested = nestedScope(node, parent, parentScope, state);
|
|
if (nested !== undefined) {
|
|
stack.push(nested);
|
|
openedScopes.add(node);
|
|
}
|
|
const scope = currentSemanticScope(stack);
|
|
state.scopeByNode.set(node, scope);
|
|
collectSemanticCallable({
|
|
node,
|
|
parent,
|
|
containerScope: parentScope,
|
|
bodyScope: nested,
|
|
state,
|
|
});
|
|
bindInnerDeclaration(node, parent, scope, state);
|
|
},
|
|
exit: (node) => {
|
|
if (openedScopes.has(node)) stack.pop();
|
|
},
|
|
});
|
|
};
|
|
|
|
const bindOuterDeclaration = (
|
|
node: t.Node,
|
|
scope: JavaScriptSemanticScopeState,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
if (t.isFunctionDeclaration(node) && t.isIdentifier(node.id))
|
|
addBinding({
|
|
state,
|
|
scope,
|
|
name: node.id.name,
|
|
kind: "function",
|
|
mutable: false,
|
|
definitionNode: node.id,
|
|
initializer: node,
|
|
});
|
|
else if (t.isClassDeclaration(node) && t.isIdentifier(node.id))
|
|
addBinding({
|
|
state,
|
|
scope,
|
|
name: node.id.name,
|
|
kind: "class",
|
|
mutable: false,
|
|
definitionNode: node.id,
|
|
initializer: node,
|
|
});
|
|
};
|
|
|
|
const bindInnerDeclaration = (
|
|
node: t.Node,
|
|
parent: t.Node | null,
|
|
scope: JavaScriptSemanticScopeState,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
if (t.isImportDeclaration(node)) bindImports(node, scope, state);
|
|
else if (t.isVariableDeclarator(node))
|
|
bindPattern({
|
|
pattern: node.id,
|
|
initializer: node.init ?? null,
|
|
scope: semanticVariableScope(scope, parent, state),
|
|
state,
|
|
mutable:
|
|
parent !== null && t.isVariableDeclaration(parent)
|
|
? parent.kind !== "const"
|
|
: true,
|
|
});
|
|
else if (t.isClassExpression(node) && t.isIdentifier(node.id))
|
|
addBinding({
|
|
state,
|
|
scope,
|
|
name: node.id.name,
|
|
kind: "class",
|
|
mutable: false,
|
|
definitionNode: node.id,
|
|
initializer: node,
|
|
});
|
|
else if (t.isFunction(node)) bindFunctionLocals(node, scope, state);
|
|
else if (t.isCatchClause(node) && node.param != null)
|
|
bindPattern({
|
|
pattern: node.param,
|
|
initializer: null,
|
|
scope,
|
|
state,
|
|
mutable: true,
|
|
kind: "catch",
|
|
});
|
|
else if (t.isAssignmentExpression(node) && t.isIdentifier(node.left))
|
|
addAssignment(node.left, node.right, scope, state);
|
|
};
|
|
|
|
const nestedScope = (
|
|
node: t.Node,
|
|
parent: t.Node | null,
|
|
parentScope: JavaScriptSemanticScopeState,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptSemanticScopeState | undefined => {
|
|
const kind = scopeKind(node, parent);
|
|
if (kind === undefined) return undefined;
|
|
const scope: JavaScriptSemanticScopeState = {
|
|
scopeId: semanticScopeId(kind, node),
|
|
parentScopeId: parentScope.scopeId,
|
|
kind,
|
|
location: range(node),
|
|
bindingsComplete: true,
|
|
bindings: new Map(),
|
|
};
|
|
state.scopes.push(scope);
|
|
state.scopesById.set(scope.scopeId, scope);
|
|
return scope;
|
|
};
|
|
|
|
const scopeKind = (
|
|
node: t.Node,
|
|
parent: t.Node | null,
|
|
): JavaScriptSemanticScopeState["kind"] | undefined => {
|
|
if (t.isFunction(node)) return "function";
|
|
if (t.isClass(node)) return "class";
|
|
if (t.isCatchClause(node)) return "catch";
|
|
if (
|
|
t.isBlockStatement(node) &&
|
|
!(parent !== null && t.isFunction(parent) && parent.body === node)
|
|
)
|
|
return "block";
|
|
return undefined;
|
|
};
|
|
|
|
const bindImports = (
|
|
node: t.ImportDeclaration,
|
|
scope: JavaScriptSemanticScopeState,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
for (const specifier of node.specifiers) {
|
|
const importedPath = t.isImportDefaultSpecifier(specifier)
|
|
? ["default"]
|
|
: t.isImportNamespaceSpecifier(specifier)
|
|
? []
|
|
: [propertyName(specifier.imported) || "[dynamic]"];
|
|
addBinding({
|
|
state,
|
|
scope,
|
|
name: specifier.local.name,
|
|
kind: "import",
|
|
mutable: false,
|
|
definitionNode: specifier.local,
|
|
initializer: null,
|
|
directOrigin: { specifier: node.source.value, importedPath },
|
|
});
|
|
}
|
|
};
|
|
|
|
const bindFunctionLocals = (
|
|
node: t.Function,
|
|
scope: JavaScriptSemanticScopeState,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
if (t.isFunctionExpression(node) && t.isIdentifier(node.id))
|
|
addBinding({
|
|
state,
|
|
scope,
|
|
name: node.id.name,
|
|
kind: "function",
|
|
mutable: false,
|
|
definitionNode: node.id,
|
|
initializer: node,
|
|
});
|
|
for (const parameter of node.params)
|
|
bindPattern({
|
|
pattern: t.isTSParameterProperty(parameter)
|
|
? parameter.parameter
|
|
: parameter,
|
|
initializer: null,
|
|
scope,
|
|
state,
|
|
mutable: true,
|
|
kind: "parameter",
|
|
});
|
|
};
|
|
|
|
const bindPattern = (input: BindPatternInput): void => {
|
|
const {
|
|
pattern,
|
|
initializer,
|
|
scope,
|
|
state,
|
|
mutable,
|
|
kind = "variable",
|
|
projection = [],
|
|
} = input;
|
|
if (t.isTSParameterProperty(pattern)) {
|
|
bindPattern({ ...input, pattern: pattern.parameter });
|
|
return;
|
|
}
|
|
if (t.isIdentifier(pattern)) {
|
|
addBinding({
|
|
state,
|
|
scope,
|
|
name: pattern.name,
|
|
kind,
|
|
mutable,
|
|
definitionNode: pattern,
|
|
initializer,
|
|
projection,
|
|
});
|
|
return;
|
|
}
|
|
if (t.isAssignmentPattern(pattern)) {
|
|
bindPattern({
|
|
...input,
|
|
pattern: pattern.left,
|
|
initializer: initializer ?? pattern.right,
|
|
});
|
|
return;
|
|
}
|
|
if (t.isRestElement(pattern)) {
|
|
bindPattern({
|
|
...input,
|
|
pattern: pattern.argument,
|
|
initializer: null,
|
|
mutable: true,
|
|
});
|
|
return;
|
|
}
|
|
if (t.isObjectPattern(pattern))
|
|
for (const property of pattern.properties) {
|
|
if (t.isRestElement(property))
|
|
bindPattern({
|
|
...input,
|
|
pattern: property.argument,
|
|
initializer: null,
|
|
mutable: true,
|
|
projection: [],
|
|
});
|
|
else {
|
|
const name = semanticStaticPropertyName(
|
|
property.key,
|
|
property.computed,
|
|
);
|
|
bindPattern({
|
|
...input,
|
|
pattern: property.value,
|
|
projection: [...projection, name === "" ? null : name],
|
|
});
|
|
}
|
|
}
|
|
else if (t.isArrayPattern(pattern))
|
|
pattern.elements.forEach((element, index) => {
|
|
if (element !== null)
|
|
bindPattern({
|
|
...input,
|
|
pattern: element,
|
|
projection: [...projection, index],
|
|
});
|
|
});
|
|
};
|
|
|
|
const addBinding = (input: AddBindingInput): void => {
|
|
const {
|
|
state,
|
|
scope,
|
|
name,
|
|
kind,
|
|
mutable,
|
|
definitionNode,
|
|
initializer,
|
|
directOrigin,
|
|
projection = [],
|
|
} = input;
|
|
let binding = scope.bindings.get(name);
|
|
if (binding === undefined) {
|
|
if (!scope.bindingsComplete) return;
|
|
binding = createBinding(scope, name, kind, mutable);
|
|
scope.bindings.set(name, binding);
|
|
state.bindingsById.set(binding.bindingId, binding);
|
|
}
|
|
binding.mutable ||= mutable;
|
|
binding.definitions.push({ kind, location: range(definitionNode) });
|
|
if (initializer !== null)
|
|
binding.initializers.push({ node: initializer, projection });
|
|
if (directOrigin !== undefined) binding.directOrigins.push(directOrigin);
|
|
};
|
|
|
|
const createBinding = (
|
|
scope: JavaScriptSemanticScopeState,
|
|
name: string,
|
|
kind: JavaScriptSemanticDefinition["kind"],
|
|
mutable: boolean,
|
|
): JavaScriptSemanticBindingState => ({
|
|
bindingId: `${scope.scopeId}:binding:${encodeURIComponent(name)}`,
|
|
scopeId: scope.scopeId,
|
|
name,
|
|
kind,
|
|
mutable,
|
|
definitions: [],
|
|
initializers: [],
|
|
directOrigins: [],
|
|
});
|
|
|
|
const addAssignment = (
|
|
identifier: t.Identifier,
|
|
initializer: t.Expression,
|
|
scope: JavaScriptSemanticScopeState,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
const binding = resolveSemanticBindingFromScope(
|
|
scope,
|
|
identifier.name,
|
|
state,
|
|
);
|
|
if (binding === undefined) return;
|
|
binding.mutable = true;
|
|
binding.definitions.push({ kind: "assignment", location: range(identifier) });
|
|
binding.initializers.push({ node: initializer, projection: [] });
|
|
};
|