fix(javascript): omit self-referential import edges with disclosure (#1130)

* fix(javascript): omit self-referential import edges with disclosure

A specifier that resolves back to the importing module itself produced an
imports edge from a module to itself, which the application graph schema
forbids; one such specifier failed result validation and lost the whole
analysis. Skip those edges at composition time and disclose the omission
count in the graph limitations.

Fixes #1129

* test(javascript): verify self-import behavior through the compiled CLI

---------

Co-authored-by: BaaRai <baarai@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
BaaRai
2026-10-09 01:10:38 +02:00
committed by GitHub
parent c1d0002470
commit 1d7115b7b4
3 changed files with 180 additions and 7 deletions
@@ -129,27 +129,40 @@ export const addJavaScriptSourceModules = (
}
};
/** Relationship compositions the application graph cannot represent, for disclosure. */
export interface JavaScriptModuleRelationshipOmissions {
/** Import specifiers that resolved back to the importing module itself. */
readonly selfImports: number;
}
type RelationshipOmissionCounter = { selfImports: number };
/** Compose bounded CommonJS and ESM binding relationships across artifact files. */
export const addJavaScriptModuleRelationships = (
context: JavaScriptArtifactGraphContext,
): void => {
): JavaScriptModuleRelationshipOmissions => {
const omissions = { selfImports: 0 };
for (const analyzed of context.analysis.files) {
const { file, semantic } = analyzed;
const source = context.sourceModuleNodes.get(file.path);
if (semantic === null || source === undefined) continue;
for (const link of semantic.ir.moduleLinks) {
const input = { context, file, semantic, source, link };
if (isExportLink(link)) addExportRelationship(input);
if (isExportLink(link)) addExportRelationship(input, omissions);
else if (link.specifier !== null)
addImportRelationship(input, source, {
addImportRelationship(input, source, omissions, {
specifier: link.specifier,
importedPath: link.importedName === null ? [] : [link.importedName],
});
}
}
return omissions;
};
const addExportRelationship = (input: RelationshipInput): void => {
const addExportRelationship = (
input: RelationshipInput,
omissions: RelationshipOmissionCounter,
): void => {
const { context, file, semantic, source, link } = input;
if (link.exportedName === null) return;
const baseCoverage = semanticCoverage(semantic);
@@ -213,15 +226,25 @@ const addExportRelationship = (input: RelationshipInput): void => {
evidence: relationshipEvidence(input, "expose-module-export", []),
});
const origin = moduleOriginForExport(semantic.ir, link);
if (origin !== null) addImportRelationship(input, exported, origin);
if (origin !== null)
addImportRelationship(input, exported, omissions, origin);
};
const addImportRelationship = (
input: RelationshipInput,
source: ApplicationNode,
omissions: RelationshipOmissionCounter,
origin: JavaScriptModuleOrigin,
): void => {
const target = resolveModuleTarget(input, origin.specifier);
// A specifier that resolves back to the importing module itself (a literal
// self-import, self-require, or artifact-confined path alias)
// cannot become an edge: the application graph forbids self-referential
// edges, and emitting one fails result validation for the whole analysis.
if (target.node.node_id === source.node_id) {
omissions.selfImports += 1;
return;
}
input.context.accumulator.addEdge({
source_node_id: source.node_id,
target_node_id: target.node.node_id,