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
229 lines
9.4 KiB
Go
229 lines
9.4 KiB
Go
package query
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// buildHierarchyGraph models a small OO subtree:
|
|
//
|
|
// interface Animal
|
|
// ↑ implements (in-edges of Animal)
|
|
// type Dog ← composes Tail
|
|
// ↑ extends (in-edges of Dog)
|
|
// type Puppy ← extends
|
|
// type ServiceDog ← extends
|
|
//
|
|
// method Animal.Speak — interface method
|
|
// method Dog.Speak — overrides Animal.Speak
|
|
// method Puppy.Speak — overrides Dog.Speak
|
|
// method ServiceDog.Speak — overrides Dog.Speak
|
|
//
|
|
// Plus an unrelated method Tail.Wag attached to the composed Tail type
|
|
// so include_methods doesn't accidentally pull it in via the composition
|
|
// edge alone.
|
|
func buildHierarchyGraph() *graph.Graph {
|
|
g := graph.New()
|
|
|
|
nodes := []*graph.Node{
|
|
{ID: "animal.go::Animal", Kind: graph.KindInterface, Name: "Animal", FilePath: "animal.go", Language: "go"},
|
|
{ID: "animal.go::Animal.Speak", Kind: graph.KindMethod, Name: "Animal.Speak", FilePath: "animal.go", Language: "go"},
|
|
|
|
{ID: "dog.go::Dog", Kind: graph.KindType, Name: "Dog", FilePath: "dog.go", Language: "go"},
|
|
{ID: "dog.go::Dog.Speak", Kind: graph.KindMethod, Name: "Dog.Speak", FilePath: "dog.go", Language: "go"},
|
|
|
|
{ID: "tail.go::Tail", Kind: graph.KindType, Name: "Tail", FilePath: "tail.go", Language: "go"},
|
|
{ID: "tail.go::Tail.Wag", Kind: graph.KindMethod, Name: "Tail.Wag", FilePath: "tail.go", Language: "go"},
|
|
|
|
{ID: "puppy.go::Puppy", Kind: graph.KindType, Name: "Puppy", FilePath: "puppy.go", Language: "go"},
|
|
{ID: "puppy.go::Puppy.Speak", Kind: graph.KindMethod, Name: "Puppy.Speak", FilePath: "puppy.go", Language: "go"},
|
|
|
|
{ID: "service.go::ServiceDog", Kind: graph.KindType, Name: "ServiceDog", FilePath: "service.go", Language: "go"},
|
|
{ID: "service.go::ServiceDog.Speak", Kind: graph.KindMethod, Name: "ServiceDog.Speak", FilePath: "service.go", Language: "go"},
|
|
}
|
|
for _, n := range nodes {
|
|
g.AddNode(n)
|
|
}
|
|
|
|
edges := []*graph.Edge{
|
|
// Type hierarchy.
|
|
{From: "dog.go::Dog", To: "animal.go::Animal", Kind: graph.EdgeImplements, FilePath: "dog.go"},
|
|
{From: "dog.go::Dog", To: "tail.go::Tail", Kind: graph.EdgeComposes, FilePath: "dog.go"},
|
|
{From: "puppy.go::Puppy", To: "dog.go::Dog", Kind: graph.EdgeExtends, FilePath: "puppy.go"},
|
|
{From: "service.go::ServiceDog", To: "dog.go::Dog", Kind: graph.EdgeExtends, FilePath: "service.go"},
|
|
|
|
// Method membership.
|
|
{From: "animal.go::Animal.Speak", To: "animal.go::Animal", Kind: graph.EdgeMemberOf, FilePath: "animal.go"},
|
|
{From: "dog.go::Dog.Speak", To: "dog.go::Dog", Kind: graph.EdgeMemberOf, FilePath: "dog.go"},
|
|
{From: "tail.go::Tail.Wag", To: "tail.go::Tail", Kind: graph.EdgeMemberOf, FilePath: "tail.go"},
|
|
{From: "puppy.go::Puppy.Speak", To: "puppy.go::Puppy", Kind: graph.EdgeMemberOf, FilePath: "puppy.go"},
|
|
{From: "service.go::ServiceDog.Speak", To: "service.go::ServiceDog", Kind: graph.EdgeMemberOf, FilePath: "service.go"},
|
|
|
|
// Method overrides.
|
|
{From: "dog.go::Dog.Speak", To: "animal.go::Animal.Speak", Kind: graph.EdgeOverrides, FilePath: "dog.go"},
|
|
{From: "puppy.go::Puppy.Speak", To: "dog.go::Dog.Speak", Kind: graph.EdgeOverrides, FilePath: "puppy.go"},
|
|
{From: "service.go::ServiceDog.Speak", To: "dog.go::Dog.Speak", Kind: graph.EdgeOverrides, FilePath: "service.go"},
|
|
}
|
|
for _, e := range edges {
|
|
g.AddEdge(e)
|
|
}
|
|
return g
|
|
}
|
|
|
|
func sortedNodeIDs(sg *SubGraph) []string {
|
|
ids := make([]string, 0, len(sg.Nodes))
|
|
for _, n := range sg.Nodes {
|
|
ids = append(ids, n.ID)
|
|
}
|
|
sort.Strings(ids)
|
|
return ids
|
|
}
|
|
|
|
func TestClassHierarchy_TypeUp(t *testing.T) {
|
|
e := NewEngine(buildHierarchyGraph())
|
|
|
|
// From a leaf type, walking "up" should reach its parent (Dog),
|
|
// the interface Dog implements (Animal), and the type Dog composes (Tail).
|
|
sg := e.ClassHierarchy("puppy.go::Puppy", HierarchyUp, 5, false, QueryOptions{})
|
|
|
|
ids := sortedNodeIDs(sg)
|
|
assert.Contains(t, ids, "puppy.go::Puppy")
|
|
assert.Contains(t, ids, "dog.go::Dog")
|
|
assert.Contains(t, ids, "animal.go::Animal")
|
|
assert.Contains(t, ids, "tail.go::Tail")
|
|
// Sibling ServiceDog should not appear in an "up"-only walk from Puppy.
|
|
assert.NotContains(t, ids, "service.go::ServiceDog")
|
|
}
|
|
|
|
func TestClassHierarchy_TypeDown(t *testing.T) {
|
|
e := NewEngine(buildHierarchyGraph())
|
|
|
|
// From the root interface Animal, "down" should reach implementers
|
|
// and their subclasses transitively.
|
|
sg := e.ClassHierarchy("animal.go::Animal", HierarchyDown, 5, false, QueryOptions{})
|
|
|
|
ids := sortedNodeIDs(sg)
|
|
assert.Contains(t, ids, "animal.go::Animal")
|
|
assert.Contains(t, ids, "dog.go::Dog")
|
|
assert.Contains(t, ids, "puppy.go::Puppy")
|
|
assert.Contains(t, ids, "service.go::ServiceDog")
|
|
// Tail is a composition-up from Dog, not a child — must not appear.
|
|
assert.NotContains(t, ids, "tail.go::Tail")
|
|
}
|
|
|
|
func TestClassHierarchy_TypeBoth(t *testing.T) {
|
|
e := NewEngine(buildHierarchyGraph())
|
|
|
|
sg := e.ClassHierarchy("dog.go::Dog", HierarchyBoth, 5, false, QueryOptions{})
|
|
ids := sortedNodeIDs(sg)
|
|
|
|
// Both directions from Dog: parents (Animal, Tail) and children (Puppy, ServiceDog).
|
|
assert.Contains(t, ids, "animal.go::Animal")
|
|
assert.Contains(t, ids, "tail.go::Tail")
|
|
assert.Contains(t, ids, "puppy.go::Puppy")
|
|
assert.Contains(t, ids, "service.go::ServiceDog")
|
|
}
|
|
|
|
func TestClassHierarchy_DepthLimit(t *testing.T) {
|
|
e := NewEngine(buildHierarchyGraph())
|
|
|
|
// Depth=1 from Animal should reach direct implementers but not
|
|
// their grandchildren.
|
|
sg := e.ClassHierarchy("animal.go::Animal", HierarchyDown, 1, false, QueryOptions{})
|
|
ids := sortedNodeIDs(sg)
|
|
assert.Contains(t, ids, "dog.go::Dog")
|
|
assert.NotContains(t, ids, "puppy.go::Puppy")
|
|
assert.NotContains(t, ids, "service.go::ServiceDog")
|
|
}
|
|
|
|
func TestClassHierarchy_MethodSeed(t *testing.T) {
|
|
e := NewEngine(buildHierarchyGraph())
|
|
|
|
// Dog.Speak — up should reach Animal.Speak, down should reach
|
|
// Puppy.Speak and ServiceDog.Speak.
|
|
sg := e.ClassHierarchy("dog.go::Dog.Speak", HierarchyBoth, 5, false, QueryOptions{})
|
|
ids := sortedNodeIDs(sg)
|
|
assert.Contains(t, ids, "animal.go::Animal.Speak")
|
|
assert.Contains(t, ids, "puppy.go::Puppy.Speak")
|
|
assert.Contains(t, ids, "service.go::ServiceDog.Speak")
|
|
}
|
|
|
|
func TestClassHierarchy_IncludeMethods(t *testing.T) {
|
|
e := NewEngine(buildHierarchyGraph())
|
|
|
|
// From Dog with include_methods, we should pull in Dog.Speak (member),
|
|
// then walk EdgeOverrides up (to Animal.Speak) and down (to Puppy.Speak,
|
|
// ServiceDog.Speak). Tail.Wag should also appear because Tail is in
|
|
// the type subgraph and we surface its members. No override chain
|
|
// rooted at Tail.Wag exists, so it stays as a leaf.
|
|
sg := e.ClassHierarchy("dog.go::Dog", HierarchyBoth, 5, true, QueryOptions{})
|
|
ids := sortedNodeIDs(sg)
|
|
|
|
assert.Contains(t, ids, "dog.go::Dog.Speak")
|
|
assert.Contains(t, ids, "animal.go::Animal.Speak")
|
|
assert.Contains(t, ids, "puppy.go::Puppy.Speak")
|
|
assert.Contains(t, ids, "service.go::ServiceDog.Speak")
|
|
assert.Contains(t, ids, "tail.go::Tail.Wag")
|
|
}
|
|
|
|
func TestClassHierarchy_NoMethods(t *testing.T) {
|
|
e := NewEngine(buildHierarchyGraph())
|
|
|
|
// Without include_methods, no method nodes should be pulled in just
|
|
// from a type seed (only types in the hierarchy plus Tail via composition).
|
|
sg := e.ClassHierarchy("dog.go::Dog", HierarchyBoth, 5, false, QueryOptions{})
|
|
for _, n := range sg.Nodes {
|
|
assert.NotEqual(t, graph.KindMethod, n.Kind, "unexpected method node %q in non-include_methods walk", n.ID)
|
|
}
|
|
}
|
|
|
|
func TestClassHierarchy_UnknownSeed(t *testing.T) {
|
|
e := NewEngine(buildHierarchyGraph())
|
|
sg := e.ClassHierarchy("does/not/exist", HierarchyBoth, 5, false, QueryOptions{})
|
|
assert.Empty(t, sg.Nodes)
|
|
assert.Empty(t, sg.Edges)
|
|
}
|
|
|
|
func TestClassHierarchy_WorkspaceScope(t *testing.T) {
|
|
g := graph.New()
|
|
// Two workspaces. ws=alpha contains Base, ws=beta contains Sub.
|
|
g.AddNode(&graph.Node{ID: "alpha::base", Kind: graph.KindType, Name: "Base", FilePath: "a/base.go", WorkspaceID: "alpha"})
|
|
g.AddNode(&graph.Node{ID: "beta::sub", Kind: graph.KindType, Name: "Sub", FilePath: "b/sub.go", WorkspaceID: "beta"})
|
|
g.AddEdge(&graph.Edge{From: "beta::sub", To: "alpha::base", Kind: graph.EdgeExtends})
|
|
|
|
e := NewEngine(g)
|
|
|
|
// Walking down from Base with no scope sees Sub.
|
|
sg := e.ClassHierarchy("alpha::base", HierarchyDown, 5, false, QueryOptions{})
|
|
require.Equal(t, 2, len(sg.Nodes))
|
|
|
|
// Confined to the alpha workspace, the cross-workspace child is dropped.
|
|
sg = e.ClassHierarchy("alpha::base", HierarchyDown, 5, false, QueryOptions{WorkspaceID: "alpha"})
|
|
ids := sortedNodeIDs(sg)
|
|
assert.Equal(t, []string{"alpha::base"}, ids)
|
|
}
|
|
|
|
func TestClassHierarchy_MinTier(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "p", Kind: graph.KindInterface, Name: "P"})
|
|
g.AddNode(&graph.Node{ID: "c1", Kind: graph.KindType, Name: "C1"})
|
|
g.AddNode(&graph.Node{ID: "c2", Kind: graph.KindType, Name: "C2"})
|
|
// One LSP-resolved implements edge, one inferred.
|
|
g.AddEdge(&graph.Edge{From: "c1", To: "p", Kind: graph.EdgeImplements, Origin: graph.OriginLSPResolved})
|
|
g.AddEdge(&graph.Edge{From: "c2", To: "p", Kind: graph.EdgeImplements, Origin: graph.OriginASTInferred})
|
|
|
|
e := NewEngine(g)
|
|
// Without min_tier, both implementers come back.
|
|
sg := e.ClassHierarchy("p", HierarchyDown, 5, false, QueryOptions{})
|
|
assert.Equal(t, 2, len(sg.Edges))
|
|
|
|
// With min_tier=lsp_resolved, only the high-confidence edge survives.
|
|
sg = e.ClassHierarchy("p", HierarchyDown, 5, false, QueryOptions{MinTier: graph.OriginLSPResolved})
|
|
require.Equal(t, 1, len(sg.Edges))
|
|
assert.Equal(t, "c1", sg.Edges[0].From)
|
|
}
|