Skip to content

fix(parity): resolve C# static receiver calls in WASM engine (#1372)#1395

Merged
carlos-alm merged 9 commits into
mainfrom
fix/csharp-static-receiver-1372
Jun 8, 2026
Merged

fix(parity): resolve C# static receiver calls in WASM engine (#1372)#1395
carlos-alm merged 9 commits into
mainfrom
fix/csharp-static-receiver-1372

Conversation

@carlos-alm

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

Copy link
Copy Markdown
Contributor

Summary

  • Root cause: `resolveByMethodOrGlobal` had no fallback for calls where the receiver is a class name used directly (e.g. `Validators.IsValidEmail(...)`). Static class names never appear in variable declarations, so `typeMap` has no entry for them. The code fell through and returned an empty target set.
  • Fix: After all typeMap and pts-key lookups fail, if `typeName` is still null, try `${effectiveReceiver}.${callName}` directly in the symbol DB with a `kind='method'` filter. This is the static dispatch path — the receiver IS the class name, so the qualified method name is exactly what the DB holds. Uses `effectiveReceiver` (this.-stripped) to handle any `this.ClassName.method()` variants.
  • Regression test: `tests/integration/issue-1372-csharp-static-receiver.test.ts` — builds a minimal two-file C# fixture with WASM and asserts two static receiver edges are produced (using a simplified fixture with `Program.Main`, `Program.Run`, `Validators.IsValidEmail`, `Validators.IsValidName`).
  • Parity test: `tests/integration/build-parity.test.ts` for the `csharp Repository` benchmark fixture now passes all 8 assertions.

Edges fixed

These edges are from the benchmark fixture in `tests/benchmarks/resolution/fixtures/csharp/`:

Caller Callee Type
`Program.Main` `Validators.IsValidEmail` static receiver
`Program.RunWithValidation` `Validators.ValidateUser` static receiver
`UserService.AddUser` `Validators.ValidateUser` static receiver

Note: the regression test uses a separate minimal fixture (different method names) that directly exercises the same code path.

Test plan

  • `npx vitest run tests/integration/issue-1372-csharp-static-receiver.test.ts` — 2 new tests pass
  • `npx vitest run tests/integration/build-parity.test.ts` — all 8 assertions pass (was 2 failures before)
  • `npx vitest run` — no new failures
  • `npm run lint` — no errors

Closes #1372

The WASM resolver had no fallback for calls where the receiver is a class
name used directly (e.g. `Validators.IsValidEmail(...)`).  Static class names
never appear in variable declarations, so typeMap has no entry for them.
The old code fell through and returned an empty target set.

Add a fallback in resolveByMethodOrGlobal: when typeName is null (no typeMap
entry for the receiver), try looking up `${receiver}.${method}` directly in
the symbol DB.  This catches static dispatch where the receiver IS the class
name, matching what the native engine already produces.  The kind='method'
filter keeps it scoped to genuine method calls.

Fixes build-parity test failures for the csharp Repository fixture:
- Program.Main → Validators.IsValidEmail
- Program.RunWithValidation → Validators.ValidateUser
- UserService.AddUser → Validators.ValidateUser
@claude

claude Bot commented Jun 8, 2026

Copy link
Copy Markdown

Claude finished @carlos-alm's task —— View job


PR Review — fix(parity): resolve C# static receiver calls in WASM engine (#1372)

  • Gathering context
  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity
  • Phase 4: Code Quality
  • Phase 5: Backlog Compliance
  • Final Assessment

Phase 1: Root Cause Analysis ✓

The root cause is correctly identified and the fix is genuine — not a symptom masker. The WASM resolver's resolveByMethodOrGlobal had no path for the case where receiver is a class name used directly in a static call (Validators.IsValidEmail(...)). Because static class names are never assigned to variables, typeMap has no entry, typeName stays null, and the old code fell through to return [].

The fix adds a !typeName fallback that directly queries ${effectiveReceiver}.${call.name} in the symbol DB, filtered to kind='method'. This is structurally correct — the DB stores C# methods under their fully qualified name (e.g. Validators.IsValidEmail), so the lookup is deterministic and precise.

This is fixing the correct engine (WASM) to match native output. No workarounds.


Phase 2: Configuration Change Detection ✓

No linting, build, or TypeScript config changes. No biome rules relaxed, no timeouts increased, no coverage thresholds modified. Clean.


