Skip to content

csharp: gate the lone-member guard keep on namespace visibility - #519

Merged
zzet merged 1 commit into
zzet:mainfrom
pbednarcik:fix/csharp-lone-member-ns-gate
Aug 9, 2026
Merged

csharp: gate the lone-member guard keep on namespace visibility#519
zzet merged 1 commit into
zzet:mainfrom
pbednarcik:fix/csharp-lone-member-ns-gate

Conversation

@pbednarcik

Copy link
Copy Markdown
Contributor

Problem

Field-testing on my production C# codebase: every LINQ .Where(...) the
type environment cannot type gets bound to the one indexed method named
Where — a static helper in an internal tooling namespace no caller
imports — at 0.7 ast_inferred. The forward edges are wrong, and the
reverse side is worse: callers on that helper returns every LINQ-using
method in the repo, which makes a niche utility look load-bearing.

.Where is not special. Any name that shadows a BCL or library member
and has exactly one indexed homonym fails the same way. Measured on my
repo after the fix, the affected population was ~7,000 call edges across
50 names — Join, Where, Any, StartsWith, Round, plus library
shapes like FluentValidation's WithMessage/NotEmpty and Newtonsoft's
SerializeObject. The true targets are all external (System.Linq,
string.Join, vendor packages), so they are not in the graph and can
never out-argue the indexed decoy.

Root cause

Two mechanisms, designed as a pair:

  1. The locality fallback's lone-member lift (resolveMethodCall): a
    member call with exactly one same-name method candidate is treated as
    a grounded inference and stamped ast_inferred/0.7 — the comment
    explicitly counts on the guard's lone-definition exception to keep it.
  2. loneMemberDefnKeep (cross_pkg_guard.go) then does exactly that: the
    bind fails import-reachability, but a member call with an unknown
    receiver type and a lone in-repo definition of the name is kept —
    "there is nowhere else the call could bind."

That last premise is what breaks: for BCL-shadowing names there IS
somewhere else the call binds — outside the graph. The lone indexed
homonym is not the only candidate, just the only visible one.

Fix

For C# targets, loneMemberDefnKeep now demands one more piece of
corroboration before keeping: the candidate's namespace (scope_ns)
must be visible from the calling file — its own namespace chain or a
using directive, project-scoped globals included, reusing the same
namespace-set evidence the extension-visibility machinery already
maintains.

The gate is narrowing-only, matching the existing extension-visibility
policy: a file with no recorded usings is stale or partial data, not
evidence of absence, so the keep is lost only when using evidence exists
and the namespace is not in it. (The suite's
TestResolveCSharpExtension_NothingVisibleFallsBackToUnique pins that
policy and caught my first, stricter cut — the shipped version keeps it
green.)

The rationale mirrors the language where it can: for the member shapes
that can bind cross-namespace without the file naming the type —
extension methods and class-qualified statics — namespace visibility is
the language rule, not a heuristic. For instance calls through
var-typed receivers it is a heuristic, and the trade is deliberate: a
rare silent miss (healed by csharp-types enrichment whenever the
receiver becomes typeable) instead of a systematic false family.

Scoped by target.Language == "csharp"; no other language's keep
changes. Reverted sites take the guard's normal path — back to the
unresolved placeholder with the guard_reverted stamp, honest instead
of wrong.

Behavior notes

  • A lone in-repo method in a never-imported namespace, called through an
    untypeable receiver that really is that type, now sits unresolved
    until enrichment can type the receiver. With using evidence absent the
    old keep still applies.
  • Existing stores heal at each file's next resolve pass (the guard runs
    on both the batch and incremental paths), fully on a rebuild —
    enrichment alone cannot retarget these, since the receivers are
    precisely the ones it cannot type.
  • Extension-method binds are untouched — they carry the
    extension_method resolution and are kept (or not) by their own
    visibility rule, which this gate deliberately parallels.

Tests

TDD, red watched first on the exact field shape:

  • TestCSharpLoneMember_UnimportedNamespaceReverts — real extractor +
    ResolveAll; an unresolvable .Where(...) with the lone Where in a
    never-imported namespace failed red bound at 0.7 ast_inferred, green
    now reverts to the unresolved placeholder (with a non-vacuity counter
    so an ID-scheme drift can't green it silently).
  • TestCSharpLoneMember_ImportedNamespaceKeeps — control: the same
    shape with the namespace imported keeps the grounded 0.7 bind.
  • TestResolveCSharpExtension_NothingVisibleFallsBackToUnique — the
    pre-existing pin that shaped the final design, still green.

Suites: resolver failure name-set byte-identical to clean main on my
Windows box (pre-existing platform failures only, worktree-verified);
tstypes and languages fully green.

Validation

Deployed on a rebuilt binary with a from-scratch index:

  • My counted C# fixture repo's decoy cell flipped from the pinned false
    bind to zero — the declared fix signal — with every other pinned cell
    stable, including the facade-fix cells from csharp: stop member calls from binding to the calling method itself #516 (their heal path
    routes through import-reachable targets and never consults this keep).
  • On the production repo: the four pinned .Where false edges from the
    original report are gone, the decoy's caller list dropped from
    repo-wide to its three real same-subsystem callers, the ~7,000-edge
    lone-homonym population across 50 names is honest-unresolved, and the
    1,000+ extension-method binds are unchanged.

@zzet
zzet merged commit c1babdd into zzet:main Aug 9, 2026
10 checks passed
@pbednarcik
pbednarcik deleted the fix/csharp-lone-member-ns-gate branch August 9, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants