Skip to content

fix(csharp): resolve a member access to the receiver type's property, so DbSet query sites link (#3528) - #3538

Open
Ayushraj06-bit wants to merge 1 commit into
Graphify-Labs:v8from
Ayushraj06-bit:fix/csharp-member-access-3528
Open

Ayushraj06-bit wants to merge 1 commit into
Graphify-Labs:v8from
Ayushraj06-bit:fix/csharp-member-access-3528

Conversation

@Ayushraj06-bit

Copy link
Copy Markdown

Fixes #3528.

The gap

An EF Core query site never names its table: db.Users.Where(...) reaches AspNetUsers through the DbSet<ApplicationUser> Users property, and only the property is in source. Users has been a node since #3006, but the only edge into it was its own type's defines. The C# invocation branch in walk_calls keeps a simple receiver only, so the chained db.Users was dropped, every db.<DbSet>.X() raw call arrived at _resolve_csharp_member_calls with receiver=None, and was skipped. On the reporter's corpus that is why explain "AppDbContext" shows 83 declaration edges and not one use.

Minimal repro (three files, --code-only): .CountOrders() containing db.Orders.Count() has zero outgoing edges; Users/Orders have exactly one inbound edge each.

The fix

recv.Prop now resolves the way recv.Method() has since #1609:

is_member_call: True is kept on access entries so every bare-name pass (shared cross-file, symbol_resolution) skips them exactly as it skips a receiver call; the C# resolver branches on is_member_access.

After the fix, explain "Users" on the repro lists .FindByEmail() L20, .CountOrders() L25, .AddUser() L30 — the "what code reads or writes this table" answer. ApplicationUser is one hop further via the declaration's existing references/generic_arg.

Measured

jasontaylordev/CleanArchitecture (76 .cs): 956 → 992 edges, node count unchanged. All 36 new edges are uses; 12 land on IApplicationDbContext.TodoItems/TodoLists from the command handlers and validators — every EF query site in the project — the rest are request.Title / _user.Id DTO reads on in-corpus types. No wrong binding on inspection.

Tests

tests/test_csharp_member_access.py, 19 tests. 11 fail without the change (the DbSet repro; access line/confidence; bare var x = db.Users and foreach; this.field.Prop; this.Prop and Type.Prop EXTRACTED; base.Prop; interface-typed receiver; inherited property; partial-class half; dedup; outer-chain/generic exclusion; raw-entry shape). 8 are guards that must stay green: untyped lambda parameter, out-of-corpus type (and not parked — flipping the guard fails this test), ambiguous type, field/method-group members, shadowing local poison, same-named C++ member, no dead raw entry for an untypable receiver.

Full suite: same 30 pre-existing Windows/env failures before and after (byte-identical set), 5580 → 5582 passed. ruff clean under the committed selection; tools.skillgen --check OK.

Alternatives considered

  • Gate on ORM vocabulary (DbSet, Model, Repository) — hard-codes framework names into a language extractor that has none, needs the property's declared type plumbed to the resolver, and loses the general answer (impact analysis on an entity column is a property read on the entity, not on the DbSet).
  • A node per call site, as the issue words it — this graph's model is nodes = declarations and call sites = edges carrying source_location; thousands of per-occurrence nodes would fight every id/dedup/explain assumption.
  • A separate resolver module like csharp_dispatch.py — would duplicate ~100 lines of receiver typing.

How to verify

pytest tests/test_csharp_member_access.py -q
graphify extract <any EF Core repo> --code-only --no-cluster && graphify explain "<a DbSet property name>"

Deliberately out of scope (issue items 2–4)

