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

489 lines
14 KiB
Go

package query
import "github.com/zzet/gortex/internal/graph"
// HierarchyDirection picks which side of the class-hierarchy graph
// ClassHierarchy traverses from the seed.
type HierarchyDirection string
const (
HierarchyUp HierarchyDirection = "up"
HierarchyDown HierarchyDirection = "down"
HierarchyBoth HierarchyDirection = "both"
)
// typeHierarchyEdgeKinds is the set traversed when the visited node is
// a type / interface. EdgeExtends covers single + multiple inheritance,
// EdgeImplements bridges concrete type ↔ interface, EdgeComposes covers
// Go struct embedding / Rust trait bounds / Python multiple inheritance
// mixins.
var typeHierarchyEdgeKinds = map[graph.EdgeKind]bool{
graph.EdgeExtends: true,
graph.EdgeImplements: true,
graph.EdgeComposes: true,
}
// methodHierarchyEdgeKinds is the set traversed when the visited node
// is a method. EdgeOverrides is method-level: child method → parent /
// interface method.
var methodHierarchyEdgeKinds = map[graph.EdgeKind]bool{
graph.EdgeOverrides: true,
}
// ClassHierarchy returns the inheritance subgraph rooted at seedID.
//
// Walks the graph through EdgeExtends + EdgeImplements + EdgeComposes
// for type nodes and EdgeOverrides for method nodes. Direction picks
// the side(s) of the hierarchy:
//
// - HierarchyUp — outgoing edges (parents / interfaces a child
// extends or implements; parent methods this method overrides).
// - HierarchyDown — incoming edges (subclasses / implementers; methods
// that override this one).
// - HierarchyBoth — union of the two.
//
// When includeMethods is true and a type / interface node is reached,
// its methods (in-edges of EdgeMemberOf whose From side is a function
// or method node) are pulled into the result and override links from
// each method are walked in the same direction(s).
//
// Workspace / project scope is enforced via opts.ScopeAllows on every
// neighbour. opts.MinTier is applied as a post-pass over the collected
// edges (consistent with the rest of the engine surface).
//
// Picks ClassHierarchyTraverser when the backend implements it: that
// path runs the BFS as one variable-length traversal per direction
// inside the engine, replacing the per-node GetNode + GetIn/OutEdges
// loop the fallback runs. On a disk backend a deep walk over a wide
// implementer set previously fired hundreds of round-trips per
// call — the pushdown drops to one or two queries.
func (e *Engine) ClassHierarchy(seedID string, direction HierarchyDirection, depth int, includeMethods bool, opts QueryOptions) *SubGraph {
if direction == "" {
direction = HierarchyBoth
}
if depth <= 0 {
depth = 5
}
if depth > 64 {
depth = 64
}
seed := e.g.GetNode(seedID)
if seed == nil {
return &SubGraph{}
}
if _, ok := e.g.(graph.ClassHierarchyTraverser); ok {
return e.classHierarchyPushdown(seed, direction, depth, includeMethods, opts)
}
return e.classHierarchyWalk(seed, direction, depth, includeMethods, opts)
}
// classHierarchyPushdown runs the BFS through the
// ClassHierarchyTraverser capability. Each direction issues one or
// two backend round-trips (the type-edge kinds, optionally chasing
// methods through EdgeMemberOf) instead of the per-frontier per-hop
// loop the fallback runs.
func (e *Engine) classHierarchyPushdown(
seed *graph.Node,
direction HierarchyDirection,
depth int,
includeMethods bool,
opts QueryOptions,
) *SubGraph {
tr := e.g.(graph.ClassHierarchyTraverser)
walkUp := direction == HierarchyUp || direction == HierarchyBoth
walkDown := direction == HierarchyDown || direction == HierarchyBoth
typeKinds := []graph.EdgeKind{graph.EdgeExtends, graph.EdgeImplements, graph.EdgeComposes}
methodKinds := []graph.EdgeKind{graph.EdgeOverrides}
// Per-direction walks: type-hierarchy kinds rooted at seed if seed
// is a type/interface; method-hierarchy kinds rooted at seed if
// seed is a method/function. Methods reached via includeMethods
// are added as separate roots in a follow-up pass.
var rows []graph.ClassHierarchyRow
seedIsType := seed.Kind == graph.KindType || seed.Kind == graph.KindInterface
seedIsMethod := seed.Kind == graph.KindMethod || seed.Kind == graph.KindFunction
if seedIsType {
if walkUp {
rows = append(rows, tr.ClassHierarchyTraverse(seed.ID, "up", typeKinds, depth)...)
}
if walkDown {
rows = append(rows, tr.ClassHierarchyTraverse(seed.ID, "down", typeKinds, depth)...)
}
} else if seedIsMethod {
if walkUp {
rows = append(rows, tr.ClassHierarchyTraverse(seed.ID, "up", methodKinds, depth)...)
}
if walkDown {
rows = append(rows, tr.ClassHierarchyTraverse(seed.ID, "down", methodKinds, depth)...)
}
}
// Collect the node IDs visited so we can resolve them in one
// batched fetch, instead of one GetNode per row.
visited := map[string]bool{seed.ID: true}
for _, r := range rows {
for _, id := range r.Path {
visited[id] = true
}
}
// includeMethods folds in EdgeMemberOf hops from every visited
// type node. The override walk on each method then runs as a
// further pushdown call.
memberLinks := []struct {
from, to string
kind graph.EdgeKind
}{}
if includeMethods {
typeIDs := make([]string, 0, len(visited))
for id := range visited {
n := e.g.GetNode(id)
if n == nil {
continue
}
if n.Kind == graph.KindType || n.Kind == graph.KindInterface {
typeIDs = append(typeIDs, id)
}
}
if len(typeIDs) > 0 {
memberIns := e.g.GetInEdgesByNodeIDs(typeIDs)
methodRoots := []string{}
for _, id := range typeIDs {
for _, ed := range memberIns[id] {
if ed == nil || ed.Kind != graph.EdgeMemberOf {
continue
}
member := e.g.GetNode(ed.From)
if member == nil {
continue
}
if member.Kind != graph.KindMethod && member.Kind != graph.KindFunction {
continue
}
memberLinks = append(memberLinks, struct {
from, to string
kind graph.EdgeKind
}{from: member.ID, to: id, kind: graph.EdgeMemberOf})
if !visited[member.ID] {
visited[member.ID] = true
methodRoots = append(methodRoots, member.ID)
}
}
}
for _, mid := range methodRoots {
if walkUp {
subRows := tr.ClassHierarchyTraverse(mid, "up", methodKinds, depth)
for _, sr := range subRows {
for _, id := range sr.Path {
visited[id] = true
}
}
rows = append(rows, methodPathsWithRoot(mid, subRows)...)
}
if walkDown {
subRows := tr.ClassHierarchyTraverse(mid, "down", methodKinds, depth)
for _, sr := range subRows {
for _, id := range sr.Path {
visited[id] = true
}
}
rows = append(rows, methodPathsWithRoot(mid, subRows)...)
}
}
}
}
// Resolve every visited node + collect the edge pointers in one
// place. The capability doesn't carry edge pointers (on-disk
// backend edges aren't first-class objects), so we re-resolve them via
// GetOutEdgesByNodeIDs / GetInEdgesByNodeIDs once per direction.
allIDs := make([]string, 0, len(visited))
for id := range visited {
allIDs = append(allIDs, id)
}
nodeMap := e.g.GetNodesByIDs(allIDs)
if nodeMap[seed.ID] == nil {
nodeMap[seed.ID] = seed
}
resultNodes := make([]*graph.Node, 0, len(allIDs))
for _, id := range allIDs {
n := nodeMap[id]
if n == nil {
continue
}
if opts.hasScopeFilter() && id != seed.ID && !opts.ScopeAllows(n) {
continue
}
resultNodes = append(resultNodes, n)
}
// Reconstruct edges: each row's Path[i] → Path[i+1] (for i>=0)
// carries an edge of EdgeKinds[i]. The seed's first hop is from
// seed → Path[0]. The direction the walk came from determines
// whether the edge points seed→neighbour or neighbour→seed.
resultEdges := make([]*graph.Edge, 0)
seenEdge := make(map[string]bool)
addEdge := func(from, to string, kind graph.EdgeKind) {
// Find the actual *Edge so the downstream FilterByMinTier
// still has the origin / tier columns to read.
var found *graph.Edge
for _, ed := range e.g.GetOutEdges(from) {
if ed == nil {
continue
}
if ed.To == to && ed.Kind == kind {
found = ed
break
}
}
if found == nil {
// Direction-flipped lookup — happens when "down" walks
// hand back paths whose hops are in-edges of the seed.
for _, ed := range e.g.GetInEdges(from) {
if ed == nil {
continue
}
if ed.From == to && ed.Kind == kind {
found = ed
break
}
}
}
if found == nil {
return
}
k := found.From + "→" + found.To + "::" + string(found.Kind) + ":" + edgeMetaTag(found)
if seenEdge[k] {
return
}
seenEdge[k] = true
resultEdges = append(resultEdges, found)
}
for _, r := range rows {
prev := seed.ID
for i, nb := range r.Path {
if i >= len(r.EdgeKinds) {
break
}
addEdge(prev, nb, r.EdgeKinds[i])
prev = nb
}
}
for _, link := range memberLinks {
addEdge(link.from, link.to, link.kind)
}
// Workspace-scope post-filter for edges (any edge whose endpoints
// were dropped from resultNodes is also dropped).
if opts.hasScopeFilter() {
nodeSet := make(map[string]bool, len(resultNodes))
for _, n := range resultNodes {
nodeSet[n.ID] = true
}
filtered := resultEdges[:0]
for _, ed := range resultEdges {
if !nodeSet[ed.From] || !nodeSet[ed.To] {
continue
}
filtered = append(filtered, ed)
}
resultEdges = filtered
}
sg := &SubGraph{
Nodes: resultNodes,
Edges: resultEdges,
TotalNodes: len(resultNodes),
TotalEdges: len(resultEdges),
}
if opts.MinTier != "" {
sg.FilterByMinTier(opts.MinTier)
}
return sg
}
// methodPathsWithRoot rebases the traversal rows so the seed prefix
// in their paths reflects the method root they came from rather than
// the outer ClassHierarchy seed. Returned rows are otherwise
// unchanged.
func methodPathsWithRoot(root string, rows []graph.ClassHierarchyRow) []graph.ClassHierarchyRow {
out := make([]graph.ClassHierarchyRow, len(rows))
for i, r := range rows {
newPath := append([]string{root}, r.Path...)
newKinds := append([]graph.EdgeKind{}, r.EdgeKinds...)
// The seed→Path[0] hop is encoded by EdgeMemberOf in the outer
// addEdge pass, so we keep the EdgeKinds slice aligned with
// the slice the caller iterates ([0]=Path[0]→Path[1]).
out[i] = graph.ClassHierarchyRow{Path: newPath[1:], EdgeKinds: newKinds}
_ = newPath
}
return out
}
// classHierarchyWalk is the in-memory BFS path. Kept verbatim so the
// in-memory backend has the same shape it had before the pushdown
// landed.
func (e *Engine) classHierarchyWalk(
seed *graph.Node,
direction HierarchyDirection,
depth int,
includeMethods bool,
opts QueryOptions,
) *SubGraph {
walkUp := direction == HierarchyUp || direction == HierarchyBoth
walkDown := direction == HierarchyDown || direction == HierarchyBoth
visitedNodes := make(map[string]bool)
visitedEdges := make(map[string]bool)
var resultNodes []*graph.Node
var resultEdges []*graph.Edge
addNode := func(n *graph.Node) {
if n == nil || visitedNodes[n.ID] {
return
}
visitedNodes[n.ID] = true
resultNodes = append(resultNodes, n)
}
edgeKey := func(ed *graph.Edge) string {
return ed.From + "→" + ed.To + "::" + string(ed.Kind) + ":" + edgeMetaTag(ed)
}
addEdge := func(ed *graph.Edge) {
if ed == nil {
return
}
k := edgeKey(ed)
if visitedEdges[k] {
return
}
visitedEdges[k] = true
resultEdges = append(resultEdges, ed)
}
addNode(seed)
type queued struct {
id string
depth int
}
queue := []queued{{id: seed.ID, depth: 0}}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
if cur.depth >= depth {
continue
}
curNode := e.g.GetNode(cur.id)
if curNode == nil {
continue
}
isType := curNode.Kind == graph.KindType || curNode.Kind == graph.KindInterface
isMethod := curNode.Kind == graph.KindMethod || curNode.Kind == graph.KindFunction
if includeMethods && isType {
for _, mEdge := range e.g.GetInEdges(cur.id) {
if mEdge.Kind != graph.EdgeMemberOf {
continue
}
member := e.g.GetNode(mEdge.From)
if member == nil {
continue
}
if member.Kind != graph.KindMethod && member.Kind != graph.KindFunction {
continue
}
if opts.hasScopeFilter() && !opts.ScopeAllows(member) {
continue
}
addNode(member)
addEdge(mEdge)
queue = append(queue, queued{id: member.ID, depth: cur.depth})
}
}
var kindSet map[graph.EdgeKind]bool
switch {
case isType:
kindSet = typeHierarchyEdgeKinds
case isMethod:
kindSet = methodHierarchyEdgeKinds
default:
continue
}
if walkUp {
for _, ed := range e.g.GetOutEdges(cur.id) {
if !kindSet[ed.Kind] {
continue
}
neighbor := e.g.GetNode(ed.To)
if neighbor == nil {
continue
}
if opts.hasScopeFilter() && !opts.ScopeAllows(neighbor) {
continue
}
addEdge(ed)
if !visitedNodes[neighbor.ID] {
addNode(neighbor)
queue = append(queue, queued{id: neighbor.ID, depth: cur.depth + 1})
}
}
}
if walkDown {
for _, ed := range e.g.GetInEdges(cur.id) {
if !kindSet[ed.Kind] {
continue
}
neighbor := e.g.GetNode(ed.From)
if neighbor == nil {
continue
}
if opts.hasScopeFilter() && !opts.ScopeAllows(neighbor) {
continue
}
addEdge(ed)
if !visitedNodes[neighbor.ID] {
addNode(neighbor)
queue = append(queue, queued{id: neighbor.ID, depth: cur.depth + 1})
}
}
}
}
sg := &SubGraph{
Nodes: resultNodes,
Edges: resultEdges,
TotalNodes: len(resultNodes),
TotalEdges: len(resultEdges),
}
if opts.MinTier != "" {
sg.FilterByMinTier(opts.MinTier)
}
return sg
}
// edgeMetaTag is a small disambiguator for edges that share From / To /
// Kind but carry distinct metadata (e.g. multiple EdgeOverrides between
// the same method pair via different language sources). Falls back to
// the edge file:line when no semantic_source is set.
func edgeMetaTag(ed *graph.Edge) string {
if ed.Meta != nil {
if src, ok := ed.Meta["semantic_source"].(string); ok && src != "" {
return src
}
}
if ed.FilePath != "" {
return ed.FilePath
}
return ""
}