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 2 commits into
Graphify-Labs:v8from
Ayushraj06-bit:fix/csharp-member-access-3528
Open

Ayushraj06-bit wants to merge 2 commits 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).

@Ayushraj06-bit

Ayushraj06-bit commented Sep 19, 2026

Copy link
Copy Markdown
Author

Follow-up commit pushed (5f090fa). Nothing changes in the graph output — verified byte-identical nodes/edges on jasontaylordev/CleanArchitecture (110 .cs) and jellyfin/jellyfin (2,208 .cs) before and after, on both this base and current v8 (0.9.64).

Changelog. The entry was written under ## 0.9.61 (unreleased); merged into today's v8 it lands inside the released 0.9.61 section — the same shape 9335cee had to clean up. Dropped the hunk; happy to leave the wording for the bump.

Perf (jellyfin: +26.6k access entries on top of 85.6k member calls, +6,347 uses edges):

  • engine.py — the access branch asked tree-sitter for node.parent (twice) to tell an invocation's callee from a property read. Node.parent re-descends from the root each call: ~700 ms over the corpus's 137k member_access_expression nodes vs ~13 ms for a set lookup. The walk is pre-order, so the invocation branch now records the callee node's id in a per-file csharp_callee_ids set and the access branch tests membership. Per-file extraction overhead over base: +1.2–1.6 s → +0.7–0.8 s. A new raw-entry test pins the callee/access split (db.Save(); db.Log(db.Users); db.Users.Add(db.Orders.First()) → accesses Orders, Users, Users; calls Add, First, Log, Save).
  • extract.py_resolve_type_name_nid is memoized per (type name, caller, file); a method reads and calls the same receiver many times over and every access is one more entry that types it (80k → 40k scope-chain walks). The defines scan that builds property_index is folded into the existing method scan — one pass over all_edges fills both twin indexes. _resolve_csharp_member_calls: 430 ms → 288 ms (base 250 ms).

Also checked against current v8: the branch rebases cleanly, full suite there has the same pre-existing Windows/env failure set as v8 itself, 20/20 in tests/test_csharp_member_access.py, ruff clean, tools.skillgen --check OK. Happy to rebase if you'd prefer that over "Update branch".

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

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds C# member-access resolution: raw db.Users/this.Count accesses stamped is_member_access are typed by the same tier logic as calls and bound to the receiver type's property node via a uses/member_access edge, making ORM query sites behind DbSet<T> properties reachable. Builds property_index from defines edges restricted to .cs targets (so bare-name fallback can't reach a same-named C++ member), generalizes the method lookup into _member_on_type_or_bases, and skips parking for accesses since only cross-repo call candidates get parked. Memoizes per-caller type-name resolution and records invocation callee ids in csharp_callee_ids to distinguish call receivers from property reads without re-walking the tree.

