fix(javascript): stop reporting self-defaulted receivers as module loads (#8109)

The CommonJS loader search treated any repeated state on its own ancestor
chain as an unknown loader. A self-default such as
`session.messages = session.messages || []` forms that cycle, because a
member read inside the assigned value was attributed to the later call's
read site and selected the same write again. Each `push` on the value was
then reported as an unresolved `require` load, and graph coverage became
partial.

Record a member read inside a flowed value as a depth-1 captured selection
at its own position, as member mutations already do for flowed origins.
Treat a revisited state as adding no source; the cycle's other entries are
still expanded once each. Remove the superseded ancestor tracking.

Fixes #8102

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-30 01:01:21 -07:00
1 parent 24398a89e4
commit 9d68aac76d
2 files changed
+110 -14

No files matched your search

@@ -970,16 +970,22 @@ const inheritLoaderSourceContext = (
),
}));
// A later read site selects only the remaining path. This member selects its
// own key when it is evaluated, so `o.k = o.k || d` reads o.k before the write.
const loaderMemberSource = (
current: ModuleObjectSource,
member: t.MemberExpression | t.OptionalMemberExpression,
): ModuleObjectSource => ({
node: member.object,
readAt: current.readAt ?? member,
capturedSelections: semanticShiftCapturedSelections(
current.capturedSelections,
1,
),
capturedSelections:
current.readAt === undefined
? semanticShiftCapturedSelections(current.capturedSelections, 1)
: semanticNormalizeCapturedSelections([
{ at: member, depth: 1 },
...(semanticShiftCapturedSelections(current.capturedSelections, 1) ??
[]),
]),
path: [
semanticStaticPropertyKey(member.property, member.computed),
...current.path,
@@ -1056,13 +1062,10 @@ const hasCommonJsModuleSource = (
readAt?: t.Node,
): boolean => {
type Work = ModuleObjectSource & {
readonly ancestors: ReadonlySet<object>;
/** Await occurs after this many requested outer property selections. */
readonly awaitDepths: readonly number[];
};
const pending: Work[] = [
{ node, path: [], readAt, ancestors: new Set(), awaitDepths: [] },
];
const pending: Work[] = [{ node, path: [], readAt, awaitDepths: [] }];
// Keep depths separate and join finite key alternatives at each source read.
const selections = new SemanticAliasSelections(() => {
state.valueResourceLimits.add("alias-selections");
@@ -1108,13 +1111,10 @@ const hasCommonJsModuleSource = (
identity = {};
context.identities.set(pathKey, identity);
}
if (next.ancestors.has(identity)) {
if (includeUnknown) return true;
continue;
}
// A revisited state adds no source. A cycle's other entries are expanded
// once each, so a cycle is not an unknown loader.
if (examined.has(identity)) continue;
examined.add(identity);
const ancestors = new Set([...next.ancestors, identity]);
const enqueueAt = (
depths: readonly number[],
...sources: ModuleObjectSource[]
@@ -1122,7 +1122,6 @@ const hasCommonJsModuleSource = (
pending.push(
...inheritLoaderSourceContext(next, sources).map((source) => ({
...source,
ancestors,
awaitDepths: depths,
})),
);