@getmikk/core 1.5.1 → 1.6.0

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.
@@ -0,0 +1,712 @@
1
+ import { hashContent } from '../../hash/file-hasher.js'
2
+ import type {
3
+ ParsedFunction, ParsedClass, ParsedImport, ParsedExport,
4
+ ParsedParam, ParsedGeneric, ParsedRoute,
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
+ ]
49
+
50
+ /**
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.
54
+ */
55
+ export class GoExtractor {
56
+ private readonly lines: string[]
57
+
58
+ constructor(
59
+ private readonly filePath: string,
60
+ private readonly content: string,
61
+ ) {
62
+ this.lines = content.split('\n')
63
+ }
64
+
65
+ // ── Public API ──────────────────────────────────────────────────────────
66
+
67
+ /** Extract all top-level functions (no receiver) */
68
+ extractFunctions(): ParsedFunction[] {
69
+ return this.scanFunctions().filter(f => !f.receiverType)
70
+ .map(f => this.buildParsedFunction(f))
71
+ }
72
+
73
+ /** Extract structs and interfaces as classes, with their receiver methods */
74
+ extractClasses(): ParsedClass[] {
75
+ const allMethods = this.scanFunctions().filter(f => !!f.receiverType)
76
+
77
+ // Group methods by receiver type
78
+ const byReceiver = new Map<string, ReturnType<typeof this.scanFunctions>[number][]>()
79
+ for (const m of allMethods) {
80
+ const arr = byReceiver.get(m.receiverType!) ?? []
81
+ arr.push(m)
82
+ byReceiver.set(m.receiverType!, arr)
83
+ }
84
+
85
+ const classes: ParsedClass[] = []
86
+
87
+ // Structs / interfaces declared in this file
88
+ for (const typeDecl of this.scanTypeDeclarations()) {
89
+ const methods = byReceiver.get(typeDecl.name) ?? []
90
+ classes.push({
91
+ id: `cls:${this.filePath}:${typeDecl.name}`,
92
+ name: typeDecl.name,
93
+ file: this.filePath,
94
+ startLine: typeDecl.startLine,
95
+ endLine: typeDecl.endLine,
96
+ isExported: isExported(typeDecl.name),
97
+ purpose: typeDecl.purpose,
98
+ methods: methods.map(m => this.buildParsedFunction(m)),
99
+ })
100
+ byReceiver.delete(typeDecl.name)
101
+ }
102
+
103
+ // Methods with no matching struct declaration (e.g. declared in another file)
104
+ for (const [receiverType, methods] of byReceiver) {
105
+ classes.push({
106
+ id: `cls:${this.filePath}:${receiverType}`,
107
+ name: receiverType,
108
+ file: this.filePath,
109
+ startLine: methods[0]?.startLine ?? 0,
110
+ endLine: methods[methods.length - 1]?.endLine ?? 0,
111
+ isExported: isExported(receiverType),
112
+ methods: methods.map(m => this.buildParsedFunction(m)),
113
+ })
114
+ }
115
+
116
+ return classes
117
+ }
118
+
119
+ extractImports(): ParsedImport[] {
120
+ return this.parseImports()
121
+ }
122
+
123
+ extractExports(): ParsedExport[] {
124
+ const fns = this.scanFunctions()
125
+ const types = this.scanTypeDeclarations()
126
+ const exports: ParsedExport[] = []
127
+
128
+ for (const fn of fns) {
129
+ if (isExported(fn.name)) {
130
+ exports.push({ name: fn.name, type: 'function', file: this.filePath })
131
+ }
132
+ }
133
+ for (const t of types) {
134
+ if (isExported(t.name)) {
135
+ exports.push({ name: t.name, type: t.kind === 'interface' ? 'interface' : 'class', file: this.filePath })
136
+ }
137
+ }
138
+
139
+ return exports
140
+ }
141
+
142
+ extractRoutes(): ParsedRoute[] {
143
+ const routes: ParsedRoute[] = []
144
+ for (let i = 0; i < this.lines.length; i++) {
145
+ const line = this.lines[i]
146
+ for (const pat of ROUTE_PATTERNS) {
147
+ const m = pat.re.exec(line)
148
+ if (!m) continue
149
+ const method = pat.fixedMethod ?? (pat.methodGroup > 0 ? m[pat.methodGroup].toUpperCase() : 'ANY')
150
+ routes.push({
151
+ method,
152
+ path: m[pat.pathGroup],
153
+ handler: m[pat.handlerGroup],
154
+ middlewares: [],
155
+ file: this.filePath,
156
+ line: i + 1,
157
+ })
158
+ }
159
+ }
160
+ return routes
161
+ }
162
+
163
+ // ── Internal scanning ───────────────────────────────────────────────────
164
+
165
+ /** Scanned raw function data (before building ParsedFunction) */
166
+ private scanFunctions(): Array<{
167
+ name: string
168
+ receiverType?: string
169
+ receiverVar?: string
170
+ paramStr: string
171
+ returnStr: string
172
+ startLine: number
173
+ bodyStart: number
174
+ endLine: number
175
+ purpose: string
176
+ }> {
177
+ const results: Array<{
178
+ name: string
179
+ receiverType?: string
180
+ receiverVar?: string
181
+ paramStr: string
182
+ returnStr: string
183
+ startLine: number
184
+ bodyStart: number
185
+ endLine: number
186
+ purpose: string
187
+ }> = []
188
+
189
+ let i = 0
190
+ while (i < this.lines.length) {
191
+ const trimmed = this.lines[i].trimStart()
192
+
193
+ // Only lines starting with `func`
194
+ if (!trimmed.startsWith('func ') && !trimmed.startsWith('func\t')) {
195
+ i++
196
+ continue
197
+ }
198
+
199
+ const funcLineStart = i
200
+
201
+ // Collect signature lines until we find the opening `{`
202
+ const sigLines: string[] = []
203
+ let j = i
204
+ let foundOpen = false
205
+ while (j < this.lines.length) {
206
+ sigLines.push(this.lines[j])
207
+ if (this.lines[j].includes('{')) {
208
+ foundOpen = true
209
+ break
210
+ }
211
+ j++
212
+ }
213
+
214
+ if (!foundOpen) { i++; continue }
215
+
216
+ const sigRaw = sigLines.join(' ').replace(/\s+/g, ' ').trim()
217
+
218
+ // Parse signature
219
+ const parsed = parseGoFuncSignature(sigRaw)
220
+ if (!parsed) { i++; continue }
221
+
222
+ // Find body bounds
223
+ const { bodyEnd } = findBodyBounds(this.lines, j)
224
+
225
+ // Extract purpose from leading comment
226
+ const purpose = extractLeadingComment(this.lines, funcLineStart)
227
+
228
+ results.push({
229
+ ...parsed,
230
+ startLine: funcLineStart + 1,
231
+ bodyStart: j + 1,
232
+ endLine: bodyEnd + 1,
233
+ purpose,
234
+ })
235
+
236
+ // Skip to after this function
237
+ i = bodyEnd + 1
238
+ }
239
+
240
+ return results
241
+ }
242
+
243
+ /** Build ParsedFunction from scanned raw data */
244
+ private buildParsedFunction(raw: ReturnType<typeof this.scanFunctions>[number]): ParsedFunction {
245
+ const name = raw.receiverType ? `${raw.receiverType}.${raw.name}` : raw.name
246
+ const id = `fn:${this.filePath}:${name}`
247
+
248
+ const bodyLines = this.lines.slice(raw.bodyStart - 1, raw.endLine)
249
+ const hash = hashContent(bodyLines.join('\n'))
250
+ const calls = extractCallsFromBody(bodyLines)
251
+ const edgeCases = extractEdgeCases(bodyLines)
252
+ const errorHandling = extractErrorHandling(bodyLines, raw.bodyStart)
253
+
254
+ return {
255
+ id,
256
+ name,
257
+ file: this.filePath,
258
+ startLine: raw.startLine,
259
+ endLine: raw.endLine,
260
+ params: parseGoParams(raw.paramStr),
261
+ returnType: cleanReturnType(raw.returnStr),
262
+ isExported: isExported(raw.name),
263
+ isAsync: false, // Go has goroutines but no async keyword
264
+ calls,
265
+ hash,
266
+ purpose: raw.purpose,
267
+ edgeCasesHandled: edgeCases,
268
+ errorHandling,
269
+ detailedLines: [],
270
+ }
271
+ }
272
+
273
+ /** Scan for type struct / type interface declarations */
274
+ private scanTypeDeclarations(): Array<{
275
+ name: string
276
+ kind: 'struct' | 'interface'
277
+ startLine: number
278
+ endLine: number
279
+ purpose: string
280
+ }> {
281
+ const results: Array<{
282
+ name: string
283
+ kind: 'struct' | 'interface'
284
+ startLine: number
285
+ endLine: number
286
+ purpose: string
287
+ }> = []
288
+
289
+ for (let i = 0; i < this.lines.length; i++) {
290
+ const trimmed = this.lines[i].trim()
291
+ const m = /^type\s+(\w+)\s+(struct|interface)\s*\{?/.exec(trimmed)
292
+ if (!m) continue
293
+
294
+ const name = m[1]
295
+ const kind = m[2] as 'struct' | 'interface'
296
+ const purpose = extractLeadingComment(this.lines, i)
297
+
298
+ // Find end of type block
299
+ const { bodyEnd } = findBodyBounds(this.lines, i)
300
+ results.push({
301
+ name,
302
+ kind,
303
+ startLine: i + 1,
304
+ endLine: bodyEnd + 1,
305
+ purpose,
306
+ })
307
+ // Don't skip — nested types possible, but rare enough to leave sequential
308
+ }
309
+
310
+ return results
311
+ }
312
+
313
+ /** Parse all import declarations */
314
+ private parseImports(): ParsedImport[] {
315
+ const imports: ParsedImport[] = []
316
+ let i = 0
317
+ while (i < this.lines.length) {
318
+ const trimmed = this.lines[i].trim()
319
+
320
+ // Block import: import (...)
321
+ if (trimmed === 'import (' || /^import\s+\($/.test(trimmed)) {
322
+ i++
323
+ while (i < this.lines.length) {
324
+ const iline = this.lines[i].trim()
325
+ if (iline === ')') break
326
+ const imp = parseImportLine(iline)
327
+ if (imp) imports.push(imp)
328
+ i++
329
+ }
330
+ i++
331
+ continue
332
+ }
333
+
334
+ // Single import: import "pkg" or import alias "pkg"
335
+ if (/^import\s+/.test(trimmed)) {
336
+ const imp = parseImportLine(trimmed.replace(/^import\s+/, ''))
337
+ if (imp) imports.push(imp)
338
+ }
339
+
340
+ i++
341
+ }
342
+ return imports
343
+ }
344
+ }
345
+
346
+ // ─── Signature parsing ────────────────────────────────────────────────────────
347
+
348
+ interface GoFuncSignature {
349
+ name: string
350
+ receiverType?: string
351
+ receiverVar?: string
352
+ paramStr: string
353
+ returnStr: string
354
+ }
355
+
356
+ /**
357
+ * Find balanced (...) starting from `fromIdx` in `s`.
358
+ * Returns the content between parens and the index of the closing paren.
359
+ * Handles nested parens correctly, so `fn func(int) bool` extracts properly.
360
+ */
361
+ function extractBalancedParens(s: string, fromIdx: number): { content: string; end: number } | null {
362
+ const start = s.indexOf('(', fromIdx)
363
+ if (start === -1) return null
364
+ let depth = 0
365
+ for (let i = start; i < s.length; i++) {
366
+ if (s[i] === '(') depth++
367
+ else if (s[i] === ')') {
368
+ depth--
369
+ if (depth === 0) return { content: s.slice(start + 1, i), end: i }
370
+ }
371
+ }
372
+ return null
373
+ }
374
+
375
+ function parseGoFuncSignature(sig: string): GoFuncSignature | null {
376
+ // Strip leading 'func ' prefix
377
+ let rest = sig.replace(/^func\s+/, '')
378
+ let receiverVar: string | undefined
379
+ let receiverType: string | undefined
380
+
381
+ // Method receiver: func (varName *ReceiverType) Name(...)
382
+ if (rest.startsWith('(')) {
383
+ const recv = extractBalancedParens(rest, 0)
384
+ if (!recv) return null
385
+ const recvMatch = /^(\w+)\s+\*?(\w+)/.exec(recv.content.trim())
386
+ if (recvMatch) {
387
+ receiverVar = recvMatch[1]
388
+ // Strip generic brackets: Stack[T] → Stack
389
+ receiverType = recvMatch[2].replace(/\[.*$/, '')
390
+ }
391
+ rest = rest.slice(recv.end + 1).trimStart()
392
+ }
393
+
394
+ // Function name, optionally with type params: Name[T any]
395
+ const nameMatch = /^(\w+)\s*(?:\[[^\]]*\])?\s*/.exec(rest)
396
+ if (!nameMatch) return null
397
+ const name = nameMatch[1]
398
+ rest = rest.slice(nameMatch[0].length)
399
+
400
+ // Parameter list — uses balanced-paren extraction so func-typed params work
401
+ const paramsResult = extractBalancedParens(rest, 0)
402
+ if (!paramsResult) return null
403
+ const paramStr = paramsResult.content.trim()
404
+
405
+ // Return type: everything after params, before trailing `{`
406
+ const returnStr = rest.slice(paramsResult.end + 1).replace(/\s*\{.*$/, '').trim()
407
+
408
+ return { name, receiverType, receiverVar, paramStr, returnStr }
409
+ }
410
+
411
+ // ─── Parameter parsing ────────────────────────────────────────────────────────
412
+
413
+ function parseGoParams(paramStr: string): ParsedParam[] {
414
+ if (!paramStr.trim()) return []
415
+
416
+ const params: Array<{ name: string; type: string }> = []
417
+ const parts = splitTopLevel(paramStr, ',').map(p => p.trim()).filter(Boolean)
418
+
419
+ for (const part of parts) {
420
+ // Variadic: name ...Type
421
+ const variadicRe = /^(\w+)\s+\.\.\.(.+)$/.exec(part)
422
+ if (variadicRe) {
423
+ params.push({ name: variadicRe[1], type: '...' + variadicRe[2].trim() })
424
+ continue
425
+ }
426
+
427
+ // Named with type: name Type (space-separated, e.g. "ctx context.Context")
428
+ const namedRe = /^(\w+)\s+(\S.*)$/.exec(part)
429
+ if (namedRe) {
430
+ params.push({ name: namedRe[1], type: namedRe[2].trim() })
431
+ continue
432
+ }
433
+
434
+ // Single token — could be a grouped name (e.g. "first" in "first, last string")
435
+ // or an unnamed type (e.g. "int" in "(int, string)").
436
+ // Heuristic: Go builtin types, pointer/slice/qualified types → unnamed; else grouped name.
437
+ if (looksLikeGoType(part)) {
438
+ params.push({ name: '_', type: part })
439
+ } else {
440
+ params.push({ name: part, type: '' }) // grouped name — back-filled below
441
+ }
442
+ }
443
+
444
+ // Back-fill grouped params: "first, last string" → first.type = last.type = "string"
445
+ // Scan right-to-left: if a param has no type but the next one does, inherit it
446
+ for (let i = params.length - 2; i >= 0; i--) {
447
+ if (params[i].type === '' && params[i + 1].type !== '') {
448
+ params[i].type = params[i + 1].type
449
+ }
450
+ }
451
+
452
+ return params.map(p => ({
453
+ name: p.name || '_',
454
+ type: p.type || 'any',
455
+ optional: false,
456
+ }))
457
+ }
458
+
459
+ /**
460
+ * Returns true when a bare single-token parameter is a type annotation rather
461
+ * than a parameter name in a grouped declaration like `(first, last string)`.
462
+ */
463
+ function looksLikeGoType(token: string): boolean {
464
+ if (!token) return false
465
+ const ch = token[0]
466
+ // Pointer (*int), slice ([]byte), or channel (chan)
467
+ if (ch === '*' || ch === '[' || ch === '<') return true
468
+ // Go builtin types and keywords
469
+ if (GO_BUILTINS.has(token)) return true
470
+ // Exported named type (e.g. Context, ResponseWriter)
471
+ if (ch >= 'A' && ch <= 'Z') return true
472
+ // Qualified type (e.g. context.Context, http.Request)
473
+ if (token.includes('.')) return true
474
+ return false
475
+ }
476
+
477
+ function cleanReturnType(ret: string): string {
478
+ // Strip trailing `{`
479
+ ret = ret.replace(/\s*\{.*$/, '').trim()
480
+ // Clean up multiple return: (Type1, Type2) → keep as-is for readability
481
+ return ret
482
+ }
483
+
484
+ // ─── Import line parsing ──────────────────────────────────────────────────────
485
+
486
+ function parseImportLine(line: string): ParsedImport | null {
487
+ const trimmed = line.trim()
488
+ if (!trimmed || trimmed.startsWith('//')) return null
489
+
490
+ // Blank import: _ "pkg"
491
+ if (/^_\s+"/.test(trimmed)) return null
492
+
493
+ // Aliased: alias "pkg"
494
+ const aliasRe = /^(\w+)\s+"([^"]+)"/.exec(trimmed)
495
+ if (aliasRe) {
496
+ const [, alias, pkg] = aliasRe
497
+ return { source: pkg, resolvedPath: '', names: [alias], isDefault: false, isDynamic: false }
498
+ }
499
+
500
+ // Plain: "pkg"
501
+ const plainRe = /^"([^"]+)"/.exec(trimmed)
502
+ if (plainRe) {
503
+ const pkg = plainRe[1]
504
+ // Package name is the last segment of the import path
505
+ const name = pkg.split('/').pop() ?? pkg
506
+ return { source: pkg, resolvedPath: '', names: [name], isDefault: false, isDynamic: false }
507
+ }
508
+
509
+ return null
510
+ }
511
+
512
+ // ─── Body analysis ────────────────────────────────────────────────────────────
513
+
514
+ /**
515
+ * Statefully track brace depth through content, handling:
516
+ * - string literals ("...", `...`), rune literals ('.')
517
+ * - line comments (//)
518
+ * - block comments (/* ... *​/)
519
+ */
520
+ function findBodyBounds(lines: string[], startLine: number): { bodyStart: number; bodyEnd: number } {
521
+ let braceDepth = 0
522
+ let bodyStart = -1
523
+ let inString = false
524
+ let stringChar = ''
525
+ let inBlockComment = false
526
+
527
+ for (let i = startLine; i < lines.length; i++) {
528
+ const line = lines[i]
529
+ let inLineComment = false
530
+
531
+ for (let j = 0; j < line.length; j++) {
532
+ const ch = line[j]
533
+ const next = line[j + 1]
534
+
535
+ if (inLineComment) break
536
+
537
+ if (inBlockComment) {
538
+ if (ch === '*' && next === '/') { inBlockComment = false; j++ }
539
+ continue
540
+ }
541
+
542
+ if (inString) {
543
+ if (ch === '\\') { j++; continue } // escape sequence
544
+ if (ch === stringChar) inString = false
545
+ continue
546
+ }
547
+
548
+ if (ch === '/' && next === '/') { inLineComment = true; break }
549
+ if (ch === '/' && next === '*') { inBlockComment = true; j++; continue }
550
+ if (ch === '"' || ch === '`' || ch === '\'') {
551
+ inString = true
552
+ stringChar = ch
553
+ continue
554
+ }
555
+
556
+ if (ch === '{') {
557
+ if (bodyStart === -1) bodyStart = i
558
+ braceDepth++
559
+ } else if (ch === '}') {
560
+ braceDepth--
561
+ if (braceDepth === 0 && bodyStart !== -1) {
562
+ return { bodyStart, bodyEnd: i }
563
+ }
564
+ }
565
+ }
566
+ }
567
+
568
+ return { bodyStart: bodyStart === -1 ? startLine : bodyStart, bodyEnd: lines.length - 1 }
569
+ }
570
+
571
+ function stripStringsAndComments(code: string): string {
572
+ let out = ''
573
+ let i = 0
574
+ let inString = false
575
+ let stringChar = ''
576
+ let inBlockComment = false
577
+
578
+ while (i < code.length) {
579
+ const ch = code[i]
580
+ const next = code[i + 1]
581
+
582
+ if (inBlockComment) {
583
+ if (ch === '*' && next === '/') { inBlockComment = false; i += 2 }
584
+ else i++
585
+ continue
586
+ }
587
+
588
+ if (inString) {
589
+ if (ch === '\\') { i += 2; continue }
590
+ if (ch === stringChar) { inString = false }
591
+ i++
592
+ continue
593
+ }
594
+
595
+ if (ch === '/' && next === '/') {
596
+ // Skip to end of line
597
+ while (i < code.length && code[i] !== '\n') i++
598
+ continue
599
+ }
600
+ if (ch === '/' && next === '*') { inBlockComment = true; i += 2; continue }
601
+ if (ch === '"' || ch === '`' || ch === '\'') {
602
+ inString = true
603
+ stringChar = ch
604
+ i++
605
+ continue
606
+ }
607
+
608
+ out += ch
609
+ i++
610
+ }
611
+ return out
612
+ }
613
+
614
+ function extractCallsFromBody(bodyLines: string[]): string[] {
615
+ const stripped = stripStringsAndComments(bodyLines.join('\n'))
616
+ const calls = new Set<string>()
617
+
618
+ // Direct calls: identifier(
619
+ const callRe = /\b([A-Za-z_]\w*)\s*\(/g
620
+ let m: RegExpExecArray | null
621
+ while ((m = callRe.exec(stripped)) !== null) {
622
+ const name = m[1]
623
+ if (!GO_BUILTINS.has(name)) calls.add(name)
624
+ }
625
+
626
+ // Method calls: receiver.Method(
627
+ const methodRe = /\b([A-Za-z_]\w+\.[A-Za-z_]\w*)\s*\(/g
628
+ while ((m = methodRe.exec(stripped)) !== null) {
629
+ calls.add(m[1])
630
+ }
631
+
632
+ return [...calls]
633
+ }
634
+
635
+ function extractEdgeCases(bodyLines: string[]): string[] {
636
+ const edgeCases: string[] = []
637
+ for (let i = 0; i < bodyLines.length; i++) {
638
+ const trimmed = bodyLines[i].trim()
639
+ // if condition followed by return/panic/error
640
+ const m = /^if\s+(.+?)\s*\{?\s*$/.exec(trimmed)
641
+ if (m && bodyLines[i + 1]?.trim().match(/^(return|panic|log\.|fmt\.)/)) {
642
+ edgeCases.push(m[1].trim())
643
+ }
644
+ }
645
+ return edgeCases
646
+ }
647
+
648
+ function extractErrorHandling(bodyLines: string[], baseLineNumber: number): { line: number; type: 'try-catch' | 'throw'; detail: string }[] {
649
+ const errors: { line: number; type: 'try-catch' | 'throw'; detail: string }[] = []
650
+ for (let i = 0; i < bodyLines.length; i++) {
651
+ const trimmed = bodyLines[i].trim()
652
+ // if err != nil { return ..., err }
653
+ if (/^if\s+\w*err\w*\s*!=\s*nil/.test(trimmed)) {
654
+ errors.push({ line: baseLineNumber + i, type: 'try-catch', detail: trimmed })
655
+ }
656
+ // panic(...)
657
+ if (/^panic\(/.test(trimmed)) {
658
+ errors.push({ line: baseLineNumber + i, type: 'throw', detail: trimmed })
659
+ }
660
+ }
661
+ return errors
662
+ }
663
+
664
+ // ─── Comment extraction ───────────────────────────────────────────────────────
665
+
666
+ function extractLeadingComment(lines: string[], funcLine: number): string {
667
+ // Scan backwards from funcLine for consecutive comment lines
668
+ const commentLines: string[] = []
669
+ let i = funcLine - 1
670
+ while (i >= 0) {
671
+ const trimmed = lines[i].trim()
672
+ if (trimmed.startsWith('//')) {
673
+ commentLines.unshift(trimmed.replace(/^\/\/\s?/, ''))
674
+ } else if (trimmed === '') {
675
+ break
676
+ } else {
677
+ break
678
+ }
679
+ i--
680
+ }
681
+ // First meaningful non-divider line
682
+ for (const line of commentLines) {
683
+ if (/^[-=*─]{3,}$/.test(line)) continue
684
+ return line.trim()
685
+ }
686
+ return ''
687
+ }
688
+
689
+ // ─── Utility helpers ──────────────────────────────────────────────────────────
690
+
691
+ function isExported(name: string): boolean {
692
+ return name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase()
693
+ }
694
+
695
+ /** Split string by delimiter, ignoring delimiters inside parens/brackets */
696
+ function splitTopLevel(str: string, delimiter: string): string[] {
697
+ const parts: string[] = []
698
+ let depth = 0
699
+ let current = ''
700
+ for (const ch of str) {
701
+ if (ch === '(' || ch === '[' || ch === '{') depth++
702
+ else if (ch === ')' || ch === ']' || ch === '}') depth--
703
+ else if (ch === delimiter && depth === 0) {
704
+ parts.push(current)
705
+ current = ''
706
+ continue
707
+ }
708
+ current += ch
709
+ }
710
+ if (current.trim()) parts.push(current)
711
+ return parts
712
+ }