From 0f8523d2de1eb00a3d2431ef778a1a50d3f2bc45 Mon Sep 17 00:00:00 2001 From: Tirth Kanani Date: Sun, 14 Jun 2026 18:01:21 +0100 Subject: [PATCH] fix(go-extractor): attach methods on generic receivers and handle grouped type declarations extractReceiverType only unwrapped a bare type_identifier or a pointer_type directly wrapping one. Generic receivers like `(s *Stack[T])` or `(s Stack[T])` are parsed as generic_type nodes (optionally pointer-wrapped), so the base type name was never found and the method was never attached to its struct (methods stayed []). The fix iteratively unwraps pointer_type and generic_type layers before grabbing the base type_identifier. extractTypeDeclaration used findChild(node, "type_spec"), which only returns the first type_spec. Grouped declarations (`type ( Foo struct{...}; Bar struct{...} )`) contain multiple type_spec children, so every type after the first was silently dropped from classes and exports. The fix iterates over all type_spec children via findChildren and passes each typeSpec as the decl node for accurate per-type line ranges. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../extractors/__tests__/go-extractor.test.ts | 51 +++++++++++++++++++ .../src/plugins/extractors/go-extractor.ts | 44 +++++++++------- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/go-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/go-extractor.test.ts index 2cf1b772..61490a2d 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/go-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/go-extractor.test.ts @@ -596,4 +596,55 @@ func helper(x int) string { parser.delete(); }); }); + + // ---- Generic receivers ---- + + describe("extractStructure - generic receivers", () => { + it("attaches methods on generic receivers to their struct", () => { + const { tree, parser, root } = parse(`package main + +type Stack[T any] struct { + items []T +} + +func (s *Stack[T]) Push(item T) {} + +func (s Stack[T]) Len() int { + return 0 +} +`); + const result = extractor.extractStructure(root); + + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("Stack"); + expect(result.classes[0].methods.sort()).toEqual(["Len", "Push"]); + + tree.delete(); + parser.delete(); + }); + }); + + // ---- Grouped type declarations ---- + + describe("extractStructure - grouped type declarations", () => { + it("handles grouped type declarations", () => { + const { tree, parser, root } = parse(`package main + +type ( + Foo struct { A int } + Bar struct { B int } +) +`); + const result = extractor.extractStructure(root); + + expect(result.classes.map((c) => c.name)).toEqual(["Foo", "Bar"]); + + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("Foo"); + expect(exportNames).toContain("Bar"); + + tree.delete(); + parser.delete(); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/go-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/go-extractor.ts index 53e3e95a..b41355cc 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/go-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/go-extractor.ts @@ -50,16 +50,21 @@ function extractReceiverType(receiverNode: TreeSitterNode): string | undefined { const decl = findChild(receiverNode, "parameter_declaration"); if (!decl) return undefined; - // Look for type_identifier directly or inside pointer_type + // Look for type_identifier directly or inside pointer_type / generic_type. + // For generic receivers the receiver is wrapped in a generic_type (and may be + // pointer-wrapped too), e.g. `(s *Stack[T])`, so unwrap those layers before + // grabbing the base type_identifier. for (let i = 0; i < decl.childCount; i++) { - const child = decl.child(i); + let child: TreeSitterNode | null = decl.child(i); if (!child) continue; - if (child.type === "type_identifier") { - return child.text; + while (child && (child.type === "pointer_type" || child.type === "generic_type")) { + child = + child.type === "generic_type" + ? child.childForFieldName("type") + : findChild(child, "type_identifier") ?? findChild(child, "generic_type"); } - if (child.type === "pointer_type") { - const typeId = findChild(child, "type_identifier"); - if (typeId) return typeId.text; + if (child && child.type === "type_identifier") { + return child.text; } } return undefined; @@ -261,17 +266,20 @@ export class GoExtractor implements LanguageExtractor { classes: StructuralAnalysis["classes"], exports: StructuralAnalysis["exports"], ): void { - const typeSpec = findChild(node, "type_spec"); - if (!typeSpec) return; - - const nameNode = typeSpec.childForFieldName("name"); - const typeNode = typeSpec.childForFieldName("type"); - if (!nameNode || !typeNode) return; - - if (typeNode.type === "struct_type") { - this.extractStruct(node, nameNode, typeNode, classes, exports); - } else if (typeNode.type === "interface_type") { - this.extractInterface(node, nameNode, typeNode, classes, exports); + // A type_declaration can hold multiple type_spec children when types are + // grouped, e.g. `type ( Foo struct{...}; Bar struct{...} )`. Iterate over + // all of them so every grouped type is captured, not just the first. + const typeSpecs = findChildren(node, "type_spec"); + for (const typeSpec of typeSpecs) { + const nameNode = typeSpec.childForFieldName("name"); + const typeNode = typeSpec.childForFieldName("type"); + if (!nameNode || !typeNode) continue; + + if (typeNode.type === "struct_type") { + this.extractStruct(typeSpec, nameNode, typeNode, classes, exports); + } else if (typeNode.type === "interface_type") { + this.extractInterface(typeSpec, nameNode, typeNode, classes, exports); + } } }