fix(csharp): strip call-site type arguments from generic calls - #2676
fix(csharp): strip call-site type arguments from generic calls#2676rohit-jsfreaky wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This PR addresses C# call-site handling when explicit type arguments are present (e.g., X.M<T>(), Bag<Payload>.Make(), Local<Payload>()). It adds a new helper _csharp_name_without_type_args that strips the type-argument list from generic_name nodes, and updates _extract_generic to use it for callee names and receivers, while also recognizing generic_name as a valid receiver/function node type alongside identifier. The test file adds a new suite of cases covering explicit type arguments on type receivers, typed local receivers, constructed generic type receivers (split across files), unqualified/this generic calls, and a check that type-argument stripping doesn't bypass receiver typing. The changed surface is limited to the C# path in graphify/extractors/engine.py and the corresponding tests in tests/test_csharp_member_calls.py.
No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 650 functions depend on the 255 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
walk_calls()— 1 callers, 16 callees
Verification — 650 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 591 function(s) in the blast radius were not formally verified this run
· 1 more finding(s) on lines outside this diff (see the check run).
Fixes #2624.
Summary
A C# call written with explicit type arguments produced no
callsedge, while the same method called with an inferred type argument resolved fine — so the call-site syntax decided it, not the declaration.tree-sitter wraps a name carrying a type-argument list in a
generic_namenode. The C# call handler read that node verbatim, so the callee becameFetch<Payload>while its declaration is stored bare as.Fetch(); the lookup key never matched and the edge was dropped silently.A second, independent defect sat next to it: in
Bag<Payload>.Make()the receiver is ageneric_name, and a receiver was only captured when it was a plainidentifier— so it was dropped entirely and_resolve_csharp_member_callshad nothing to bind.Changes
_csharp_name_without_type_args(), returning the bare name of a call-site name node. It reads thenamefield first and falls back to the firstidentifierchild — the same order_csharp_collect_type_refsuses (the pinned grammar exposes nonamefield ongeneric_name; trying the field first keeps this working on versions that do).recv.M<T>(), to ageneric_namereceiver (Bag<Payload>.Make()), and to an unqualified call (Local<Payload>()), which previously fell through to the raw-text scan and kept the<...>.C# only — all three call sites sit inside the
tree_sitter_c_sharpbranch and the helper is used nowhere else. A node that is not ageneric_namereturns_read_textunchanged, so non-generic calls take exactly the path they did before.This does not loosen resolution. Stripping the type-argument list only makes the lookup key match; the call still goes through receiver typing and the god-node guard, so an ambiguous or untypable receiver still yields no edge rather than a wrong one. A qualified generic (
Demo.Bag<Payload>) has no bareidentifierchild and keeps its previous name minus the type arguments, so nothing that used to be recorded stops being recorded.Reproduction
The issue's five files,
graphify extract . --code-only --no-cluster:A() -> Registry.Fetch<Payload>()B() -> box.Read<Payload>()C() -> Registry.Has()(control)D() -> Registry.Pick(item)(control)E() -> Bag<Payload>.Make()2 of 5 edges before, 5 of 5 after. Two further shapes were broken by the same cause and are not in the report — both now covered:
Local<Payload>("k")(unqualified, no receiver) andthis.Local<Payload>("k").Tests
Five tests appended to
tests/test_csharp_member_calls.py. Each was confirmed to fail with the source change reverted and the tests kept, and to pass with it.One is a guard rather than a repro: a generic call on a typed field must still bind to that field's type and not to a same-named method on an unrelated class (#1609), now exercised through a generic call. The constructed-generic-receiver test is split across two files deliberately — in a single file the callee resolves by an in-file label match and never reaches receiver typing, so a same-file version passes even with the receiver dropped.
uv run --no-sync pytest tests/test_csharp_member_calls.py tests/test_csharp_type_resolution.py tests/test_csharp_partial_classes.py tests/test_dotnet.py -q— 126 passed.uv run --no-sync ruff check graphify/extractors/engine.py tests/test_csharp_member_calls.py— passed.Full suite: 22 failed, 4304 passed, 13 skipped, against a clean
v8baseline of 22 failed, 4299 passed, 13 skipped — the same 22 in both runs, all pre-existing and platform-sensitive on this Windows machine, none touching this path. The +5 are the new tests.graphify update .re-run per AGENTS.md: 765 files, 13,426 nodes, no errors.Tested against the pinned
tree_sitter_c_sharp0.23.5 on Windows; other grammar versions and platforms were not exercised.