mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
c0d550b231
Distinguish call-form require from TypeScript import-equals before resolving inventory paths. Probe CommonJS files before indexes and honor package main without requiring an index. Retain selected manifest directory identities and reuse the path lookup owner for canonical ambiguity. Validated with source/reader tests and a public CLI fixture controlled by the pinned Node loader. Windows no-follow reader behavior remains unverified. Refs #3895, #3896, #3973, #4017. Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com> Co-authored-by: IssueHunter Worker <issuehunter@users.noreply.github.com>
394 lines
11 KiB
TypeScript
394 lines
11 KiB
TypeScript
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;
|
|
/** Distinguishes call-form CommonJS require from TS import-equals `requires`. */
|
|
readonly resolutionStrategy?: "runtime-require";
|
|
}
|
|
|
|
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<string> =
|
|
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: "<dynamic-import>", 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;
|
|
readonly resolutionStrategy?: "runtime-require";
|
|
},
|
|
): 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,
|
|
...(input.resolutionStrategy === undefined
|
|
? {}
|
|
: { resolutionStrategy: input.resolutionStrategy }),
|
|
});
|
|
};
|
|
|
|
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<CallExpression>,
|
|
): 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<CallExpression | ImportExpression> = [];
|
|
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 ?? "<dynamic-import>",
|
|
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,
|
|
resolutionStrategy: "runtime-require",
|
|
});
|
|
}
|
|
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,
|
|
};
|
|
};
|