Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,60 @@ type reader interface {
tree.delete();
parser.delete();
});

it("exports defined types and type aliases (not just structs/interfaces)", () => {
const { tree, parser, root } = parse(`package main

type Count int
type Celsius float64
type ID = string

type count int
type id = string
`);
const result = extractor.extractStructure(root);

const exportNames = result.exports.map((e) => e.name);
// Exported defined types and aliases are surfaced...
expect(exportNames).toContain("Count");
expect(exportNames).toContain("Celsius");
expect(exportNames).toContain("ID");
// ...lowercase (unexported) ones are not.
expect(exportNames).not.toContain("count");
expect(exportNames).not.toContain("id");
// They are not modeled as classes (no fields/methods).
expect(result.classes.map((c) => c.name)).not.toContain("Count");

tree.delete();
parser.delete();
});

it("captures every kind in a grouped type block (struct, interface, defined, alias)", () => {
const { tree, parser, root } = parse(`package main

type (
Foo struct{ x int }
Bar interface{ M() }
Gid = string
N int
)
`);
const result = extractor.extractStructure(root);

const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toEqual(
expect.arrayContaining(["Foo", "Bar", "Gid", "N"]),
);
// Struct/interface are modeled as classes; defined-type/alias are not.
expect(result.classes.map((c) => c.name)).toEqual(
expect.arrayContaining(["Foo", "Bar"]),
);
expect(result.classes.map((c) => c.name)).not.toContain("Gid");
expect(result.classes.map((c) => c.name)).not.toContain("N");

tree.delete();
parser.delete();
});
});

// ---- Call Graph ----
Expand Down Expand Up @@ -596,4 +650,150 @@ 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();
});

it("attaches methods on multi-parameter generic receivers", () => {
const { tree, parser, root } = parse(`package main

type Box[K comparable, V any] struct {
data map[K]V
}

func (b *Box[K, V]) Get(k K) V {
var zero V
return zero
}

func (b Box[K, V]) Len() int {
return 0
}
`);
const result = extractor.extractStructure(root);

expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Box");
expect(result.classes[0].methods.sort()).toEqual(["Get", "Len"]);

tree.delete();
parser.delete();
});

it("attaches methods on constrained generic receivers", () => {
const { tree, parser, root } = parse(`package main

type Result[T comparable] struct {
value T
}

func (r Result[T comparable]) Ok() bool {
return true
}
`);
const result = extractor.extractStructure(root);

expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Result");
expect(result.classes[0].methods).toContain("Ok");

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();
});

it("handles a grouped block mixing struct and interface specs", () => {
const { tree, parser, root } = parse(`package main

type (
Foo struct { A int }
Bar interface { Read() error }
)
`);
const result = extractor.extractStructure(root);

expect(result.classes.map((c) => c.name)).toEqual(["Foo", "Bar"]);

const foo = result.classes.find((c) => c.name === "Foo")!;
expect(foo.properties).toEqual(["A"]);
expect(foo.methods).toEqual([]);

const bar = result.classes.find((c) => c.name === "Bar")!;
expect(bar.methods).toContain("Read");
expect(bar.properties).toEqual([]);

tree.delete();
parser.delete();
});

it("handles a grouped block of two interfaces", () => {
const { tree, parser, root } = parse(`package main

type (
Reader interface { Read() error }
Writer interface { Write(p []byte) (int, error) }
)
`);
const result = extractor.extractStructure(root);

expect(result.classes.map((c) => c.name)).toEqual(["Reader", "Writer"]);

const reader = result.classes.find((c) => c.name === "Reader")!;
expect(reader.methods).toEqual(["Read"]);
expect(reader.properties).toEqual([]);

const writer = result.classes.find((c) => c.name === "Writer")!;
expect(writer.methods).toEqual(["Write"]);
expect(writer.properties).toEqual([]);

tree.delete();
parser.delete();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -261,17 +266,50 @@ 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;
// 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.
//
// We use the per-spec node (not the outer `type_declaration`) as the
// declNode so each grouped type reports its own line range; for a
// non-grouped `type Foo struct{...}` the spec and the `type` keyword sit on
// the same physical line, so the line range is unchanged.
//
// struct_type / interface_type specs are modeled as `classes` (with their
// fields/methods). Defined types (`type Count int`) and type aliases
// (`type MyID = string`, parsed as a separate `type_alias` node) have no
// fields or methods to model, but they are still top-level package
// declarations: when exported they are surfaced in `exports` so public
// types aren't silently dropped from the structural view.
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);
} else if (isExported(nameNode.text)) {
// Defined type, e.g. `type Count int` / `type Celsius float64`.
exports.push({
name: nameNode.text,
lineNumber: typeSpec.startPosition.row + 1,
});
}
}

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);
// Type aliases (`type MyID = string`) parse as `type_alias`, a sibling of
// `type_spec` under `type_declaration`, so they are not visited above.
for (const alias of findChildren(node, "type_alias")) {
const aliasName = alias.childForFieldName("name");
if (aliasName && isExported(aliasName.text)) {
exports.push({
name: aliasName.text,
lineNumber: alias.startPosition.row + 1,
});
}
}
}

Expand Down
Loading