@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.
- package/README.md +44 -6
- package/out.log +0 -0
- package/package.json +4 -3
- package/src/contract/adr-manager.ts +75 -0
- package/src/contract/index.ts +2 -0
- package/src/graph/dead-code-detector.ts +194 -0
- package/src/graph/graph-builder.ts +6 -2
- package/src/graph/impact-analyzer.ts +53 -2
- package/src/graph/index.ts +4 -1
- package/src/graph/types.ts +21 -0
- package/src/parser/go/go-extractor.ts +712 -0
- package/src/parser/go/go-parser.ts +41 -0
- package/src/parser/go/go-resolver.ts +70 -0
- package/src/parser/index.ts +27 -6
- package/src/parser/types.ts +1 -1
- package/src/parser/typescript/ts-extractor.ts +65 -18
- package/src/parser/typescript/ts-parser.ts +41 -1
- package/test-output.txt +0 -0
- package/tests/adr-manager.test.ts +97 -0
- package/tests/dead-code.test.ts +134 -0
- package/tests/go-parser.test.ts +366 -0
- package/tests/impact-classified.test.ts +78 -0
|
@@ -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
|
+
}
|