fix(javascript): retain compiler-dependent module resolution facts

Compare import and require outcomes for static TypeScript references when compiler options are unknown. Preserve branch facts through worker IR and both graph projections; retain declaration fallbacks as candidates without changing explicitly selected paths.

Validation: 210 focused resolver and semantic tests; 21 production-boundary and source-extension tests. A bounded 2,000-reference probe adds about 16–17 ms wall time for dual resolution; full task token costs and real compiled application behavior remain unmeasured.
This commit is contained in:
morluto
2026-10-13 15:58:25 +08:00
parent ded199abcd
commit c923fb0dc9
18 changed files with 503 additions and 100 deletions
@@ -12,7 +12,6 @@ import {
javascriptAnalysisCoverage,
linkElectronRoleToAsset,
moduleLookupKey,
resolveArtifactPath,
selfReferenceOmissions,
sourceNodeFor,
type JavaScriptArtifactGraphContext,
@@ -23,9 +22,11 @@ import {
staticInferenceEvidence,
} from "./JavaScriptArtifactGraphEvidence.js";
import {
moduleKindForJavaScriptReference,
artifactPathResolutionProperties,
resolveArtifactPathByContext,
type ArtifactPathResolution,
} from "./JavaScriptArtifactPathResolution.js";
import { moduleResolutionModeForJavaScriptReference } from "../../domain/javascript/javascriptModuleResolutionMode.js";
type StaticReference = JavaScriptStaticAnalysis["references"][number];
type StaticEndpoint = JavaScriptStaticAnalysis["endpoints"][number];
@@ -41,9 +42,12 @@ interface FindingInput<Value> {
}
interface ResolvedReference {
readonly path: string;
readonly file: JavaScriptArtifactFile;
readonly path: string | null;
readonly file: JavaScriptArtifactFile | null;
readonly node: ApplicationNode;
readonly limitations: readonly string[];
readonly resolutionStatus?: string;
readonly resolution?: ArtifactPathResolution;
}
/** Project imports, workers, roles, endpoints, storage, and source-map links. */
@@ -90,7 +94,7 @@ const addReference = (
const resolved =
input.value.specifier === null
? null
: resolveReference(context, input.file.path, input.value);
: resolveReference(context, input.file.path, input.value, input.coverage);
if (input.value.kind === "worker" || input.value.kind === "service-worker") {
const worker = context.accumulator.addNode({
kind: input.value.kind === "worker" ? "worker" : "service-worker",
@@ -126,7 +130,7 @@ const addReference = (
relation: "loads",
properties: { kind: input.value.kind },
});
if (resolved !== null)
if (resolved !== null && resolved.path !== null)
addStaticInferenceEdge(context, {
source: worker,
target: resolved.node,
@@ -135,6 +139,7 @@ const addReference = (
coverage: input.coverage,
relation: "maps_to",
properties: { resolved_path: resolved.path },
limitations: resolved.limitations,
});
return;
}
@@ -154,13 +159,25 @@ const addReference = (
file: input.file,
range: input.value.location,
coverage: input.coverage,
relation: resolved?.file.kind === "native-addon" ? "loads" : "imports",
relation: resolved?.file?.kind === "native-addon" ? "loads" : "imports",
properties: {
kind: input.value.kind,
specifier: input.value.specifier,
expression: input.value.expression,
resolution_mode:
input.value.kind === "static-import" ||
input.value.kind === "dynamic-import" ||
input.value.kind === "require"
? moduleResolutionModeForJavaScriptReference(
input.file.path,
input.value.kind,
)
: null,
resolved_path: resolved?.path ?? null,
resolution_status: resolved?.resolutionStatus ?? null,
...artifactPathResolutionProperties(resolved?.resolution),
},
...(resolved === null ? {} : { limitations: resolved.limitations }),
});
};
@@ -373,6 +390,7 @@ const resolveReference = (
context: JavaScriptArtifactGraphContext,
sourcePath: string,
reference: StaticReference,
coverage: JavaScriptArtifactGraphCoverage,
): ResolvedReference | null => {
const specifier = reference.specifier;
if (specifier === null) return null;
@@ -392,30 +410,70 @@ const resolveReference = (
const file = context.filesByPath.get(sourcePath);
return file === undefined
? null
: { path: `${sourcePath}#module:${specifier}`, file, node: module };
: {
path: `${sourcePath}#module:${specifier}`,
file,
node: module,
limitations: [],
};
}
// Worker scripts are URLs relative to the script, not module specifiers.
const path = isWorker
const resolution = isWorker
? resolveArtifactPathByContext({
declaredPath: specifier,
sourcePath,
context: "url-reference",
files: context.filesByPath,
}).resolved_path
: resolveArtifactPath(
specifier,
})
: resolveArtifactPathByContext({
declaredPath: specifier,
sourcePath,
context.filesByPath,
reference.kind === "require" ||
reference.kind === "static-import" ||
reference.kind === "dynamic-import"
? moduleKindForJavaScriptReference(sourcePath, reference.kind)
: undefined,
);
if (path === null) return null;
context: "module-specifier",
files: context.filesByPath,
...("require" === reference.kind ||
"static-import" === reference.kind ||
"dynamic-import" === reference.kind
? {
moduleKind: moduleResolutionModeForJavaScriptReference(
sourcePath,
reference.kind,
),
}
: {}),
});
const path = resolution.resolved_path;
if (path === null) {
if (isWorker) return null;
const source = context.filesByPath.get(sourcePath);
return source === undefined
? null
: {
path: null,
file: null,
node: unresolvedReferenceNode(
context,
source,
reference,
coverage,
resolution,
),
limitations: resolution.limitations,
resolutionStatus: resolution.resolution_status,
resolution,
};
}
const file = context.filesByPath.get(path);
const node = context.fileNodes.get(path);
return file === undefined || node === undefined ? null : { path, file, node };
return file === undefined || node === undefined
? null
: {
path,
file,
node,
limitations: resolution.limitations,
resolutionStatus: resolution.resolution_status,
resolution,
};
};
const resolveBundlerChunkReference = (
@@ -453,7 +511,7 @@ const resolveBundlerChunkReference = (
const node = unique[0];
return node === undefined
? null
: { path: `${sourcePath}#chunk:${key}`, file, node };
: { path: `${sourcePath}#chunk:${key}`, file, node, limitations: [] };
};
const unresolvedReferenceNode = (
@@ -461,6 +519,7 @@ const unresolvedReferenceNode = (
file: JavaScriptArtifactFile,
reference: StaticReference,
coverage: JavaScriptArtifactGraphCoverage,
resolution?: ArtifactPathResolution,
): ApplicationNode =>
context.accumulator.addNode({
kind: "javascript-module",
@@ -477,6 +536,10 @@ const unresolvedReferenceNode = (
specifier: reference.specifier,
expression: reference.expression,
resolution: "unresolved",
...(resolution === undefined
? {}
: { resolution_status: resolution.resolution_status }),
...artifactPathResolutionProperties(resolution),
},
evidence: staticInferenceEvidence({
sha256: file.sha256,
@@ -487,6 +550,7 @@ const unresolvedReferenceNode = (
confidence: "low",
limitations: [
"The static reference could not be resolved within the inventoried artifact.",
...(resolution?.limitations ?? []),
],
}),
},