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