diff --git a/internal/lang/go_lang.go b/internal/lang/go_lang.go index e678ac8b2..13ae2ecfc 100644 --- a/internal/lang/go_lang.go +++ b/internal/lang/go_lang.go @@ -5,7 +5,7 @@ func init() { Language: Go, FileExtensions: []string{".go"}, FunctionNodeTypes: []string{"function_declaration", "method_declaration"}, - ClassNodeTypes: []string{"type_declaration"}, + ClassNodeTypes: []string{"type_spec", "type_alias"}, ModuleNodeTypes: []string{"source_file"}, CallNodeTypes: []string{"call_expression"}, ImportNodeTypes: []string{"import_declaration"}, diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 11349f3ff..0e575c491 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -494,6 +494,18 @@ func (p *Pipeline) extractClass( // Determine specific label label := classLabelForKind(node.Kind()) + // Go type_spec: determine actual type from child (interface vs struct) + if node.Kind() == "type_spec" { + if typeNode := node.ChildByFieldName("type"); typeNode != nil { + switch typeNode.Kind() { + case "interface_type": + label = "Interface" + case "struct_type": + label = "Class" + } + } + } + startLine := int(node.StartPosition().Row) + 1 endLine := int(node.EndPosition().Row) + 1 @@ -838,11 +850,11 @@ func isExported(name string, language lang.Language) bool { func classLabelForKind(kind string) string { switch kind { - case "interface_declaration", "trait_item", "trait_definition": + case "interface_declaration", "trait_item", "trait_definition", "trait_declaration": return "Interface" case "enum_declaration", "enum_item", "enum_specifier": return "Enum" - case "type_declaration", "type_alias_declaration", "type_item": + case "type_declaration", "type_alias_declaration", "type_item", "type_spec", "type_alias": return "Type" case "union_specifier", "union_item": return "Union" diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index 2cda1491c..1a18de408 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -369,6 +369,125 @@ def run(): } } +// TestGoTypeClassification verifies that Go interfaces, structs, and type aliases +// are correctly classified as Interface, Class, and Type respectively. +func TestGoTypeClassification(t *testing.T) { + dir, err := os.MkdirTemp("", "cgm-go-types-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + writeFile(t, filepath.Join(dir, "types.go"), `package types + +type Reader interface { + Read(p []byte) (n int, err error) +} + +type Writer interface { + Write(p []byte) (n int, err error) +} + +type Config struct { + Host string + Port int +} + +type ID = string +`) + + s, err := store.OpenMemory() + if err != nil { + t.Fatal(err) + } + defer s.Close() + + p := New(s, dir) + if err := p.Run(); err != nil { + t.Fatalf("Pipeline.Run: %v", err) + } + + // Check Interface nodes (Reader, Writer) + interfaces, _ := s.FindNodesByLabel(p.ProjectName, "Interface") + if len(interfaces) != 2 { + t.Errorf("expected 2 Interface nodes, got %d", len(interfaces)) + for _, n := range interfaces { + t.Logf(" Interface: %s", n.Name) + } + } + + // Check Class nodes (Config struct) + classes, _ := s.FindNodesByLabel(p.ProjectName, "Class") + if len(classes) != 1 { + t.Errorf("expected 1 Class node (struct), got %d", len(classes)) + for _, n := range classes { + t.Logf(" Class: %s", n.Name) + } + } else if classes[0].Name != "Config" { + t.Errorf("expected Class name 'Config', got %q", classes[0].Name) + } + + // Check Type nodes (ID type alias) + types, _ := s.FindNodesByLabel(p.ProjectName, "Type") + if len(types) != 1 { + t.Errorf("expected 1 Type node (alias), got %d", len(types)) + for _, n := range types { + t.Logf(" Type: %s", n.Name) + } + } else if types[0].Name != "ID" { + t.Errorf("expected Type name 'ID', got %q", types[0].Name) + } +} + +// TestGoGroupedTypeDeclaration verifies that grouped type declarations +// (type ( ... )) also work correctly. +func TestGoGroupedTypeDeclaration(t *testing.T) { + dir, err := os.MkdirTemp("", "cgm-go-grouped-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + writeFile(t, filepath.Join(dir, "models.go"), `package models + +type ( + Request struct { + URL string + } + + Response struct { + Status int + } + + Handler interface { + Handle(req Request) Response + } +) +`) + + s, err := store.OpenMemory() + if err != nil { + t.Fatal(err) + } + defer s.Close() + + p := New(s, dir) + if err := p.Run(); err != nil { + t.Fatalf("Pipeline.Run: %v", err) + } + + // Should find 2 structs (Class) and 1 interface + classes, _ := s.FindNodesByLabel(p.ProjectName, "Class") + if len(classes) != 2 { + t.Errorf("expected 2 Class nodes (structs), got %d", len(classes)) + } + + interfaces, _ := s.FindNodesByLabel(p.ProjectName, "Interface") + if len(interfaces) != 1 { + t.Errorf("expected 1 Interface node, got %d", len(interfaces)) + } +} + // TestFunctionRegistry tests the registry in isolation. func TestFunctionRegistry(t *testing.T) { r := NewFunctionRegistry()