package resolver import "github.com/zzet/gortex/internal/graph" // isCSharpExtension reports whether n is a C# extension method (a static method // whose first parameter carries the `this` modifier). Such methods are bound // only by the type-directed extension rule, never by the locality fallback. The // Language check keeps this C#-only: other languages (e.g. Scala) also stamp // Meta["extension"], and their locality resolution must be left unchanged. func isCSharpExtension(n *graph.Node) bool { if n == nil || n.Language != "csharp" || n.Meta == nil { return false } v, _ := n.Meta["extension"].(bool) return v } // csharpHasCompetingMethod reports whether a non-extension method of the same // name is among the candidates. C# resolves an instance/interface member over // an extension, so without receiver-type evidence the extension must not // preempt a competing member the locality fallback would otherwise bind. func csharpHasCompetingMethod(candidates []*graph.Node) bool { for _, c := range candidates { if c != nil && c.Kind == graph.KindMethod && !isCSharpExtension(c) { return true } } return false } // tryBindCSharpExtension binds a failed C# member call `x.Foo(...)` to a static // extension method `Foo(this X x)`. It runs after the receiver-type passes (an // instance or interface member always wins over an extension in C#) and before // the locality fallback. Candidates are the raw same-name in-repo nodes, so a // reachability drop cannot hide a valid extension. Returns true when it binds. // // Precision rules — never guess on ambiguity, which would recreate the // same-name-wrong-type misattribution the receiver-type gate exists to prevent: // - with receiver-type evidence: bind when exactly one extension's // this_param_type matches the receiver; more than one stays unresolved. // - without a matching type: bind only when the name maps to exactly one // extension method in the repo; otherwise stay unresolved. func (r *Resolver) tryBindCSharpExtension(e *graph.Edge, methodName, receiverType string, candidates []*graph.Node, stats *ResolveStats) bool { // C#-only: a non-C# caller must never bind to a C# extension method even // when a same-named one exists in a mixed-language repo. if cn := r.cachedGetNode(e.From); cn == nil || cn.Language != "csharp" { return false } var exts []*graph.Node for _, c := range candidates { if isCSharpExtension(c) { exts = append(exts, c) } } if len(exts) == 0 { return false } // With receiver-type evidence, prefer the extension whose this_param_type // matches the receiver. Exactly one match binds; more than one is an // overload/ambiguity we refuse to guess on. if receiverType != "" { var typed []*graph.Node for _, c := range exts { if tp, _ := c.Meta["this_param_type"].(string); tp != "" && tp == receiverType { typed = append(typed, c) } } if len(typed) == 1 { r.bindCSharpExtension(e, typed[0], 0.9, stats) return true } if len(typed) > 1 { return false } } // No type evidence (or no typed match): bind only when the name is // unambiguous across the repo's extension methods AND no non-extension // member of that name competes (C# instance-method precedence — let the // locality fallback bind the instance method instead). if len(exts) == 1 && !csharpHasCompetingMethod(candidates) { r.bindCSharpExtension(e, exts[0], 0.75, stats) return true } return false } // bindCSharpExtension points a member-call edge at a resolved extension method // at the ast_inferred tier — the binding is type-directed but not compiler- // verified (extension visibility depends on `using` scope we do not fully model). func (r *Resolver) bindCSharpExtension(e *graph.Edge, target *graph.Node, conf float64, stats *ResolveStats) { e.To = target.ID e.Origin = graph.OriginASTInferred e.Confidence = conf e.ConfidenceLabel = graph.ConfidenceLabelFor(graph.EdgeCalls, conf) if e.Meta == nil { e.Meta = map[string]any{} } e.Meta["resolution"] = "extension_method" stats.Resolved++ }