fix(csharp): resolve a type reference to the type, not a same-named property (#3034) - #3037
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).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes C# type references binding to same-named members: _build_csharp_type_def_index now filters candidates to _callable_class-stamped type declarations, so a DbSet<Widget> Widget property no longer wins the (namespace, name) bucket and steal every Widget type reference while the class keeps only its own contains edge. Tie-breaks in the new _type_def_sort_key now parse the L<line> location into an integer so first-declaration ordering holds across the L5/L12 boundary instead of sorting lexically.
No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1037 functions depend on the 48 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 494 callers, 42 callees - new:
_rebuild_code()— 98 callers, 50 callees - new:
dispatch_command()— 2 callers, 122 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
watch()— 5 callers, 7 callees - new:
_build()— 7 callers, 3 callees - new:
_resolve_csharp_type_references()— 2 callers, 10 callees - new:
main()— 1 callers, 6 callees - …and 6 more — each is listed as a finding
Verification — 1037 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: 467 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_build\_csharp\_type\_def\_index.
The verifier did not have enough to check \_build\_csharp\_type\_def\_index, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
· 14 more finding(s) on lines outside this diff (see the check run).
…roperty (Graphify-Labs#3034) _build_csharp_type_def_index documents itself as an index of "C# type definitions", but its filter accepted any code node whose label is a bare identifier. A property clears every one of those checks, so `public DbSet<Widget> Widget { get; set; }` put a PROPERTY named Widget into the same (namespace, name) bucket as the CLASS Widget. Whichever of the two sorted first then became the resolution target for every cross-file `Widget` type reference. With the DbContext in its own file -- the normal EF layout -- the property sorts first on filename and wins, so parameter_type, generic_arg and return_type references all bound to the property and the class node was left holding only its own file's `contains` edge. `affected` on the type then reported nothing. Filter the index on `_callable_class`, which is stamped on every C# type declaration (class, interface, enum, record, struct, static and abstract) and on no property or method. The property node and its `defines/field` edge are untouched; only its claim to be a type definition goes away. Second, smaller correction in the same function: the tie-break sorted source_location as a string, so "L12" ranked above "L5" and "first declaration wins" was false for any pair straddling line 10. Parse the line number, keeping the raw string and id as stable secondary keys. This is separable from the fix above -- happy to drop it if you would rather take it on its own. Measured on a private ~90k-node C# graph, querying the type declaration node directly, production files only, against `git grep -w`. Four EF entity types, each sharing its name with a DbSet property: entity A 0% -> 36% (returned nothing at all before) entity B 11% -> 23% entity C 11% -> 33% entity D 8% -> 9% two interfaces, unchanged at 97% and 100% Interfaces were already resolving correctly and are unaffected; the gain is entirely on types that share a name with a DbSet property. The corpus also loses ~5,250 phantom stub nodes, since references now land on the real declaration instead of minting a sourceless placeholder. Not fixed here: `new Widget { ... }` still leaves its `calls` edge on a sourceless stub. That edge carries no metadata distinguishing construction from a method call, so repointing it needs a change at the emit site rather than in this resolver -- a larger change that seemed worth separating. Full suite before and after: identical set of 25 pre-existing failures (terraform/skillgen/ollama, unrelated), 4673 -> 4678 passed, the five new tests.
dd28d47 to
d8082ab
Compare
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# type reference resolution so a DbSet<Widget> Widget property no longer hijacks references meant for the Widget class: _build_csharp_type_def_index now indexes only nodes carrying _callable_class, the discriminator stamped on every type declaration kind (class, interface, enum, record, struct, static, abstract) and never on members. Also replaces the lexical tie-break with _type_def_sort_key, which parses the L<n> source location into an integer so same-name types resolve to the earliest-declared one instead of ranking L12 above L5.
No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1037 functions depend on the 48 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 494 callers, 42 callees - new:
_rebuild_code()— 98 callers, 50 callees - new:
dispatch_command()— 2 callers, 122 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
watch()— 5 callers, 7 callees - new:
_build()— 7 callers, 3 callees - new:
_resolve_csharp_type_references()— 2 callers, 10 callees - new:
main()— 1 callers, 6 callees - …and 6 more — each is listed as a finding
Verification — 1037 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: 467 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_build\_csharp\_type\_def\_index.
The verifier did not have enough to check \_build\_csharp\_type\_def\_index, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
· 14 more finding(s) on lines outside this diff (see the check run).
Fixes #3034.
The bug
_build_csharp_type_def_indexdescribes itself as an index of "C# type definitions", but its filter accepts any code node whose label is a bare identifier — it rules out namespaces, nested types, method labels (endswith(")")), dotted and.-prefixed labels, and nothing else. A property clears every one of those checks.So
public DbSet<Widget> Widget { get; set; }puts a property namedWidgetinto the same(namespace, name)bucket as the classWidget, and whichever sorts first becomes the resolution target for every cross-fileWidgettype reference.With the DbContext in its own file — the normal EF layout — the property sorts first on filename and wins:
The class node is left holding only its own file's
containsedge, sographify affected <the class>reports nothing.There is a second-order effect worth noting: when the two happen to sort the other way the index sees two candidates and the reference is left dangling on a sourceless stub instead. Same user-visible outcome — the class has no inbound references — reached by a different path, which is why the symptom looked inconsistent depending on whether the scan path was relative or absolute.
The fix
Filter the index on
_callable_class. It is stamped on every C# type declaration and on no member — I pinned the whole set rather than assuming:_callable_classTrueThe property node and its
defines/fieldedge are untouched; only its claim to be a type definition goes away.Second, smaller correction in the same function. The tie-break sorted
source_locationas a string, so"L12"ranked above"L5"and "first declaration wins" was false for any pair straddling line 10. It now parses the line number, keeping the raw string and id as stable secondary keys. This is independent of the fix above and easy to drop if you would rather take it separately — though note it is also what made the single-file version of the repro appear to work, by accidentally ordering the class ahead of the property.Tests
Five, in
tests/test_csharp_type_vs_member_resolution.py:definesedge_callable_classwould silently break any type kind that lacks itI checked they actually fail without the change:
A first version of the primary test put both declarations in one file and passed even with the fix reverted — the numeric-sort change alone rescued it. Splitting the files is what makes it a real guard.
Verification
Full suite, before and after: the same set of 25 pre-existing failures (terraform, skillgen, ollama — all unrelated, and identical by test name), 4673 → 4678 passed, the difference being the five new tests.
ruff checkclean under the committed lint selection.Measured on a private ~90k-node C# graph, querying the type declaration node directly, production files only, against
git grep -w. Four EF entity types, each sharing its name with a DbSet property, plus two interfaces as controls:Interfaces were already resolving correctly and are unchanged — the gain is entirely on types sharing a name with a DbSet property, which in an EF codebase is every entity. The corpus also sheds ~5,250 phantom stub nodes, since references now land on the real declaration instead of minting a placeholder.
(The graph is of a private codebase, so the type names are elided; the shapes are all the
DbSet<T> Tcollision the tests reproduce.)Deliberately not fixed here
new Widget { ... }still leaves itscallsedge on a sourceless stub. That edge carries no metadata distinguishing construction from a method call, so repointing it safely means stamping the emit site rather than changing this resolver — a bigger change touching the shared config-driven call path, which seemed worth separating from a resolution fix. Happy to follow up if you'd like it in the same place.The numbers above stop well short of grep parity for the same reason, plus
db.Widgetmember access emitting no edge at all (noted in #3034). This PR fixes attribution, not coverage.