Skip to content

fix(csharp): resolve a type reference to the type, not a same-named property (#3034) - #3037

Open
brobl2008 wants to merge 1 commit into
Graphify-Labs:v8from
brobl2008:fix/csharp-type-vs-property-resolution
Open

fix(csharp): resolve a type reference to the type, not a same-named property (#3034)#3037
brobl2008 wants to merge 1 commit into
Graphify-Labs:v8from
brobl2008:fix/csharp-type-vs-property-resolution

Conversation

@brobl2008

@brobl2008 brobl2008 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #3034.

The bug

_build_csharp_type_def_index describes 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 named Widget into the same (namespace, name) bucket as the class Widget, and whichever sorts first becomes 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:

.Describe()  --references/parameter_type-->  Widget @ AppContext.cs:L5   (the property)

The class node is left holding only its own file's contains edge, so graphify 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_class
class, interface, enum, record, struct, static class, abstract class True
property, method, namespace, file absent

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. 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:

  • the DbSet repro, with the context in its own file so the property wins on filename — this is the one that fails without the fix
  • the property keeps its own node and its owner's defines edge
  • every type kind still resolves — class, interface, enum, record, struct, abstract, static. This is the guard that matters: filtering on _callable_class would silently break any type kind that lacks it
  • a direct unit test of the index, and one of the sort key

I checked they actually fail without the change:

FAILED test_type_reference_binds_to_the_type_not_a_same_named_property
-  widget_app_data_widget
+  appcontext_app_data_appcontext_widget

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 check clean 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:

Before After
entity A 0% 36%
entity B 11% 23%
entity C 11% 33%
entity D 8% 9%
interface A 97% 97%
interface B 100% 100%

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> T collision the tests reproduce.)

Deliberately 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 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.Widget member access emitting no edge at all (noted in #3034). This PR fixes attribution, not coverage.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@brobl2008
brobl2008 force-pushed the fix/csharp-type-vs-property-resolution branch from dd28d47 to d8082ab Compare August 25, 2026 12:24

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C#: type references resolve to a same-named property (DbSet<T>) instead of the type declaration

1 participant