Repository navigation
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
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 inboundon the correct (BackOffice) variant returnscallers_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
- I searched existing issues and this is not a duplicate. (Closest: TypeScript: cross-file method calls produce no CALLS edge — type-aware tier resolves same-file calls only, so trace_path(inbound) returns 0 callers #1354 TypeScript same-file-only resolution, method calls through object instances/pointers are dropped in CALLER EDGES #1153 instance calls — different language/shape.)
- My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Activity
ersintarhan commentedon Sep 9, 2026
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
UserIdentityInfo.ParseFromClaim×3 (BackOffice prod, plainusing)Contoso.Platform.Identity.UserIdentityInfoBackOfficeIdentityInfo.ParseFromClaim×4 (using BackOfficeIdentityInfo = Contoso.Platform.BackOffice.Identity.UserIdentityInfo;)Contoso.Platform.Identity.PlayerIdentityInfo.ParseFromClaimPartnerIdentityInfo.ParseFromClaim×4Contoso.Platform.Partner.Identity.PartnerIdentityInfoSecond family,
UserClaimPrincipleFactory(3 variants: Platform / BackOffice / Partner):new UserClaimPrincipleFactory(...)then.CreateAsync(...)— the Partner one, resolved by a plainusing— attached toContoso.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..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:
PartnerIdentityInforesolves fine in the very same file where the alias calls go wrong)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:
Fresh re-index, then the same Cypher:
Two observations from this:
usingmis-attachment is stable across re-indexes and file additions —Handler.Handlekeeps landing on the root-namespace variant.ParseFromClaimcall 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-
usingcall also landed on the wrong variant — so plain-usingbreakage does not depend on the alias file being present.AmirF194 commentedon Sep 10, 2026
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:
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.
DeusData commentedon Sep 25, 2026
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
usinglookups never matched a project type, and among same-named classes the one registered first won. It was never yourusingthat was wrong.The fix (#2347) records each type's declared namespace and binds names the way C# does: enclosing namespaces first, then
usingdirectives; fully-qualified names andusing X = Ns.Typealiases work too. It also treatspartialpieces 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!
ersintarhan commentedon Sep 28, 2026
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
usingdirectives: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. thePlatform.Database.Models.Identityone): 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.cshas bothusing Platform.Partner.Identity.ClaimPrincipleFactories;andusing BackOfficeClaimsFactory = ...BackOffice...UserClaimPrincipleFactory;: the plainnew UserClaimPrincipleFactory(...)calls bind to the Partner variant, theBackOfficeClaimsFactoryalias 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.)
ersintarhan commentedon Sep 28, 2026
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 likenew Command.Result(...)and chained-constructor receivers likenew 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.fix(cs-lsp): bind same-name C# types by declared namespace (#2120)
fix(cs-lsp): resolve nested types through type qualifiers, bind globa…
fix(cs-lsp): bind same-name C# types by declared namespace (#2120)
Merge pull request #2347 from DeusData/fix/issue-2120
DeusData commentedon Oct 9, 2026
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
usingnamespaces, withusingaliases 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.
Merge feat/doc-links-core (with main) into feat/doc-links-documents