@getmikk/core 2.0.14 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/src/analysis/type-flow.ts +1 -1
  4. package/src/cache/incremental-cache.ts +86 -80
  5. package/src/contract/contract-reader.ts +1 -0
  6. package/src/contract/lock-compiler.ts +95 -13
  7. package/src/contract/schema.ts +2 -0
  8. package/src/error-handler.ts +2 -1
  9. package/src/graph/cluster-detector.ts +2 -4
  10. package/src/graph/dead-code-detector.ts +303 -117
  11. package/src/graph/graph-builder.ts +21 -161
  12. package/src/graph/impact-analyzer.ts +1 -0
  13. package/src/graph/index.ts +2 -0
  14. package/src/graph/rich-function-index.ts +1080 -0
  15. package/src/graph/symbol-table.ts +252 -0
  16. package/src/hash/hash-store.ts +1 -0
  17. package/src/index.ts +2 -0
  18. package/src/parser/base-extractor.ts +19 -0
  19. package/src/parser/boundary-checker.ts +31 -12
  20. package/src/parser/error-recovery.ts +5 -4
  21. package/src/parser/function-body-extractor.ts +248 -0
  22. package/src/parser/go/go-extractor.ts +249 -676
  23. package/src/parser/index.ts +132 -318
  24. package/src/parser/language-registry.ts +57 -0
  25. package/src/parser/oxc-parser.ts +166 -28
  26. package/src/parser/oxc-resolver.ts +179 -11
  27. package/src/parser/parser-constants.ts +1 -0
  28. package/src/parser/rust/rust-extractor.ts +109 -0
  29. package/src/parser/tree-sitter/parser.ts +369 -62
  30. package/src/parser/tree-sitter/queries.ts +106 -10
  31. package/src/parser/types.ts +20 -1
  32. package/src/search/bm25.ts +21 -8
  33. package/src/search/direct-search.ts +472 -0
  34. package/src/search/embedding-provider.ts +249 -0
  35. package/src/search/index.ts +12 -0
  36. package/src/search/semantic-search.ts +435 -0
  37. package/src/utils/artifact-transaction.ts +1 -0
  38. package/src/utils/atomic-write.ts +1 -0
  39. package/src/utils/errors.ts +89 -4
  40. package/src/utils/fs.ts +104 -50
  41. package/src/utils/json.ts +1 -0
  42. package/src/utils/language-registry.ts +84 -6
  43. package/src/utils/path.ts +26 -0
  44. package/tests/dead-code.test.ts +3 -2
  45. package/tests/direct-search.test.ts +435 -0
  46. package/tests/error-recovery.test.ts +143 -0
  47. package/tests/fixtures/simple-api/src/index.ts +1 -1
  48. package/tests/go-parser.test.ts +19 -335
  49. package/tests/js-parser.test.ts +18 -1089
  50. package/tests/language-registry-all.test.ts +276 -0
  51. package/tests/language-registry.test.ts +6 -4
  52. package/tests/parse-diagnostics.test.ts +9 -96
  53. package/tests/parser.test.ts +42 -771
  54. package/tests/polyglot-parser.test.ts +117 -0
  55. package/tests/rich-function-index.test.ts +703 -0
  56. package/tests/tree-sitter-parser.test.ts +108 -80
  57. package/tests/ts-parser.test.ts +8 -8
  58. package/tests/verification.test.ts +175 -0
  59. package/src/parser/base-parser.ts +0 -16
  60. package/src/parser/go/go-parser.ts +0 -43
  61. package/src/parser/javascript/js-extractor.ts +0 -278
  62. package/src/parser/javascript/js-parser.ts +0 -101
  63. package/src/parser/typescript/ts-extractor.ts +0 -447
  64. package/src/parser/typescript/ts-parser.ts +0 -36
@@ -1,731 +1,304 @@
1
- import { hashContent } from '../../hash/file-hasher.js'
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { LanguageRegistry } from '../language-registry.js';
3
+ import { BaseExtractor } from '../base-extractor.js';
4
+ import { hashContent } from '../../hash/file-hasher.js';
2
5
  import type {
3
- ParsedFunction, ParsedClass, ParsedImport, ParsedExport,
4
- ParsedParam, ParsedGeneric, ParsedRoute, CallExpression
5
- } from '../types.js'
6
-
7
- // --- Go builtins / keywords to skip when extracting calls -------------------
8
- const GO_BUILTINS = new Set([
9
- 'if', 'else', 'for', 'switch', 'select', 'case', 'default', 'break',
10
- 'continue', 'goto', 'fallthrough', 'return', 'go', 'defer', 'range',
11
- 'func', 'type', 'var', 'const', 'package', 'import', 'struct', 'interface',
12
- 'map', 'chan', 'make', 'new', 'len', 'cap', 'append', 'copy', 'delete',
13
- 'close', 'panic', 'recover', 'print', 'println', 'nil', 'true', 'false',
14
- 'iota', 'string', 'int', 'int8', 'int16', 'int32', 'int64', 'uint',
15
- 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr', 'float32', 'float64',
16
- 'complex64', 'complex128', 'bool', 'byte', 'rune', 'error', 'any',
17
- ])
18
-
19
- // --- Route detection patterns (Gin, Echo, Chi, Mux, net/http, Fiber) --------
20
- type RoutePattern = { re: RegExp; methodGroup: number; pathGroup: number; handlerGroup: number; fixedMethod?: string }
21
-
22
- const ROUTE_PATTERNS: RoutePattern[] = [
23
- // Gin / Echo / Chi: r.GET("/path", handler)
24
- {
25
- re: /\b(?:router|r|e|app|v\d*|api|g|group|server)\.(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|Any|Use)\s*\(\s*"([^"]+)"\s*,\s*([\w.]+)/,
26
- methodGroup: 1, pathGroup: 2, handlerGroup: 3,
27
- },
28
- // Gorilla Mux: r.HandleFunc("/path", handler).Methods("GET")
29
- {
30
- re: /\b(?:r|router|mux)\.HandleFunc\s*\(\s*"([^"]+)"\s*,\s*([\w.]+).*?\.Methods\s*\(\s*"(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)"/,
31
- methodGroup: 3, pathGroup: 1, handlerGroup: 2,
32
- },
33
- // Gorilla Mux (no Methods): r.HandleFunc("/path", handler)
34
- {
35
- re: /\b(?:r|router|mux)\.HandleFunc\s*\(\s*"([^"]+)"\s*,\s*([\w.]+)/,
36
- methodGroup: -1, pathGroup: 1, handlerGroup: 2, fixedMethod: 'ANY',
37
- },
38
- // net/http: http.HandleFunc("/path", handler)
39
- {
40
- re: /http\.HandleFunc\s*\(\s*"([^"]+)"\s*,\s*([\w.]+)/,
41
- methodGroup: -1, pathGroup: 1, handlerGroup: 2, fixedMethod: 'ANY',
42
- },
43
- // Fiber / lowercase Chi: app.Get("/path", handler)
44
- {
45
- re: /\b(?:app|server)\.(Get|Post|Put|Delete|Patch|Options|Head|Use)\s*\(\s*"([^"]+)"\s*,\s*([\w.]+)/,
46
- methodGroup: 1, pathGroup: 2, handlerGroup: 3,
47
- },
48
- ]
6
+ ParsedFile,
7
+ ParsedFunction,
8
+ ParsedClass,
9
+ ParsedImport,
10
+ ParsedExport,
11
+ ParsedParam,
12
+ ParsedGeneric,
13
+ ParsedRoute,
14
+ CallExpression,
15
+ ParsedVariable
16
+ } from '../types.js';
49
17
 
50
18
  /**
51
- * GoExtractor -- pure regex + stateful line scanner for .go files.
52
- * Extracts functions, structs (as classes), imports, exports, and HTTP routes
53
- * without any external Go AST dependency.
19
+ * GoExtractor: robust regex-based extractor for Go source.
20
+ * Handles structs, interfaces, functions, and common routing patterns.
54
21
  */