Phase 3: Test Integrity — ⚠️ Minor Issues

Concern 1: Test fixture name mismatch with PR body

The PR body says the fix resolves three edges, including Program.RunWithValidation → Validators.ValidateUser, but the regression test (issue-1372-csharp-static-receiver.test.ts) uses a simplified fixture with a method named Run (not RunWithValidation):

// test fixture at line 36:
'public static void Run() {'
// but assertion at line 100:
'src: \'Program.Run\''

The PR body's "Edges fixed" table references Program.RunWithValidation (from the benchmark fixture in tests/benchmarks/resolution/fixtures/csharp/), but the regression test exercises different code. The regression test is still valid, but it doesn't directly test Program.RunWithValidation as claimed. This is a documentation/accuracy issue in the PR body, not a test defect.

Concern 2: Test covers only 2 of the 3 claimed edges

The UserService.AddUser → Validators.ValidateUser edge (the third in the PR body's table) is only validated indirectly via build-parity.test.ts. The new regression test doesn't cover it. Given that the regression test creates its own minimal fixture, it would be better to have it test all 3 shapes, or at least acknowledge the gap.

Concern 3: build-parity.test.ts is skipped without native engine

The parity test that covers the full C# benchmark fixture (tests/integration/build-parity.test.ts:35) is skipped when native is unavailable (describe.skip). On a WASM-only CI environment (no native addon installed), the 8 assertions mentioned in the PR body won't run. The regression test is the only hard gate, and it only covers 2 edges.


Phase 4: Code Quality

The fix itself (lines 128–139 of call-resolver.ts):

// Static receiver fallback: treat receiver as a direct class/struct name.
if (!typeName) {
  const staticCandidates = lookup
    .byName(`${effectiveReceiver}.${call.name}`)
    .filter((n) => n.kind === 'method');
  if (staticCandidates.length > 0) return staticCandidates;
}

Structural concern: No confidence check or file-scope narrowing

Every other resolution path in this function applies computeConfidence(relPath, t.file, null) >= 0.5 before returning candidates. The static fallback is the only path that skips confidence entirely and returns raw global DB results. In a polyglot project with two unrelated classes both having a Validate method, byName("Foo.Validate") could match across languages or across unrelated namespaces. The kind='method' filter helps, but isn't sufficient on its own.

Compare to the typed lookup at line 94, which also skips confidence, but that lookup is gated on a typeMap entry that was established from actual source analysis. The static fallback has no such prior evidence — it's a "try it and see" lookup. At minimum, it should apply confidence scoring, or if scoring isn't appropriate for static dispatch (since the class name is the type), that should be documented.

effectiveReceiver scope is correct — the this.-strip is already done at line 72, and effectiveReceiver is in scope at line 136. The second commit (#1390) that fixed call.receivereffectiveReceiver is correct.

The !typeName guard: This correctly fires only when typeName is null after both the typeMap lookup and the new Expression regex. However, there's a subtle edge: what if typeName is set but the typed lookup (line 94) finds 0 results? In that case the code falls through to prototype alias, pts-key checks, and then exits the if (typeName) block — but !typeName is still false, so the static fallback is skipped. For pure C# static classes this is fine (there's no typeMap entry so typeName stays null), but if future code seeds a typeMap entry with the wrong type for a receiver, the static fallback would be silently bypassed. This is an edge case rather than a bug in the current context.


Phase 5: Backlog Compliance

  • Zero-dep: ✓ — No new runtime dependencies. Builds on existing SQLite + symbol lookup.
  • Foundation-aligned: ✓ — Pure local analysis, no cloud, no code generation. Directly improves graph accuracy.
  • Problem-fit: 4/5 — Directly fixes a false-negative in the dependency graph that would cause AI agents to miss real call relationships in C# codebases, leading to incorrect blast-radius analysis.
  • Breaking: No — purely additive. Existing behavior is unchanged; new edges are added for previously unresolved calls.
  • Tier: 1 (zero-dep ✓, foundation-aligned ✓)

Critical Concerns

  1. Missing confidence filter on static fallback (call-resolver.ts:134-139): The byName() call returns global DB results without any file-scope narrowing or confidence scoring. All other resolution paths apply computeConfidence. In large polyglot repos, this could produce false edges where two unrelated classes share a method name. A file-proximity check (same namespace/directory) or at least a confidence floor would reduce false positive risk.

  2. Regression test fixture diverges from PR claims: The test at line 100 asserts Program.Run → Validators.IsValidName, not the Program.RunWithValidation → Validators.ValidateUser edge listed in the PR body's "Edges fixed" table. These are different methods in a different fixture. The PR body descriptions don't match what the test actually exercises.


Root Cause Analysis

Problem Being Solved: WASM engine was producing no edges for C# static class method calls where the receiver is the class name directly (e.g. Validators.IsValidEmail(...)). Native engine resolved these correctly, causing a parity gap.

Why This Approach: The fully qualified name ClassName.MethodName is already how methods are stored in the DB. When typeMap has no entry for the receiver (because static classes are never assigned to variables), looking up ${receiver}.${method} directly is the natural fix. Correct approach.

Risk Assessment: Low-to-medium. The fix is logically sound for C#. The missing confidence filter creates a theoretical false positive risk in large multi-language repos that should be addressed — but the kind='method' guard provides some protection.

Backlog Compliance

  • Zero-dep: ✓
  • Foundation-aligned: ✓
  • Problem-fit: 4/5 — fixes a real gap in C# static dispatch resolution
  • Breaking: No
  • Tier: 1

Final Recommendation

  • Rating: ⭐⭐⭐☆☆ (3/5)
  • Action: APPROVE WITH CONDITIONS
  • Reasoning: The root cause analysis is correct, the fix is genuinely solving the issue (not hiding it), and the approach is architecturally sound. Two issues hold it from a higher rating: (1) the static fallback at call-resolver.ts:134-139 skips the confidence check applied by every other resolution path — this should either add computeConfidence filtering or document explicitly why it's inapplicable for static dispatch; (2) the regression test fixture names (Program.Run, Validators.IsValidName) don't match the edges described in the PR body (Program.RunWithValidation, Validators.ValidateUser), creating confusion about what's actually being tested. Neither is a blocker, but the confidence gap is worth a follow-up issue before the next language parity sprint.

@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a regression test for issue #1372 (C# static receiver call edges missing in the WASM engine) and pins an existing prototype-method-resolution integration test to the WASM engine explicitly.

  • New test (issue-1372-csharp-static-receiver.test.ts): builds a minimal two-file C# fixture (Validators.cs + Program.cs) via the WASM engine and asserts that two static dispatch call edges (Program.Main → Validators.IsValidEmail, Program.Run → Validators.IsValidName) are present in the SQLite output.
  • Existing test update (prototype-method-resolution.test.ts): passes engine: 'wasm' explicitly to buildGraph, formalising what was already the effective behaviour in environments where the native binary is not installed.

Confidence Score: 5/5

Safe to merge — both changes are test-only and introduce no production code modifications.

All changes are in integration tests. The new regression test is well-structured with proper initialisation, timeout, and cleanup. The engine pin in the prototype-method-resolution test formalises already-implicit WASM behaviour and is partially backstopped by build-parity.test.ts which exercises both engines on the benchmark fixtures.

No files require special attention.

Important Files Changed

Filename Overview
tests/integration/issue-1372-csharp-static-receiver.test.ts New regression test for #1372; exercises static receiver call resolution in the WASM engine with a minimal two-file C# fixture. edges is correctly initialised to [], cleanup is guarded, and the DB query is scoped to kind='calls'.
tests/integration/prototype-method-resolution.test.ts One-line change explicitly pins engine: 'wasm' to formalise the engine this test exercises; formalises existing implicit behaviour in environments where native is not installed.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildGraph - engine: wasm] --> B[WASM parser\nproduces call sites]
    B --> C[resolveByMethodOrGlobal]
    C --> D{typeMap has entry\nfor receiver?}
    D -- Yes --> E[typed method lookup\ntypeName.callName]
    D -- No --> F{inline new\nexpression?}
    F -- Yes --> E
    F -- No --> G[static fallback\neffectiveReceiver.callName\nkind = method or function]
    G --> H[emit call edge\ne.g. Program.Main → Validators.IsValidEmail]
    E --> H
