fix(csharp): walk generic type arguments in field position - #2913
fix(csharp): walk generic type arguments in field position#2913brobl2008 wants to merge 1 commit into
Conversation
The field_declaration handler read only the outer type name via _read_csharp_type_name and emitted a single references edge, so the inner argument of `Box<Widget>` was never linked. Properties, return types and parameters already walk the whole type expression through _csharp_collect_type_refs, which left fields the odd one out -- the property handler's own comment even points at the Java/PHP/Kotlin siblings it was mirroring, and the tree_sitter_java field_declaration handler directly below does the same thing correctly. Two very common shapes lose their edge as a result: a stored dependency such as `IDbContextFactory<SomeContext>` loses SomeContext, and `Mock<IThing>` loses IThing across an entire test suite. Classic constructor injection stores its dependency in a field, so this is the same blind spot Graphify-Labs#2829 removed from primary constructors, just moved to where the dependency is kept. The loss is silent: `affected` returns a smaller, confident answer rather than an error. The fix routes the field handler through _csharp_collect_type_refs with the in-scope type parameters as the skip set, mirroring the property handler exactly. The outer type still emits context="field"; arguments emit context="generic_arg". This also stops fields fabricating nodes for predefined types -- `private string _s` previously created a `string` node, because _read_csharp_type_name returns builtins while _csharp_collect_type_refs returns early on predefined_type. That matches the builtin-not-fabricated behaviour added for primary constructors in 1eb356c. Eight regression tests cover both directions: the generic argument is linked, a field and a property of the same type now agree, nested arguments resolve, and type parameters and builtins are still never fabricated. Five of the eight fail on unpatched HEAD. Full suite: 30 failures before, 25 after, and a set-difference of the failing test ids shows nothing newly broken -- the delta is exactly the five new tests. test_labeling's batching test is flaky independently of this change (it fails intermittently on unpatched HEAD too). Scoped deliberately to field position. The call-site half of Graphify-Labs#2911 (`services.AddScoped<IThing, Thing>()`) lives in the call-resolution path and overlaps Graphify-Labs#2676, so it is better as its own change. Refs Graphify-Labs#2911.
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).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes C# field references in _extract_generic to walk the full type expression via _csharp_collect_type_refs instead of reading only the outer type name, so generic arguments like the SomeContext in IDbContextFactory<SomeContext> now get linked (with generic_arg context) the same way properties, return types, and parameters already do. Type parameters and predefined types are excluded from node creation. Adds tests/test_csharp_field_generic_args.py covering nested generics, multiple declarators, field/property parity, and non-fabrication of builtins and type params.
No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 627 functions depend on the 216 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_extract_generic()— 18 callers, 24 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_objc()— 27 callers, 9 callees - new:
extract_js()— 80 callers, 3 callees - new:
extract_julia()— 16 callers, 7 callees - new:
extract_cpp()— 27 callers, 3 callees - new:
extract_vue()— 10 callers, 6 callees - new:
walk()— 1 callers, 56 callees - …and 8 more — each is listed as a finding
Verification — 627 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: 567 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 16 more finding(s) on lines outside this diff (see the check run).
Fixes the field half of #2911.
Problem
The
field_declarationhandler reads only the outer type name via_read_csharp_type_nameand emits a singlereferencesedge, so the inner argument ofBox<Widget>is never linked.Properties, return types and parameters already walk the whole type expression through
_csharp_collect_type_refs. That makes fields the odd one out — and theproperty_declarationhandler's own comment points at exactly the siblings it was mirroring:The
tree_sitter_javafield_declarationhandler immediately below does the same thing correctly. C# fields were simply never converted.references[generic_arg]references[generic_arg]references[generic_arg]references[generic_arg]Why it matters
Two very common shapes lose their edge:
Classic constructor injection stores its dependency in a field, so this is the same blind spot #2829 removed from primary constructors — just relocated to where the dependency is kept. Mock-based unit tests lose their subject across an entire suite.
The failure is silent.
affectedreturns a smaller, confident answer rather than an error or a warning, which is the same reason #2829 was worth fixing.Fix
Route the field handler through
_csharp_collect_type_refs, passing the in-scope type parameters as the skip set, mirroring the property handler exactly. The outer type still emitscontext="field"; arguments emitcontext="generic_arg". The existingcsharp_field_typesreceiver registration is untouched.Bonus: this also stops fields fabricating nodes for predefined types.
private string _spreviously created astringnode, because_read_csharp_type_namereturns builtins while_csharp_collect_type_refsreturns early onpredefined_type. That aligns fields with the builtin-not-fabricated behaviour added for primary constructors in 1eb356c.Verification
Eight regression tests in
tests/test_csharp_field_generic_args.py, covering both directions — the argument is linked, a field and a property of the same type now agree, nested arguments resolve, and type parameters and builtins are still never fabricated.-k csharp)A set-difference of the failing test ids between the two full runs shows nothing newly broken — the delta is exactly the five new tests.
One caveat stated plainly:
test_labeling.py::test_label_communities_batches_when_over_batch_sizeis flaky independently of this change. It fails intermittently on unpatched HEAD too (confirmed over repeated runs), so please don't read it either way.Scope
Deliberately field-only. #2911 also covers call-site type arguments (
services.AddScoped<IThing, Thing>()), but that lives in the call-resolution path and overlaps #2676, which strips call-site type arguments to repair the callee match. The two want different things from the same tokens — one discards them, the other needs them emitted — so that half is better as its own change once #2676 settles. Happy to write it.