Follow-up from the review of #677. Three shapes where the merged head is less correct than the previous head. None has a verified patch yet — each needs a design decision, not a one-liner.
Revisions referenced: f9e3f442 (merge base), 1a939e45 (previous head), 786a3929 (merged head).
1. The byte-interval fallback attributes a call to a member that does not contain it
enclosingAt (internal/parser/languages/csharp.go, ~line 2129) now ends with return l.enclosing(line) where it previously refused. Its comment justifies this as letting "a member kind without them (property, indexer, field initializer) sharing a line with one that has them" degrade to line attribution rather than lose the call.
That mechanism does not exist. buildFuncRanges selects only KindFunction / KindMethod, and both emitMethod and emitConstructor record a funcBytes entry unconditionally. So every candidate in l.ranges always has an extent, properties and indexers are never in ranges at all, and the fallback cannot return the intended member — it can only return a different one.
namespace App {
public class Bag { public int Tally(){return 1;} public void Clear(){} }
public class Box {
private readonly Bag _bag = new Bag();
public int Count => _bag.Tally(); public void Reset() { _bag.Clear(); }
}
}
| revision |
call edges from Box.Reset |
f9e3f442 |
*.Tally (false — lives in Count), *.Clear |
1a939e45 |
*.Clear only |
786a3929 |
*.Tally (false, back), *.Clear |
It also crosses class boundaries:
public class A { public void M() { Helper.Stop(); } } public class B { public int P => Helper.Go(); }
gives A.M -> *.Go at the merge base and merged head, nothing at the previous head. ambiguousAt does not mark these (only one range covers the line), so the false edge carries unmarked, confident receiver evidence.
For the record: this fallback was my own review recommendation in the previous round, and it was wrong — I diagnosed the dropped calls without checking that the members losing them were never attribution candidates. The author implemented it faithfully.
Two honest options: record byte extents for property / indexer / field-initializer members so they become real attribution targets, or restore the refusal.
2. A legally repeated base class across partial parts becomes implements from a class to a class
C# permits every partial part to repeat the base class as long as the parts agree. The shared extends budget threaded through emitCSharpBaseList is a bare boolean that never inspects the base target, so the second and later fragments get demoted to EdgeImplements.
namespace App {
public class BaseA {}
public interface IA {}
public interface IB {}
public partial class Box : BaseA, IA {}
public partial class Box : BaseA, IB {}
}
| revision |
base edges from Box |
f9e3f442 |
extends BaseA, implements IA (second fragment dropped) |
1a939e45 |
extends BaseA, extends BaseA, implements IA, implements IB |
786a3929 |
extends BaseA, implements BaseA, implements IA, implements IB |
A three-part version yields two false implements BaseA edges. This feeds interface-implementor fan-out and class-hierarchy walks with a class posing as an interface. The repeat should be dropped, not demoted — it names the same base class the budget already spent.
3. Sibling var redeclaration flipped the function-wide type env from first-wins to last-wins
Swapping the tier-1/tier-2 var guards from the function-wide map to typedRecordAt correctly mints the per-scope offset records, but it also means every sibling redeclaration now runs setLocalType, whose first line writes env[l.name] = typeName on the function-wide tenvByOwner. Offset-aware records are consulted only for a bare receiver; a chained receiver falls through to resolveChainType(..., tenvByOwner[callerID], ...) and an awaited local to csharpAwaitedCallType(..., tenvByOwner[owner], ...), both offset-blind.
public sealed class ChainFlow {
public void Run() {
{ var h = new MakerA(); h.Make().Ping(); } // Ping receiver should be Widget
{ var h = new MakerB(); h.Make().Ping(); } // Ping receiver should be Gadget
}
}
receiver_type on the two Ping calls:
| revision |
first site |
second site |
f9e3f442 |
Widget ✅ |
Widget ❌ |
1a939e45 |
Widget ✅ |
Widget ❌ |
786a3929 |
Gadget ❌ |
Gadget ✅ |
So this is first-wins → last-wins, not a clean regression: every revision is wrong on one of the two sites, and the merged head moves which one. It is still a regression for the first site, and the wrongness is confident rather than absent — the resolver binds on it. The real fix is to make the chained and awaited paths offset-aware like the bare-receiver path, rather than to pick a different wrong winner.
Follow-up from the review of #677. Three shapes where the merged head is less correct than the previous head. None has a verified patch yet — each needs a design decision, not a one-liner.
Revisions referenced:
f9e3f442(merge base),1a939e45(previous head),786a3929(merged head).1. The byte-interval fallback attributes a call to a member that does not contain it
enclosingAt(internal/parser/languages/csharp.go, ~line 2129) now ends withreturn l.enclosing(line)where it previously refused. Its comment justifies this as letting "a member kind without them (property, indexer, field initializer) sharing a line with one that has them" degrade to line attribution rather than lose the call.That mechanism does not exist.
buildFuncRangesselects onlyKindFunction/KindMethod, and bothemitMethodandemitConstructorrecord afuncBytesentry unconditionally. So every candidate inl.rangesalways has an extent, properties and indexers are never inrangesat all, and the fallback cannot return the intended member — it can only return a different one.Box.Resetf9e3f442*.Tally(false — lives inCount),*.Clear1a939e45*.Clearonly786a3929*.Tally(false, back),*.ClearIt also crosses class boundaries:
gives
A.M -> *.Goat the merge base and merged head, nothing at the previous head.ambiguousAtdoes not mark these (only one range covers the line), so the false edge carries unmarked, confident receiver evidence.For the record: this fallback was my own review recommendation in the previous round, and it was wrong — I diagnosed the dropped calls without checking that the members losing them were never attribution candidates. The author implemented it faithfully.
Two honest options: record byte extents for property / indexer / field-initializer members so they become real attribution targets, or restore the refusal.
2. A legally repeated base class across partial parts becomes
implementsfrom a class to a classC# permits every partial part to repeat the base class as long as the parts agree. The shared extends budget threaded through
emitCSharpBaseListis a bare boolean that never inspects the base target, so the second and later fragments get demoted toEdgeImplements.Boxf9e3f442extends BaseA,implements IA(second fragment dropped)1a939e45extends BaseA,extends BaseA,implements IA,implements IB786a3929extends BaseA,implements BaseA,implements IA,implements IBA three-part version yields two false
implements BaseAedges. This feeds interface-implementor fan-out and class-hierarchy walks with a class posing as an interface. The repeat should be dropped, not demoted — it names the same base class the budget already spent.3. Sibling
varredeclaration flipped the function-wide type env from first-wins to last-winsSwapping the tier-1/tier-2
varguards from the function-wide map totypedRecordAtcorrectly mints the per-scope offset records, but it also means every sibling redeclaration now runssetLocalType, whose first line writesenv[l.name] = typeNameon the function-widetenvByOwner. Offset-aware records are consulted only for a bare receiver; a chained receiver falls through toresolveChainType(..., tenvByOwner[callerID], ...)and an awaited local tocsharpAwaitedCallType(..., tenvByOwner[owner], ...), both offset-blind.receiver_typeon the twoPingcalls:f9e3f4421a939e45786a3929So this is first-wins → last-wins, not a clean regression: every revision is wrong on one of the two sites, and the merged head moves which one. It is still a regression for the first site, and the wrongness is confident rather than absent — the resolver binds on it. The real fix is to make the chained and awaited paths offset-aware like the bare-receiver path, rather than to pick a different wrong winner.