Skip to content

C#: CALLS edge attaches to wrong same-name class variant (shared namespace root) — using-directive scoping ignored; trace_path inbound returns callers_total: 0 #2120

Description

@ersintarhan

Version

codebase-memory-mcp 0.10.8

Platform

Linux (x64)

Install channel

AUR

Binary variant

standard

What happened, and what did you expect?

When two projects contain classes with the same simple name in namespaces that share a common root (Contoso.Platform.Identity.UserIdentityInfo vs Contoso.Platform.BackOffice.Identity.UserIdentityInfo), a call from BackOffice code — with an explicit using Contoso.Platform.BackOffice.Identity; — gets its CALLS edge attached to the wrong variant (Contoso.Platform.Identity).

Consequences:

  • trace_path --direction inbound on the correct (BackOffice) variant returns callers_total: 0 — a false "unused" signal.
  • trace_path/Cypher on the wrong (Platform) variant reports callers that never call it — a false positive.
  • Dead-code analysis and impact maps built on either side are silently incorrect.

Expected: Roslyn binding rules — the file's using-directive (and enclosing-namespace proximity) resolve UserIdentityInfo to the BackOffice variant; the CALLS edge should target that node.

On the real repository where we found this (private, ~46,186 nodes / 121,081 edges / ~2,500 files): 3 production callers + 6 test callers of the BackOffice variant all came back callers_total: 0, while the Platform variant "gained" the 3 production callers.

Reproduction

Dummy snippet (two SDK-style projects + three files):

repro/
├── src/Contoso.Platform/
│   ├── Contoso.Platform.csproj
│   └── Identity/UserIdentityInfo.cs
├── src/Contoso.Platform.BackOffice/
│   ├── Contoso.Platform.BackOffice.csproj   (ProjectReference → Contoso.Platform.csproj)
│   ├── Identity/UserIdentityInfo.cs
│   └── CqrsHandlers/Handler.cs

src/Contoso.Platform/Identity/UserIdentityInfo.cs:

namespace Contoso.Platform.Identity;

public class UserIdentityInfo
{
    public static UserIdentityInfo ParseFromClaim(string claim)
        => new UserIdentityInfo();
}

src/Contoso.Platform.BackOffice/Identity/UserIdentityInfo.cs:

namespace Contoso.Platform.BackOffice.Identity;

public class UserIdentityInfo
{
    public static UserIdentityInfo ParseFromClaim(string claim)
        => new UserIdentityInfo();
}

src/Contoso.Platform.BackOffice/CqrsHandlers/Handler.cs:

using Contoso.Platform.BackOffice.Identity;

namespace Contoso.Platform.BackOffice.CqrsHandlers;

public class Handler
{
    public string Handle(string claim)
    {
        var info = UserIdentityInfo.ParseFromClaim(claim);
        return info.ToString();
    }
}

Commands:

codebase-memory-mcp cli index_repository --repo-path /tmp/repro --mode full
codebase-memory-mcp cli query_graph --project tmp-repro \
  --query "MATCH (f)-[:CALLS]->(g) WHERE g.name = 'ParseFromClaim' RETURN f.name, g.qualified_name"

Result:

Handle  tmp-repro.src.Contoso.Platform.Identity.UserIdentityInfo.UserIdentityInfo.ParseFromClaim

Expected:

Handle  tmp-repro.src.Contoso.Platform.BackOffice.Identity.UserIdentityInfo.UserIdentityInfo.ParseFromClaim

Notes from narrowing it down:

  • Single-project repro with the same two namespaces does not trigger it — two projects (class referenced across a project boundary) appear to be part of the trigger.
  • Same simple class name + shared namespace root across projects is the shape we hit on every "Identity"-style helper in our codebase.
  • Static-method resolution itself works (the edge exists); it is the type/target selection among same-name candidates that ignores using-directive scoping.

Logs

$ codebase-memory-mcp cli index_repository --repo-path /tmp/repro --mode full
Completed index_repository (...)

$ codebase-memory-mcp cli trace_path --project tmp-repro \
  --function-name "tmp-repro.src.Contoso.Platform.BackOffice.Identity.UserIdentityInfo.UserIdentityInfo.ParseFromClaim" \
  --direction inbound
callers_total: 0

Project scale (if relevant)

Real repo where observed: 46,186 nodes / 121,081 edges / ~2,500 files (1,408 .cs + 1,013 .sql). Repro above: ~dozens of nodes.

Confirmations

Activity

  1. ersintarhan commented on Sep 9, 2026

    @ersintarhan
    Author

    Additional data from the same private repo (46,186 nodes / 121,081 edges), a second symbol family, and a minimal-repro extension. Namespaces anonymized as in the report.

    Three distinct failure modes on the real repo — not one

    Call as written in source CALLS edge target Correct?
    UserIdentityInfo.ParseFromClaim ×3 (BackOffice prod, plain using) Contoso.Platform.Identity.UserIdentityInfo no — wrong variant
    BackOfficeIdentityInfo.ParseFromClaim ×4 (using BackOfficeIdentityInfo = Contoso.Platform.BackOffice.Identity.UserIdentityInfo;) Contoso.Platform.Identity.PlayerIdentityInfo.ParseFromClaim no — a class with a different simple name, which has zero real callers (verified by grep)
    PartnerIdentityInfo.ParseFromClaim ×4 Contoso.Platform.Partner.Identity.PartnerIdentityInfo yes

    Second family, UserClaimPrincipleFactory (3 variants: Platform / BackOffice / Partner):

    • new UserClaimPrincipleFactory(...) then .CreateAsync(...) — the Partner one, resolved by a plain using — attached to Contoso.Platform.…UserClaimPrincipleFactory.CreateAsync. Same root-namespace preference. Interestingly the constructor/type edge on the same statement resolved correctly to the Partner variant; only the method call went wrong.
    • Three .CreateAsync(...) calls made through a using alias (BackOfficeClaimsFactory) produced no CALLS edge at all.

    Inferred rule

    The resolver appears to match the textual receiver identifier against class simple names rather than binding it:

    • exactly one match → correct (PartnerIdentityInfo resolves fine in the very same file where the alias calls go wrong)
    • multiple matches → the shortest / root namespace wins, using-directives ignored
    • zero matches (alias) → either an arbitrary node carrying the same method name, or no edge

    Severity note for the false-positive direction: the phantom callers landed on PlayerIdentityInfo.ParseFromClaim, which has zero real callers in the repo. Dead-code analysis would report it as live.

    Breadth in this codebase — classes sharing a simple name across projects: Handler ×104, Request ×47, Result ×26, Session ×6, Login ×6, IdentityExtensions ×4. This is the default shape of an ASP.NET/CQRS solution, not a corner case.

    Minimal-repro extension (verified just now)

    I extended the two-project repro from the report with a third class and an alias caller:

    // src/Contoso.Platform/Identity/PlayerIdentityInfo.cs
    namespace Contoso.Platform.Identity;
    
    public class PlayerIdentityInfo
    {
        public static PlayerIdentityInfo ParseFromClaim(string claim)
            => new PlayerIdentityInfo();
    }
    
    // src/Contoso.Platform.BackOffice/CqrsHandlers/AliasHandler.cs
    using BOIdentity = Contoso.Platform.BackOffice.Identity.UserIdentityInfo;
    
    namespace Contoso.Platform.BackOffice.CqrsHandlers;
    
    public class AliasHandler
    {
        public string Handle(string claim)
        {
            var info = BOIdentity.ParseFromClaim(claim);
            return info.ToString();
        }
    }

    Fresh re-index, then the same Cypher:

    AliasHandler.Handle  →  …Contoso.Platform.BackOffice.Identity.UserIdentityInfo…ParseFromClaim   (correct)
    Handler.Handle       →  …Contoso.Platform.Identity.UserIdentityInfo…ParseFromClaim               (wrong — the original bug, still)
    

    Two observations from this:

    1. The plain-using mis-attachment is stable across re-indexes and file additions — Handler.Handle keeps landing on the root-namespace variant.
    2. In the minimal shape the alias actually resolved correctly, so the phantom-edge mode from our real repo (alias call landing on a same-method-name class, or producing no edge) did not reproduce in isolation — it may require additional shape (several ParseFromClaim call sites in one file, registration order, etc.). Reporting it as observed-but-not-minimized.

    One more instability note: in an earlier run of the same repro before the alias file was added, the single plain-using call also landed on the wrong variant — so plain-using breakage does not depend on the alias file being present.

  2. added
    cypherCypher query language parser/executor bugs
    parsing/qualityGraph extraction bugs, false positives, missing edges
    on Sep 9, 2026
  3. added
    bugSomething isn't working
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    and removed
    cypherCypher query language parser/executor bugs
    on Sep 9, 2026
  4. AmirF194 commented on Sep 10, 2026

    @AmirF194
    Contributor

    I tried to reproduce this against current main (055fbb7) in a clean container: built from source, laid out the exact two-project repro from this issue (Contoso.Platform / Contoso.Platform.BackOffice, Handler.cs with the plain using Contoso.Platform.BackOffice.Identity;), and ran the same index_repository + query_graph commands.

    Result on this HEAD, 5 fresh re-indexes in a row:

    Handle tmp-repro2120.src.Contoso.Platform.BackOffice.Identity.UserIdentityInfo.UserIdentityInfo.ParseFromClaim
    

    That is the correct (BackOffice) target, not the root-namespace one from the report. I also padded the repro to 63 files to force the parallel indexing path (MIN_FILES_FOR_PARALLEL is 50), in case this only shows up at scale, and got the same correct result every time.

    One data point that might explain the gap: the issue says version 0.10.8 (AUR), which tags to 2026-08-18. Between that release and current main, PR #1897 landed a receiver-chain guard in src/pipeline/registry.c that fixed a related fabricated-edge shape (I ran into it separately on #2121). I could not find any commit touching internal/cbm/lsp/cs_lsp.c in that same range, so if #1897 is why this now resolves correctly, it would be a side effect through the generic fallback path rather than a direct fix to the C# resolver.

    Not clearing this since I can't confirm it's actually fixed rather than just not reproducing in my setup, but wanted to flag that current main behaves correctly on the exact repro given here.

  5. DeusData commented on Sep 25, 2026

    @DeusData
    Owner

    Thank you so much, @ersintarhan, for this report. The minimal two-project repro, the alias extension and the breadth numbers made this very easy to pin down. Thanks also to @AmirF194 for testing against main; your "correct on main" result was real, just order-dependent.

    The root cause: C# node names in the graph are built from file paths, not namespaces, so the resolver's namespace and using lookups never matched a project type, and among same-named classes the one registered first won. It was never your using that was wrong.

    The fix (#2347) records each type's declared namespace and binds names the way C# does: enclosing namespaces first, then using directives; fully-qualified names and using X = Ns.Type aliases work too. It also treats partial pieces and reference-assembly copies of one type as the same type, so a method declared in another file of a partial class is still found. On dotnet/runtime this moved about 6,200 CALLS edges from classes the caller cannot see to the ones it can, and added about 8,400 correct edges into partial classes.

    Once the fix ships, please reindex from scratch (delete the project and index again) so existing edges are recomputed. Thanks again!

  6. ersintarhan commented on Sep 28, 2026

    @ersintarhan
    Author

    Verified on the original repo from this report (the private one — now 53,310 nodes / 154,398 edges), built from #2347 head b220ecec, fresh from-scratch index.

    The same-name families called out here now resolve correctly, verified against the callers' actual using directives:

    • UserClaimPrincipleFactory (BackOffice / Platform / Partner): Platform variant has 0 callers (it no longer "gains" the production calls), Partner and BackOffice each get their own real ones.
    • IdentityUserStorage (3 variants): 10 / 1 / 8 callers, each set matching the variant's project.
    • IdentityExtensions (4 variants incl. the Platform.Database.Models.Identity one): 27 / 33 / 8 / 3 — every caller set scoped to its own namespace.

    The nastiest mode from my earlier comment — same file, plain using + using-alias — is also fixed. IdentityContractTests.cs has both using Platform.Partner.Identity.ClaimPrincipleFactories; and using BackOfficeClaimsFactory = ...BackOffice...UserClaimPrincipleFactory;: the plain new UserClaimPrincipleFactory(...) calls bind to the Partner variant, the BackOfficeClaimsFactory alias calls bind to BackOffice. Source-verified line by line. On 0.10.8 these were the no-edge / phantom-edge cases.

    Thanks @DeusData — happy to close once this ships. (Will reindex from scratch per the release note.)

  7. ersintarhan commented on Sep 28, 2026

    @ersintarhan
    Author

    Full-repo sweep follow-up (all 2,735 CALLS edges into same-name families, adjudicated against source): the modes reported here — plain using, using-alias, phantom variant absorbing calls — are fixed by #2347 ✅. The sweep did surface three residual mis-binding patterns (~125 edges, source-verified, incl. nested-type constructors like new Command.Result(...) and chained-constructor receivers like new GrainA.Handler(dep).Handle(...) losing the receiver). Details and repro sketches: #2347 (comment) — probably worth handling before the release, but they're a strict subset of what was broken on 0.10.8.

  8. DeusData commented on Oct 9, 2026

    @DeusData
    Owner

    Fixed on main in 50d19f8 (#2347). Thank you very much, @ersintarhan: the report itself, and then your verification on the original repository and the full sweep of all 2,735 CALLS edges into same-name families, gave this fix a check no synthetic fixture could.

    C# types now carry their declared namespace, and a type name binds to the declaration visible from the caller in C# lookup order: enclosing namespaces innermost outward, then using namespaces, with using aliases bound the same way. It ships in the next release; existing indexes need a reindex.

    The three residual patterns your sweep found are not part of this merge. Your follow-up #2401 fixes the two type-name-resolution shapes and traces the third to other layers. With #2347 on main, it can now be reviewed against main directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions