Skip to content

Commit b6efcc4

Browse files
invalidclaude
andcommitted
feat: add Go language adapter
Implement GoAdapter for tree-sitter-go that extracts functions (including methods with receivers), imports, exports (capitalized identifiers), structs (as classes), and type declarations (struct/interface/type). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b1371a4 commit b6efcc4

6 files changed

Lines changed: 774 additions & 0 deletions

File tree

cli/src/languages/go.js

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { LanguageAdapter } from './base.js';
2+
3+
/**
4+
* Go language adapter.
5+
*
6+
* Walks a tree-sitter AST produced by the Go grammar and extracts
7+
* functions, imports, exports (capitalized identifiers), structs
8+
* (mapped to "classes") and type declarations.
9+
*/
10+
export class GoAdapter extends LanguageAdapter {
11+
constructor() {
12+
super('go');
13+
}
14+
15+
// ---------------------------------------------------------------------------
16+
// Functions
17+
// ---------------------------------------------------------------------------
18+
19+
/**
20+
* Extract function_declaration and method_declaration nodes.
21+
*
22+
* For methods the receiver is included in the signature, e.g.
23+
* "(h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)"
24+
*
25+
* For plain functions:
26+
* "NewHandler(db *Database) *Handler"
27+
*/
28+
extractFunctions(tree, sourceCode) {
29+
const functions = [];
30+
this._walkNodes(tree.rootNode, (node) => {
31+
if (node.type === 'function_declaration') {
32+
const fn = this._parseFuncDecl(node, sourceCode);
33+
if (fn) functions.push(fn);
34+
} else if (node.type === 'method_declaration') {
35+
const fn = this._parseMethodDecl(node, sourceCode);
36+
if (fn) functions.push(fn);
37+
}
38+
});
39+
return functions;
40+
}
41+
42+
/** Parse a function_declaration node. */
43+
_parseFuncDecl(node, _sourceCode) {
44+
const nameNode = node.childForFieldName('name');
45+
if (!nameNode) return null;
46+
47+
const params = node.childForFieldName('parameters');
48+
const result = node.childForFieldName('result');
49+
50+
const paramsText = params ? params.text : '()';
51+
const resultText = result ? ' ' + result.text : '';
52+
const signature = `${nameNode.text}${paramsText}${resultText}`;
53+
54+
return {
55+
name: nameNode.text,
56+
signature,
57+
startLine: node.startPosition.row + 1,
58+
endLine: node.endPosition.row + 1,
59+
};
60+
}
61+
62+
/** Parse a method_declaration node (includes receiver). */
63+
_parseMethodDecl(node, _sourceCode) {
64+
const nameNode = node.childForFieldName('name');
65+
if (!nameNode) return null;
66+
67+
const receiver = node.childForFieldName('receiver');
68+
const params = node.childForFieldName('parameters');
69+
const result = node.childForFieldName('result');
70+
71+
const receiverText = receiver ? receiver.text + ' ' : '';
72+
const paramsText = params ? params.text : '()';
73+
const resultText = result ? ' ' + result.text : '';
74+
const signature = `${receiverText}${nameNode.text}${paramsText}${resultText}`;
75+
76+
return {
77+
name: nameNode.text,
78+
signature,
79+
startLine: node.startPosition.row + 1,
80+
endLine: node.endPosition.row + 1,
81+
};
82+
}
83+
84+
// ---------------------------------------------------------------------------
85+
// Imports
86+
// ---------------------------------------------------------------------------
87+
88+
/**
89+
* Extract import declarations.
90+
*
91+
* Go imports come in two forms:
92+
* import "fmt"
93+
* import ( "fmt" ; alias "net/http" )
94+
*
95+
* Each import_spec has an optional alias (identifier, dot, or blank_identifier)
96+
* and a path (interpreted_string_literal).
97+
*/
98+
extractImports(tree, _sourceCode) {
99+
const imports = [];
100+
this._walkNodes(tree.rootNode, (node) => {
101+
if (node.type !== 'import_spec') return;
102+
103+
let pathNode = null;
104+
let aliasNode = null;
105+
106+
for (let i = 0; i < node.childCount; i++) {
107+
const child = node.child(i);
108+
if (child.type === 'interpreted_string_literal') {
109+
pathNode = child;
110+
} else if (
111+
child.type === 'package_identifier' ||
112+
child.type === 'identifier' ||
113+
child.type === 'dot' ||
114+
child.type === 'blank_identifier'
115+
) {
116+
aliasNode = child;
117+
}
118+
}
119+
120+
if (!pathNode) return;
121+
122+
const source = this._stripQuotes(pathNode.text);
123+
124+
// Symbol is either the explicit alias or the last path segment
125+
let symbol;
126+
if (aliasNode) {
127+
symbol = aliasNode.text;
128+
} else {
129+
const parts = source.split('/');
130+
symbol = parts[parts.length - 1];
131+
}
132+
133+
imports.push({
134+
source,
135+
symbols: [symbol],
136+
isExternal: true, // Go has no relative imports
137+
});
138+
});
139+
return imports;
140+
}
141+
142+
// ---------------------------------------------------------------------------
143+
// Exports
144+
// ---------------------------------------------------------------------------
145+
146+
/**
147+
* In Go, an identifier is exported if its first character is uppercase.
148+
* Collect all top-level function names and type names that start with an
149+
* uppercase letter.
150+
*/
151+
extractExports(tree, _sourceCode) {
152+
const exports = [];
153+
this._walkNodes(tree.rootNode, (node) => {
154+
// Top-level functions / methods
155+
if (node.type === 'function_declaration' || node.type === 'method_declaration') {
156+
const nameNode = node.childForFieldName('name');
157+
if (nameNode && this._isExported(nameNode.text)) {
158+
exports.push(nameNode.text);
159+
}
160+
}
161+
162+
// Type declarations (struct, interface, type alias)
163+
if (node.type === 'type_spec') {
164+
const nameNode = node.childForFieldName('name');
165+
if (nameNode && this._isExported(nameNode.text)) {
166+
exports.push(nameNode.text);
167+
}
168+
}
169+
});
170+
return exports;
171+
}
172+
173+
// ---------------------------------------------------------------------------
174+
// Classes (Go structs)
175+
// ---------------------------------------------------------------------------
176+
177+
/**
178+
* Go has no classes, but struct types serve a similar role.
179+
* Find type_spec nodes whose type child is a struct_type.
180+
*/
181+
extractClasses(tree, _sourceCode) {
182+
const classes = [];
183+
this._walkNodes(tree.rootNode, (node) => {
184+
if (node.type !== 'type_spec') return;
185+
186+
const typeNode = node.childForFieldName('type');
187+
if (!typeNode || typeNode.type !== 'struct_type') return;
188+
189+
const nameNode = node.childForFieldName('name');
190+
if (!nameNode) return;
191+
192+
// Use the parent type_declaration for full line span
193+
const declNode = node.parent && node.parent.type === 'type_declaration'
194+
? node.parent
195+
: node;
196+
197+
classes.push({
198+
name: nameNode.text,
199+
startLine: declNode.startPosition.row + 1,
200+
endLine: declNode.endPosition.row + 1,
201+
});
202+
});
203+
return classes;
204+
}
205+
206+
// ---------------------------------------------------------------------------
207+
// Types
208+
// ---------------------------------------------------------------------------
209+
210+
/**
211+
* Extract all type_spec nodes.
212+
*
213+
* kind is determined by the type child:
214+
* - struct_type -> "struct"
215+
* - interface_type -> "interface"
216+
* - anything else -> "type"
217+
*/
218+
extractTypes(tree, _sourceCode) {
219+
const types = [];
220+
this._walkNodes(tree.rootNode, (node) => {
221+
if (node.type !== 'type_spec') return;
222+
223+
const nameNode = node.childForFieldName('name');
224+
if (!nameNode) return;
225+
226+
const typeNode = node.childForFieldName('type');
227+
let kind = 'type';
228+
if (typeNode) {
229+
if (typeNode.type === 'struct_type') kind = 'struct';
230+
else if (typeNode.type === 'interface_type') kind = 'interface';
231+
}
232+
233+
// Use the parent type_declaration for full line span
234+
const declNode = node.parent && node.parent.type === 'type_declaration'
235+
? node.parent
236+
: node;
237+
238+
types.push({
239+
name: nameNode.text,
240+
kind,
241+
startLine: declNode.startPosition.row + 1,
242+
endLine: declNode.endPosition.row + 1,
243+
});
244+
});
245+
return types;
246+
}
247+
248+
// ---------------------------------------------------------------------------
249+
// Helpers
250+
// ---------------------------------------------------------------------------
251+
252+
/** Walk all nodes depth-first, calling visitor(node) for each. */
253+
_walkNodes(root, visitor) {
254+
const stack = [root];
255+
while (stack.length > 0) {
256+
const node = stack.pop();
257+
visitor(node);
258+
for (let i = node.childCount - 1; i >= 0; i--) {
259+
stack.push(node.child(i));
260+
}
261+
}
262+
}
263+
264+
/** Check whether a Go identifier is exported (starts with uppercase). */
265+
_isExported(name) {
266+
if (!name || name.length === 0) return false;
267+
const first = name.charCodeAt(0);
268+
return first >= 65 && first <= 90; // A-Z
269+
}
270+
271+
/** Strip surrounding quotes from a string literal. */
272+
_stripQuotes(text) {
273+
return text.replace(/^['"`]|['"`]$/g, '');
274+
}
275+
}

0 commit comments

Comments
 (0)