Loading

Reviews (8): Last reviewed commit: "Merge branch 'main' into fix/csharp-stat..." | Re-trigger Greptile

Comment on lines +134 to +139
if (!typeName) {
const staticCandidates = lookup
.byName(`${effectiveReceiver}.${call.name}`)
.filter((n) => n.kind === 'method');
if (staticCandidates.length > 0) return staticCandidates;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Static fallback may fire for unresolved instance variables, not only static classes

typeName is null for two distinct reasons: the receiver is a static class name (the intended case), or the receiver is an instance variable that simply wasn't seeded into typeMap. In the second case the fallback performs a DB lookup of effectiveReceiver.callName — if a class in the codebase happens to share the same identifier as the unresolved variable, a spurious call edge is created. Languages where variable names collide with class names (e.g. lowercase-named Python/Go classes, or a TypeScript variable named cache when a cache module exports a get method) are affected. The typed branch at line 94 is similarly unguarded but only runs when typeName was positively resolved from typeMap, so that gap is narrower.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged. The concern is valid — typeName being null covers both intended (static class name) and unintended (unresolved instance variable) cases, and the DB lookup without a confidence filter could produce false edges when two unrelated classes share a method name.

Adding confidence scoring to the static fallback (like all other byName() paths use) is the right long-term fix, but it requires care: static dispatch is typically cross-file by design, so a 0.5 confidence floor could suppress legitimate edges for callers in different directories. That analysis goes beyond this PR's scope.

Filed #1398 to track this follow-up. The current kind='method' guard plus the fully-qualified Receiver.Method key makes false positives unlikely in practice.

Comment on lines +76 to +77
let tmpDir: string;
let edges: Array<{ src: string; tgt: string; kind: string }>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Initialising edges to [] ensures the it blocks produce a clear assertion failure ("expected [] to contain...") rather than a cryptic "Cannot read properties of undefined" when beforeAll rejects.

Suggested change
let tmpDir: string;
let edges: Array<{ src: string; tgt: string; kind: string }>;
let tmpDir: string;
let edges: Array<{ src: string; tgt: string; kind: string }> = [];

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — edges is now initialised to [] in commit f367fbd.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

1 functions changed14 callers affected across 5 files

  • resolveByMethodOrGlobal in src/domain/graph/builder/call-resolver.ts:62 (14 transitive callers)

… Array.from (JS)

Fixes more1 jelly-micro benchmark from 0% → 100% recall (10/10 named edges).

The PTS resolution for for-of, Set+for-of, Array.from, and spread patterns was
already fully implemented. The 0% recall was caused by a naming mismatch in the
fixture: _iterPlain/_iterSet (underscore-prefixed) vs the expected-edges.json
which referenced iterPlain/iterSet (without prefix). Renaming the fixture
functions to match the expected edges exposes all four PTS patterns resolving
correctly.

Closes #1378
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Responding to the Claude review concerns:

Concern 1 — Missing confidence filter on static fallback:
Valid point, and consistent with Greptile's P2 comment on the same block. Adding confidence scoring to the static fallback requires care because static dispatch is inherently cross-file — a 0.5 confidence floor based on file proximity could suppress legitimate edges when the caller and the static class live in different directories. The kind='method' guard and the fully-qualified Receiver.Method key already make accidental matches unlikely. Tracked in #1398 for the next resolution sprint.

Concern 2 — Regression test fixture diverges from PR description:
Fixed. The PR body now accurately describes both fixtures: the "Edges fixed" table refers to the benchmark fixture (tests/benchmarks/resolution/fixtures/csharp/), and the regression test description clarifies it uses a simplified two-file fixture with different method names (Program.Run, Validators.IsValidName). Both exercise the same code path.

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

The test was using auto engine (native-preferred), causing it to pick the
published npm native binary which predates the prototype-method fixes.
WASM correctly extracts Dog.prototype.bark and resolves all call edges.

Fixes #1381
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Merged with main and removed a duplicate static-receiver fallback block that had been superseded by the broader version landed in #1382. The PR's original !typeName block (kind='method' only) is now gone — main's version (kind='method' || kind='function', positioned earlier in the chain) covers the same cases plus more. All regression tests pass.

@greptileai

@carlos-alm carlos-alm merged commit c199368 into main Jun 8, 2026
22 checks passed
@carlos-alm carlos-alm deleted the fix/csharp-static-receiver-1372 branch June 8, 2026 22:31
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(parity): C# native engine resolves static receiver calls that WASM misses

1 participant