fix(csharp): resolve a member access to the receiver type's property, so DbSet query sites link (#3528) - #3538
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
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-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— impact, full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— impact, full-run-safetytests/test_cpp_objc_cross_file_calls.py— impact, full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— impact, full-run-safetytests/test_cross_repo_member_calls.py— impact, full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— impact, full-run-safetytests/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.
d4c9f77 to
ab92f98
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
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-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— impact, full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— impact, full-run-safetytests/test_cpp_objc_cross_file_calls.py— impact, full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— impact, full-run-safetytests/test_cross_repo_member_calls.py— impact, full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— impact, full-run-safetytests/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).
Fixes #3528.
The gap
An EF Core query site never names its table:
db.Users.Where(...)reachesAspNetUsersthrough theDbSet<ApplicationUser> Usersproperty, and only the property is in source.Usershas been a node since #3006, but the only edge into it was its own type'sdefines. The C# invocation branch inwalk_callskeeps a simple receiver only, so the chaineddb.Userswas dropped, everydb.<DbSet>.X()raw call arrived at_resolve_csharp_member_callswithreceiver=None, and was skipped. On the reporter's corpus that is whyexplain "AppDbContext"shows 83 declaration edges and not one use.Minimal repro (three files,
--code-only):.CountOrders()containingdb.Orders.Count()has zero outgoing edges;Users/Ordershave exactly one inbound edge each.The fix
recv.Propnow resolves the wayrecv.Method()has since #1609:member_access_expressionthat is not an invocation's callee, with an identifier member and a simple receiver (recv/this/base/this.field/Type), is recorded as araw_callsentry stampedis_member_access, typed by the same scoped receiver table (C#: file-scoped receiver typing drops truecallsedges in unrelated methods (regression in 0.9.27) #2299/C#: an untypeable declaration poisons an already-typed receiver name within the same method #2472). Only entries the resolver can act on are recorded (typed receiver,this/base, or a type name) sinceraw_callsride the AST cache. The four receiver-capture arms of the invocation branch became_csharp_member_receiver, shared by both sites — behaviour-identical._resolve_csharp_member_callsgains aproperty_index(fromdefinesedges whose target is a.csnode) besidemethod_index;_method_on_type_or_basestakes the index as a parameter. An access goes through the identical typing tiers (this→ enclosing type,base→ single base,Type.→ named, typed → INFERRED), the sameCsharpNameResolverscoping, base-chain poison and god-node guard, then binds to the property asuses/context=member_access, EXTRACTED 1.0 or INFERRED 0.8, at the access line. It is never parked (merge-graphs / global add can never produce cross-repo member-call edges: the resolved receiver type is discarded at bail-out instead of persisted #3152): parked entries are cross-repo call candidates.is_member_call: Trueis 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 onis_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.ApplicationUseris one hop further via the declaration's existingreferences/generic_arg.Measured
jasontaylordev/CleanArchitecture (76
.cs): 956 → 992 edges, node count unchanged. All 36 new edges areuses; 12 land onIApplicationDbContext.TodoItems/TodoListsfrom the command handlers and validators — every EF query site in the project — the rest arerequest.Title/_user.IdDTO 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; barevar x = db.Usersandforeach;this.field.Prop;this.PropandType.PropEXTRACTED;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.
ruffclean under the committed selection;tools.skillgen --checkOK.Alternatives considered
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).source_location; thousands of per-occurrence nodes would fight every id/dedup/explain assumption.csharp_dispatch.py— would duplicate ~100 lines of receiver typing.How to verify
Deliberately out of scope (issue items 2–4)
entity → tablevia[Table]/ToTable;explainpreferring table vocabulary over a same-named local (aquery/explainranking change); DDL inside migrations (#2884's territory). Also left alone: making the property node carry its owngeneric_argto the entity (would double every C# property's type edges; interacts with #3037), enum-member access, property-level interface dispatch, and unqualifiedUsers.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 bindsCountto 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.