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(); 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: [] }); };