Skip to content

fix(csharp): resolve null-conditional member calls (#3797) - #3976

Closed
Cintu07 wants to merge 1 commit into
Graphify-Labs:v8from
Cintu07:fix/csharp-null-conditional-calls
Closed

Cintu07 wants to merge 1 commit into
Graphify-Labs:v8from
Cintu07:fix/csharp-null-conditional-calls

Conversation

@Cintu07

@Cintu07 Cintu07 commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

what does this pr do?

fixes the ?. case from #3797. _window?.Refresh() parses as a conditional_access_expression, not a member_access_expression, so the c# invocation branch fell through to the raw text fallback. that split "_window?.Refresh" on the dot and set the receiver to "_window?", which no receiver table can type, so the call was dropped and never parked as unresolved. _window.Refresh() on the same field resolved fine.

added _csharp_member_call_parts, which returns the name and receiver nodes for both shapes, and the existing receiver logic runs on it unchanged. the call-site generic arg walk from #2911 had the same member_access_expression check, so recv?.Do() lost its references edges too. it uses the helper now.

the cast receiver and the partial class call from #3797 are separate fixes and not in here.

type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

verification & invariants

recv?.M() resolves exactly like recv.M(): same receiver typing, same god-node guard, INFERRED. an untypable receiver still gives no edge, and chained receivers like _h?.Inner?.Save() produce the same output as before. with Window and Panel both declaring Refresh(), _window?.Refresh() lands on Window.Refresh only.

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

how was this tested?

windows 11 arm64, python 3.14, venv from uv sync --all-extras.

python -m pytest tests/test_csharp_member_calls.py tests/test_csharp_call_site_generic_args.py tests/test_csharp_generic_callsites.py    53 passed, the 4 new ones fail on v8
python -m pytest tests/test_csharp*.py tests/test_languages.py tests/test_multilang.py    682 passed, 1 failed (test_markdown_wikilink_fallback_unicode_normalization, fails on v8 too, cp1252 locale)
python -m ruff check .                                          all checks passed
python -m pyright graphify/extractors/engine.py                 34 errors, the same 34 as v8
before
nothing leaves Editor.Redraw, Editor.RedrawThis or Editor.Lookup
after
.Redraw()     --calls/INFERRED-->         Window.Refresh()   L13
.RedrawThis() --calls/INFERRED-->         Window.Refresh()   L18
.Lookup()     --calls/INFERRED-->         Registry.Get()     L23
.Lookup()     --references/generic_arg--> Widget             L23
Changed?.Invoke(...) on an event field still produces nothing

graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments.
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

_window?.Refresh() parses as a conditional_access_expression, not a
member_access_expression, so the invocation branch fell through to the
raw-text fallback. That split "_window?.Refresh" on the dot and recorded
the receiver as "_window?", which no receiver table types, so the call
was dropped with no unresolved entry.

Read both shapes through one helper that returns the name and receiver
nodes, and use it for the call-site generic-argument walk as well, so
recv?.Do<T>() keeps its references edges. The cast and partial-class
receivers from Graphify-Labs#3797 are separate and not touched here.
@Cintu07
Cintu07 requested a review from safishamsi as a code owner October 1, 2026 18:26
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Thanks for the pull request, @Cintu07. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@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

Fixes C# null-conditional calls like _s?.Save() and this._s?.Save() being dropped. A new _csharp_member_call_parts reads the receiver and method name from both member_access_expression and conditional_access_expression, so ?. calls resolve through the receiver-type table like plain . calls. Previously the raw-text fallback typed the receiver as _s?, which matched nothing. The same helper lets r?.Do<T>() emit its generic_arg reference to T.

No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 809 functions depend on the 329 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _extract_generic() — 18 callers, 29 callees
  • new: extract_js() — 87 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: extract_julia() — 19 callers, 7 callees
  • new: extract_cpp() — 31 callers, 3 callees
  • new: extract_vue() — 10 callers, 7 callees
  • new: walk() — 1 callers, 66 callees
  • …and 9 more — each is listed as a finding

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

Test selection

Test selection

27 of 312 test file(s) selected (9%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_build.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact, changed-test
  • tests/test_csharp_member_calls.py — impact, changed-test
  • tests/test_dotnet.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_php_closures.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_indirect_call_block_scoped_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_js_exported_scalar_bindings.py — impact
  • tests/test_languages.py — impact
  • tests/test_multilang.py — impact
  • tests/test_python_underscore_resolution.py — impact
  • tests/test_rationale.py — impact
  • tests/test_ruby_resolution.py — impact
  • tests/test_scala_self_type.py — impact
  • tests/test_swift_computed_properties.py — impact
  • tests/test_swift_protocol_requirements.py — impact
  • tests/test_trailing_newline_not_a_syntax_error.py — impact
  • tests/test_ts_new_expression_calls.py — impact
  • tests/test_typescript_module_extensions.py — impact
  • tests/test_vue_extraction.py — impact

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.

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

@safishamsi

Copy link
Copy Markdown
Member

Shipped in v0.9.74 (live on PyPI as graphifyy==0.9.74). Landed on v8 as 18b1498 via an authorship-preserving cherry-pick, so your original commit authorship is kept intact. Thanks @Cintu07 for C# null-conditional member call resolution 🙏

Closing as shipped.

@safishamsi safishamsi closed this Oct 2, 2026
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.

2 participants