import type { CallExpression, File, ImportExpression, Node, StringLiteral, } from "@babel/types"; import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js"; import { parseRecoveredJavaScript, parserPluginsForPath, } from "./javascript/javascriptSourceParser.js"; import { unshadowedReferenceSourceRequireCalls } from "./referenceSourceRequireBindings.js"; import { isRelativeJavaScriptModuleSpecifier } from "./javascript/javascriptModuleSpecifier.js"; import { isCallExpression, isExportAllDeclaration, isExportNamedDeclaration, isIdentifier, isImport, isImportDeclaration, isImportExpression, isMemberExpression, isStringLiteral, isTSExternalModuleReference, isTSImportEqualsDeclaration, isTSModuleDeclaration, } from "@babel/types"; type ReferenceSourceImportKind = | "imports" | "requires" | "references" | "declares-module"; type ReferenceSourceImportResolution = | "internal" | "external" | "unresolved" | "unknown"; type ReferenceSourceImportParseState = "parsed" | "partial" | "unknown"; interface ReferenceSourceImportRelationship { readonly from_path: string; readonly to: string; readonly kind: ReferenceSourceImportKind; readonly resolution: ReferenceSourceImportResolution; readonly parse_state: ReferenceSourceImportParseState; } interface ReferenceSourceParseFailure { readonly path: string; readonly parser: string; readonly reason: string; } export interface ReferenceSourceImportParseResult { readonly relationships: readonly ReferenceSourceImportRelationship[]; readonly parse_failures: readonly ReferenceSourceParseFailure[]; readonly limitations: readonly string[]; } export const PARSEABLE_REFERENCE_SOURCE_LANGUAGES: ReadonlySet = new Set(["JavaScript", "TypeScript", "JSX", "TSX"]); const safeModuleName = (node: Node | null | undefined): string | undefined => { if (isStringLiteral(node)) return node.value; return undefined; }; const resolveSpecifier = ( specifier: string, ): ReferenceSourceImportResolution => { if ( isRelativeJavaScriptModuleSpecifier(specifier) || specifier.startsWith("/") ) return "internal"; return "external"; }; const isModuleExpression = ( expression: Node | null | undefined, ): expression is StringLiteral | CallExpression => isStringLiteral(expression) || isCallExpression(expression); const moduleSpecifierFromExpression = ( expression: Node | null | undefined, ): | { specifier: string; parseState: ReferenceSourceImportParseState } | undefined => { if (isStringLiteral(expression)) return { specifier: expression.value, parseState: "parsed" }; if ( isCallExpression(expression) && isImport(expression.callee) && expression.arguments.length > 0 ) { const first = expression.arguments[0]; if (isStringLiteral(first)) { return { specifier: first.value, parseState: "parsed" }; } return { specifier: "", parseState: "partial" }; } return undefined; }; const appendRelationship = ( relationships: ReferenceSourceImportRelationship[], input: { readonly fromPath: string; readonly to: string | undefined; readonly kind: ReferenceSourceImportKind; readonly parseState: ReferenceSourceImportParseState; readonly resolution?: ReferenceSourceImportResolution; }, ): void => { if (input.to === undefined) return; relationships.push({ from_path: input.fromPath, to: input.to, kind: input.kind, resolution: input.resolution ?? resolveSpecifier(input.to), parse_state: input.parseState, }); }; const extractImportDeclarations = ( body: readonly Node[], from_path: string, relationships: ReferenceSourceImportRelationship[], ): void => { for (const statement of body) { if (isImportDeclaration(statement)) { appendRelationship(relationships, { fromPath: from_path, to: safeModuleName(statement.source), kind: "imports", parseState: "parsed", }); continue; } if (isExportNamedDeclaration(statement)) { if (statement.source !== null && statement.source !== undefined) { appendRelationship(relationships, { fromPath: from_path, to: safeModuleName(statement.source), kind: "imports", parseState: "parsed", }); } continue; } if (isExportAllDeclaration(statement)) { appendRelationship(relationships, { fromPath: from_path, to: safeModuleName(statement.source), kind: "imports", parseState: "parsed", }); continue; } if (isTSImportEqualsDeclaration(statement)) { const reference = statement.moduleReference; if (isTSExternalModuleReference(reference)) { appendRelationship(relationships, { fromPath: from_path, to: safeModuleName(reference.expression), kind: "requires", parseState: "parsed", }); } continue; } if (isTSModuleDeclaration(statement)) { if (statement.declare === true && isStringLiteral(statement.id)) { appendRelationship(relationships, { fromPath: from_path, to: statement.id.value, kind: "declares-module", parseState: "parsed", resolution: "unknown", }); } } } }; const isRequireCallee = (callee: Node | null | undefined): boolean => { if (isIdentifier(callee) && callee.name === "require") return true; if ( isMemberExpression(callee) && isIdentifier(callee.object) && callee.object.name === "require" && ((!callee.computed && isIdentifier(callee.property)) || isStringLiteral(callee.property)) ) { const propertyName = isIdentifier(callee.property) ? callee.property.name : isStringLiteral(callee.property) ? callee.property.value : undefined; return propertyName === "resolve" || propertyName === "main"; } return false; }; const extractRequireAndDynamicImports = ( body: readonly Node[], from_path: string, relationships: ReferenceSourceImportRelationship[], unshadowedRequireCalls: ReadonlySet, ): void => { // Single traversal owner: iterative, VISITOR_KEYS-gated, no recursion over // loc/comment objects. Survives deeply nested generated member chains that // overflowed the previous hand-rolled Object.values walker. const expressions: Array = []; for (const statement of body) traverseJavaScriptAst(statement, { enter: (node) => { if (isCallExpression(node) || isImportExpression(node)) expressions.push(node); }, }); for (const call of expressions) { if (isImportExpression(call)) { const specifier = safeModuleName(call.source); appendRelationship(relationships, { fromPath: from_path, to: specifier ?? "", kind: "imports", parseState: specifier === undefined ? "partial" : "parsed", ...(specifier === undefined ? { resolution: "unknown" } : {}), }); continue; } const first = call.arguments[0]; if ( isRequireCallee(call.callee) && unshadowedRequireCalls.has(call) && isModuleExpression(first) ) { const result = moduleSpecifierFromExpression(first); if (result !== undefined) { appendRelationship(relationships, { fromPath: from_path, to: result.specifier, kind: "requires", parseState: result.parseState, }); } continue; } if (isImport(call.callee) && isModuleExpression(first)) { const result = moduleSpecifierFromExpression(first); if (result !== undefined) { appendRelationship(relationships, { fromPath: from_path, to: result.specifier, kind: "imports", parseState: result.parseState, }); } } } }; const parseWithBabel = ( path: string, source: string, language: string | null, ): { ast: File | undefined; reasons: readonly string[]; moduleOnlyErrors: readonly string[]; } => { // Single parser-mode owner: path-driven plugins (dts/JSX/mts) shared with // the semantic pipeline. `language` only gates non-JS/TS callers upstream; // TypeScript syntax must parse identically here and in semantic analysis. void language; const plugins = parserPluginsForPath(path); try { const { file: ast, moduleOnlyErrors } = parseRecoveredJavaScript( source, path, { allowImportExportEverywhere: false, allowReturnOutsideFunction: true, allowUndeclaredExports: true, plugins, }, ); return { ast, reasons: ast.errors.map((error) => error.message), moduleOnlyErrors, }; } catch (cause: unknown) { return { ast: undefined, reasons: [cause instanceof Error ? cause.message : "Parse failed"], moduleOnlyErrors: [], }; } }; /** Parse ESM/CommonJS imports and dynamic imports from source bytes. */ export const parseReferenceSourceImports = ( path: string, bytes: Uint8Array, language: string | null, ): ReferenceSourceImportParseResult => { if (language !== null && !PARSEABLE_REFERENCE_SOURCE_LANGUAGES.has(language)) return { relationships: [], parse_failures: [], limitations: [] }; let source: string; try { source = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch { return { relationships: [], parse_failures: [ { path, parser: "utf-8", reason: "Source bytes are not valid UTF-8; import targets were not parsed.", }, ], limitations: [], }; } const { ast, reasons, moduleOnlyErrors } = parseWithBabel( path, source, language, ); const limitations = moduleOnlyErrors.length === 0 ? [] : [ `Strict-mode errors apply only if this file is an ES module: ${moduleOnlyErrors.join("; ")}`, ]; const parseFailures = reasons.map((reason) => ({ path, parser: "babel", reason, })); if (ast === undefined) { return { relationships: [], parse_failures: parseFailures, limitations, }; } const relationships: ReferenceSourceImportRelationship[] = []; extractImportDeclarations(ast.program.body, path, relationships); extractRequireAndDynamicImports( ast.program.body, path, relationships, unshadowedReferenceSourceRequireCalls(ast, path), ); return { relationships: relationships.map((relationship) => ({ ...relationship, parse_state: parseFailures.length > 0 && relationship.parse_state === "parsed" ? "partial" : relationship.parse_state, })), parse_failures: parseFailures, limitations, }; };