Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

409 lines
14 KiB
Go

package resolver
import (
"strconv"
"github.com/zzet/gortex/internal/graph"
)
// Member-level C# interface-dispatch synthesis: the implements-family cascade.
//
// Roslyn — the reference C# resolver — treats an interface method and every
// method that implements it (directly, or through a base class that implements
// the interface) as ONE linked family, and reports the union of the family's
// call sites for every member. Two mechanisms feed that union:
//
// 1. Through-interface calls: `x.Convert(1)` where `x` is typed as the
// interface binds to the interface member node. Those calls must surface
// on every concrete implementation.
// 2. Sibling implementation calls: a converter's own `Convert(-number)`
// (a self/recursive or same-class call) binds directly to that class's
// method node — it never touches the interface node. Roslyn still reports
// that site for the interface method AND for every sibling implementation.
//
// A fan-out anchored only on calls bound to the interface member (mechanism 1)
// misses the dominant mass of real-corpus usages, which are mechanism 2. This
// pass therefore builds the full implements-family per (interface, method
// name) — the interface member plus the same-named method on every type whose
// implements/extends chain reaches the interface — and, for every call edge
// bound to ANY family member, synthesizes call edges to ALL other members.
//
// Tier: ast_inferred / ConfidenceTyped (non-speculative, type-keyed) — the
// same tier the sibling one-to-many dispatch passes use (MediatR Publish ->
// every handler, Spring publishEvent -> every listener), so the cascade rides
// in the DEFAULT find_usages / get_callers result. Family membership is
// established strictly through the implements/extends chain — never by name
// matching alone — so unrelated same-named methods are never linked.
// csharpIfaceDispatchCap bounds the family size (every interface-member
// overload node plus every implementing method node). C# overloads mint one
// node each, so a broadly-localised interface — one implementation per locale,
// several overloads per class — legitimately runs to ~70+ member nodes
// (Humanizer's INumberToWordsConverter.Convert family measures 72) and is
// exactly the shape this pass exists to cover, so the cap sits above it with
// headroom; a family wider than the cap is dropped whole as noise
// (pathological hub interfaces like a monorepo-wide Dispose).
const csharpIfaceDispatchCap = 128
// MetaViaMethodSetInference is the Meta["via"] marker the resolver stamps on
// EdgeImplements edges minted by structural method-set inference (as opposed
// to a source-declared base list). Hierarchy-walking passes that must follow
// only declared subtyping filter on it.
const MetaViaMethodSetInference = "method-set-inference"
// csharpCallSiteKey identifies one attributed call site. Line is part of the
// key on purpose: ground truth is line-based, so every call-site line of every
// family member must fan out to every other member, not one edge per
// (caller, callee) pair.
func csharpCallSiteKey(from, to, filePath string, line int) string {
return from + "\x00" + to + "\x00" + filePath + "\x00" + strconv.Itoa(line)
}
// ResolveCSharpInterfaceDispatch fans every call bound to a member of a C#
// implements-family out to all other members of that family. Returns the
// number of fan-out edges landed.
func ResolveCSharpInterfaceDispatch(g graph.Store) int {
if g == nil {
return 0
}
// Subtype adjacency over the resolved type hierarchy: super → subs.
// EdgeImplements and EdgeExtends both count — a class reaches an interface
// through any chain of base classes / base interfaces (e.g. Afrikaans
// extends Genderless which implements INumberToWordsConverter).
//
// Only SOURCE-DECLARED hierarchy edges qualify. The method-set inference
// pass mints EdgeImplements from every type whose bare method names cover
// an interface — with a single-method interface like IOrdinalizer.Convert
// that "links" every Convert-bearing class in the repo, and a family built
// over it would union unrelated hierarchies (NumberToWords converters into
// the Ordinalizer family). Those edges carry the inference marker; skip
// them. Origin cannot discriminate here: it is stamped/backfilled at
// different pipeline stages, so declared and inferred edges converge.
// This pass can run BEFORE the resolver has bound base-list targets (the
// pipeline settles hierarchy targets across several later passes), so an
// `unresolved::Name` target is resolved here by an exact, same-repo,
// unique type/interface name lookup — ambiguity means skip, never guess.
children := map[string][]string{}
for _, kind := range []graph.EdgeKind{graph.EdgeImplements, graph.EdgeExtends} {
for e := range g.EdgesByKind(kind) {
if e == nil || e.From == "" || e.To == "" {
continue
}
if e.Meta != nil && e.Meta["via"] == MetaViaMethodSetInference {
continue
}
toID := e.To
if graph.IsUnresolvedTarget(toID) {
toID = csharpResolveHierarchyTarget(g, e.From, toID)
if toID == "" {
continue
}
}
children[toID] = append(children[toID], e.From)
}
}
if len(children) == 0 {
return 0
}
// implementation/interface type node id → member name → method nodes.
// Every overload matters: C# overloads mint one node each (Convert,
// Convert_L39, ...) sharing the same Name, and real call sites bind to any
// of them — a single-node-per-name projection would silently drop the
// overload the corpus actually calls through.
membersByType := csharpMemberMethodsAllByType(g)
if len(membersByType) == 0 {
return 0
}
// Anchor discovery: every C# interface member method node, via its
// EdgeMemberOf owner, grouped by (interface, name) so the interface's own
// overload nodes land in ONE family rather than seeding duplicates.
type anchorGroup struct {
ifaceID string
name string
repoPrefix string
nodeIDs []string
}
anchorGroups := map[string]*anchorGroup{}
var anchorOrder []string
for e := range g.EdgesByKind(graph.EdgeMemberOf) {
if e == nil || graph.IsUnresolvedTarget(e.To) {
continue
}
m := g.GetNode(e.From)
if m == nil || m.Kind != graph.KindMethod || m.Language != "csharp" || !csharpIsIfaceMember(m) {
continue
}
key := e.To + "\x00" + m.Name
ag := anchorGroups[key]
if ag == nil {
ag = &anchorGroup{ifaceID: e.To, name: m.Name, repoPrefix: m.RepoPrefix}
anchorGroups[key] = ag
anchorOrder = append(anchorOrder, key)
}
ag.nodeIDs = append(ag.nodeIDs, m.ID)
}
if len(anchorGroups) == 0 {
return 0
}
// Descendant closure per interface, computed once and shared across that
// interface's anchors (one per member name).
descCache := map[string][]string{}
descendants := func(ifaceID string) []string {
if d, ok := descCache[ifaceID]; ok {
return d
}
var out []string
visited := map[string]bool{ifaceID: true}
queue := append([]string(nil), children[ifaceID]...)
for len(queue) > 0 {
t := queue[0]
queue = queue[1:]
if visited[t] {
continue
}
visited[t] = true
out = append(out, t)
queue = append(queue, children[t]...)
}
descCache[ifaceID] = out
return out
}
// Build families and the member → families index.
type family struct {
ifaceID string
members []string
}
var families []family
famsOfMember := map[string][]int{}
for _, key := range anchorOrder {
ag := anchorGroups[key]
memberIDs := append([]string(nil), ag.nodeIDs...)
anchorSet := map[string]bool{}
for _, id := range ag.nodeIDs {
anchorSet[id] = true
}
implCount := 0
for _, sub := range descendants(ag.ifaceID) {
byName := membersByType[sub]
if byName == nil {
continue
}
for _, m := range byName[ag.name] {
if m == nil || anchorSet[m.ID] {
continue
}
// In-repo only: cross-repo dispatch is CrossRepoResolver's domain.
if m.RepoPrefix != ag.repoPrefix {
continue
}
memberIDs = append(memberIDs, m.ID)
implCount++
}
}
// A family needs an interface member plus at least one implementation
// to cascade; one wider than the cap is dropped whole as noise.
if implCount == 0 || len(memberIDs) > csharpIfaceDispatchCap {
continue
}
idx := len(families)
families = append(families, family{ifaceID: ag.ifaceID, members: memberIDs})
for _, id := range memberIDs {
famsOfMember[id] = append(famsOfMember[id], idx)
}
}
if len(families) == 0 {
return 0
}
// Existing resolved call sites, keyed per line, so a fan-out edge never
// duplicates a real call at the same site (a caller that already reaches
// the member directly, or a prior run of this pass).
existing := map[string]bool{}
for e := range g.EdgesByKind(graph.EdgeCalls) {
if e == nil || e.IsSpeculative() || graph.IsUnresolvedTarget(e.To) {
continue
}
existing[csharpCallSiteKey(e.From, e.To, e.FilePath, e.Line)] = true
}
var batch []*graph.Edge
seen := map[string]bool{}
for e := range g.EdgesByKind(graph.EdgeCalls) {
if e == nil || e.IsSpeculative() || graph.IsUnresolvedTarget(e.To) {
continue
}
// Never re-fan from this pass's own output — real call sites only.
if e.Meta != nil && e.Meta[MetaSynthesizedBy] == SynthCSharpIfaceDispatch {
continue
}
fams := famsOfMember[e.To]
if len(fams) == 0 {
continue
}
// Tier-gate the SOURCE: a typed or scope-resolved binding (and an
// untagged legacy edge, which carries unknown — not low — confidence,
// mirroring SuppressRedundantTextMatches) fans from any caller. A
// text_matched binding is a name-only guess that can land on a family
// member from a completely unrelated same-named method (an
// IOrdinalizer.Convert self-call text-matched into the
// INumberToWordsConverter family); those fan ONLY when the caller is
// itself a member of the same family — the intra-family self/sibling-
// call shape the weak tier legitimately carries (overload self-calls
// bind text_matched).
weakSource := e.Origin == graph.OriginTextMatched
var fromFams []int
if weakSource {
fromFams = famsOfMember[e.From]
if len(fromFams) == 0 {
continue
}
}
for _, fi := range fams {
if weakSource && !containsInt(fromFams, fi) {
continue
}
f := families[fi]
for _, member := range f.members {
if member == e.To {
continue
}
k := csharpCallSiteKey(e.From, member, e.FilePath, e.Line)
if existing[k] || seen[k] {
continue
}
seen[k] = true
batch = append(batch, csharpIfaceDispatchEdge(e, member, f.ifaceID, len(f.members)-1))
}
}
}
for _, ne := range batch {
g.AddEdge(ne)
}
return len(batch)
}
// csharpIsIfaceMember reports whether n is a bodyless (or default) interface
// member declaration emitted by the C# extractor.
func csharpIsIfaceMember(n *graph.Node) bool {
if n == nil || n.Meta == nil {
return false
}
v, _ := n.Meta["iface_member"].(bool)
return v
}
// csharpIfaceDispatchEdge builds one fan-out call edge from the call site e to
// another family member, at the non-speculative ast_inferred tier so it
// survives the default speculative filter on find_usages / get_callers. The
// fan-out width rides in candidate_count for auditing; only one implementation
// runs at a site, but Roslyn reports the reference on every family member and
// this pass mirrors that.
func csharpIfaceDispatchEdge(e *graph.Edge, to, ifaceTypeID string, fanout int) *graph.Edge {
ne := &graph.Edge{
From: e.From, To: to, Kind: graph.EdgeCalls,
FilePath: e.FilePath, Line: e.Line,
Origin: graph.OriginASTInferred,
Confidence: ConfidenceTyped,
ConfidenceLabel: graph.ConfidenceLabelFor(graph.EdgeCalls, ConfidenceTyped),
Meta: map[string]any{
"via": "csharp-iface-dispatch",
"iface_type": ifaceTypeID,
"candidate_count": fanout,
},
}
StampSynthesized(ne, SynthCSharpIfaceDispatch)
return ne
}
// csharpMemberMethodsAllByType is the overload-preserving variant of
// memberMethodNodesByType: type node id → member name → EVERY method node with
// that name (C# overloads mint one node per declaration, so a name maps to
// several nodes). Uses the backend's MemberMethodsByType projection when
// available, else walks EdgeMemberOf.
func csharpMemberMethodsAllByType(g graph.Store) map[string]map[string][]*graph.Node {
if cap, ok := g.(graph.MemberMethodsByType); ok {
raw := cap.MemberMethodsByType()
if len(raw) == 0 {
return nil
}
out := make(map[string]map[string][]*graph.Node, len(raw))
for typeID, methods := range raw {
set := make(map[string][]*graph.Node, len(methods))
for _, m := range methods {
set[m.Name] = append(set[m.Name], &graph.Node{
ID: m.MethodID,
Kind: graph.KindMethod,
Name: m.Name,
FilePath: m.FilePath,
StartLine: m.StartLine,
RepoPrefix: m.RepoPrefix,
})
}
out[typeID] = set
}
return out
}
out := map[string]map[string][]*graph.Node{}
for e := range g.EdgesByKind(graph.EdgeMemberOf) {
method := g.GetNode(e.From)
if method == nil || method.Kind != graph.KindMethod {
continue
}
set := out[e.To]
if set == nil {
set = make(map[string][]*graph.Node)
out[e.To] = set
}
set[method.Name] = append(set[method.Name], method)
}
return out
}
// containsInt reports whether xs contains v. Family lists are tiny (a method
// belongs to one or two families), so a linear scan beats a map.
func containsInt(xs []int, v int) bool {
for _, x := range xs {
if x == v {
return true
}
}
return false
}
// csharpResolveHierarchyTarget binds an `unresolved::Name` base-list target to
// the unique same-repo C# type/interface node with that exact name, or ""
// when the caller is not C#, the name is unknown, or the name is ambiguous —
// a wrong hierarchy link unions unrelated families, so no guess is ever made.
func csharpResolveHierarchyTarget(g graph.Store, fromID, unresolvedTo string) string {
name := graph.UnresolvedName(unresolvedTo)
if name == "" {
return ""
}
from := g.GetNode(fromID)
if from == nil || from.Language != "csharp" {
return ""
}
var cand *graph.Node
for _, n := range g.FindNodesByNameInRepo(name, from.RepoPrefix) {
if n == nil || (n.Kind != graph.KindType && n.Kind != graph.KindInterface) {
continue
}
if n.Language != "csharp" {
continue
}
if cand != nil {
return "" // ambiguous — do not guess
}
cand = n
}
if cand == nil {
return ""
}
return cand.ID
}