package resolver import ( "path/filepath" "strings" "github.com/zzet/gortex/internal/graph" ) // rebindGoMethodReceivers fixes Go EdgeMemberOf edges whose target is // a phantom `::TypeName` ID — the artefact of the Go // extractor building the receiver-type endpoint from the method's own // file rather than the file the type is actually declared in. Methods // spread across multiple files in the same package each emit a // different `::Type` target even though they all logically // belong to the single type node defined elsewhere. // // Without this pass: // - the on-disk backend materialises phantom Node rows to satisfy the // rel-table FK on every cross-file method-receiver edge; // - InferImplements builds a typeID → method-set map keyed on the // phantom IDs, so a type whose methods span N files appears as N // partial types each with a fraction of the real method set, and // interface satisfaction is under-detected; // - find_implementations / get_class_hierarchy / get_callers over // interface methods all return partial results for cross-file- // method types (which is most of any non-trivial Go codebase). // // Algorithm: index every Go KindType / KindInterface node by // (filepath.Dir(file), name); walk EdgeMemberOf; for each Go method // whose To doesn't resolve, look up (its file's dir, type name); if // exactly one match, rewrite edge.To to the canonical type ID via // ReindexEdges (one batched commit instead of per-edge round-trips). // // Scope: Go only — other languages (Java / TS / Python) group methods // inside the class body in the same file, so the cross-file pattern // doesn't arise. The method node's Language gates the rebind. // pkgKey identifies a Go type by its package directory and name — the // key the receiver-rebind type index is built on. Package-level so the // whole-graph and per-file builders share it. type pkgKey struct{ pkg, name string } func (r *Resolver) rebindGoMethodReceivers() { typesIdx := make(map[pkgKey]string) for _, kind := range []graph.NodeKind{graph.KindType, graph.KindInterface} { // Server-side language scope: only Go type/interface nodes cross // the cgo boundary. On a graph with few/no Go types (e.g. a TS // repo) this avoids marshaling + meta-decoding every type node // just to discard the non-Go majority — the bulk of this pass's // cost on a large single-language graph. for n := range r.nodesByKindLang(kind, "go") { addGoTypeToIndex(typesIdx, n) } } if len(typesIdx) == 0 { return } var memberOf []*graph.Edge for e := range r.graph.EdgesByKind(graph.EdgeMemberOf) { memberOf = append(memberOf, e) } r.rebindMemberOf(typesIdx, memberOf) } // rebindGoMethodReceiversForFile is the single-file scope of // rebindGoMethodReceivers. A Go method's receiver type lives in the same // package (directory) as the method, and only the edited file's methods // carry a freshly-extracted phantom `::Type` receiver — so the type // index is built from just that package's files and the MemberOf edges // from just the edited file's methods, instead of indexing every Go type // and scanning every MemberOf edge in the graph on each save (the // dominant per-edit cost on a large graph). dirIndex (built by // buildPassIndexes before this runs) supplies the package's files. func (r *Resolver) rebindGoMethodReceiversForFile(filePath string) { // The package's type index is a pure function of the directory's files and // never mutated by the tail passes, so memoize it: a scoped tail visiting // every file of a D-file package builds it once (O(D)) instead of per file // (O(D^2)). The empty result is cached too so a package with no Go types // isn't re-scanned per file. dir := filepath.Dir(filePath) typesIdx, ok := r.receiverTypeIdxByDir[dir] if !ok { typesIdx = make(map[pkgKey]string) for _, fn := range r.dirIndex[dir] { for _, n := range r.graph.GetFileNodes(fn.FilePath) { if n != nil && n.Language == "go" && (n.Kind == graph.KindType || n.Kind == graph.KindInterface) { addGoTypeToIndex(typesIdx, n) } } } if r.receiverTypeIdxByDir == nil { r.receiverTypeIdxByDir = make(map[string]map[pkgKey]string) } r.receiverTypeIdxByDir[dir] = typesIdx } if len(typesIdx) == 0 { return } var memberOf []*graph.Edge for _, n := range r.graph.GetFileNodes(filePath) { for _, e := range r.graph.GetOutEdges(n.ID) { if e.Kind == graph.EdgeMemberOf { memberOf = append(memberOf, e) } } } r.rebindMemberOf(typesIdx, memberOf) } // addGoTypeToIndex records a Go type/interface node under (dir, name). // Two distinct nodes with the same name in the same package directory // shouldn't happen in valid Go; if they do, the entry is poisoned to "" // so the rebind leaves the edge alone rather than pick an arbitrary // winner. func addGoTypeToIndex(idx map[pkgKey]string, n *graph.Node) { if n == nil || n.Name == "" || n.FilePath == "" { return } k := pkgKey{filepath.Dir(n.FilePath), n.Name} if existing, ok := idx[k]; ok && existing != n.ID { idx[k] = "" return } idx[k] = n.ID } // rebindMemberOf lifts each Go method's MemberOf edge from a phantom // `::TypeName` target onto the canonical type node from // typesIdx. Endpoints are batch-loaded in one GetNodesByIDs (a per-edge // GetNode here is two query round-trips per method on a disk backend). func (r *Resolver) rebindMemberOf(typesIdx map[pkgKey]string, memberOf []*graph.Edge) { if len(memberOf) == 0 { return } ids := make(map[string]struct{}, len(memberOf)*2) for _, e := range memberOf { if e.From != "" { ids[e.From] = struct{}{} } if e.To != "" { ids[e.To] = struct{}{} } } idList := make([]string, 0, len(ids)) for id := range ids { idList = append(idList, id) } nodes := r.graph.GetNodesByIDs(idList) var batch []graph.EdgeReindex for _, e := range memberOf { method := nodes[e.From] if method == nil || method.Language != "go" || method.Kind != graph.KindMethod { continue } // Already resolves to a real type node — same-file methods // land here. Nothing to do. if n := nodes[e.To]; n != nil && (n.Kind == graph.KindType || n.Kind == graph.KindInterface) { continue } // Parse `::`. The split is on the LAST // `::` so paths embedded in the ID (none in Go, but stay // defensive) can't trip us up. i := strings.LastIndex(e.To, "::") if i <= 0 { continue } file := e.To[:i] typeName := e.To[i+2:] if file == "" || typeName == "" { continue } canonicalID, ok := typesIdx[pkgKey{filepath.Dir(file), typeName}] if !ok || canonicalID == "" || canonicalID == e.To { continue } oldTo := e.To e.To = canonicalID batch = append(batch, graph.EdgeReindex{Edge: e, OldTo: oldTo}) } if len(batch) > 0 { r.graph.ReindexEdges(batch) } }