Skip to content
Merged
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
82 changes: 82 additions & 0 deletions packages/core/src/scanner/__tests__/fixtures/go/generics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package generics

// Stack is a generic stack data structure.
type Stack[T any] struct {
items []T
}

// Push adds an item to the stack.
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}

// Pop removes and returns the top item.
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}

// Pair holds two values of potentially different types.
type Pair[K comparable, V any] struct {
Key K
Value V
}

// NewPair creates a new Pair.
func NewPair[K comparable, V any](key K, value V) *Pair[K, V] {
return &Pair[K, V]{Key: key, Value: value}
}

// Map applies a function to each element of a slice.
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}

// Filter returns elements that satisfy the predicate.
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}

// Reduce reduces a slice to a single value.
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}

// Comparable is an interface for comparable types.
type Comparable[T any] interface {
Compare(other T) int
}

// Ordered is an interface for ordered types.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 | ~string
}

// Min returns the minimum of two ordered values.
func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}
99 changes: 99 additions & 0 deletions packages/core/src/scanner/__tests__/go.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,103 @@ describe('GoScanner', () => {
});
});
});

describe('generics support', () => {
let genericsDocuments: Document[];

beforeAll(async () => {
genericsDocuments = await scanner.scan(['generics.go'], fixturesDir);
});

describe('generic structs', () => {
it('should extract generic struct Stack[T any]', () => {
const stack = genericsDocuments.find(
(d) => d.metadata.name === 'Stack' && d.type === 'class'
);
expect(stack).toBeDefined();
expect(stack?.metadata.custom?.isGeneric).toBe(true);
expect(stack?.metadata.custom?.typeParameters).toEqual(['T any']);
expect(stack?.metadata.signature).toContain('[T any]');
});

it('should extract generic struct with multiple type parameters', () => {
const pair = genericsDocuments.find(
(d) => d.metadata.name === 'Pair' && d.type === 'class'
);
expect(pair).toBeDefined();
expect(pair?.metadata.custom?.isGeneric).toBe(true);
expect(pair?.metadata.custom?.typeParameters).toEqual(['K comparable', 'V any']);
});
});

describe('generic functions', () => {
it('should extract generic function Map[T, U any]', () => {
const mapFn = genericsDocuments.find(
(d) => d.metadata.name === 'Map' && d.type === 'function'
);
expect(mapFn).toBeDefined();
expect(mapFn?.metadata.custom?.isGeneric).toBe(true);
expect(mapFn?.metadata.custom?.typeParameters).toEqual(['T', 'U any']);
});

it('should extract generic function with constraints', () => {
const minFn = genericsDocuments.find(
(d) => d.metadata.name === 'Min' && d.type === 'function'
);
expect(minFn).toBeDefined();
expect(minFn?.metadata.custom?.isGeneric).toBe(true);
expect(minFn?.metadata.custom?.typeParameters).toEqual(['T Ordered']);
});

it('should extract NewPair generic constructor', () => {
const newPair = genericsDocuments.find(
(d) => d.metadata.name === 'NewPair' && d.type === 'function'
);
expect(newPair).toBeDefined();
expect(newPair?.metadata.custom?.isGeneric).toBe(true);
expect(newPair?.metadata.custom?.typeParameters).toEqual(['K comparable', 'V any']);
});
});

describe('generic methods', () => {
it('should extract method on generic receiver Stack[T]', () => {
const push = genericsDocuments.find(
(d) => d.metadata.name === 'Stack.Push' && d.type === 'method'
);
expect(push).toBeDefined();
expect(push?.metadata.custom?.receiver).toBe('Stack');
expect(push?.metadata.custom?.isGeneric).toBe(true);
});

it('should extract Pop method on generic Stack', () => {
const pop = genericsDocuments.find(
(d) => d.metadata.name === 'Stack.Pop' && d.type === 'method'
);
expect(pop).toBeDefined();
expect(pop?.metadata.custom?.receiver).toBe('Stack');
expect(pop?.metadata.custom?.receiverPointer).toBe(true);
});
});

describe('generic interfaces', () => {
it('should extract generic interface Comparable[T any]', () => {
const comparable = genericsDocuments.find(
(d) => d.metadata.name === 'Comparable' && d.type === 'interface'
);
expect(comparable).toBeDefined();
expect(comparable?.metadata.custom?.isGeneric).toBe(true);
expect(comparable?.metadata.custom?.typeParameters).toEqual(['T any']);
});
});

describe('non-generic items in generic file', () => {
it('should not mark non-generic interface as generic', () => {
const ordered = genericsDocuments.find(
(d) => d.metadata.name === 'Ordered' && d.type === 'interface'
);
expect(ordered).toBeDefined();
expect(ordered?.metadata.custom?.isGeneric).toBeUndefined();
});
});
});
});
79 changes: 71 additions & 8 deletions packages/core/src/scanner/go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ const GO_QUERIES = {
name: (identifier) @name) @definition
`,

// Method declarations with receivers
// Method declarations with receivers (handles both regular and generic types)
methods: `
(method_declaration
receiver: (parameter_list
(parameter_declaration
name: (identifier)? @receiver_name
type: [
(pointer_type (type_identifier) @receiver_type)
(pointer_type (generic_type (type_identifier) @receiver_type))
(type_identifier) @receiver_type
(generic_type (type_identifier) @receiver_type)
])) @receiver
name: (field_identifier) @name) @definition
`,
Expand Down Expand Up @@ -196,6 +198,9 @@ export class GoScanner implements Scanner {
const exported = this.isExported(name);
const snippet = this.truncateSnippet(fullText);

// Check for generics
const { isGeneric, typeParameters } = this.extractTypeParameters(signature);

documents.push({
id: `${file}:${name}:${startLine}`,
text: this.buildEmbeddingText('function', name, signature, docstring),
Expand All @@ -210,7 +215,10 @@ export class GoScanner implements Scanner {
exported,
docstring,
snippet,
custom: isTestFile ? { isTest: true } : undefined,
custom: {
...(isTestFile ? { isTest: true } : {}),
...(isGeneric ? { isGeneric, typeParameters } : {}),
},
},
});
}
Expand Down Expand Up @@ -240,7 +248,9 @@ export class GoScanner implements Scanner {

const methodName = nameCapture.node.text;
const receiverType = receiverTypeCapture?.node.text || 'Unknown';
const name = `${receiverType}.${methodName}`;
// Strip type parameters from receiver for cleaner name (Stack[T] -> Stack)
const baseReceiverType = receiverType.replace(/\[.*\]/, '');
const name = `${baseReceiverType}.${methodName}`;
const startLine = defCapture.node.startPosition.row + 1;
const endLine = defCapture.node.endPosition.row + 1;
const fullText = defCapture.node.text;
Expand All @@ -253,6 +263,12 @@ export class GoScanner implements Scanner {
const receiverText = receiverCapture?.node.text || '';
const receiverPointer = receiverText.includes('*');

// Check for generics (receiver has type params like Stack[T])
const receiverHasGenerics = receiverType.includes('[');
const { isGeneric: signatureHasGenerics, typeParameters } =
this.extractTypeParameters(signature);
const isGeneric = receiverHasGenerics || signatureHasGenerics;

documents.push({
id: `${file}:${name}:${startLine}`,
text: this.buildEmbeddingText('method', name, signature, docstring),
Expand All @@ -268,9 +284,10 @@ export class GoScanner implements Scanner {
docstring,
snippet,
custom: {
receiver: receiverType,
receiver: baseReceiverType,
receiverPointer,
...(isTestFile ? { isTest: true } : {}),
...(isGeneric ? { isGeneric, typeParameters } : {}),
},
},
});
Expand Down Expand Up @@ -301,7 +318,13 @@ export class GoScanner implements Scanner {
const startLine = defCapture.node.startPosition.row + 1;
const endLine = defCapture.node.endPosition.row + 1;
const fullText = defCapture.node.text;
const signature = `type ${name} struct`;

// Check for generics in the full declaration text
const { isGeneric, typeParameters } = this.extractTypeParameters(fullText);
const signature = isGeneric
? `type ${name}[${typeParameters?.join(', ')}] struct`
: `type ${name} struct`;

const docstring = extractGoDocComment(sourceText, startLine);
const exported = this.isExported(name);
const snippet = this.truncateSnippet(fullText);
Expand All @@ -320,7 +343,10 @@ export class GoScanner implements Scanner {
exported,
docstring,
snippet,
custom: isTestFile ? { isTest: true } : undefined,
custom: {
...(isTestFile ? { isTest: true } : {}),
...(isGeneric ? { isGeneric, typeParameters } : {}),
},
},
});
}
Expand Down Expand Up @@ -350,7 +376,13 @@ export class GoScanner implements Scanner {
const startLine = defCapture.node.startPosition.row + 1;
const endLine = defCapture.node.endPosition.row + 1;
const fullText = defCapture.node.text;
const signature = `type ${name} interface`;

// Check for generics in the full declaration text
const { isGeneric, typeParameters } = this.extractTypeParameters(fullText);
const signature = isGeneric
? `type ${name}[${typeParameters?.join(', ')}] interface`
: `type ${name} interface`;

const docstring = extractGoDocComment(sourceText, startLine);
const exported = this.isExported(name);
const snippet = this.truncateSnippet(fullText);
Expand All @@ -369,7 +401,10 @@ export class GoScanner implements Scanner {
exported,
docstring,
snippet,
custom: isTestFile ? { isTest: true } : undefined,
custom: {
...(isTestFile ? { isTest: true } : {}),
...(isGeneric ? { isGeneric, typeParameters } : {}),
},
},
});
}
Expand Down Expand Up @@ -498,6 +533,34 @@ export class GoScanner implements Scanner {
return fullText.slice(0, braceIndex).trim();
}

/**
* Extract type parameters from a generic declaration.
* Returns { isGeneric, typeParameters } where typeParameters is an array like ["T any", "K comparable"]
*/
private extractTypeParameters(signature: string): {
isGeneric: boolean;
typeParameters?: string[];
} {
// Match type parameters in brackets: func Name[T any, K comparable](...) or type Name[T any] struct
// Look for [ before ( for functions, or [ after type name for types
const typeParamMatch = signature.match(/\[([^\]]+)\]/);
if (!typeParamMatch) {
return { isGeneric: false };
}

const params = typeParamMatch[1];
// Split by comma, but handle constraints like "~int | ~string"
const typeParameters = params
.split(/,\s*/)
.map((p) => p.trim())
.filter((p) => p.length > 0);

return {
isGeneric: true,
typeParameters,
};
}

/**
* Build embedding text for vector search
*/
Expand Down