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
1156 lines
28 KiB
Go
1156 lines
28 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func TestGoExtractor_SimpleFunction(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
func Hello() {
|
|
fmt.Println("hello")
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
|
|
require.Len(t, funcs, 1)
|
|
assert.Equal(t, "Hello", funcs[0].Name)
|
|
assert.Equal(t, "main.go::Hello", funcs[0].ID)
|
|
assert.Equal(t, 3, funcs[0].StartLine)
|
|
assert.Equal(t, "func Hello()", funcs[0].Meta["signature"])
|
|
}
|
|
|
|
func TestGoExtractor_Method(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Server struct{}
|
|
|
|
func (s *Server) Start() error {
|
|
return nil
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("server.go", src)
|
|
require.NoError(t, err)
|
|
|
|
methods := nodesOfKind(result.Nodes, graph.KindMethod)
|
|
require.Len(t, methods, 1)
|
|
assert.Equal(t, "Start", methods[0].Name)
|
|
assert.Equal(t, "server.go::Server.Start", methods[0].ID)
|
|
assert.Equal(t, "Server", methods[0].Meta["receiver"])
|
|
|
|
// Check member_of edge.
|
|
memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf)
|
|
require.Len(t, memberEdges, 1)
|
|
assert.Equal(t, "server.go::Server.Start", memberEdges[0].From)
|
|
assert.Equal(t, "server.go::Server", memberEdges[0].To)
|
|
}
|
|
|
|
func TestGoExtractor_Struct(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Config struct {
|
|
Port int
|
|
Host string
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("config.go", src)
|
|
require.NoError(t, err)
|
|
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 1)
|
|
assert.Equal(t, "Config", types[0].Name)
|
|
}
|
|
|
|
func TestGoExtractor_Interface(t *testing.T) {
|
|
src := []byte(`package store
|
|
|
|
type Repository interface {
|
|
FindByID(id string) (*User, error)
|
|
Save(u *User) error
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("store.go", src)
|
|
require.NoError(t, err)
|
|
|
|
ifaces := nodesOfKind(result.Nodes, graph.KindInterface)
|
|
require.Len(t, ifaces, 1)
|
|
assert.Equal(t, "Repository", ifaces[0].Name)
|
|
|
|
// Verify method names are extracted into Meta.
|
|
methods, ok := ifaces[0].Meta["methods"].([]string)
|
|
require.True(t, ok, "Meta[\"methods\"] should be []string")
|
|
assert.Len(t, methods, 2)
|
|
assert.Contains(t, methods, "FindByID")
|
|
assert.Contains(t, methods, "Save")
|
|
}
|
|
|
|
func TestGoExtractor_EmptyInterface(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Any interface{}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("any.go", src)
|
|
require.NoError(t, err)
|
|
|
|
ifaces := nodesOfKind(result.Nodes, graph.KindInterface)
|
|
require.Len(t, ifaces, 1)
|
|
assert.Equal(t, "Any", ifaces[0].Name)
|
|
|
|
// Empty interface should have empty methods slice (not nil).
|
|
methods, ok := ifaces[0].Meta["methods"]
|
|
require.True(t, ok)
|
|
// The slice should be nil or empty.
|
|
if ms, ok := methods.([]string); ok {
|
|
assert.Empty(t, ms)
|
|
}
|
|
}
|
|
|
|
func TestGoExtractor_Imports(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
mylib "github.com/me/lib"
|
|
)
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
imports := edgesOfKind(result.Edges, graph.EdgeImports)
|
|
require.Len(t, imports, 3)
|
|
|
|
targets := make([]string, len(imports))
|
|
for i, e := range imports {
|
|
targets[i] = e.To
|
|
}
|
|
assert.Contains(t, targets, "unresolved::import::fmt")
|
|
assert.Contains(t, targets, "unresolved::import::os")
|
|
assert.Contains(t, targets, "unresolved::import::github.com/me/lib")
|
|
}
|
|
|
|
func TestGoExtractor_CallSites(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
import "fmt"
|
|
|
|
func Hello() {
|
|
fmt.Println("hello")
|
|
helper()
|
|
}
|
|
|
|
func helper() {}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
require.GreaterOrEqual(t, len(calls), 2)
|
|
|
|
var targets []string
|
|
for _, c := range calls {
|
|
targets = append(targets, c.To)
|
|
}
|
|
assert.Contains(t, targets, "unresolved::helper")
|
|
// fmt.Println is now attributed to the import path so the resolver
|
|
// can render cross-boundary badges instead of a bare `*.Println`.
|
|
assert.Contains(t, targets, "unresolved::extern::fmt::Println")
|
|
}
|
|
|
|
func TestGoExtractor_Variables(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
var MaxRetries int
|
|
|
|
const DefaultPort = 8080
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("config.go", src)
|
|
require.NoError(t, err)
|
|
|
|
vars := nodesOfKind(result.Nodes, graph.KindVariable)
|
|
require.Len(t, vars, 1, "var declarations stay in KindVariable")
|
|
assert.Equal(t, "MaxRetries", vars[0].Name)
|
|
|
|
consts := nodesOfKind(result.Nodes, graph.KindConstant)
|
|
require.Len(t, consts, 1, "const declarations are peeled into KindConstant")
|
|
assert.Equal(t, "DefaultPort", consts[0].Name)
|
|
}
|
|
|
|
func TestGoExtractor_FullFile(t *testing.T) {
|
|
src := []byte(`package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
type Claims struct {
|
|
UserID string
|
|
Exp int64
|
|
}
|
|
|
|
func ValidateToken(ctx context.Context, raw string) (*Claims, error) {
|
|
if raw == "" {
|
|
return nil, errors.New("empty token")
|
|
}
|
|
return &Claims{UserID: "123"}, nil
|
|
}
|
|
|
|
func (c *Claims) IsExpired() bool {
|
|
return false
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("auth/token.go", src)
|
|
require.NoError(t, err)
|
|
|
|
// File node.
|
|
files := nodesOfKind(result.Nodes, graph.KindFile)
|
|
require.Len(t, files, 1)
|
|
|
|
// Type.
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 1)
|
|
assert.Equal(t, "Claims", types[0].Name)
|
|
|
|
// Function.
|
|
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
|
|
require.Len(t, funcs, 1)
|
|
assert.Equal(t, "ValidateToken", funcs[0].Name)
|
|
|
|
// Method.
|
|
methods := nodesOfKind(result.Nodes, graph.KindMethod)
|
|
require.Len(t, methods, 1)
|
|
assert.Equal(t, "IsExpired", methods[0].Name)
|
|
|
|
// Imports.
|
|
imports := edgesOfKind(result.Edges, graph.EdgeImports)
|
|
assert.Len(t, imports, 2)
|
|
}
|
|
|
|
func TestGoExtractor_TypeAlias(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type ID = string
|
|
type Handler func(w http.ResponseWriter, r *http.Request)
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("types.go", src)
|
|
require.NoError(t, err)
|
|
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 2)
|
|
names := []string{types[0].Name, types[1].Name}
|
|
assert.Contains(t, names, "ID")
|
|
assert.Contains(t, names, "Handler")
|
|
}
|
|
|
|
func TestGoExtractor_TypeFlavor(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Point struct{ X, Y int }
|
|
type Stringer interface{ String() string }
|
|
type ID = string
|
|
type Celsius float64
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("types.go", src)
|
|
require.NoError(t, err)
|
|
|
|
point := nodeByName(result.Nodes, "Point")
|
|
require.NotNil(t, point)
|
|
assert.Equal(t, "struct", point.Meta["type_flavor"])
|
|
|
|
stringer := nodeByName(result.Nodes, "Stringer")
|
|
require.NotNil(t, stringer)
|
|
assert.Equal(t, graph.KindInterface, stringer.Kind)
|
|
assert.Equal(t, "interface", stringer.Meta["type_flavor"])
|
|
|
|
id := nodeByName(result.Nodes, "ID")
|
|
require.NotNil(t, id)
|
|
assert.Equal(t, "type_alias", id.Meta["type_flavor"])
|
|
// Dual-write: the legacy alias marker must survive beside type_flavor.
|
|
assert.Equal(t, true, id.Meta["alias"])
|
|
|
|
celsius := nodeByName(result.Nodes, "Celsius")
|
|
require.NotNil(t, celsius)
|
|
assert.Equal(t, "newtype", celsius.Meta["type_flavor"])
|
|
}
|
|
|
|
// --- Type environment tests ---
|
|
|
|
func TestGoExtractor_TypeEnv_ExplicitVar(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type User struct{}
|
|
|
|
func (u *User) Save() {}
|
|
|
|
func main() {
|
|
var user User
|
|
user.Save()
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
var saveCall *graph.Edge
|
|
for _, c := range calls {
|
|
if strings.HasSuffix(c.To, "Save") {
|
|
saveCall = c
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, saveCall, "expected a call edge to Save")
|
|
require.NotNil(t, saveCall.Meta, "expected Meta on Save call edge")
|
|
assert.Equal(t, "User", saveCall.Meta["receiver_type"])
|
|
}
|
|
|
|
func TestGoExtractor_TypeEnv_CompositeLiteral(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type User struct{ Name string }
|
|
|
|
func (u User) Save() {}
|
|
|
|
func main() {
|
|
user := User{Name: "Alice"}
|
|
user.Save()
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
var saveCall *graph.Edge
|
|
for _, c := range calls {
|
|
if strings.HasSuffix(c.To, "Save") {
|
|
saveCall = c
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, saveCall)
|
|
require.NotNil(t, saveCall.Meta)
|
|
assert.Equal(t, "User", saveCall.Meta["receiver_type"])
|
|
}
|
|
|
|
func TestGoExtractor_TypeEnv_AddressOf(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Server struct{}
|
|
|
|
func (s *Server) Start() {}
|
|
|
|
func main() {
|
|
srv := &Server{}
|
|
srv.Start()
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
var startCall *graph.Edge
|
|
for _, c := range calls {
|
|
if strings.HasSuffix(c.To, "Start") {
|
|
startCall = c
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, startCall)
|
|
require.NotNil(t, startCall.Meta)
|
|
assert.Equal(t, "Server", startCall.Meta["receiver_type"])
|
|
}
|
|
|
|
func TestGoExtractor_TypeEnv_Constructor(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Client struct{}
|
|
|
|
func NewClient() *Client { return &Client{} }
|
|
|
|
func (c *Client) Connect() {}
|
|
|
|
func main() {
|
|
client := NewClient()
|
|
client.Connect()
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
var connectCall *graph.Edge
|
|
for _, c := range calls {
|
|
if strings.HasSuffix(c.To, "Connect") {
|
|
connectCall = c
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, connectCall)
|
|
require.NotNil(t, connectCall.Meta)
|
|
assert.Equal(t, "Client", connectCall.Meta["receiver_type"])
|
|
}
|
|
|
|
func TestGoExtractor_TypeEnv_Unknown(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
func getUser() interface{} { return nil }
|
|
|
|
func main() {
|
|
user := getUser()
|
|
user.Save()
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
var saveCall *graph.Edge
|
|
for _, c := range calls {
|
|
if strings.HasSuffix(c.To, "Save") {
|
|
saveCall = c
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, saveCall, "expected a call edge to Save")
|
|
assert.NotContains(t, saveCall.Meta, "receiver_type", "unknown type should not produce a receiver_type hint")
|
|
}
|
|
|
|
// --- Tier 2: Chain resolution tests ---
|
|
|
|
func TestGoExtractor_TypeEnv_Chain(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Service struct{}
|
|
type User struct{}
|
|
|
|
func (s *Service) GetUser() *User { return nil }
|
|
func (u *User) Save() error { return nil }
|
|
|
|
func main() {
|
|
svc := &Service{}
|
|
svc.GetUser().Save()
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
// Verify return_type is set on GetUser method.
|
|
var getUserNode *graph.Node
|
|
for _, n := range result.Nodes {
|
|
if n.Name == "GetUser" && n.Kind == graph.KindMethod {
|
|
getUserNode = n
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, getUserNode)
|
|
assert.Equal(t, "User", getUserNode.Meta["return_type"])
|
|
|
|
// Verify chain resolution: svc.GetUser().Save() should have receiver_type=User.
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
var saveCall *graph.Edge
|
|
for _, c := range calls {
|
|
if strings.HasSuffix(c.To, "Save") {
|
|
saveCall = c
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, saveCall, "expected a call edge to Save")
|
|
require.NotNil(t, saveCall.Meta, "Save call should have Meta from chain resolution")
|
|
assert.Equal(t, "User", saveCall.Meta["receiver_type"])
|
|
}
|
|
|
|
func TestGoExtractor_TypeEnv_ChainUnresolvable(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
func getService() interface{} { return nil }
|
|
|
|
func main() {
|
|
svc := getService()
|
|
svc.Process().Finish()
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
var finishCall *graph.Edge
|
|
for _, c := range calls {
|
|
if strings.HasSuffix(c.To, "Finish") {
|
|
finishCall = c
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, finishCall)
|
|
assert.NotContains(t, finishCall.Meta, "receiver_type", "unresolvable chain should not produce a receiver_type hint")
|
|
}
|
|
|
|
func TestGoExtractor_ReturnType(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type User struct{}
|
|
|
|
func NewUser() *User { return &User{} }
|
|
func (u *User) Name() string { return "" }
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
for _, n := range result.Nodes {
|
|
switch n.Name {
|
|
case "NewUser":
|
|
assert.Equal(t, "User", n.Meta["return_type"], "NewUser should return User")
|
|
case "Name":
|
|
assert.Nil(t, n.Meta["return_type"], "string return type should be skipped (primitive)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoExtractor_DocAndVisibility(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
// Hello greets the user.
|
|
// It also returns nothing.
|
|
func Hello() {}
|
|
|
|
// helper is internal.
|
|
func helper() {}
|
|
|
|
// Server is the HTTP server.
|
|
type Server struct{}
|
|
|
|
// Start brings the server up.
|
|
func (s *Server) Start() {}
|
|
|
|
// MaxRetries is the cap.
|
|
const MaxRetries = 3
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
byID := map[string]*graph.Node{}
|
|
for _, n := range result.Nodes {
|
|
byID[n.ID] = n
|
|
}
|
|
|
|
hello := byID["main.go::Hello"]
|
|
require.NotNil(t, hello)
|
|
assert.Equal(t, "Hello greets the user. It also returns nothing.", hello.Meta["doc"])
|
|
assert.Equal(t, "public", hello.Meta["visibility"])
|
|
|
|
helper := byID["main.go::helper"]
|
|
require.NotNil(t, helper)
|
|
assert.Equal(t, "helper is internal.", helper.Meta["doc"])
|
|
assert.Equal(t, "package", helper.Meta["visibility"])
|
|
|
|
server := byID["main.go::Server"]
|
|
require.NotNil(t, server)
|
|
assert.Equal(t, "Server is the HTTP server.", server.Meta["doc"])
|
|
assert.Equal(t, "public", server.Meta["visibility"])
|
|
|
|
start := byID["main.go::Server.Start"]
|
|
require.NotNil(t, start)
|
|
assert.Equal(t, "Start brings the server up.", start.Meta["doc"])
|
|
assert.Equal(t, "public", start.Meta["visibility"])
|
|
|
|
mr := byID["main.go::MaxRetries"]
|
|
require.NotNil(t, mr)
|
|
assert.Equal(t, "public", mr.Meta["visibility"])
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func nodesOfKind(nodes []*graph.Node, kind graph.NodeKind) []*graph.Node {
|
|
var out []*graph.Node
|
|
for _, n := range nodes {
|
|
if n.Kind == kind {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func edgesOfKind(edges []*graph.Edge, kind graph.EdgeKind) []*graph.Edge {
|
|
var out []*graph.Edge
|
|
for _, e := range edges {
|
|
if e.Kind == kind {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestGoExtractor_StructFieldNodes(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
// User holds the basics.
|
|
type User struct {
|
|
// Name is the public name.
|
|
Name string
|
|
age int
|
|
*Base
|
|
pkg.Embedded
|
|
}
|
|
|
|
type Base struct{}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("user.go", src)
|
|
require.NoError(t, err)
|
|
|
|
byID := map[string]*graph.Node{}
|
|
for _, n := range result.Nodes {
|
|
byID[n.ID] = n
|
|
}
|
|
|
|
name := byID["user.go::User.Name"]
|
|
require.NotNil(t, name)
|
|
assert.Equal(t, graph.KindField, name.Kind)
|
|
assert.Equal(t, "public", name.Meta["visibility"])
|
|
assert.Equal(t, "string", name.Meta["field_type"])
|
|
assert.Equal(t, "Name is the public name.", name.Meta["doc"])
|
|
|
|
age := byID["user.go::User.age"]
|
|
require.NotNil(t, age)
|
|
assert.Equal(t, "package", age.Meta["visibility"])
|
|
assert.Equal(t, "int", age.Meta["field_type"])
|
|
|
|
// Embedded pointer field — name is "Base".
|
|
base := byID["user.go::User.Base"]
|
|
require.NotNil(t, base, "embedded pointer field Base missing")
|
|
|
|
// Qualified embedded field — name is "Embedded".
|
|
emb := byID["user.go::User.Embedded"]
|
|
require.NotNil(t, emb, "qualified embedded field Embedded missing")
|
|
|
|
// MemberOf edges should hop field → owner type.
|
|
memberOf := edgesOfKind(result.Edges, graph.EdgeMemberOf)
|
|
hasNameToUser := false
|
|
for _, edge := range memberOf {
|
|
if edge.From == "user.go::User.Name" && edge.To == "user.go::User" {
|
|
hasNameToUser = true
|
|
}
|
|
}
|
|
if !hasNameToUser {
|
|
t.Fatalf("missing MemberOf edge User.Name → User")
|
|
}
|
|
}
|
|
|
|
func TestGoExtractor_FieldWrites(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Server struct {
|
|
port int
|
|
addr string
|
|
}
|
|
|
|
func (s *Server) SetPort(p int) {
|
|
s.port = p
|
|
s.addr += "x"
|
|
s.port++
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("server.go", src)
|
|
require.NoError(t, err)
|
|
|
|
writes := edgesOfKind(result.Edges, graph.EdgeWrites)
|
|
if len(writes) < 3 {
|
|
t.Fatalf("expected ≥3 EdgeWrites (one per LHS selector), got %d", len(writes))
|
|
}
|
|
// All writes should originate from SetPort.
|
|
for _, w := range writes {
|
|
if w.From != "server.go::Server.SetPort" {
|
|
t.Errorf("unexpected EdgeWrites source %q (want SetPort)", w.From)
|
|
}
|
|
}
|
|
// Writes target the unresolved field path; the resolver
|
|
// post-pass lands them on the field node.
|
|
hasPort, hasAddr := false, false
|
|
for _, w := range writes {
|
|
if w.To == "unresolved::*.port" {
|
|
hasPort = true
|
|
}
|
|
if w.To == "unresolved::*.addr" {
|
|
hasAddr = true
|
|
}
|
|
}
|
|
if !hasPort {
|
|
t.Fatalf("expected EdgeWrites → unresolved::*.port")
|
|
}
|
|
if !hasAddr {
|
|
t.Fatalf("expected EdgeWrites → unresolved::*.addr")
|
|
}
|
|
}
|
|
|
|
func TestGoExtractor_FieldReads_SelectorValue(t *testing.T) {
|
|
// Selector used as a value (passed to a call, not on LHS) should
|
|
// emit EdgeReads — schema reserves EdgeReferences for type refs.
|
|
src := []byte(`package main
|
|
|
|
type Server struct {
|
|
port int
|
|
addr string
|
|
}
|
|
|
|
func (s *Server) Snapshot() (int, string) {
|
|
return s.port, s.addr
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("server.go", src)
|
|
require.NoError(t, err)
|
|
|
|
reads := edgesOfKind(result.Edges, graph.EdgeReads)
|
|
hasPort, hasAddr := false, false
|
|
for _, r := range reads {
|
|
if r.To == "unresolved::*.port" {
|
|
hasPort = true
|
|
}
|
|
if r.To == "unresolved::*.addr" {
|
|
hasAddr = true
|
|
}
|
|
}
|
|
if !hasPort {
|
|
t.Errorf("expected EdgeReads → unresolved::*.port; got reads=%v",
|
|
edgeTargets(reads))
|
|
}
|
|
if !hasAddr {
|
|
t.Errorf("expected EdgeReads → unresolved::*.addr; got reads=%v",
|
|
edgeTargets(reads))
|
|
}
|
|
|
|
// Same identifiers should NOT also have surfaced as EdgeReferences,
|
|
// otherwise the resolver double-counts.
|
|
for _, e := range result.Edges {
|
|
if e.Kind == graph.EdgeReferences && (e.To == "unresolved::*.port" || e.To == "unresolved::*.addr") {
|
|
t.Errorf("EdgeReferences leaked for value-side field %s — should be EdgeReads", e.To)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoExtractor_FieldReads_StructLiteralFieldValue(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Cfg struct {
|
|
Handler func() string
|
|
}
|
|
|
|
func runClean() string { return "ok" }
|
|
|
|
func newCfg() *Cfg {
|
|
return &Cfg{Handler: runClean}
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("cfg.go", src)
|
|
require.NoError(t, err)
|
|
|
|
reads := edgesOfKind(result.Edges, graph.EdgeReads)
|
|
found := false
|
|
for _, r := range reads {
|
|
if r.To == "unresolved::runClean" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected EdgeReads → unresolved::runClean for struct-literal field value; reads=%v",
|
|
edgeTargets(reads))
|
|
}
|
|
}
|
|
|
|
func edgeTargets(edges []*graph.Edge) []string {
|
|
out := make([]string, 0, len(edges))
|
|
for _, e := range edges {
|
|
out = append(out, e.To)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestGoExtractor_FieldWrites_TrackedReceiver(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Cfg struct{ port int }
|
|
|
|
func main() {
|
|
c := Cfg{port: 8080}
|
|
c.port = 9090
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
writes := edgesOfKind(result.Edges, graph.EdgeWrites)
|
|
require.GreaterOrEqual(t, len(writes), 1)
|
|
hasReceiverType := false
|
|
for _, w := range writes {
|
|
if rt, _ := w.Meta["receiver_type"].(string); rt == "Cfg" {
|
|
hasReceiverType = true
|
|
}
|
|
}
|
|
if !hasReceiverType {
|
|
t.Fatalf("expected EdgeWrites with receiver_type=Cfg when receiver is tenv-tracked")
|
|
}
|
|
}
|
|
|
|
func TestGoExtractor_FieldWrites_ResolveAfterIndex(t *testing.T) {
|
|
// Verifies the extractor + indexer + resolver chain: a write to
|
|
// `c.port` lands on the field node when the receiver type is
|
|
// known to the type env.
|
|
src := []byte(`package main
|
|
|
|
type Cfg struct{ port int }
|
|
|
|
func main() {
|
|
c := Cfg{port: 8080}
|
|
c.port = 9090
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
// Field node should exist.
|
|
var fieldID string
|
|
for _, n := range result.Nodes {
|
|
if n.Kind == graph.KindField && n.Name == "port" {
|
|
fieldID = n.ID
|
|
}
|
|
}
|
|
if fieldID != "main.go::Cfg.port" {
|
|
t.Fatalf("expected field node main.go::Cfg.port, got %q", fieldID)
|
|
}
|
|
|
|
// Write edge should be unresolved at extraction time and carry
|
|
// receiver_type=Cfg (set from tenv).
|
|
writes := edgesOfKind(result.Edges, graph.EdgeWrites)
|
|
require.Len(t, writes, 1)
|
|
if rt, _ := writes[0].Meta["receiver_type"].(string); rt != "Cfg" {
|
|
t.Fatalf("expected receiver_type=Cfg, got %q", rt)
|
|
}
|
|
assert.Equal(t, "unresolved::*.port", writes[0].To)
|
|
}
|
|
|
|
func TestGoExtractor_GenericTypeParams(t *testing.T) {
|
|
src := []byte(`package x
|
|
|
|
func Map[T any, R comparable](in []T, f func(T) R) []R { return nil }
|
|
|
|
func (m *Map[K, V]) Get(k K) V { var v V; return v }
|
|
|
|
type List[T any] struct {
|
|
items []T
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("x.go", src)
|
|
require.NoError(t, err)
|
|
|
|
byID := map[string]*graph.Node{}
|
|
for _, n := range result.Nodes {
|
|
byID[n.ID] = n
|
|
}
|
|
|
|
mapFn := byID["x.go::Map"]
|
|
require.NotNil(t, mapFn)
|
|
tp, _ := mapFn.Meta["type_params"].([]map[string]string)
|
|
require.Len(t, tp, 2)
|
|
assert.Equal(t, "T", tp[0]["name"])
|
|
assert.Equal(t, "any", tp[0]["bound"])
|
|
assert.Equal(t, "R", tp[1]["name"])
|
|
assert.Equal(t, "comparable", tp[1]["bound"])
|
|
}
|
|
|
|
func TestGoExtractor_ThrowsErrorReturn(t *testing.T) {
|
|
src := []byte(`package x
|
|
|
|
func DoWork() error { return nil }
|
|
|
|
func ParseInt(s string) (int, error) { return 0, nil }
|
|
|
|
func MakeUser(name string) (*User, *MyError) { return nil, nil }
|
|
|
|
func NoError() string { return "" }
|
|
|
|
type MyError struct{}
|
|
type User struct{}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("x.go", src)
|
|
require.NoError(t, err)
|
|
|
|
throws := edgesOfKind(result.Edges, graph.EdgeThrows)
|
|
throwsFrom := map[string]string{}
|
|
for _, e := range throws {
|
|
throwsFrom[e.From] = e.To
|
|
}
|
|
assert.Equal(t, "external::error", throwsFrom["x.go::DoWork"], "DoWork should throw external::error")
|
|
assert.Equal(t, "external::error", throwsFrom["x.go::ParseInt"], "ParseInt's last return is error")
|
|
assert.Equal(t, "unresolved::MyError", throwsFrom["x.go::MakeUser"], "MakeUser throws *MyError")
|
|
|
|
if _, ok := throwsFrom["x.go::NoError"]; ok {
|
|
t.Fatalf("NoError shouldn't have an EdgeThrows")
|
|
}
|
|
}
|
|
|
|
func TestGoExtractor_ImportNodes(t *testing.T) {
|
|
src := []byte(`package x
|
|
|
|
import (
|
|
"fmt"
|
|
"context"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
g "github.com/zzet/gortex/internal/graph"
|
|
)
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("x.go", src)
|
|
require.NoError(t, err)
|
|
|
|
imports := nodesOfKind(result.Nodes, graph.KindImport)
|
|
require.GreaterOrEqual(t, len(imports), 4)
|
|
|
|
byID := map[string]*graph.Node{}
|
|
for _, n := range imports {
|
|
byID[n.ID] = n
|
|
}
|
|
|
|
fmtNode := byID["x.go::import::fmt"]
|
|
require.NotNil(t, fmtNode, "fmt import node missing")
|
|
assert.Equal(t, "fmt", fmtNode.Meta["path"])
|
|
assert.Equal(t, false, fmtNode.Meta["is_external"], "fmt is stdlib, not external")
|
|
|
|
graphNode := byID["x.go::import::github.com/zzet/gortex/internal/graph"]
|
|
require.NotNil(t, graphNode, "module import node missing")
|
|
assert.Equal(t, true, graphNode.Meta["is_external"], "github.com path should be external")
|
|
|
|
// Aliased imports: the same path appears once with rawAlias != ""
|
|
// (the second `g "..."` reuses the same ID since we key by path,
|
|
// not by alias). The alias surfaces on the node Meta when
|
|
// declared; for plain re-imports of the same path we keep the
|
|
// last-write-wins behaviour the indexer expects.
|
|
hasAlias := false
|
|
for _, n := range imports {
|
|
if a, _ := n.Meta["alias"].(string); a == "g" {
|
|
hasAlias = true
|
|
}
|
|
}
|
|
assert.True(t, hasAlias, "aliased import 'g' should surface in Meta")
|
|
}
|
|
|
|
func TestGoExtractor_Complexity(t *testing.T) {
|
|
src := []byte(`package x
|
|
|
|
func Simple() int { return 1 }
|
|
|
|
func Branchy(x int) int {
|
|
if x > 0 {
|
|
return 1
|
|
}
|
|
for i := 0; i < x; i++ {
|
|
switch i {
|
|
case 0:
|
|
return 0
|
|
case 1:
|
|
return 1
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("x.go", src)
|
|
require.NoError(t, err)
|
|
|
|
byID := map[string]*graph.Node{}
|
|
for _, n := range result.Nodes {
|
|
byID[n.ID] = n
|
|
}
|
|
|
|
// Simple has no branches → no complexity meta.
|
|
simple := byID["x.go::Simple"]
|
|
require.NotNil(t, simple)
|
|
if _, ok := simple.Meta["complexity"]; ok {
|
|
t.Fatalf("Simple should have no complexity meta, got %v", simple.Meta["complexity"])
|
|
}
|
|
|
|
// Branchy: 1 (base) + if + for + switch + 2 cases = 6.
|
|
branchy := byID["x.go::Branchy"]
|
|
require.NotNil(t, branchy)
|
|
c, ok := branchy.Meta["complexity"].(int)
|
|
require.True(t, ok, "Branchy should have integer complexity")
|
|
if c < 4 {
|
|
t.Fatalf("Branchy complexity = %d, expected >= 4", c)
|
|
}
|
|
}
|
|
|
|
// TestGoExtractor_TypeEnv_FunctionParam covers the case `func F(reg
|
|
// *Registry) { reg.Register(...) }`. Before this test was wired the
|
|
// extractor only learned receiver types from `var` and `:=` decls, so
|
|
// parameter-bound receivers fell through to the resolver's name-only
|
|
// fallback and produced wrong edges (the original RegisterAll →
|
|
// OverlayManager.Register bug).
|
|
func TestGoExtractor_TypeEnv_FunctionParam(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Registry struct{}
|
|
|
|
func (r *Registry) Register(_ any) {}
|
|
|
|
func RegisterAll(reg *Registry) {
|
|
reg.Register(nil)
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("register.go", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
var registerCall *graph.Edge
|
|
for _, c := range calls {
|
|
if strings.HasSuffix(c.To, ".Register") || strings.HasSuffix(c.To, "*.Register") {
|
|
registerCall = c
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, registerCall, "expected a call edge to Register")
|
|
require.NotNil(t, registerCall.Meta, "Register call should carry receiver_type Meta from the function parameter")
|
|
assert.Equal(t, "Registry", registerCall.Meta["receiver_type"])
|
|
}
|
|
|
|
// TestGoExtractor_TypeEnv_FunctionParam_PerScope verifies that two
|
|
// functions in the same file with the same parameter name but
|
|
// different types each see the right type at their own call sites.
|
|
// File-wide tenv would have collapsed both to a single type, so this
|
|
// guards against accidental loss of per-function scoping.
|
|
func TestGoExtractor_TypeEnv_FunctionParam_PerScope(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Registry struct{}
|
|
type OverlayManager struct{}
|
|
|
|
func (r *Registry) Register(_ any) {}
|
|
func (m *OverlayManager) Register(_ string) string { return "" }
|
|
|
|
func RegisterAll(reg *Registry) {
|
|
reg.Register(nil)
|
|
}
|
|
|
|
func WireOverlay(reg *OverlayManager) {
|
|
reg.Register("ws")
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("scopes.go", src)
|
|
require.NoError(t, err)
|
|
|
|
wantByCaller := map[string]string{
|
|
"scopes.go::RegisterAll": "Registry",
|
|
"scopes.go::WireOverlay": "OverlayManager",
|
|
}
|
|
|
|
got := map[string]string{}
|
|
for _, e := range edgesOfKind(result.Edges, graph.EdgeCalls) {
|
|
if !strings.Contains(e.To, "Register") {
|
|
continue
|
|
}
|
|
if e.Meta == nil {
|
|
continue
|
|
}
|
|
if rt, ok := e.Meta["receiver_type"].(string); ok {
|
|
got[e.From] = rt
|
|
}
|
|
}
|
|
|
|
for caller, want := range wantByCaller {
|
|
assert.Equal(t, want, got[caller], "caller %s should have receiver_type=%s", caller, want)
|
|
}
|
|
}
|
|
|
|
// TestGoExtractor_TypeEnv_MethodReceiver covers `(s *Server) Foo() {
|
|
// s.client.Do() }` — the method receiver `s` itself is parameter-
|
|
// bound, so its type must enter the per-function scope or chain walks
|
|
// fail. We also need to be sure the receiver name doesn't bleed into
|
|
// other functions (per-scope guarantee).
|
|
func TestGoExtractor_TypeEnv_MethodReceiver(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Inner struct{}
|
|
type Server struct{}
|
|
|
|
func (i *Inner) Bump() {}
|
|
|
|
func (s *Server) Process() {
|
|
var x *Inner
|
|
x.Bump()
|
|
_ = s
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("server.go", src)
|
|
require.NoError(t, err)
|
|
|
|
for _, c := range edgesOfKind(result.Edges, graph.EdgeCalls) {
|
|
if !strings.Contains(c.To, "Bump") {
|
|
continue
|
|
}
|
|
require.NotNil(t, c.Meta)
|
|
assert.Equal(t, "Inner", c.Meta["receiver_type"])
|
|
}
|
|
}
|
|
|
|
func TestGoExtractor_TypeAssertionReferences(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type Doer interface{ Do() }
|
|
|
|
func run(x any) {
|
|
if d, ok := x.(Doer); ok {
|
|
d.Do()
|
|
}
|
|
_ = x.(other.Fooer)
|
|
}
|
|
`)
|
|
e := NewGoExtractor()
|
|
result, err := e.Extract("main.go", src)
|
|
require.NoError(t, err)
|
|
|
|
var targets []string
|
|
for _, r := range edgesOfKind(result.Edges, graph.EdgeReferences) {
|
|
targets = append(targets, r.To)
|
|
}
|
|
// Local type assertion x.(Doer) must reference the asserted type, so an
|
|
// interface used only via an assertion is not seen as dead code.
|
|
assert.Contains(t, targets, "unresolved::Doer")
|
|
// Qualified assertion x.(other.Fooer): the package qualifier is dropped
|
|
// and the type is resolved by bare name like every other type reference.
|
|
assert.Contains(t, targets, "unresolved::Fooer")
|
|
}
|