Files
rea/src/domain/referenceSourceImportParsing.ts
T
Hemang VarshneyandHemang Varshney 4842f23765 fix(source): retain CommonJS requires spelled as exact no-substitution templates (#4511)
* fix(source): retain CommonJS requires spelled as exact no-substitution templates

Admit expression-free template arguments at the CommonJS require gate and
read their single cooked quasi through the shared exact-literal reader.
Scope is the require path only: static ESM template declarations keep their
parse failures and dynamic import() keeps the explicit unknown/partial
<dynamic-import> relationship with no invented target.

Refs: #4382

* fix(source): admit TypeScript-wrapped exact require arguments via the shared literal reader

Address review: the require gate matched the outer AST node, so
runtime-transparent TS wrappers (as const, !, satisfies) around exact
strings or expression-free templates were still silently omitted even
though readExactJavaScriptLiteral unwraps them. Remove the gate and let
the shared exact-literal reader own argument exactness; computed
arguments remain silent and dynamic import() behavior is unchanged.

Refs: #4382

---------

Co-authored-by: Hemang Varshney <218273197+hemv-857@users.noreply.github.com>
2026-10-22 06:12:42 +08:00

406 lines
12 KiB
TypeScript

import type {
CallExpression,
File,
ImportExpression,
Node,
StringLiteral,
} from "@babel/types";
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
import { readExactJavaScriptLiteral } from "./javascript/javascriptAstValues.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 (expression === null || expression === undefined) return undefined;
const literal = readExactJavaScriptLiteral(expression);
if (literal.found && typeof literal.value === "string")
return { specifier: literal.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[],
limitations: string[],
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;
if (input.to.length === 0) {
limitations.push(
`Empty module specifier in ${input.fromPath} was not recorded as a relationship.`,
);
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[],
limitations: string[],
): void => {
for (const statement of body) {
if (isImportDeclaration(statement)) {
appendRelationship(relationships, limitations, {
fromPath: from_path,
to: safeModuleName(statement.source),
kind: "imports",
parseState: "parsed",
});
continue;
}
if (isExportNamedDeclaration(statement)) {
if (statement.source !== null && statement.source !== undefined) {
appendRelationship(relationships, limitations, {
fromPath: from_path,
to: safeModuleName(statement.source),
kind: "imports",
parseState: "parsed",
});
}
continue;
}
if (isExportAllDeclaration(statement)) {
appendRelationship(relationships, limitations, {
fromPath: from_path,
to: safeModuleName(statement.source),
kind: "imports",
parseState: "parsed",
});
continue;
}
if (isTSImportEqualsDeclaration(statement)) {
const reference = statement.moduleReference;
if (isTSExternalModuleReference(reference)) {
appendRelationship(relationships, limitations, {
fromPath: from_path,
to: safeModuleName(reference.expression),
kind: "requires",
parseState: "parsed",
});
}
continue;
}
if (isTSModuleDeclaration(statement)) {
if (statement.declare === true && isStringLiteral(statement.id)) {
appendRelationship(relationships, limitations, {
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[],
limitations: string[],
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, limitations, {
fromPath: from_path,
to: specifier ?? "<dynamic-import>",
kind: "imports",
parseState: specifier === undefined ? "partial" : "parsed",
...(specifier === undefined ? { resolution: "unknown" } : {}),
});
continue;
}
const first = call.arguments[0];
// The shared exact-literal reader owns argument exactness: it unwraps
// runtime-transparent TS wrappers (`as const`, `!`, `satisfies`) and
// admits quoted and expression-free template spellings alike.
if (isRequireCallee(call.callee) && unshadowedRequireCalls.has(call)) {
const result = moduleSpecifierFromExpression(first);
if (result !== undefined) {
appendRelationship(relationships, limitations, {
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, limitations, {
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, limitations);
extractRequireAndDynamicImports(
ast.program.body,
path,
relationships,
limitations,
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,
};
};