55
- export class GoExtractor {
56
- private readonly lines: string[]
57
- private cachedFunctions: ReturnType<typeof this.scanFunctions> | null = null
58
-
59
- constructor(
60
- private readonly filePath: string,
61
- private readonly content: string,
62
- ) {
63
- this.lines = content.split('\n')
22
+ export class GoExtractor extends BaseExtractor {
23
+ async extract(filePath: string, content: string): Promise<ParsedFile> {
24
+ const normalizedPath = filePath.replace(/\\/g, '/');
25
+ const worker = new GoExtractorWorker(normalizedPath, content);
26
+
27
+ return {
28
+ path: normalizedPath,
29
+ language: 'go',
30
+ functions: worker.extractFunctions(),
31
+ classes: worker.extractClasses(),
32
+ variables: worker.extractVariables(),
33
+ generics: worker.extractGenerics(),
34
+ imports: worker.extractImports(),
35
+ exports: worker.extractExports(),
36
+ routes: worker.extractRoutes(),
37
+ calls: worker.extractModuleCalls(),
38
+ hash: hashContent(content),
39
+ parsedAt: Date.now()
40
+ };
64
41
  }
65
42
 
66
- // --- Public API ---------------------------------------------------------
67
-
68
- /** Extract all top-level functions (no receiver) */
69
- extractFunctions(): ParsedFunction[] {
70
- return this.scanFunctions().filter(f => !f.receiverType)
71
- .map(f => this.buildParsedFunction(f))
43
+ async resolveImports(files: ParsedFile[], _projectRoot: string): Promise<ParsedFile[]> {
44
+ // Go import resolution is usually handled by the Resolver
45
+ return files;
72
46
  }
47
+ }
73
48
 
74
- /** Extract structs and interfaces as classes, with their receiver methods */
75
- extractClasses(): ParsedClass[] {
76
- const allMethods = this.scanFunctions().filter(f => !!f.receiverType)
77
-
78
- // Group methods by receiver type
79
- const byReceiver = new Map<string, ReturnType<typeof this.scanFunctions>[number][]>()
80
- for (const m of allMethods) {
81
- const arr = byReceiver.get(m.receiverType!) ?? []
82
- arr.push(m)
83
- byReceiver.set(m.receiverType!, arr)
84
- }
49
+ class GoExtractorWorker {
50
+ private nameCounter = new Map<string, number>();
85
51
 
86
- const classes: ParsedClass[] = []
52
+ constructor(
53
+ private readonly filePath: string,
54
+ private readonly content: string
55
+ ) {}
56
+
57
+ private allocateId(prefix: string, name: string): string {
58
+ const count = (this.nameCounter.get(name) ?? 0) + 1;
59
+ this.nameCounter.set(name, count);
60
+ const suffix = count === 1 ? '' : `#${count}`;
61
+ return `${prefix}:${this.filePath}:${name}${suffix}`.toLowerCase();
62
+ }
87
63
 
88
- // Structs / interfaces declared in this file
89
- for (const typeDecl of this.scanTypeDeclarations()) {
90
- const methods = byReceiver.get(typeDecl.name) ?? []
91
- classes.push({
92
- id: `cls:${this.filePath}:${typeDecl.name}`,
93
- name: typeDecl.name,
64
+ extractFunctions(): ParsedFunction[] {
65
+ const functions: ParsedFunction[] = [];
66
+ // Pattern: func Name(args) ret { or func (r Receiver) Name(args) ret {
67
+ const funcRegex = /func\s+(?:\(\s*[^)]+\s*\)\s*)?(\w+)\s*\(([^)]*)\)\s*([^{]*)\{/g;
68
+ let match;
69
+ while ((match = funcRegex.exec(this.content)) !== null) {
70
+ const name = match[1];
71
+ const params = this.parseParams(match[2]);
72
+ const returnType = match[3].trim() || 'void';
73
+ const startLine = this.getLineNumber(match.index);
74
+ const body = this.extractBracedContent(match.index + match[0].length - 1);
75
+
76
+ functions.push({
77
+ id: this.allocateId('fn', name),
78
+ name,
94
79
  file: this.filePath,
95
- startLine: typeDecl.startLine,
96
- endLine: typeDecl.endLine,
97
- isExported: isExported(typeDecl.name),
98
- purpose: typeDecl.purpose,
99
- methods: methods.map(m => this.buildParsedFunction(m)),
100
- properties: [],
101
- hash: '',
102
- })
103
- byReceiver.delete(typeDecl.name)
80
+ startLine,
81
+ endLine: this.getLineNumber(match.index + match[0].length + body.length),
82
+ params,
83
+ returnType,
84
+ isExported: /^[A-Z]/.test(name),
85
+ isAsync: false, // Go uses goroutines, not async keywords
86
+ calls: this.extractCalls(body),
87
+ hash: hashContent(body),
88
+ purpose: this.extractDocComment(match.index),
89
+ edgeCasesHandled: [],
90
+ errorHandling: [],
91
+ detailedLines: []
92
+ });
104
93
  }
94
+ return functions;
95
+ }
105
96
 
106
- // Methods with no matching struct declaration (e.g. declared in another file)
107
- for (const [receiverType, methods] of byReceiver) {
97
+ extractClasses(): ParsedClass[] {
98
+ const classes: ParsedClass[] = [];
99
+ // Pattern: type Name struct {
100
+ const structRegex = /type\s+(\w+)\s+struct\s+\{/g;
101
+ let match;
102
+ while ((match = structRegex.exec(this.content)) !== null) {
103
+ const name = match[1];
104
+ const startLine = this.getLineNumber(match.index);
105
+ const body = this.extractBracedContent(match.index + match[0].length - 1);
106
+
108
107
  classes.push({
109
- id: `cls:${this.filePath}:${receiverType}`,
110
- name: receiverType,
108
+ id: this.allocateId('class', name),
109
+ name,
111
110
  file: this.filePath,
112
- startLine: methods[0]?.startLine ?? 0,
113
- endLine: methods[methods.length - 1]?.endLine ?? 0,
114
- isExported: isExported(receiverType),
115
- methods: methods.map(m => this.buildParsedFunction(m)),
116
- properties: [],
117
- hash: '',
118
- purpose: '',
119
- })
120
- }
121
-
122
- return classes
123
- }
124
-
125
- extractImports(): ParsedImport[] {
126
- return this.parseImports()
127
- }
128
-
129
- extractExports(): ParsedExport[] {
130
- const fns = this.scanFunctions()
131
- const types = this.scanTypeDeclarations()
132
- const exports: ParsedExport[] = []
133
-
134
- for (const fn of fns) {
135
- if (isExported(fn.name)) {
136
- exports.push({ name: fn.name, type: 'function', file: this.filePath })
137
- }
138
- }
139
- for (const t of types) {
140
- if (isExported(t.name)) {
141
- exports.push({ name: t.name, type: t.kind === 'interface' ? 'interface' : 'class', file: this.filePath })
142
- }
143
- }
144
-
145
- return exports
146
- }
147
-
148
- extractRoutes(): ParsedRoute[] {
149
- const routes: ParsedRoute[] = []
150
- for (let i = 0; i < this.lines.length; i++) {
151
- const line = this.lines[i]
152
- for (const pat of ROUTE_PATTERNS) {
153
- const m = pat.re.exec(line)
154
- if (!m) continue
155
- const method = pat.fixedMethod ?? (pat.methodGroup > 0 ? m[pat.methodGroup].toUpperCase() : 'ANY')
156
- routes.push({
157
- method,
158
- path: m[pat.pathGroup],
159
- handler: m[pat.handlerGroup],
160
- middlewares: [],
161
- file: this.filePath,
162
- line: i + 1,
163
- })
164
- }
111
+ startLine,
112
+ endLine: this.getLineNumber(match.index + match[0].length + body.length),
113
+ methods: [], // Methods are extracted as functions in Go
114
+ properties: this.parseStructFields(body),
115
+ isExported: /^[A-Z]/.test(name),
116
+ hash: hashContent(body),
117
+ purpose: this.extractDocComment(match.index)
118
+ });
165
119
  }
166
- return routes
120
+ return classes;
167
121
  }
168
122
 
169
- // --- Internal scanning ---------------------------------------------------
170
-
171
- /** Scanned raw function data (before building ParsedFunction) */
172
- private scanFunctions(): Array<{
173
- name: string
174
- receiverType?: string
175
- receiverVar?: string
176
- paramStr: string
177
- returnStr: string
178
- startLine: number
179
- bodyStart: number
180
- endLine: number
181
- purpose: string
182
- }> {
183
- if (this.cachedFunctions) return this.cachedFunctions
184
- const results: Array<{
185
- name: string
186
- receiverType?: string
187
- receiverVar?: string
188
- paramStr: string
189
- returnStr: string
190
- startLine: number
191
- bodyStart: number
192
- endLine: number
193
- purpose: string
194
- }> = []
195
-
196
- let i = 0
197
- while (i < this.lines.length) {
198
- const trimmed = this.lines[i].trimStart()
199
-
200
- // Only lines starting with `func`
201
- if (!trimmed.startsWith('func ') && !trimmed.startsWith('func\t')) {
202
- i++
203
- continue
204
- }
205
-
206
- const funcLineStart = i
207
-
208
- // Collect signature lines until we find the opening `{`
209
- const sigLines: string[] = []
210
- let j = i
211
- let foundOpen = false
212
- while (j < this.lines.length) {
213
- sigLines.push(this.lines[j])
214
- if (this.lines[j].includes('{')) {
215
- foundOpen = true
216
- break
217
- }
218
- j++
219
- }
220
-
221
- if (!foundOpen) { i++; continue }
222
-
223
- const sigRaw = sigLines.join(' ').replace(/\s+/g, ' ').trim()
224
-
225
- // Parse signature
226
- const parsed = parseGoFuncSignature(sigRaw)
227
- if (!parsed) { i++; continue }
228
-
229
- // Find body bounds
230
- const { bodyEnd } = findBodyBounds(this.lines, j)
231
-
232
- // Extract purpose from leading comment
233
- const purpose = extractLeadingComment(this.lines, funcLineStart)
234
-
235
- results.push({
236
- ...parsed,
237
- startLine: funcLineStart + 1,
238
- bodyStart: j + 1,
239
- endLine: bodyEnd + 1,
240
- purpose,
241
- })
242
-
243
- // Skip to after this function
244
- i = bodyEnd + 1
245
- }
246
-
247
- this.cachedFunctions = results
248
- return results
249
- }
250
-
251
- /** Build ParsedFunction from scanned raw data */
252
- private buildParsedFunction(raw: ReturnType<typeof this.scanFunctions>[number]): ParsedFunction {
253
- const name = raw.receiverType ? `${raw.receiverType}.${raw.name}` : raw.name
254
- const id = `fn:${this.filePath}:${name}`
255
-
256
- const bodyLines = this.lines.slice(raw.bodyStart - 1, raw.endLine)
257
- const hash = hashContent(bodyLines.join('\n'))
258
- const calls = extractCallsFromBody(bodyLines, raw.bodyStart)
259
- const edgeCases = extractEdgeCases(bodyLines)
260
- const errorHandling = extractErrorHandling(bodyLines, raw.bodyStart)
261
-
262
- return {
263
- id,
264
- name,
265
- file: this.filePath,
266
- startLine: raw.startLine,
267
- endLine: raw.endLine,
268
- params: parseGoParams(raw.paramStr),
269
- returnType: cleanReturnType(raw.returnStr),
270
- isExported: isExported(raw.name),
271
- isAsync: false, // Go has goroutines but no async keyword
272
- calls,
273
- hash,
274
- purpose: raw.purpose,
275
- edgeCasesHandled: edgeCases,
276
- errorHandling,
277
- detailedLines: [],
278
- }
279
- }
280
-
281
- /** Scan for type struct / type interface declarations */
282
- private scanTypeDeclarations(): Array<{
283
- name: string
284
- kind: 'struct' | 'interface'
285
- startLine: number
286
- endLine: number
287
- purpose: string
288
- }> {
289
- const results: Array<{
290
- name: string
291
- kind: 'struct' | 'interface'
292
- startLine: number
293
- endLine: number
294
- purpose: string
295
- }> = []
296
-
297
- for (let i = 0; i < this.lines.length; i++) {
298
- const trimmed = this.lines[i].trim()
299
- const m = /^type\s+(\w+)\s+(struct|interface)\s*\{?/.exec(trimmed)
300
- if (!m) continue
301
-
302
- const name = m[1]
303
- const kind = m[2] as 'struct' | 'interface'
304
- const purpose = extractLeadingComment(this.lines, i)
305
-
306
- // Find end of type block
307
- const { bodyEnd } = findBodyBounds(this.lines, i)
308
- results.push({
123
+ extractGenerics(): ParsedGeneric[] {
124
+ const generics: ParsedGeneric[] = [];
125
+ // Pattern: type Name interface {
126
+ const interfaceRegex = /type\s+(\w+)\s+interface\s+\{/g;
127
+ let match;
128
+ while ((match = interfaceRegex.exec(this.content)) !== null) {
129
+ const name = match[1];
130
+ const body = this.extractBracedContent(match.index + match[0].length - 1);
131
+ generics.push({
132
+ id: this.allocateId('type', name),
309
133
  name,
310
- kind,
311
- startLine: i + 1,
312
- endLine: bodyEnd + 1,
313
- purpose,
314
- })
315
- // Don't skip — nested types possible, but rare enough to leave sequential
134
+ type: 'interface',
135
+ file: this.filePath,
136
+ startLine: this.getLineNumber(match.index),
137
+ endLine: this.getLineNumber(match.index + match[0].length + body.length),
138
+ isExported: /^[A-Z]/.test(name),
139
+ typeParameters: [],
140
+ hash: hashContent(body),
141
+ purpose: this.extractDocComment(match.index)
142
+ });
316
143
  }
317
-
318
- return results
144
+ return generics;
319
145
  }
320
146
 
321
- /** Parse all import declarations */
322
- private parseImports(): ParsedImport[] {
323
- const imports: ParsedImport[] = []
324
- let i = 0
325
- while (i < this.lines.length) {
326
- const trimmed = this.lines[i].trim()
327
-
328
- // Block import: import (...)
329
- if (trimmed === 'import (' || /^import\s+\($/.test(trimmed)) {
330
- i++
331
- while (i < this.lines.length) {
332
- const iline = this.lines[i].trim()
333
- if (iline === ')') break
334
- const imp = parseImportLine(iline)
335
- if (imp) imports.push(imp)
336
- i++
147
+ extractImports(): ParsedImport[] {
148
+ const imports: ParsedImport[] = [];
149
+ const importRegex = /import\s+(?:\(\s*([^)]+)\s*\)|"([^"]+)")/g;
150
+ let match;
151
+ while ((match = importRegex.exec(this.content)) !== null) {
152
+ if (match[1]) {
153
+ const lines = match[1].split('\n');
154
+ for (const line of lines) {
155
+ const parts = line.trim().match(/(?:(\w+)\s+)??"([^"]+)"/);
156
+ if (parts) {
157
+ imports.push({
158
+ source: parts[2],
159
+ resolvedPath: '',
160
+ names: parts[1] ? [parts[1]] : [],
161
+ isDefault: !parts[1],
162
+ isDynamic: false
163
+ });
164
+ }
337
165
  }
338
- i++
339
- continue
340
- }
341
-
342
- // Single import: import "pkg" or import alias "pkg"
343
- if (/^import\s+/.test(trimmed)) {
344
- const imp = parseImportLine(trimmed.replace(/^import\s+/, ''))
345
- if (imp) imports.push(imp)
166
+ } else {
167
+ imports.push({
168
+ source: match[2],
169
+ resolvedPath: '',
170
+ names: [],
171
+ isDefault: true,
172
+ isDynamic: false
173
+ });
346
174
  }
347
-
348
- i++
349
- }
350
- return imports
351
- }
352
- }
353
-
354
- // --- Signature parsing --------------------------------------------------------
355
-
356
- interface GoFuncSignature {
357
- name: string
358
- receiverType?: string
359
- receiverVar?: string
360
- paramStr: string
361
- returnStr: string
362
- }
363
-
364
- /**
365
- * Find balanced (...) starting from `fromIdx` in `s`.
366
- * Returns the content between parens and the index of the closing paren.
367
- * Handles nested parens correctly, so `fn func(int) bool` extracts properly.
368
- */
369
- function extractBalancedParens(s: string, fromIdx: number): { content: string; end: number } | null {
370
- const start = s.indexOf('(', fromIdx)
371
- if (start === -1) return null
372
- let depth = 0
373
- for (let i = start; i < s.length; i++) {
374
- if (s[i] === '(') depth++
375
- else if (s[i] === ')') {
376
- depth--
377
- if (depth === 0) return { content: s.slice(start + 1, i), end: i }
378
175
  }
176
+ return imports;
379
177
  }
380
- return null
381
- }
382
-
383
- function parseGoFuncSignature(sig: string): GoFuncSignature | null {
384
- // Strip leading 'func ' prefix
385
- let rest = sig.replace(/^func\s+/, '')
386
- let receiverVar: string | undefined
387
- let receiverType: string | undefined
388
178
 
389
- // Method receiver: func (varName *ReceiverType) Name(...)
390
- if (rest.startsWith('(')) {
391
- const recv = extractBalancedParens(rest, 0)
392
- if (!recv) return null
393
- const recvMatch = /^(\w+)\s+\*?(\w+)/.exec(recv.content.trim())
394
- if (recvMatch) {
395
- receiverVar = recvMatch[1]
396
- // Strip generic brackets: Stack[T] → Stack
397
- receiverType = recvMatch[2].replace(/\[.*$/, '')
398
- }
399
- rest = rest.slice(recv.end + 1).trimStart()
179
+ extractExports(): ParsedExport[] {
180
+ // In Go, any name starting with UpperCase is exported
181
+ const exports: ParsedExport[] = [];
182
+ return exports;
400
183
  }
401
184
 
402
- // Function name, optionally with type params: Name[T any]
403
- const nameMatch = /^(\w+)\s*(?:\[[^\]]*\])?\s*/.exec(rest)
404
- if (!nameMatch) return null
405
- const name = nameMatch[1]
406
- rest = rest.slice(nameMatch[0].length)
407
-
408
- // Parameter list — uses balanced-paren extraction so func-typed params work
409
- const paramsResult = extractBalancedParens(rest, 0)
410
- if (!paramsResult) return null
411
- const paramStr = paramsResult.content.trim()
412
-
413
- // Return type: everything after params, before trailing `{`
414
- const returnStr = rest.slice(paramsResult.end + 1).replace(/\s*\{.*$/, '').trim()
415
-
416
- return { name, receiverType, receiverVar, paramStr, returnStr }
417
- }
418
-
419
- // ─── Parameter parsing ────────────────────────────────────────────────────────
420
-
421
- function parseGoParams(paramStr: string): ParsedParam[] {
422
- if (!paramStr.trim()) return []
423
-
424
- const params: Array<{ name: string; type: string }> = []
425
- const parts = splitTopLevel(paramStr, ',').map(p => p.trim()).filter(Boolean)
426
-
427
- for (const part of parts) {
428
- // Variadic: name ...Type
429
- const variadicRe = /^(\w+)\s+\.\.\.(.+)$/.exec(part)
430
- if (variadicRe) {
431
- params.push({ name: variadicRe[1], type: '...' + variadicRe[2].trim() })
432
- continue
433
- }
434
-
435
- // Named with type: name Type (space-separated, e.g. "ctx context.Context")
436
- const namedRe = /^(\w+)\s+(\S.*)$/.exec(part)
437
- if (namedRe) {
438
- params.push({ name: namedRe[1], type: namedRe[2].trim() })
439
- continue
440
- }
441
-
442
- // Single token — could be a grouped name (e.g. "first" in "first, last string")
443
- // or an unnamed type (e.g. "int" in "(int, string)").
444
- // Heuristic: Go builtin types, pointer/slice/qualified types → unnamed; else grouped name.
445
- if (looksLikeGoType(part)) {
446
- params.push({ name: '_', type: part })
447
- } else {
448
- params.push({ name: part, type: '' }) // grouped name — back-filled below
185
+ extractRoutes(): ParsedRoute[] {
186
+ const routes: ParsedRoute[] = [];
187
+ // Simplified route detection for common Go frameworks (Gin, Echo, Chi)
188
+ const routeRegex = /\.(GET|POST|PUT|DELETE|PATCH)\s*\(\s*"([^"]+)"\s*,\s*([\w.]+)/g;
189
+ let match;
190
+ while ((match = routeRegex.exec(this.content)) !== null) {
191
+ routes.push({
192
+ method: match[1] as any,
193
+ path: match[2],
194
+ handler: match[3],
195
+ middlewares: [],
196
+ file: this.filePath,
197
+ line: this.getLineNumber(match.index)
198
+ });
449
199
  }
200
+ return routes;
450
201
  }
451
202
 
452
- // Back-fill grouped params: "first, last string" → first.type = last.type = "string"
453
- // Scan right-to-left: if a param has no type but the next one does, inherit it
454
- for (let i = params.length - 2; i >= 0; i--) {
455
- if (params[i].type === '' && params[i + 1].type !== '') {
456
- params[i].type = params[i + 1].type
457
- }
203
+ extractVariables(): ParsedVariable[] {
204
+ return [];
458
205
  }
459
206
 
460
- return params.map(p => ({
461
- name: p.name || '_',
462
- type: p.type || 'any',
463
- optional: false,
464
- }))
465
- }
466
-
467
- /**
468
- * Returns true when a bare single-token parameter is a type annotation rather
469
- * than a parameter name in a grouped declaration like `(first, last string)`.
470
- */
471
- function looksLikeGoType(token: string): boolean {
472
- if (!token) return false
473
- const ch = token[0]
474
- // Pointer (*int), slice ([]byte), or channel (chan)
475
- if (ch === '*' || ch === '[' || ch === '<') return true
476
- // Go builtin types and keywords
477
- if (GO_BUILTINS.has(token)) return true
478
- // Exported named type (e.g. Context, ResponseWriter)
479
- if (ch >= 'A' && ch <= 'Z') return true
480
- // Qualified type (e.g. context.Context, http.Request)
481
- if (token.includes('.')) return true
482
- return false
483
- }
484
-
485
- function cleanReturnType(ret: string): string {
486
- // Strip trailing `{`
487
- ret = ret.replace(/\s*\{.*$/, '').trim()
488
- // Clean up multiple return: (Type1, Type2) → keep as-is for readability
489
- return ret
490
- }
491
-
492
- // --- Import line parsing ------------------------------------------------------
493
-
494
- function parseImportLine(line: string): ParsedImport | null {
495
- const trimmed = line.trim()
496
- if (!trimmed || trimmed.startsWith('//')) return null
497
-
498
- // Blank import: _ "pkg"
499
- if (/^_\s+"/.test(trimmed)) return null
500
-
501
- // Aliased: alias "pkg"
502
- const aliasRe = /^(\w+)\s+"([^"]+)"/.exec(trimmed)
503
- if (aliasRe) {
504
- const [, alias, pkg] = aliasRe
505
- return { source: pkg, resolvedPath: '', names: [alias], isDefault: false, isDynamic: false }
207
+ extractModuleCalls(): CallExpression[] {
208
+ return [];
506
209
  }
507
210
 
508
- // Plain: "pkg"
509
- const plainRe = /^"([^"]+)"/.exec(trimmed)
510
- if (plainRe) {
511
- const pkg = plainRe[1]
512
- // Package name is the last segment of the import path
513
- const name = pkg.split('/').pop() ?? pkg
514
- return { source: pkg, resolvedPath: '', names: [name], isDefault: false, isDynamic: false }
211
+ private parseParams(paramsStr: string): ParsedParam[] {
212
+ if (!paramsStr.trim()) return [];
213
+ return paramsStr.split(',').map(p => {
214
+ const parts = p.trim().split(/\s+/);
215
+ return {
216
+ name: parts[0],
217
+ type: parts[1] || 'any',
218
+ optional: false
219
+ };
220
+ });
515
221
  }
516
222
 
517
- return null
518
- }
519
-
520
- // --- Body analysis ------------------------------------------------------------
521
-
522
- /**
523
- * Statefully track brace depth through content, handling:
524
- * - string literals ("...", `...`), rune literals ('.')
525
- * - line comments (//)
526
- * - block comments (/* ... * /)
527
- */
528
- function findBodyBounds(lines: string[], startLine: number): { bodyStart: number; bodyEnd: number } {
529
- let braceDepth = 0
530
- let bodyStart = -1
531
- let inString = false
532
- let stringChar = ''
533
- let inBlockComment = false
534
-
535
- for (let i = startLine; i < lines.length; i++) {
536
- const line = lines[i]
537
- let inLineComment = false
538
-
539
- for (let j = 0; j < line.length; j++) {
540
- const ch = line[j]
541
- const next = line[j + 1]
542
-
543
- if (inLineComment) break
544
-
545
- if (inBlockComment) {
546
- if (ch === '*' && next === '/') { inBlockComment = false; j++ }
547
- continue
548
- }
549
-
550
- if (inString) {
551
- if (ch === '\\') { j++; continue } // escape sequence
552
- if (ch === stringChar) inString = false
553
- continue
554
- }
555
-
556
- if (ch === '/' && next === '/') { inLineComment = true; break }
557
- if (ch === '/' && next === '*') { inBlockComment = true; j++; continue }
558
- if (ch === '"' || ch === '`') {
559
- inString = true
560
- stringChar = ch
561
- continue
562
- }
563
- if (ch === '\'') {
564
- // Go rune literal: consume exactly one character (or escape) then close
565
- if (next === '\\') j += 3 // '\n' or '\x00' etc.
566
- else j += 2 // 'a'
567
- continue
568
- }
569
-
570
- if (ch === '{') {
571
- if (bodyStart === -1) bodyStart = i
572
- braceDepth++
573
- } else if (ch === '}') {
574
- braceDepth--
575
- if (braceDepth === 0 && bodyStart !== -1) {
576
- return { bodyStart, bodyEnd: i }
577
- }
223
+ private parseStructFields(body: string): ParsedVariable[] {
224
+ const fields: ParsedVariable[] = [];
225
+ const lines = body.split('\n');
226
+ for (let i = 0; i < lines.length; i++) {
227
+ const line = lines[i].trim();
228
+ const parts = line.split(/\s+/);
229
+ if (parts.length >= 2 && !line.startsWith('//')) {
230
+ fields.push({
231
+ id: this.allocateId('var', parts[0]),
232
+ name: parts[0],
233
+ type: parts[1],
234
+ file: this.filePath,
235
+ line: i + 1, // Approximate
236
+ isExported: /^[A-Z]/.test(parts[0])
237
+ });
578
238
  }
579
239
  }
240
+ return fields;
580
241
  }
581
242
 
582
- return { bodyStart: bodyStart === -1 ? startLine : bodyStart, bodyEnd: lines.length - 1 }
583
- }
584
-
585
- function stripStringsAndComments(code: string): string {
586
- let out = ''
587
- let i = 0
588
- let inString = false
589
- let stringChar = ''
590
- let inBlockComment = false
591
-
592
- while (i < code.length) {
593
- const ch = code[i]
594
- const next = code[i + 1]
595
-
596
- if (inBlockComment) {
597
- if (ch === '*' && next === '/') { inBlockComment = false; i += 2 }
598
- else i++
599
- continue
600
- }
601
-
602
- if (inString) {
603
- if (ch === '\\') { i += 2; continue }
604
- if (ch === stringChar) { inString = false }
605
- i++
606
- continue
243
+ private extractCalls(body: string): CallExpression[] {
244
+ const calls: CallExpression[] = [];
245
+ const callRegex = /([\w.]+)\s*\(/g;
246
+ let match;
247
+ while ((match = callRegex.exec(body)) !== null) {
248
+ calls.push({
249
+ name: match[1],
250
+ line: this.getLineNumber(match.index),
251
+ type: match[1].includes('.') ? 'method' : 'function'
252
+ });
607
253
  }
608
-
609
- if (ch === '/' && next === '/') {
610
- // Skip to end of line
611
- while (i < code.length && code[i] !== '\n') i++
612
- continue
613
- }
614
- if (ch === '/' && next === '*') { inBlockComment = true; i += 2; continue }
615
- if (ch === '"' || ch === '`' || ch === '\'') {
616
- inString = true
617
- stringChar = ch
618
- i++
619
- continue
620
- }
621
-
622
- out += ch
623
- i++
254
+ return calls;
624
255
  }
625
- return out
626
- }
627
256
 
628
- function extractCallsFromBody(bodyLines: string[], baseLine: number = 1): CallExpression[] {
629
- const code = bodyLines.join('\n')
630
- const stripped = stripStringsAndComments(code)
631
- const calls: CallExpression[] = []
632
-
633
- // Direct calls: identifier(
634
- const callRe = /\b([A-Za-z_]\w*)\s*\(/g
635
- let m: RegExpExecArray | null
636
- while ((m = callRe.exec(stripped)) !== null) {
637
- const name = m[1]
638
- if (!GO_BUILTINS.has(name)) {
639
- // Heuristic for line number: find the line in bodyLines
640
- // This is a rough estimation but good enough for static analysis
641
- calls.push({ name, line: baseLine, type: 'function' })
642
- }
643
- }
644
-
645
- // Method calls: receiver.Method(
646
- const methodRe = /\b([A-Za-z_]\w+\.[A-Za-z_]\w*)\s*\(/g
647
- while ((m = methodRe.exec(stripped)) !== null) {
648
- calls.push({ name: m[1], line: baseLine, type: 'method' })
649
- }
650
-
651
- return calls
652
- }
653
-
654
- function extractEdgeCases(bodyLines: string[]): string[] {
655
- const edgeCases: string[] = []
656
- for (let i = 0; i < bodyLines.length; i++) {
657
- const trimmed = bodyLines[i].trim()
658
- // if condition followed by return/panic/error
659
- const m = /^if\s+(.+?)\s*\{?\s*$/.exec(trimmed)
660
- if (m && bodyLines[i + 1]?.trim().match(/^(return|panic|log\.|fmt\.)/)) {
661
- edgeCases.push(m[1].trim())
257
+ private extractBracedContent(startIndex: number): string {
258
+ let depth = 0;
259
+ let result = '';
260
+ for (let i = startIndex; i < this.content.length; i++) {
261
+ const char = this.content[i];
262
+ if (char === '{') depth++;
263
+ if (char === '}') depth--;
264
+ result += char;
265
+ if (depth === 0) break;
662
266
  }
267
+ return result;
663
268
  }
664
- return edgeCases
665
- }
666
269
 
667
- function extractErrorHandling(bodyLines: string[], baseLineNumber: number): { line: number; type: 'try-catch' | 'throw'; detail: string }[] {
668
- const errors: { line: number; type: 'try-catch' | 'throw'; detail: string }[] = []
669
- for (let i = 0; i < bodyLines.length; i++) {
670
- const trimmed = bodyLines[i].trim()
671
- // if err != nil { return ..., err }
672
- if (/^if\s+\w*err\w*\s*!=\s*nil/.test(trimmed)) {
673
- errors.push({ line: baseLineNumber + i, type: 'try-catch', detail: trimmed })
674
- }
675
- // panic(...)
676
- if (/^panic\(/.test(trimmed)) {
677
- errors.push({ line: baseLineNumber + i, type: 'throw', detail: trimmed })
678
- }
270
+ private getLineNumber(offset: number): number {
271
+ return this.content.substring(0, offset).split('\n').length;
679
272
  }
680
- return errors
681
- }
682
-
683
- // --- Comment extraction -------------------------------------------------------
684
273
 
685
- function extractLeadingComment(lines: string[], funcLine: number): string {
686
- // Scan backwards from funcLine for consecutive comment lines
687
- const commentLines: string[] = []
688
- let i = funcLine - 1
689
- while (i >= 0) {
690
- const trimmed = lines[i].trim()
691
- if (trimmed.startsWith('//')) {
692
- commentLines.unshift(trimmed.replace(/^\/\/\s?/, ''))
693
- } else if (trimmed === '') {
694
- break
695
- } else {
696
- break
274
+ private extractDocComment(offset: number): string {
275
+ const lines = this.content.substring(0, offset).split('\n');
276
+ const comments = [];
277
+ for (let i = lines.length - 2; i >= 0; i--) {
278
+ const line = lines[i].trim();
279
+ if (line.startsWith('//')) {
280
+ comments.unshift(line.replace('//', '').trim());
281
+ } else if (line === '') {
282
+ continue;
283
+ } else {
284
+ break;
285
+ }
697
286
  }
698
- i--
699
- }
700
- // First meaningful non-divider line
701
- for (const line of commentLines) {
702
- if (/^[-=*─]{3,}$/.test(line)) continue
703
- return line.trim()
287
+ return comments.join(' ');
704
288
  }
705
- return ''
706
289
  }
707
290
 
708
- // --- Utility helpers ----------------------------------------------------------
709
-
710
- function isExported(name: string): boolean {
711
- return name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase()
712
- }
713
-
714
- /** Split string by delimiter, ignoring delimiters inside parens/brackets */
715
- function splitTopLevel(str: string, delimiter: string): string[] {
716
- const parts: string[] = []
717
- let depth = 0
718
- let current = ''
719
- for (const ch of str) {
720
- if (ch === '(' || ch === '[' || ch === '{') depth++
721
- else if (ch === ')' || ch === ']' || ch === '}') depth--
722
- else if (ch === delimiter && depth === 0) {
723
- parts.push(current)
724
- current = ''
725
- continue
726
- }
727
- current += ch
291
+ // Register in the global registry
292
+ LanguageRegistry.getInstance().register({
293
+ name: 'go',
294
+ extensions: ['.go'],
295
+ treeSitterGrammar: '',
296
+ extractor: new GoExtractor(),
297
+ semanticFeatures: {
298
+ hasTypeSystem: true,
299
+ hasGenerics: true,
300
+ hasMacros: false,
301
+ hasAnnotations: false,
302
+ hasPatternMatching: false
728
303
  }
729
- if (current.trim()) parts.push(current)
730
- return parts
731
- }
304
+ });