Skip to content

fix(parity): resolve C# same-class bare static calls + confidence filter for static receiver fallback#1417

Merged
carlos-alm merged 12 commits into
mainfrom
fix/resolver-static-confidence-1398-2
Jun 10, 2026
Merged

fix(parity): resolve C# same-class bare static calls + confidence filter for static receiver fallback#1417
carlos-alm merged 12 commits into
mainfrom
fix/resolver-static-confidence-1398-2

Conversation

@carlos-alm

@carlos-alm carlos-alm commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix 1 — confidence filter on static receiver fallback (original)

  • The direct static receiver fallback in resolveByMethodOrGlobal (added in fix(parity): resolve C# static receiver calls in WASM engine (#1372) #1395) was the only speculative lookup.byName() call in that function without a computeConfidence(relPath, n.file, null) >= 0.5 guard
  • All other non-type-resolved resolution paths apply the confidence floor — this brings the static fallback in line with the rest

Fix 2 — C# same-class bare static call resolution (Closes #1416)

  • Both WASM and native engines were missing call edges for bare static method calls within the same C# class — e.g. IsValidEmail() inside Validators.ValidateUser had no same-class fallback for no-receiver calls
  • WASM (build-edges.ts): add bare-call same-class fallback after the existing this.method() fallback — when targets is empty and call.receiver is null, try CallerClass.callName in the same file
  • Rust native (edge_builder.rs): mirror the same fallback in step 5 of resolve_call_targets
  • Role parity (native-orchestrator.ts): Rust classifies roles before JS CHA/this-dispatch post-passes add edges (stale fan-out medians). After those post-passes insert new edges, run a full role re-classification so the final roles reflect the complete graph

Fix 3 — C# var-declared instance type inference (Closes #1402)

  • var service = new UserService(repo); service.AddUser(user) was producing 0/4 recall on the receiver-typed benchmark — the typeMap had no entry for service
  • Two AST facts that were wrong in the initial approach: (1) C# tree-sitter uses implicit_type not var_keyword for var, and (2) object_creation_expression is a direct child of variable_declarator with no equals_value_clause wrapper
  • WASM (src/extractors/csharp.ts): handleCSharpVarDecl now handles implicit_type by walking declarator children for object_creation_expression and seeding typeMap at confidence 1.0
  • Rust native (crates/codegraph-core/src/extractors/csharp.rs): identical logic via find_child

Results:

  • C# receiver-typed recall: 0/4 → 4/4 (100%)
  • C# same-file static recall: 0/2 → 2/2 (100%)
  • Build-parity test: 8/8 pass (nodes, edges, roles, ast_nodes identical for WASM and native)
  • Full test suite: 3022 passed, 0 regressions

Test plan

  • tests/integration/build-parity.test.ts — 8/8 pass
  • tests/benchmarks/resolution/resolution-benchmark.test.ts — C# receiver-typed 4/4, same-file 2/2
  • Full test suite — all pass, 0 regressions

… resolveByMethodOrGlobal

Every other lookup.byName() path in this function applies computeConfidence >= 0.5
before returning candidates. The direct static receiver fallback (added in #1395) was
the only exception, risking false-positive edges across distant directories. All same-
directory static calls (e.g. C# fixture) still resolve at confidence 0.7.

Closes #1398
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three parity gaps between the WASM and native C# engines: a confidence guard on the static receiver fallback, same-class bare static call resolution, and var-declared instance type inference. All fixes are mirrored across the TypeScript WASM path and the Rust native path, with a consolidated role re-classification replacing two separate incremental passes in native-orchestrator.ts.

  • C# var type inference (csharp.ts, csharp.rs): Removes the old equals_value_clause wrapper search and adopts implicit_type + direct object_creation_expression child lookup at confidence 1.0, fixing 4/4 receiver-typed benchmark recall.
  • Same-class bare static call (build-edges.ts, edge_builder.rs): Adds a no-receiver fallback after resolveCallTargets that qualifies the bare call with the caller's own class name, file-scoped, fixing 2/2 same-file static recall.
  • Role re-classification consolidation (native-orchestrator.ts): Replaces two separate post-pass re-classifications (one incremental, one full) with a single full classifyNodeRoles(db, null) call after all edge-writing post-passes complete.

Confidence Score: 5/5

Safe to merge — all changes are additive fallbacks or simplifications with 100% benchmark recall and 3022-test suite green.

The three fixes are well-isolated: var-type inference now correctly reads the actual tree-sitter AST structure, the bare-call fallback is file-scoped and gated on non-JS/TS extensions, and the role re-classification consolidation removes a stale-median bug without regressing any existing behaviour. Test coverage directly validates the changed paths.

No files require special attention.

Important Files Changed

Filename Overview
src/domain/graph/builder/stages/build-edges.ts Adds bare-call same-class fallback and fixes this.method() fallback to use lastIndexOf instead of indexOf for correct namespace-qualified callerName handling; both fallbacks use file-scoped lookup, consistent with the existing pattern.
src/domain/graph/builder/call-resolver.ts Only change is exporting isModuleScopedLanguage so build-edges.ts can reuse it for the new bare-call guard; no logic changes.
src/domain/graph/builder/stages/native-orchestrator.ts Consolidates two separate post-pass role re-classifications (CHA and this-dispatch) into one full classifyNodeRoles(db, null) call; semantically equivalent to the old approach but avoids the stale fan-out median problem from the incremental this-dispatch pass.
src/extractors/csharp.ts Rewrites handleCSharpVarDecl to use implicit_type branch with direct object_creation_expression child lookup (confidence 1.0) and removes the extractVarInitType helper; keeps var_keyword as a skip guard for safety.
crates/codegraph-core/src/extractors/csharp.rs Mirrors the TypeScript implicit_type var inference fix in Rust: uses find_child for direct object_creation_expression lookup at confidence 1.0, removes extract_var_init_type, keeps var_keyword as a skip guard.
tests/parsers/csharp.test.ts Updates expected typeMap confidence for var-inferred types from 0.9 to 1.0, matching the new code path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildFileCallEdges - WASM] --> B[resolveCallTargets]
    B --> C{targets found?}
    C -- yes --> G[insert edges]
    C -- no --> D{call.receiver === 'this'?}
    D -- yes --> E[this.method fallback\nbyNameAndFile]
    E --> F{targets found?}
    F -- yes --> G
    F -- no --> H{no receiver AND not JS/TS?}
    D -- no --> H
    H -- yes --> I[NEW: bare-call same-class fallback\nbyNameAndFile]
    I --> J{targets found?}
    J -- yes --> G
    J -- no --> K[Object.defineProperty accessor fallback]
    H -- no --> K
    K --> G
Loading

Reviews (16): Last reviewed commit: "fix: resolve merge conflicts with main" | Re-trigger Greptile

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

5 functions changed22 callers affected across 10 files

  • match_csharp_type_map in crates/codegraph-core/src/extractors/csharp.rs:453 (0 transitive callers)
  • isModuleScopedLanguage in src/domain/graph/builder/call-resolver.ts:43 (13 transitive callers)
  • buildFileCallEdges in src/domain/graph/builder/stages/build-edges.ts:1296 (3 transitive callers)
  • tryNativeOrchestrator in src/domain/graph/builder/stages/native-orchestrator.ts:1436 (5 transitive callers)
  • handleCSharpVarDecl in src/extractors/csharp.ts:333 (3 transitive callers)

@carlos-alm

Copy link
Copy Markdown
Contributor Author

Addressed Greptile review feedback:

The typeName-resolved typed-method path at line 109 (lookup.byName('TypeName.method')) intentionally omits the computeConfidence guard. Here's why:

  1. The lookup is already fully qualified. When typeName comes from typeMap (e.g. var d = new Dog() seeds typeMap['d'] = 'Dog'), the lookup is Dog.bark — a precise, class-qualified name. File-proximity scoring is a disambiguation heuristic for name-only lookups where multiple unrelated symbols share the same name. A qualified name like Dog.bark already uniquely identifies the target, so proximity scoring would only harm cross-module resolution (e.g. calling a method on a class from another directory).

  2. The !typeName path (line 132) is intentionally different. This path uses effectiveReceiver.method where the receiver has no tracked type — the receiver IS being speculatively treated as a class name. Here, confidence filtering is essential to avoid false positives from unrelated same-named symbols across the codebase.

  3. The PR description was slightly imprecise. Updated the description to clarify that the typeName-resolved path at line 109 and the inline-new-expression path (line 103) are both exempt from the confidence filter by design — they produce qualified names that already carry their own precision.

The PR description has been updated to note this distinction explicitly.

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

…ve (closes #1416)

Both WASM and native engines were missing call edges for bare static method
calls within the same C# class — e.g. `IsValidEmail()` inside
`Validators.ValidateUser` should resolve to `Validators.IsValidEmail`, but
neither engine had a same-class fallback for no-receiver calls.

Three-part fix:

1. WASM (`build-edges.ts`): after the `this.method()` same-class fallback,
   add a parallel fallback for no-receiver calls: when `targets` is empty
   and the call has no receiver, try `CallerClass.callName` in the same file.
   Only fires after the global exact lookup already failed, so module-level
   functions always win.

2. Native Rust (`edge_builder.rs`): mirror the WASM fallback in step 5
   of `resolve_call_targets` — when `call.receiver.is_none()` and the
   global exact lookup returns nothing, try `CallerClass.callName` scoped
   to the same file.

3. Role parity (`native-orchestrator.ts`): the Rust pipeline classifies
   roles before JS CHA/this-dispatch post-passes add edges, giving stale
   fan-out medians. When those post-passes insert new edges, run a full
   role re-classification so the final roles see the complete graph.

Result: C# same-file static recall improves from 0/2 (0%) to 2/2 (100%).
Build-parity test: 8/8 pass (nodes, edges, roles, ast_nodes identical).
docs check acknowledged
@carlos-alm carlos-alm changed the title fix(resolver): apply confidence filter to static receiver fallback fix(parity): resolve C# same-class bare static calls + confidence filter + role re-classification after CHA Jun 9, 2026
@carlos-alm carlos-alm changed the title fix(parity): resolve C# same-class bare static calls + confidence filter + role re-classification after CHA fix(parity): resolve C# same-class bare static calls + confidence filter for static receiver fallback Jun 9, 2026
…ifications

The full classifyNodeRoles(null) pass added in this PR already subsumes
both the CHA-only and thisDispatch-only file-scoped partial passes that
preceded it. Remove the two partial passes to avoid running the classifier
twice on thisDispatch-only builds.
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Addressed the redundant partial re-classification concern: removed both the CHA file-scoped partial pass and the thisDispatch file-scoped partial pass. The full classifyNodeRoles(null) added in this PR already subsumes both — running the partial passes first then the full pass was pure waste. Now only the single full pass runs when either post-pass adds edges.

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm

Copy link
Copy Markdown
Contributor Author

Merge conflict with main resolved. The two conflicting files were:

  • crates/codegraph-core/src/edge_builder.rs: took main's unified class-scoped lookup with confidence >= 0.5 filter (strictly better — covers both receiver and no-receiver calls, adds the confidence guard that was missing in the PR's version)
  • src/domain/graph/builder/stages/native-orchestrator.ts: merged PR's single consolidated full re-classification design with main's runPostNativeCha returning a count (number) instead of Set — the condition is now chaEdgeCount > 0 || thisDispatchTargetIds.size > 0 triggering the single classifyNodeRoles(null) pass

All tests pass (3017/3017). CI running.

@carlos-alm

Copy link
Copy Markdown
Contributor Author

Merge conflict with main resolved. The conflict was in src/domain/graph/builder/stages/native-orchestrator.ts: origin/main added verbose comments on the CHA block explaining the full-pass motivation; our PR's refactoring replaced them with a shorter comment. Kept our PR's concise comment since the motivation is now captured elsewhere in the block (which was restructured to consolidate both the CHA and this-dispatch re-classifications into one single full pass).

Rebuilt dist/ from updated src/ — a stale build was causing a false positive in the JS precision benchmark (runCallThis → handler) because the .call()/.apply()/.bind() guard added in #1405 hadn't been compiled yet.

All 3022 tests pass locally. CI running.

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

carlos-alm and others added 2 commits June 9, 2026 12:09
…n in same-class fallbacks

Both the this.method() fallback and the bare-call same-class fallback were
using indexOf('.') which takes the first dot segment. For a caller named
MyNS.Validators.ValidateUser this yields MyNS instead of Validators, causing
the sibling edge lookup to miss. Align with call-resolver.ts which uses
lastIndexOf + prevDot to isolate only the segment immediately before the
method name.
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Fixed the namespace-aware class extraction issue raised by Greptile.

Both the this.method() fallback and the bare-call same-class fallback in build-edges.ts were using indexOf('.') which takes the first dot segment. For a caller like MyNS.Validators.ValidateUser this would yield MyNS instead of Validators, causing sibling calls to be silently missed.

Both are now aligned with call-resolver.ts which uses lastIndexOf + prevDot to isolate only the segment immediately before the method name — so MyNS.Validators.ValidateUser correctly yields Validators.

Commit: 9be5c71

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

…ion initializers

When a local variable is declared with `var` and initialized via `new
ClassName(...)`, seed the typeMap with the constructor type at
confidence 1.0 so that subsequent method calls on that variable resolve
correctly.  Fixes 0/4 recall on the csharp receiver-typed benchmark.

The C# tree-sitter grammar uses `implicit_type` (not `var_keyword`) for
`var` declarations, and the `object_creation_expression` sits as a
direct child of `variable_declarator` with no `equals_value_clause`
wrapper.  Applied to both the WASM (TypeScript) and native (Rust)
extractors to maintain engine parity.

Closes #1402
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Merge conflicts with main resolved (two rounds):

Round 1 — origin/main at 76907e6 (#1415):

  • src/extractors/csharp.ts: origin/main added extractVarInitType helper + isVar flag approach with confidence 0.9. This PR's HEAD used implicit_type directly with findChild at confidence 1.0. Kept PR's approach (correct C# grammar semantics — implicit_type is the right node type, findChild is more accurate than equals_value_clause traversal). Removed dead extractVarInitType function that origin/main had added.
  • crates/codegraph-core/src/extractors/csharp.rs: Same as above for Rust mirror.
  • Test update: tests/parsers/csharp.test.ts expected confidence 0.9 (from main), corrected to 1.0 to match PR's intentional change.

Round 2 — origin/main at 5a06fa2 (#1422):

  • Same C# extractor conflicts as above (same resolution).
  • Auto-merge of call-resolver.ts correctly restored MODULE_SCOPED_BARE_CALL_EXTENSIONS and isModuleScopedLanguage guard.
  • Found regression: The bare-call fallback in build-edges.ts lacked the module-scoped language guard, causing JS precision failure (Processor.run → Processor.flush false positive from class-scope.js fixture). Fixed by exporting isModuleScopedLanguage from call-resolver.ts and gating the bare-call fallback in build-edges.ts on it.

All 3030 tests pass locally.

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm carlos-alm merged commit 9037c13 into main Jun 10, 2026
28 checks passed
@carlos-alm carlos-alm deleted the fix/resolver-static-confidence-1398-2 branch June 10, 2026 01:31
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

1 participant