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
186 lines
6.7 KiB
Go
186 lines
6.7 KiB
Go
package resolver
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// rebindGoMethodReceivers fixes Go EdgeMemberOf edges whose target is
|
|
// a phantom `<methodfile>::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 `<file>::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 `<file>::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
|
|
// `<methodfile>::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 `<methodfile>::<typename>`. 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)
|
|
}
|
|
}
|