Namespace-aware C# type-reference resolution - #401
Merged
Conversation
added 6 commits
July 29, 2026 17:48
Same-named C# types across namespaces resolved by lexicographic ID — every reference in one module could bind to a sibling module's type, orphaning the real target. Narrow candidates to namespaces the file imports (EdgeImports) or encloses (scope_ns prefixes) before ranking. Narrowing only: no visible namespace keeps the previous pick.
The compiler searches enclosing namespaces before using directives, so an internal type in the file's own namespace shadows a public same-named type in an imported one — the exported-first ranker was inverting that. Split the visible set into enclosing (deepest match wins) and imported tiers.
A file-scoped namespace declaration spans only its own statement in the tree-sitter AST — governed declarations are siblings, not children — so the ancestor walk never found it and every type in such a file lost its scope_ns. Fall back to scanning the compilation unit's children.
Canonicalisation stripped Shared.Reporting.Foo to Foo, so a fully qualified reference — written qualified precisely because the file imports nothing relevant — lost the one namespace fact the source stated outright. Stamp the dotted spelling as Meta["target_fqn"] and narrow by qualifier suffix first, above the enclosing/imported tiers.
- Narrowing gates to type/interface candidates in the dotnet family: methods carry scope_ns too and could steal the bind (or lose the edge on the type-only path); razor components now participate. - Using evidence rides the file node as Meta["usings"] — resolveImport rewrites a using edge to a file-node target when the namespace tail matches a directory basename, which silently emptied the imported tier. Aliases and using-static grant no bare-name visibility and are excluded. Edge shapes remain as fallback for pre-stamp graphs. - Nested block namespaces join outer-to-inner (A.B, not B). - Base-list spellings carry target_fqn so extends/implements reach the qualifier tier; phpNarrowByTargetFQN rejects dotted C# qualifiers.
zzet
approved these changes
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
C# type references resolve by bare name, so when several types share a name across namespaces — routine in module-per-domain solutions, where each module declares its own
XProfile/XConfiguration— the ranker ties and falls to the lexicographically-smallest ID. Measured on a real solution: all 9 references to one module's mapping profile bound to a sibling module's same-named profile, leaving the real target a dead-code false positive and giving the rival 9 phantom callers.The evidence to disambiguate is already in the graph: every using directive is an
EdgeImportsoff the file node, and every C# type carriesMeta["scope_ns"]. This PR joins the two in a narrowing step that applies the compiler's own lookup order — written qualifier, then enclosing namespaces (deepest wins), then using directives — ahead ofbestTypeCandidate's ranking. Same narrowing-only contract asphpNarrowByTargetFQN: when no candidate namespace is visible, the previous ranking stands untouched, so a binding can sharpen but never be lost.Changes
csharpNarrowByNamespace(csharp_ns_narrow.go), hooked beside the PHP narrow inresolveTypeOrFuncandresolveTypeRef. Only type/interface candidates in the dotnet family participate — methods carryscope_nstoo and would otherwise steal the bind (or lose the edge on the type-only path). Per-file namespace sets are memoised behind an RWMutex (built lazily inside the parallel workers, cleared with the per-pass lookup caches — theimportFilesByCallerdiscipline).Meta["usings"], stamped at extraction with C# visibility semantics (aliases andusing staticexcluded — they grant no bare-name namespace visibility). The per-directive import edges are not a stable source:resolveImportrewrites one to a file-node target whenever the namespace tail matches a directory basename — the standard folder-mirrors-namespace layout — which would silently and order-dependently empty the tier. Edge shapes remain a fallback for pre-stamp graphs.internaltype in the file's own namespace must shadow apublicsame-named type in an imported one — the exported-first ranker was inverting that on real code.fix(csharp): C#10 file-scoped namespaces (namespace X;) never gotscope_ns— the governed declarations are AST siblings of the directive, not children, so the ancestor walk missed them. Every type in such files was invisible to any namespace evidence (and to thescope_ns-keyed partial merge). Nested block namespaces now also join outer-to-inner (A.B, previously the bare inner segment).A.B.FootoFoo, discarding the one namespace fact the source states outright — and aggregator-style files qualify precisely because they import nothing. The dotted spelling ridesMeta["target_fqn"]on reference, instantiates, and base-list edges (the key the PHP extractor already uses; the PHP reader now rejects dotted C# qualifiers) and narrows by dot-boundary suffix first, covering C#'s partially-qualified forms.Known gaps (documented, not silent)
using X = Full.Name;) is not modelled — aliases are excluded from the using set rather than misread as namespace imports; a file importing only via aliases falls back to the previous ranking.global usingcounts for its declaring file only.internaltypes across project boundaries) is not consulted — candidates are namespace-filtered, not assembly-filtered.Ns.Outer.Inner) readsNs.Outeras a namespace; standard namespace-qualified spellings are unaffected.scope_nsand never match a tier; they resolve exactly as before.Real-world results (reference Autofac + EF solution, 10,375 files)
scope_nsat all to fully resolvable.Testing
go test -race ./...) — Linux-equivalent scope; Windows dev box shows only the known pre-existing failuresscope_ns, usings Meta incl. alias/static exclusion, qualified-spellingtarget_fqnon reference and base-list forms)Checklist
importFilesByCaller)