You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A C# call through an interface lands on the interface's method node and stops there. The implementation is a separate node with nothing joining the two, so any chain that passes through a constructor-injected dependency is cut at that point.
Two .Build() nodes, ireport_ireport_build and report_report_build, and no edge between them. Go calls the interface one, Format hangs off the implementation one, so a directed walk from .Go() never reaches .Format(). The receiver typing is doing its job here: _report is correctly typed as IReport, and the interface really is what the call site names.
How much it costs on a real codebase
A .NET 8 service, 1457 C# files, constructor injection throughout (Scrutor assembly scanning, so every dependency is an interface):
1511 calls edges land on a method owned by an interface and dead-end there
115 interface types have at least one implementer
107 of those 115 have exactly one implementer
So on this corpus the ambiguous case is 8 types out of 115. The rest have one possible target each.
graphify path between two ends of such a chain returns "No directed path found", and affected stops at the interface, which makes a blast radius look far smaller than it is.
What other languages already do here
Pascal has the closest analogue, and its docstring states the principle I want to borrow:
walking inherits is a structurally justified resolution, not a heuristic guess; guessing by name across an entire multi-thousand-file corpus is not the same bet.
resolve_pascal_inherited_calls walks the caller's inherits chain across files and emits an edge only when exactly one class at the nearest matching level owns a same-named method. resolve_ruby_member_calls uses the same single-owner guard. Interface dispatch in C# is the same kind of bet in the other direction: instead of walking up to find where a method is declared, walk down from a declaration to the one type that implements it.
Proposed shape
A csharp_interface_dispatch resolver in the tail registry that adds a member-level edge from an interface method to the implementing method:
the interface has exactly one implementer (implements edges into it)
that implementer owns exactly one method of the same name
zero or several candidates at either step leaves the pair alone
Confidence INFERRED, not EXTRACTED. The target is structurally forced when there is one implementer, but the source text does not name it, and that is what the existing tiers mean.
On the corpus above this emits 862 edges, +2.8% on 30852, and skips nothing as ambiguous. It reconnects the 1511 dead-ending call edges through 862 links rather than 1511, because the join is per method rather than per call site.
Alternatives I ruled out
Retarget the existing call from the interface method to the implementation. Loses the fact that the call site names the interface, which is true and worth keeping.
Add caller -> implementationcalls edges. 1511 new edges on that corpus, and it inflates every caller's call list with a target the source never mentions.
Reuse implements at member level. Its type-level direction is implementation to interface, and a directed walk needs the opposite direction to be useful, so overloading the name would make traversal read backwards.
Open question: dispatches_to is my guess at a name that fits the existing vocabulary. Happy to use whatever you prefer.
Related
Add type-aware cross-file resolver for C# #355 proposes a full C# type registry that resolves receivers through fields, properties, locals, this, base and inheritance chains. It is open at +9900/-593 and covers much more than this. This issue is deliberately one narrow slice with one guard, so it can be reviewed on its own.
A C# call through an interface lands on the interface's method node and stops there. The implementation is a separate node with nothing joining the two, so any chain that passes through a constructor-injected dependency is cut at that point.
Three files:
Graph (graphify 0.9.48, edges other than
contains):Two
.Build()nodes,ireport_ireport_buildandreport_report_build, and no edge between them.Gocalls the interface one,Formathangs off the implementation one, so a directed walk from.Go()never reaches.Format(). The receiver typing is doing its job here:_reportis correctly typed asIReport, and the interface really is what the call site names.How much it costs on a real codebase
A .NET 8 service, 1457 C# files, constructor injection throughout (Scrutor assembly scanning, so every dependency is an interface):
callsedges land on a method owned by an interface and dead-end thereSo on this corpus the ambiguous case is 8 types out of 115. The rest have one possible target each.
graphify pathbetween two ends of such a chain returns "No directed path found", andaffectedstops at the interface, which makes a blast radius look far smaller than it is.What other languages already do here
Pascal has the closest analogue, and its docstring states the principle I want to borrow:
resolve_pascal_inherited_callswalks the caller'sinheritschain across files and emits an edge only when exactly one class at the nearest matching level owns a same-named method.resolve_ruby_member_callsuses the same single-owner guard. Interface dispatch in C# is the same kind of bet in the other direction: instead of walking up to find where a method is declared, walk down from a declaration to the one type that implements it.Proposed shape
A
csharp_interface_dispatchresolver in the tail registry that adds a member-level edge from an interface method to the implementing method:Guards, matching the existing resolvers:
implementsedges into it)Confidence
INFERRED, notEXTRACTED. The target is structurally forced when there is one implementer, but the source text does not name it, and that is what the existing tiers mean.On the corpus above this emits 862 edges, +2.8% on 30852, and skips nothing as ambiguous. It reconnects the 1511 dead-ending call edges through 862 links rather than 1511, because the join is per method rather than per call site.
Alternatives I ruled out
caller -> implementationcallsedges. 1511 new edges on that corpus, and it inflates every caller's call list with a target the source never mentions.implementsat member level. Its type-level direction is implementation to interface, and a directed walk needs the opposite direction to be useful, so overloading the name would make traversal read backwards.Open question:
dispatches_tois my guess at a name that fits the existing vocabulary. Happy to use whatever you prefer.Related
this,baseand inheritance chains. It is open at +9900/-593 and covers much more than this. This issue is deliberately one narrow slice with one guard, so it can be reviewed on its own.new Foo(...)produces no edge, so message publishers and locally built types look unused #2997 is the other half of what broke tracing on this codebase.I have a patch with tests and will open a PR against
v8.