fix(reference-imports): retain exported import-equals dependencies

Route exported external import-equals declarations through the same relationship owner as plain declarations so their required target is not dropped.

Fixes #6655
This commit is contained in:
morluto
2026-10-23 19:18:09 +08:00
parent 16e64d58ff
commit 9200adea61
4 changed files with 180 additions and 12 deletions
+8 -2
View File
@@ -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
+28 -10
View File
@@ -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;
}
@@ -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: "<dynamic-import>",
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);
},
);
@@ -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);
});