diff --git a/docs/cli.md b/docs/cli.md index 6974ca6fe3..d052d8ba4a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -398,11 +398,17 @@ Entries that change during an import retain an unavailable-entry diagnostic alongside readable siblings. If enumeration of an already admitted root fails, the graph keeps its observed children and records the root failure as a top-level limitation. That partial inventory does not establish absence among -unexamined children. +unexamined children. POSIX directory-entry bytes that are not valid UTF-8 cannot +be represented as graph paths; the containing directory (or root) is marked +unsupported, and REA does not access the entry through a replacement-character +name. Readable siblings remain inventoried. JavaScript and TypeScript import parsing requires valid UTF-8. Malformed source bytes retain their original hashes and sizes with a decoding diagnostic; REA -does not infer module targets from replacement characters. +does not infer module targets from replacement characters. Exact dynamic-import +strings, expression-free templates and runtime-transparent TypeScript wrappers +retain their targets; computed imports remain unknown. TypeScript import-equals +dependencies remain `requires` relationships whether or not they are exported. Historical-source import requires safe no-follow file opens on Linux or macOS. Native Windows returns `unsupported_host`; use Linux REA inside WSL diff --git a/src/domain/referenceSourceImportParsing.ts b/src/domain/referenceSourceImportParsing.ts index ac7d90c622..6a9d9f845b 100644 --- a/src/domain/referenceSourceImportParsing.ts +++ b/src/domain/referenceSourceImportParsing.ts @@ -152,6 +152,23 @@ const appendRelationship = ( }); }; +const appendImportEquals = ( + statement: Node, + from_path: string, + relationships: ReferenceSourceImportRelationship[], + limitations: string[], +): void => { + if (!isTSImportEqualsDeclaration(statement)) return; + const reference = statement.moduleReference; + if (!isTSExternalModuleReference(reference)) return; + appendRelationship(relationships, limitations, { + fromPath: from_path, + to: safeModuleName(reference.expression), + kind: "requires", + parseState: "parsed", + }); +}; + const extractImportDeclarations = ( body: readonly Node[], from_path: string, @@ -177,7 +194,16 @@ const extractImportDeclarations = ( kind: "imports", parseState: "parsed", }); - } + } else if ( + statement.declaration !== null && + statement.declaration !== undefined + ) + appendImportEquals( + statement.declaration, + from_path, + relationships, + limitations, + ); continue; } @@ -192,15 +218,7 @@ const extractImportDeclarations = ( } if (isTSImportEqualsDeclaration(statement)) { - const reference = statement.moduleReference; - if (isTSExternalModuleReference(reference)) { - appendRelationship(relationships, limitations, { - fromPath: from_path, - to: safeModuleName(reference.expression), - kind: "requires", - parseState: "parsed", - }); - } + appendImportEquals(statement, from_path, relationships, limitations); continue; } diff --git a/tests/boundary/cli/referenceSourceExactImports.test.ts b/tests/boundary/cli/referenceSourceExactImports.test.ts new file mode 100644 index 0000000000..fee6d570fc --- /dev/null +++ b/tests/boundary/cli/referenceSourceExactImports.test.ts @@ -0,0 +1,93 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect } from "vitest"; + +import { historicalSourceGraphSchema } from "../../../src/domain/referenceSourceGraph.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest( + "preserves exact historical dynamic and exported import-equals edges", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-reference-exact-imports-"); + const config = await createTestTempDirectory( + "rea-reference-exact-imports-config-", + ); + await Promise.all([ + writeFile( + join(root, "main.ts"), + [ + "const templated = import(`./template.mjs`);", + 'const wrapped = import("./wrapped.mjs" as const);', + 'const name = "./not-guessed.mjs"; import(name);', + ].join("\n"), + ), + writeFile( + join(root, "exported.cts"), + 'export import dep = require("./dep.cjs");\n', + ), + writeFile( + join(root, "plain.cts"), + 'import dep = require("./dep.cjs"); export { dep };\n', + ), + ...["template.mjs", "wrapped.mjs"].map((path) => + writeFile(join(root, path), "export {};\n"), + ), + writeFile(join(root, "dep.cjs"), "module.exports = { value: 1 };\n"), + ]); + + const result = await cli.run({ + arguments: ["import-reference-source", root, "--json"], + environment: { + HOME: config, + XDG_CONFIG_HOME: config, + XDG_CACHE_HOME: config, + }, + }); + expect(result.exitCode, result.stderr + JSON.stringify(result.json)).toBe( + 0, + ); + const graph = historicalSourceGraphSchema.parse(result.json); + expect(graph.relationships).toEqual( + expect.arrayContaining([ + { + from_path: "main.ts", + to: "template.mjs", + kind: "imports", + resolution: "internal", + parse_state: "parsed", + }, + { + from_path: "main.ts", + to: "wrapped.mjs", + kind: "imports", + resolution: "internal", + parse_state: "parsed", + }, + { + from_path: "main.ts", + to: "", + kind: "imports", + resolution: "unknown", + parse_state: "partial", + }, + { + from_path: "exported.cts", + to: "dep.cjs", + kind: "requires", + resolution: "internal", + parse_state: "parsed", + }, + { + from_path: "plain.cts", + to: "dep.cjs", + kind: "requires", + resolution: "internal", + parse_state: "parsed", + }, + ]), + ); + expect(graph.relationships).toHaveLength(5); + }, +); diff --git a/tests/boundary/filesystem/referenceSourceExportedImportEquals.test.ts b/tests/boundary/filesystem/referenceSourceExportedImportEquals.test.ts new file mode 100644 index 0000000000..be9d91d19d --- /dev/null +++ b/tests/boundary/filesystem/referenceSourceExportedImportEquals.test.ts @@ -0,0 +1,51 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect, it } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { importReferenceSource } from "../../support/referenceSourceResourceScope.js"; + +it("retains external-module targets from exported and plain import-equals declarations", async () => { + const root = await createTestTempDirectory( + "rea-reference-exported-import-equals-", + ); + await Promise.all([ + writeFile( + join(root, "exported.cts"), + 'export import dep = require("./dep.cjs");\n', + ), + writeFile( + join(root, "plain.cts"), + 'import dep = require("./dep.cjs"); export { dep };\n', + ), + writeFile(join(root, "dep.cjs"), "module.exports = { value: 1 };\n"), + ]); + + const result = await importReferenceSource({ + root, + caller: "reference-exported-import-equals-test", + policy: { secretPatterns: [] }, + }); + if (!result.ok) throw result.error; + expect(result.value.parse_failures).toEqual([]); + expect(result.value.relationships).toEqual( + expect.arrayContaining([ + { + from_path: "exported.cts", + to: "dep.cjs", + kind: "requires", + resolution: "internal", + parse_state: "parsed", + }, + { + from_path: "plain.cts", + to: "dep.cjs", + kind: "requires", + resolution: "internal", + parse_state: "parsed", + }, + ]), + ); + expect(result.value.relationships).toHaveLength(2); +});