Files
zzet--gortex/internal/resolver/generic_param_bind.go
2026-07-31 23:09:01 +02:00

178 lines
5.1 KiB
Go

package resolver
import (
"strings"
"github.com/zzet/gortex/internal/graph"
)
// bindGenericParamRefs rewrites `unresolved::<name>` edges where the
// name is a generic type parameter declared by the source's
// enclosing function. The Go extractor already materialises
// KindGenericParam nodes with IDs `<func>#tparam:<name>` and an
// EdgeMemberOf back to the owner — the resolver just hasn't been
// consulting them when an in-body reference (`var x T`, return type
// `T`, etc.) lands as `unresolved::T`.
//
// Side benefit beyond stub reduction: `find_usages` on a generic
// type parameter starts working — *"where in this generic function
// is T used?"* — which is a real refactoring query.
//
// Scope is per-function: a function's tparams are visible only
// inside its body. The owner-keyed index built here lets each edge
// resolve in O(1) without re-walking the graph.
func (r *Resolver) bindGenericParamRefs() {
owned := r.buildGenericParamOwnerIndex()
if len(owned) == 0 {
return
}
var batch []graph.EdgeReindex
// We don't know up front which edge kinds carry type-param refs:
// EdgeReferences for `var x T`, EdgeTypedAs for parameters typed
// as T, EdgeReturns for return signature, EdgeInstantiates for
// generic instantiation expressions. Walk the union.
for _, k := range []graph.EdgeKind{
graph.EdgeReferences,
graph.EdgeTypedAs,
graph.EdgeReturns,
graph.EdgeInstantiates,
} {
for e := range r.graph.EdgesByKind(k) {
if old := r.tryBindGenericParam(e, owned); old != "" {
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: old})
}
}
}
if len(batch) > 0 {
r.graph.ReindexEdges(batch)
}
}
func (r *Resolver) buildGenericParamOwnerIndex() map[string]map[string]string {
// owner-function ID → set of tparam-name → tparam-node-id.
owned := map[string]map[string]string{}
for n := range graph.NamedLanguageNodesSeq(r.graph, graph.KindGenericParam) {
if n.Language != "go" || n.Name == "" {
continue
}
owner := enclosingFunctionForBinding(n.ID)
if owner == "" || owner == n.ID {
continue
}
set, ok := owned[owner]
if !ok {
set = map[string]string{}
owned[owner] = set
}
// Don't overwrite — two tparams with the same name in the
// same function shouldn't happen in valid Go, but be defensive.
if _, dup := set[n.Name]; dup {
set[n.Name] = ""
continue
}
set[n.Name] = n.ID
}
return owned
}
func (r *Resolver) bindGenericParamRefsFromCensus(census *postBareAttributionCensus) {
if census == nil {
return
}
owned := r.buildGenericParamOwnerIndex()
if len(owned) == 0 {
return
}
r.reindexAttributionIdentitiesBatched(
census.finder,
census.genericCandidates,
func(edge *graph.Edge) string {
return r.tryBindGenericParam(edge, owned)
},
)
}
// bindGenericParamRefsForFile is the single-file-resolve form of
// bindGenericParamRefs. A type parameter is visible only inside its own
// function's body, which lives in this file, so both the owner index and the
// edges to rewrite are scoped to the file — no whole-graph EdgesByKind sweep
// (the dominant cost of an incremental edit on a large graph).
func (r *Resolver) bindGenericParamRefsForFile(filePath string) {
owned := map[string]map[string]string{}
for _, n := range r.incrementalFileNodes(filePath) {
if n == nil || n.Kind != graph.KindGenericParam || n.Language != "go" || n.Name == "" {
continue
}
owner := enclosingFunctionForBinding(n.ID)
if owner == "" || owner == n.ID {
continue
}
set, ok := owned[owner]
if !ok {
set = map[string]string{}
owned[owner] = set
}
if _, dup := set[n.Name]; dup {
set[n.Name] = ""
continue
}
set[n.Name] = n.ID
}
if len(owned) == 0 {
return
}
var batch []graph.EdgeReindex
for _, e := range r.fileOutEdges(filePath) {
switch e.Kind {
case graph.EdgeReferences, graph.EdgeTypedAs, graph.EdgeReturns, graph.EdgeInstantiates:
if old := r.tryBindGenericParam(e, owned); old != "" {
batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: old})
}
}
}
r.persistAttributionReindexes(batch)
}
// tryBindGenericParam returns the old To value (for batched reindex)
// when the edge was rewritten, or "" when left alone.
func (r *Resolver) tryBindGenericParam(e *graph.Edge, owned map[string]map[string]string) string {
if e == nil {
return ""
}
name, ok := genericParamTargetName(e.To)
if !ok {
return ""
}
ownerID := enclosingFunctionForBinding(e.From)
if ownerID == "" {
return ""
}
set := owned[ownerID]
if len(set) == 0 {
return ""
}
target, ok := set[name]
if !ok || target == "" || target == e.To {
return ""
}
oldTo := e.To
e.To = target
return oldTo
}
// genericParamTargetName is the exact target-shape gate shared by the light
// census and tryBindGenericParam. Repo-qualified unresolved targets remain
// excluded: type parameters are owned by the source function and use the bare
// unresolved::<name> representation.
func genericParamTargetName(to string) (string, bool) {
if !strings.HasPrefix(to, "unresolved::") {
return "", false
}
name := strings.TrimPrefix(to, "unresolved::")
if name == "" || strings.ContainsAny(name, ".*:#") {
return "", false
}
return name, true
}