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

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")
}