entity → table via [Table]/ToTable; explain preferring table vocabulary over a same-named local (a query/explain ranking change); DDL inside migrations (#2884's territory). Also left alone: making the property node carry its own generic_arg to the entity (would double every C# property's type edges; interacts with #3037), enum-member access, property-level interface dispatch, and unqualified Users.Add() inside the context itself.

One pre-existing wrong edge surfaced while reproducing, not touched here: a chained-receiver call (_context.TodoItems.Count()) falls through to the in-file bare-name match and binds Count to the enclosing method's own .Count() — a false self-recursion edge. Fixing it removes some currently-correct chained-receiver edges, so it wants its own PR/decision.

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


Graphify review — findings

Resolves a C# member access on a typed receiver (db.Users, this.Count, Config.Instance) to the receiver type's property node as a uses edge with member_access context, mirroring how recv.Method() resolves to a method, so ORM query sites behind a DbSet<T> property become reachable even when the table name never appears in source. Types the receiver through the same tier logic via a new property_index restricted to .cs-declared defines targets to avoid colliding with same-named C++ members, and skips parking for accesses since parked entries are cross-repo call candidates. Extracts the receiver-name logic into _csharp_member_receiver, which returns the bare name for identifiers, this/base, and this.field, and bails on any other chain.

No blocking issues surfaced. 5 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2404 functions depend on the 685 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 591 callers, 44 callees
  • new: _rebuild_code() — 116 callers, 51 callees
  • new: _extract_generic() — 18 callers, 26 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • …and 38 more — each is listed as a finding

Verification — 2404 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: 2229 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

276 of 276 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — impact, full-run-safety
  • tests/test_astro_import_ids.py — impact, full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — impact, full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — impact, full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — impact, full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — impact, full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — impact, full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — impact, full-run-safety
  • tests/test_cross_language_call_resolution.py — impact, full-run-safety
  • tests/test_cross_repo_member_calls.py — impact, full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — impact, full-run-safety
  • tests/test_csharp_enum_members.py — impact, full-run-safety
  • … and 226 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

· 46 more finding(s) on lines outside this diff (see the check run).

…raphify-Labs#3528)

`db.Users` inside `db.Users.Where(...)` was dropped: the invocation branch
keeps only a simple receiver, so every ORM query site behind a DbSet<T>
property reached _resolve_csharp_member_calls with receiver=None and the
property node (Graphify-Labs#3006) had no inbound edge but its own type's `defines`.

Record a non-call member_access_expression on a simple receiver (recv /
this / base / this.field / Type) as a raw_calls entry stamped
is_member_access, typed from the same scoped receiver table the member
calls use, and bind it in _resolve_csharp_member_calls through the same
this/base/Type/typed tiers to a property_index built from `defines`
edges to .cs nodes — a `uses` edge at the access line, EXTRACTED when the
type is named in source and INFERRED when the receiver is typed. Never
parked: parked entries are cross-repo call candidates (Graphify-Labs#3152).

The four receiver-capture arms of the invocation branch become
_csharp_member_receiver, shared by both sites.
@Ayushraj06-bit
Ayushraj06-bit force-pushed the fix/csharp-member-access-3528 branch from d4c9f77 to ab92f98 Compare September 13, 2026 18:40

@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

Adds C# member-access resolution so a typed receiver read like db.Users, this.Count, or Config.Instance binds to the receiver type's property node as a uses edge, mirroring how recv.Method() resolves to a method. This makes ORM query sites behind a DbSet<T> property reachable even though the table name never appears in source, since the property node is now the link. Property targets are restricted to .cs-declared members to keep bare-name-typed receivers from reaching same-named C++ data members, and member accesses are never parked (parked entries are cross-repo call candidates only). Refactors the shared type-on-bases walk into _member_on_type_or_bases, parameterized over method_index or the new property_index, and extracts C# receiver-name detection into _csharp_member_receiver.

No blocking issues surfaced. 4 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2404 functions depend on the 685 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 591 callers, 44 callees
  • new: _rebuild_code() — 116 callers, 51 callees
  • new: _extract_generic() — 18 callers, 26 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • …and 38 more — each is listed as a finding

Verification — 2404 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: 2229 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

276 of 276 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — impact, full-run-safety
  • tests/test_astro_import_ids.py — impact, full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — impact, full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — impact, full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — impact, full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — impact, full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — impact, full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — impact, full-run-safety
  • tests/test_cross_language_call_resolution.py — impact, full-run-safety
  • tests/test_cross_repo_member_calls.py — impact, full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — impact, full-run-safety
  • tests/test_csharp_enum_members.py — impact, full-run-safety
  • … and 226 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_resolve\_csharp\_member\_calls.

The verifier did not have enough to check \_resolve\_csharp\_member\_calls, 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

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

· 46 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.

ORM query call-sites are invisible: the table name is never in the source, so no SQL-literal fix can link them (EF Core / C#)

1 participant