A C# method called through a static factory or fluent chain — `Foo.Create().Bar()`, `JObject.Parse(s).Property(...)`, `Instant.FromUtc(...).InZone(zone)` — lost the receiver's type, so the chained method didn't resolve and the call was invisible to callers/impact/trace. Ports the #645/#608 mechanism to C# (additive, like Java #751): - Part 1: capture C# return types in the extractor, reading the `returns` field (`static Foo Create()` -> `Foo`); predefined/array/generic/nullable/namespaced types are normalized or skipped. - Part 2: encode a chained `member_access_expression` receiver (`Foo.Create(args).Bar()`) as `inner().Bar` with normalized empty parens, so factory calls that take arguments still split. Non-chained member calls keep their existing `recv.Method` text. - Part 3: resolve via the shared matchDottedCallChain (now Java/Kotlin/C#), validated by resolveMethodOnType so a wrong inference yields NO edge. Known limitation (safe): C# extension-method chains don't resolve, since the method lives on the extension class, not the receiver's type — no edge, never a wrong one. Validated: synthetic decoy + args + absent-method safety tests; full suite green; real-repo A/B on Newtonsoft.Json (945 .cs: +3, 0 lost) and nodatime (488 .cs: +73, 0 lost) — node count identical (no explosion), 0 edges lost, precision spot-checked verbatim (Instant.FromUtc().InZone(), Offset.FromHoursAndMinutes().Plus(), OffsetDateTimePattern.CreateWithInvariantCulture().WithTwoDigitYearMax()). EXTRACTION_VERSION 7 -> 8. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3e04650850
commit
aa07dc59d4
@@ -2336,4 +2336,70 @@ class Caller {
|
||||
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C# chained static-factory call resolution (#645/#608 mechanism)', () => {
|
||||
function callerNamesOf(qualifiedName: string): string[] {
|
||||
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
|
||||
if (!target) return [];
|
||||
const names = cg
|
||||
.getIncomingEdges(target.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.source)?.name)
|
||||
.filter((n): n is string => !!n);
|
||||
return [...new Set(names)].sort();
|
||||
}
|
||||
|
||||
it('resolves Foo.Create().Bar() via the factory return type, never a same-named decoy', async () => {
|
||||
// Aaa sorts first and has a same-named Bar() — it must never win the chain.
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'Main.cs'),
|
||||
`class Aaa { void Bar() {} }
|
||||
class Foo {
|
||||
static Foo Create() { return new Foo(); }
|
||||
void Bar() {}
|
||||
}
|
||||
class Caller {
|
||||
void Run() { Foo.Create().Bar(); }
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Foo::Bar')).toEqual(['Run']);
|
||||
expect(callerNamesOf('Aaa::Bar')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a factory chain that passes arguments — Foo.Make(cfg).Build()', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'Main.cs'),
|
||||
`class Config {}
|
||||
class Foo {
|
||||
static Foo Make(Config c) { return new Foo(); }
|
||||
void Build() {}
|
||||
}
|
||||
class Caller {
|
||||
void Run() { Foo.Make(new Config()).Build(); }
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Foo::Build')).toEqual(['Run']);
|
||||
});
|
||||
|
||||
it('creates NO edge when the factory return type lacks the method (silent miss, not a wrong edge)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'Main.cs'),
|
||||
`class Foo {
|
||||
static Foo Create() { return new Foo(); }
|
||||
}
|
||||
class Other { void OnlyOther() {} }
|
||||
class Caller {
|
||||
void Run() { Foo.Create().OnlyOther(); }
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// Foo has no OnlyOther() — must not mis-attach to the same-named Other::OnlyOther.
|
||||
expect(callerNamesOf('Other::OnlyOther')).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user