Worth a look

  • caller_nid may be undefined for member-access branchgraphify/extractors/engine.py:6115 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2199 functions depend on the 480 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 — 2199 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit b9cd957 (diverged from this PR's base — delta is approximate).

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

Test selection

Test selection

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

  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_access.py — impact, changed-test
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/test_indirect_call_arrow_single_param_shadow.py — impact
  • tests/test_indirect_call_catch_binding_shadow.py — impact
  • tests/test_indirect_call_external_import_shadow.py — impact
  • tests/test_indirect_call_for_of_binding_shadow.py — impact
  • tests/test_indirect_call_function_expression_shadow.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_inferred_confidence_rubric.py — impact
  • tests/test_inherited_field_receivers.py — impact
  • tests/test_issue_3405_python_resolution.py — impact
  • tests/test_java_member_calls.py — impact
  • … and 59 more

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: no capturable inputs from the test suite; property tier: 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: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

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

…hangelog hunk (Graphify-Labs#3528)

Measured on jellyfin/jellyfin (2,208 .cs files, +26.6k access entries on top
of 85.6k member calls, +6,347 `uses` edges), graph output byte-identical
before and after:

- engine.py: the member-access branch asked tree-sitter for `node.parent`
  twice per member_access_expression to tell a callee (`db.Users.Where`)
  from a property read (`db.Users`); `Node.parent` re-descends from the
  root on every call (~700ms over the corpus's 137k such nodes vs ~13ms
  for a set lookup). The walk is pre-order, so the invocation branch now
  records the callee node's id in a per-file `csharp_callee_ids` set and
  the access branch tests membership instead. Per-file extraction goes
  from +1.2-1.6s over base to +0.7-0.8s.

- extract.py: `_resolve_type_name_nid` memoizes per (type_name, caller,
  file) — a method reads and calls the same receiver many times over, and
  every access is one more entry that types it (80k -> 40k scope-chain
  walks on the corpus). The `defines` scan that builds property_index is
  folded into the existing `method` scan: one pass over all_edges fills
  both twin indexes. `_resolve_csharp_member_calls` goes from 430ms to
  288ms (base: 250ms).

- CHANGELOG.md: the entry was written under `## 0.9.61 (unreleased)`; after
  rebasing onto v8 it lands inside the released 0.9.61 section. Release
  notes here are written at the version bump (see 9335cee), so the hunk is
  dropped rather than moved.
@Ayushraj06-bit
Ayushraj06-bit force-pushed the fix/csharp-member-access-3528 branch from fd5b241 to 5f090fa Compare September 19, 2026 17:28

@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. 2 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_id\_prefixes changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_id\_prefixes behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"source\_file":"'h\\u00e9llo w\\u00f6rld'"\}, the old code produced \{'héllo\_wörld'\} but the new code produces \{'h\_llo\_w\_rld'\}. Paste that input straight into a regression test.

Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_claude\_pretooluse\_hooks behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"strict":"True","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search', 'timeout': 10\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'grap… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar…. Paste that input straight into a regression test.


Graphify review — findings

Adds C# member-access resolution to the graph: raw accesses stamped is_member_access (db.Users, this.Count) are typed by the same receiver tiers as calls and bound to the receiver type's property node via a uses/member_access edge, making ORM query sites behind DbSet<T> properties reachable even though the table name never appears in source. Generalizes the resolver so _member_on_type_or_bases walks the inherits chain against either method_index or a new property_index (restricted to .cs-declared defines targets so a bare-name receiver can't hit a same-named C++ member), and never parks accesses since parked entries are cross-repo call candidates only. Adds _csharp_member_receiver to extract simple receivers (identifier, this/base, this.field) while bailing on complex chains, memoizes type-name resolution per caller, and tracks invocation callee node ids to distinguish a call's callee member-access from a plain property read without re-walking the tree.

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

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2199 functions depend on the 480 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 — 2199 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: 2024 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

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

  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_access.py — impact, changed-test
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/test_indirect_call_arrow_single_param_shadow.py — impact
  • tests/test_indirect_call_catch_binding_shadow.py — impact
  • tests/test_indirect_call_external_import_shadow.py — impact
  • tests/test_indirect_call_for_of_binding_shadow.py — impact
  • tests/test_indirect_call_function_expression_shadow.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_inferred_confidence_rubric.py — impact
  • tests/test_inherited_field_receivers.py — impact
  • tests/test_issue_3405_python_resolution.py — impact
  • tests/test_java_member_calls.py — impact
  • … and 59 more

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

Behavior changes: \_id\_prefixes changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_id\_prefixes behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"source\_file":"'h\\u00e9llo w\\u00f6rld'"\}, the old code produced \{'héllo\_wörld'\} but the new code produces \{'h\_llo\_w\_rld'\}. Paste that input straight into a regression test.

Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_claude\_pretooluse\_hooks behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"strict":"True","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search', 'timeout': 10\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'grap… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar…. Paste that input straight into a regression test.

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, 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: no capturable inputs from the test suite; property tier: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_merge.

The verifier did not have enough to check build\_merge, 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: no capturable inputs from the test suite; property tier: parameter `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify prefix\_graph\_for\_global.

The verifier did not have enough to check prefix\_graph\_for\_global, 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: no capturable inputs from the test suite; property tier: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous

Could not verify: Could not verify save\_semantic\_cache.

The verifier did not have enough to check save\_semantic\_cache, 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: no capturable inputs from the test suite; property tier: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, 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: no capturable inputs from the test suite; property tier: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in cohesion\_score (not a proof).

The verifier ran both versions of cohesion\_score on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in link\_cross\_repo\_member\_calls (not a proof).

The verifier ran both versions of link\_cross\_repo\_member\_calls on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_is\_prose\_note.

The verifier did not have enough to check \_is\_prose\_note, 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: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify diagnose\_extraction.

The verifier did not have enough to check diagnose\_extraction, 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: no capturable inputs from the test suite; property tier: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify format\_diagnostic\_report.

The verifier did not have enough to check format\_diagnostic\_report, 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: no capturable inputs from the test suite; property tier: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

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

The verifier did not have enough to check \_resolve\_cpp\_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: no capturable inputs from the test suite; property tier: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

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: no capturable inputs from the test suite; property tier: 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.

The verifier did not have enough to check extract, 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: no capturable inputs from the test suite; property tier: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify extract\_php.

The verifier did not have enough to check extract\_php, 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: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify extract\_elixir.

The verifier did not have enough to check extract\_elixir, 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: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

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: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_js\_extra\_walk.

The verifier did not have enough to check \_js\_extra\_walk, 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: no capturable inputs from the test suite; property tier: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_ruby\_extra\_walk.

The verifier did not have enough to check \_ruby\_extra\_walk, 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: no capturable inputs from the test suite; property tier: signature changed: no shared positional arity

Could not verify: Could not verify extract\_markdown.

The verifier did not have enough to check extract\_markdown, 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: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_resolve\_js\_import\_target (not a proof).

The verifier ran both versions of \_resolve\_js\_import\_target on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify extract\_rust.

The verifier did not have enough to check extract\_rust, 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: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify extract\_terraform.

The verifier did not have enough to check extract\_terraform, 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: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify generate.

The verifier did not have enough to check generate, 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: no capturable inputs from the test suite; property tier: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ValueError — names the real obstacle, not a sampling gap)

· 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