Skip to content

csharp: the new scoping and attribution code is largely unpinned #726

Description

@zzet

Follow-up from the review of #677. Several behaviours added or changed there are not pinned by any test — reverting them leaves the whole suite green. Given that every round of that PR produced the next round's regressions, these are the surfaces most likely to drift silently.

All results below are from reverting the change in place and running GOWORK=off go test -count=1 ./internal/parser/languages/ ./internal/resolver/.

csharp_binding_scopes.go is entirely unpinned

Inserting if true { return } as the first statement of csharpCollectExtraBindingScopes — turning all 298 lines into a no-op — leaves both whole packages green.

The cause is that every new test asserts only the negative direction (the binder must not shadow at this call site). The file's actual purpose — that foreach variables, lambda parameters, out vars, patterns and using resources do shadow a same-named field inside their extent — has no coverage. A probe with a field _box and three methods (foreach (var _box in xs) { _box.Touch(); }, Array.ForEach(xs, _box => _box.Touch()), using (var _box = new PBag()) { _box.Touch(); }) goes from 0 to 3 spurious field reads under the mutation, and nothing notices.

A coverage run puts 43% of the file's lines at zero executions under the PR's own suite.

Two of the three per-record guard changes are inert or unpinned

The per-record typedRecordAt guard replaced the function-wide guard in three places. Only one is load-bearing:

guard revert leaves suite green? pinned by
tier 1 (b.typ != "", first var pass) no TestResolveCSharpTypedLocal_SiblingScopeRedeclarationKeepsItsRecord
tier 2 (b.typ != "", await pass) yes
shape pass (b.shape != "") yes

Instrumenting both conditions to log every disagreement and running the entire C# suite: the tier-2 guard never diverges at all — the per-record and function-wide answers are identical on every fixture in the repo. The shape guard diverges exactly once (Sibling.cs::SbRunner.Run, name conv) and reverting it changes no assertion, because tier 1 already rescues that site.

Either add a fixture that distinguishes each, or revert the two that buy nothing — a speculative untested guard change is exactly the surface that bit in previous rounds.

ambiguousAt is unpinned

Reverting it leaves the suite green, and the test written to restore its pin passes for a different reason than the one it names.

Two new tests cannot fail

  • TestResolveCSharpInterfaceDispatch_RespelledBasesKeepTheFamily passes unchanged at the merge base f9e3f442 and survives removal of the fix it is named for.
  • The widgetSurvives assertion in TestResolveCSharp_UnequalSpanMethodsSharingALine is vacuous — the fan-out it checks has no type-argument gate now that the gate is deferred.
  • The write half of TestResolveCSharpTypedLocal_AliveAcrossSwitchSections cannot fail: its fixture declares no field, so the "no field write may be minted" branch is unreachable.

Suggested approach

For each item above, the cheap check is the same one used to find it: revert the line, run the two packages, and confirm something goes red. Worth making that a habit for this extractor specifically — the mutation sweep took a few minutes and found five unpinned behaviours across a diff that was otherwise well tested.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions