Skip to content

csharp: extract this./base.-qualified member calls - #466

Merged
zzet merged 2 commits into
zzet:mainfrom
pbednarcik:fix/csharp-this-base-calls
Aug 5, 2026
Merged

csharp: extract this./base.-qualified member calls#466
zzet merged 2 commits into
zzet:mainfrom
pbednarcik:fix/csharp-this-base-calls

Conversation

@pbednarcik

Copy link
Copy Markdown
Contributor

this.Foo() and base.Foo() currently emit no call edge at all — both member-call query patterns capture the receiver with expression: (_), but this and base are anonymous tokens in tree-sitter-c-sharp, and a named-node wildcard never matches them. Every qualified self/base call in a C# codebase silently vanishes from the graph (noticeable on base.OnActionExecuting(...)-style overrides, which are everywhere in ASP.NET code).

Fix (extraction only, resolver untouched):

  • Two new query alternations match the anonymous token literally ((member_access_expression "this" name: (identifier)) and the base twin).
  • The receiver type is resolved at capture time from the enclosing declaration — this → the innermost type's name, base → its declared base class (discrimination mirrors emitCSharpBaseList: first base-list entry that is a ctor-base or not I-prefixed; structs and interface-only bases stamp nothing and the call still emits as a plain member_call).
  • The stamped receiver_type then rides the existing receiver-typed member binding path, so this.Foo() binds the hiding member and base.Foo() the base member with no new resolver machinery.

Tests: extractor spec (edges + receiver_type stamps for both qualifiers, new-hiding case) and an end-to-end resolver spec (this.Foo() → hiding member, base.Foo() → base member). Both watched red before the fix. internal/parser/languages fully green; internal/resolver and internal/indexer at their pre-existing Windows baselines with zero new failures.

The second commit bumps the csharp extractor version 3→4 so existing stores restage .cs files and grow the new edges. Validated live on a counted C# fixture repo via exactly that path — binary swap, no store wipe, salt-triggered restage — with both call shapes binding their compiler-correct targets (this.X() → the new-hiding member, base.X() → the base member) at 0.95 with receiver_type stamped.

@zzet zzet left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Two adjacent gaps found (inspired by PR, not directly related to PR)

  • this?.Foo() / base?.Foo() emits nothing — the conditional-access pattern has the identical condition: (_) blindness. Rare in practice (this is never null), but it's the same bug left half-fixed.

  • C# records emit no member nodes at all. public record Tower(int N) { public int Chime() {...} } produces a type node and its extends edge, and nothing else — every method in every record is invisible to the graph, so no call edges leave them. Records are idiomatic C# 9+, so this is a substantially bigger gap than the one this PR closes.

@zzet
zzet merged commit a27e61d into zzet:main Aug 5, 2026
10 checks passed
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