repair.go raw
1 package emit
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "regexp"
10 "sort"
11 "strings"
12 "sync"
13 "time"
14 "unicode"
15 )
16
17 // compileMu serializes CompileAndRepair calls. Each go build invocation
18 // uses ~200MB+ per compiler process; running multiple concurrently from
19 // colony instances blows past 16GB RAM. One at a time.
20 var compileMu sync.Mutex
21
22 // CompileAndRepair attempts to compile source, and if compilation fails,
23 // iteratively fixes errors by adding type/var stubs and removing unused
24 // imports. Returns the repaired source or error if it cannot be fixed
25 // within maxPasses attempts.
26 func CompileAndRepair(source, goRoot string, maxPasses int) (string, error) {
27 compileMu.Lock()
28 defer compileMu.Unlock()
29
30 // Hard deadline for the entire repair process.
31 deadline := time.Now().Add(2 * time.Minute)
32 originalSize := len(source)
33
34 if maxPasses < 8 {
35 maxPasses = 8
36 }
37
38 // Create a persistent workspace for all compile attempts in this repair
39 // session. Reusing the same directory avoids re-running go mod tidy and
40 // lets the Go build cache warm up across iterations.
41 workDir, err := os.MkdirTemp("", "repair-*")
42 if err != nil {
43 return source, err
44 }
45 defer os.RemoveAll(workDir)
46
47 // Write go.mod once — it doesn't change between iterations.
48 modContent := buildRepairGoMod(source)
49 os.WriteFile(filepath.Join(workDir, "go.mod"), []byte(modContent), 0o644)
50 if strings.Contains(source, selfModule) {
51 modRoot := findRepairModuleRoot()
52 if modRoot != "" {
53 if data, err := os.ReadFile(filepath.Join(modRoot, "go.sum")); err == nil {
54 os.WriteFile(filepath.Join(workDir, "go.sum"), data, 0o644)
55 }
56 }
57 }
58
59 goBin := filepath.Join(goRoot, "bin", "go")
60 env := repairCleanEnv(goRoot)
61
62 // Write initial source and run go mod tidy once. Go 1.24+ requires
63 // the go.sum to be current before building. We write the source first
64 // so tidy can see which imports are actually used.
65 os.WriteFile(filepath.Join(workDir, "main.go"), []byte(source), 0o644)
66 tidyCtx, tidyCancel := context.WithTimeout(context.Background(), 30*time.Second)
67 tidy := exec.CommandContext(tidyCtx, goBin, "mod", "tidy")
68 tidy.Dir = workDir
69 tidy.Env = env
70 tidy.CombinedOutput()
71 tidyCancel()
72
73 // tryCompileLocal is the fast path: only rewrites main.go and builds.
74 // Uses -p 1 to limit build parallelism (each compiler process uses
75 // ~200MB; with 16 cores that's 3.2GB just for the compiler).
76 // Each compile gets a 60-second timeout to prevent hanging.
77 tryCompileLocal := func(src string) (string, error) {
78 os.WriteFile(filepath.Join(workDir, "main.go"), []byte(src), 0o644)
79 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
80 defer cancel()
81 cmd := exec.CommandContext(ctx, goBin, "build", "-p", "1", "-gcflags=-e", "-o", filepath.Join(workDir, "offspring"), ".")
82 cmd.Dir = workDir
83 cmd.Env = env
84 out, err := cmd.CombinedOutput()
85 return string(out), err
86 }
87
88 // Pre-scan: find names used as method receivers so we stub them as
89 // struct types instead of vars.
90 receiverTypes := findReceiverTypes(source)
91
92 // Phase A: Iterative stub + import/redecl fixes.
93 // Run until no more stubbable errors or maxPasses reached.
94 for pass := range maxPasses * 2 {
95 if time.Now().After(deadline) || len(source) > originalSize*3 {
96 break
97 }
98 compileOutput, compileErr := tryCompileLocal(source)
99 if compileErr == nil {
100 return source, nil
101 }
102
103 fixes := parseCompileErrors(compileOutput)
104 promoteReceiverFixes(fixes, receiverTypes)
105
106 if len(fixes) == 0 {
107 break // no more stubbable errors — move to removal
108 }
109
110 prev := source
111 source = applyFixes(source, fixes)
112 if source == prev {
113 break // no progress from stubs
114 }
115 _ = pass
116 }
117
118 // Phase B: Incremental repair — neutralize before removing.
119 // Strategy: (1) try to neutralize broken lines by replacing RHS with
120 // zero values, preserving variable declarations; (2) if neutralization
121 // makes no progress, remove error lines; (3) as last resort, replace
122 // entire broken functions with signature-preserving stubs.
123 for pass := range maxPasses * 4 {
124 if time.Now().After(deadline) || len(source) > originalSize*3 {
125 break
126 }
127 _ = pass
128 compileOutput, compileErr := tryCompileLocal(source)
129 if compileErr == nil {
130 return source, nil
131 }
132
133 prev := source
134
135 // Step 1: Try to neutralize error lines — replace broken RHS
136 // with zero values while keeping the variable declaration.
137 source = neutralizeErrorLines(source, compileOutput)
138
139 // Step 2: If neutralization didn't help, remove error lines.
140 if source == prev {
141 source = removeErrorLines(source, compileOutput)
142 }
143
144 // Step 3: If line removal didn't help, replace broken functions
145 // with signature-preserving stubs.
146 if source == prev {
147 source = removeErrorFunctions(source, compileOutput)
148 }
149
150 if source == prev {
151 break // no progress
152 }
153
154 // After each change, run stub/import fixes until stable.
155 // Cap at 3 rounds and abort if source grows beyond 3x original.
156 for innerPass := range 3 {
157 if len(source) > originalSize*3 {
158 break
159 }
160 compileOutput, compileErr = tryCompileLocal(source)
161 if compileErr == nil {
162 return source, nil
163 }
164 fixes := parseCompileErrors(compileOutput)
165 promoteReceiverFixes(fixes, receiverTypes)
166 if len(fixes) == 0 {
167 break
168 }
169 prevInner := source
170 source = applyFixes(source, fixes)
171 if source == prevInner {
172 break
173 }
174 _ = innerPass
175 }
176 }
177
178 // Ensure main() exists — it may have been emptied or removed.
179 source = ensureMain(source)
180
181 // Remove broken top-level declarations (unclosed braces, etc.)
182 source = removeIncompleteDecls(source)
183
184 // Final cleanup: remove any remaining orphaned blocks.
185 source = cleanOrphanedBlocks(source)
186
187 // One last compile attempt.
188 _, finalErr := tryCompileLocal(source)
189 if finalErr == nil {
190 return source, nil
191 }
192 return source, fmt.Errorf("still has errors after %d passes", maxPasses)
193 }
194
195
196 const selfModule = "git.mleku.dev/mleku/dendrite"
197
198 // buildRepairGoMod generates a minimal go.mod for repair compilation.
199 func buildRepairGoMod(source string) string {
200 var b strings.Builder
201 b.WriteString("module offspring\n\ngo 1.24\n")
202
203 if !strings.Contains(source, selfModule) {
204 return b.String()
205 }
206
207 modRoot := findRepairModuleRoot()
208 if modRoot == "" {
209 return b.String()
210 }
211
212 // Read parent go.mod for require blocks.
213 parentMod, err := os.ReadFile(filepath.Join(modRoot, "go.mod"))
214 if err != nil {
215 return b.String()
216 }
217
218 // Extract require blocks.
219 lines := strings.Split(string(parentMod), "\n")
220 inBlock := false
221 for _, line := range lines {
222 trimmed := strings.TrimSpace(line)
223 if trimmed == "require (" {
224 inBlock = true
225 b.WriteString(line + "\n")
226 continue
227 }
228 if inBlock {
229 b.WriteString(line + "\n")
230 if trimmed == ")" {
231 inBlock = false
232 }
233 continue
234 }
235 if strings.HasPrefix(trimmed, "require ") && !strings.HasPrefix(trimmed, "require (") {
236 b.WriteString(line + "\n")
237 }
238 }
239
240 b.WriteString("\nrequire " + selfModule + " v0.0.0\n")
241 b.WriteString("\nreplace " + selfModule + " => " + modRoot + "\n")
242
243 return b.String()
244 }
245
246 // findRepairModuleRoot walks up from cwd looking for go.mod declaring selfModule.
247 func findRepairModuleRoot() string {
248 dir, err := os.Getwd()
249 if err != nil {
250 return ""
251 }
252 for {
253 data, err := os.ReadFile(filepath.Join(dir, "go.mod"))
254 if err == nil {
255 for _, line := range strings.Split(string(data), "\n") {
256 line = strings.TrimSpace(line)
257 if strings.HasPrefix(line, "module ") {
258 mod := strings.TrimSpace(strings.TrimPrefix(line, "module"))
259 if mod == selfModule {
260 return dir
261 }
262 }
263 }
264 }
265 parent := filepath.Dir(dir)
266 if parent == dir {
267 break
268 }
269 dir = parent
270 }
271 return ""
272 }
273
274 // repairCleanEnv returns a clean Go environment.
275 func repairCleanEnv(root string) []string {
276 env := os.Environ()
277 clean := make([]string, 0, len(env)+3)
278 for _, e := range env {
279 if strings.HasPrefix(e, "GOROOT=") ||
280 strings.HasPrefix(e, "GOTOOLCHAIN=") ||
281 strings.HasPrefix(e, "PATH=") {
282 continue
283 }
284 clean = append(clean, e)
285 }
286 clean = append(clean,
287 "GOROOT="+root,
288 "GOTOOLCHAIN=local",
289 "GOMAXPROCS=1",
290 "PATH="+filepath.Join(root, "bin")+":"+os.Getenv("PATH"),
291 )
292 return clean
293 }
294
295 // reReceiverType matches method declarations to extract receiver type names.
296 // Matches: func (x TypeName), func (x *TypeName)
297 var reReceiverType = regexp.MustCompile(`func\s+\(\s*\w+\s+\*?(\w+)\)`)
298
299 // findReceiverTypes scans source for method declarations and returns a set
300 // of type names used as receivers.
301 func findReceiverTypes(source string) map[string]bool {
302 types := make(map[string]bool)
303 for _, line := range strings.Split(source, "\n") {
304 if m := reReceiverType.FindStringSubmatch(line); m != nil {
305 types[m[1]] = true
306 }
307 }
308 return types
309 }
310
311 // promoteReceiverFixes upgrades "undefined_var" fixes to "undefined_type"
312 // if the name is used as a method receiver.
313 func promoteReceiverFixes(fixes []fix, receiverTypes map[string]bool) {
314 for i := range fixes {
315 if fixes[i].kind == "undefined_var" && receiverTypes[fixes[i].name] {
316 fixes[i].kind = "undefined_type"
317 }
318 }
319 }
320
321 // ensureMain adds an empty func main() if one is not present.
322 func ensureMain(source string) string {
323 for _, line := range strings.Split(source, "\n") {
324 trimmed := strings.TrimSpace(line)
325 if strings.HasPrefix(trimmed, "func main()") {
326 return source
327 }
328 }
329 return source + "\nfunc main() {}\n"
330 }
331
332 // fix represents a single repair action.
333 type fix struct {
334 kind string // "undefined_type", "undefined_var", "unused_import", "redeclared", "missing_return"
335 name string // the identifier, import path, or line number (for missing_return)
336 }
337
338 // Patterns for parsing Go compiler errors.
339 var (
340 reUndefined = regexp.MustCompile(`undefined:\s+([\w.]+)`)
341 reUnusedImport = regexp.MustCompile(`"([^"]+)" imported and not used`)
342 reRedeclared = regexp.MustCompile(`(\w+) redeclared in this block`)
343 reMissingReturn = regexp.MustCompile(`^(.+):(\d+):\d+: missing return`)
344 reNotImplement = regexp.MustCompile(`does not implement (\w+)`)
345 reErrorAtLine = regexp.MustCompile(`\./main\.go:(\d+):\d+:`)
346 reAssignMismatch = regexp.MustCompile(`assignment mismatch: (\d+) variables but .* returns (\d+) values?`)
347 reMultiValue = regexp.MustCompile(`multiple-value (\w+)\(`)
348 reDeclNotUsed = regexp.MustCompile(`declared and not used: (\w+)`)
349 )
350
351 // parseCompileErrors extracts fixable errors from compiler output.
352 func parseCompileErrors(output string) []fix {
353 seen := make(map[string]bool)
354 var fixes []fix
355
356 for _, line := range strings.Split(output, "\n") {
357 if m := reUndefined.FindStringSubmatch(line); m != nil {
358 name := m[1]
359 key := "undef:" + name
360 if !seen[key] {
361 seen[key] = true
362 if strings.Contains(name, ".") {
363 // Qualified reference (e.g., hash.Len) — can't stub,
364 // will be handled by removeErrorFunctions.
365 } else if isTypeName(name) {
366 fixes = append(fixes, fix{kind: "undefined_type", name: name})
367 } else {
368 fixes = append(fixes, fix{kind: "undefined_var", name: name})
369 }
370 }
371 }
372 if m := reUnusedImport.FindStringSubmatch(line); m != nil {
373 path := m[1]
374 key := "unused:" + path
375 if !seen[key] {
376 seen[key] = true
377 fixes = append(fixes, fix{kind: "unused_import", name: path})
378 }
379 }
380 if m := reRedeclared.FindStringSubmatch(line); m != nil {
381 name := m[1]
382 key := "redecl:" + name
383 if !seen[key] {
384 seen[key] = true
385 fixes = append(fixes, fix{kind: "redeclared", name: name})
386 }
387 }
388 // "missing return" at a line — remove the enclosing function.
389 if reMissingReturn.MatchString(line) {
390 // Extract line number and mark for function removal.
391 if m := reMissingReturn.FindStringSubmatch(line); m != nil {
392 key := "misret:" + m[2]
393 if !seen[key] {
394 seen[key] = true
395 fixes = append(fixes, fix{kind: "missing_return", name: m[2]})
396 }
397 }
398 }
399 // "does not implement X" — the struct is used as an interface
400 // it doesn't satisfy. Remove the offending statement.
401 if reNotImplement.MatchString(line) {
402 // Not easily fixable — skip for now, will be handled by
403 // removal of the enclosing function if it also has other errors.
404 }
405
406 // "X declared and not used" — suppress by adding "_ = X".
407 if m := reDeclNotUsed.FindStringSubmatch(line); m != nil {
408 name := m[1]
409 key := "declnotused:" + name
410 if !seen[key] {
411 seen[key] = true
412 fixes = append(fixes, fix{kind: "decl_not_used", name: name})
413 }
414 }
415
416 // "assignment mismatch: N variables but F returns M values" or
417 // "multiple-value X() used in single-value context" — the func
418 // stub has wrong return arity. Fix by regenerating the stub.
419 if m := reMultiValue.FindStringSubmatch(line); m != nil {
420 name := m[1]
421 key := "multiret:" + name
422 if !seen[key] {
423 seen[key] = true
424 fixes = append(fixes, fix{kind: "fix_return_arity", name: name})
425 }
426 }
427 if m := reAssignMismatch.FindStringSubmatch(line); m != nil {
428 // The error is on this line — we need to find the function name
429 // being called. Extract from the error line reference.
430 if lineM := reErrorAtLine.FindStringSubmatch(line); lineM != nil {
431 key := "assignmis:" + lineM[1]
432 if !seen[key] {
433 seen[key] = true
434 fixes = append(fixes, fix{kind: "fix_assign_mismatch", name: lineM[1]})
435 }
436 }
437 }
438 }
439
440 return fixes
441 }
442
443 // isTypeName heuristically determines if an identifier is likely a type name
444 // (starts with uppercase letter).
445 func isTypeName(name string) bool {
446 if len(name) == 0 {
447 return false
448 }
449 return unicode.IsUpper(rune(name[0]))
450 }
451
452 // applyFixes modifies the source to fix the given errors.
453 func applyFixes(source string, fixes []fix) string {
454 for _, f := range fixes {
455 switch f.kind {
456 case "undefined_type":
457 source = addTypeStub(source, f.name)
458 case "undefined_var":
459 source = addVarStub(source, f.name)
460 case "unused_import":
461 source = removeImport(source, f.name)
462 case "redeclared":
463 source = removeRedeclaration(source, f.name)
464 case "missing_return":
465 source = removeFuncAtLine(source, f.name)
466 case "fix_return_arity":
467 source = fixReturnArity(source, f.name)
468 case "fix_assign_mismatch":
469 source = fixAssignMismatchAtLine(source, f.name)
470 case "decl_not_used":
471 source = suppressUnusedVar(source, f.name)
472 }
473 }
474 return source
475 }
476
477 // fixReturnArity fixes a func stub whose return arity doesn't match call sites.
478 // Removes the existing stub and re-adds it with correct arity detection.
479 func fixReturnArity(source, name string) string {
480 // Remove the existing func stub declaration.
481 lines := strings.Split(source, "\n")
482 var cleaned []string
483 for _, line := range lines {
484 trimmed := strings.TrimSpace(line)
485 if strings.HasPrefix(trimmed, "func "+name+"(args ...interface{})") {
486 continue // remove old stub
487 }
488 cleaned = append(cleaned, line)
489 }
490 source = strings.Join(cleaned, "\n")
491
492 // Re-add with correct arity.
493 return addVarStub(source, name)
494 }
495
496 // fixAssignMismatchAtLine handles "assignment mismatch" errors by finding
497 // the function call on the error line and adjusting its stub.
498 func fixAssignMismatchAtLine(source, lineNumStr string) string {
499 lineNum := 0
500 for _, ch := range lineNumStr {
501 if ch >= '0' && ch <= '9' {
502 lineNum = lineNum*10 + int(ch-'0')
503 }
504 }
505 if lineNum == 0 {
506 return source
507 }
508
509 lines := strings.Split(source, "\n")
510 if lineNum > len(lines) {
511 return source
512 }
513
514 // Extract the line and find the function name being called.
515 line := lines[lineNum-1]
516 trimmed := strings.TrimSpace(line)
517
518 // Look for patterns: "a, b := funcName(" or "a, b = funcName("
519 assignOps := []string{":=", "="}
520 for _, op := range assignOps {
521 idx := strings.Index(trimmed, op)
522 if idx < 0 {
523 continue
524 }
525 rhs := strings.TrimSpace(trimmed[idx+len(op):])
526 // rhs should start with funcName(
527 parenIdx := strings.IndexByte(rhs, '(')
528 if parenIdx <= 0 {
529 continue
530 }
531 funcName := strings.TrimSpace(rhs[:parenIdx])
532 // Check if this function has a stub we can fix.
533 if strings.Contains(source, "func "+funcName+"(args ...interface{})") {
534 return fixReturnArity(source, funcName)
535 }
536 }
537
538 return source
539 }
540
541 // addTypeStub adds a type stub after the import block.
542 // Uses struct{} so methods can be defined on the type.
543 func addTypeStub(source, name string) string {
544 stub := fmt.Sprintf("type %s struct{}\n", name)
545
546 // Check if already declared (any declaration form).
547 if nameIsDeclared(source, name) {
548 return source
549 }
550
551 return insertAfterImports(source, stub)
552 }
553
554 // addVarStub adds a var or func stub after the import block.
555 // If the name is used as a function call in the source, it emits a
556 // func stub. When another function in the source calls this name and
557 // assigns the result, the stub uses types inferred from the assignment
558 // context (e.g., "s, err := name()" → (string, error) if the vars
559 // are used with string/error operations). Falls back to interface{}.
560 func addVarStub(source, name string) string {
561 // Check if already declared (any declaration form).
562 if nameIsDeclared(source, name) {
563 return source
564 }
565
566 // If this name is called as a function, emit a func stub.
567 // Use word-boundary matching: the char before name( must not be
568 // an identifier char (to avoid matching "runGeneration(" for "gen(").
569 if isCalledAsFunc(source, name) {
570 retSig := inferReturnSignature(source, name)
571 if retSig != "" {
572 zr := zeroReturn(retSig)
573 if zr == "" {
574 zr = "return"
575 }
576 stub := fmt.Sprintf("func %s(args ...interface{}) %s { %s }\n", name, retSig, zr)
577 return insertAfterImports(source, stub)
578 }
579 // Fallback: infer arity from assignment LHS count.
580 n := inferReturnArity(source, name)
581 if n <= 1 {
582 stub := fmt.Sprintf("func %s(args ...interface{}) interface{} { return nil }\n", name)
583 return insertAfterImports(source, stub)
584 }
585 retTypes := make([]string, n)
586 retVals := make([]string, n)
587 for i := range n {
588 retTypes[i] = "interface{}"
589 retVals[i] = "nil"
590 }
591 stub := fmt.Sprintf("func %s(args ...interface{}) (%s) { return %s }\n",
592 name, strings.Join(retTypes, ", "), strings.Join(retVals, ", "))
593 return insertAfterImports(source, stub)
594 }
595
596 stub := fmt.Sprintf("var %s interface{}\n", name)
597 return insertAfterImports(source, stub)
598 }
599
600 // findWordBoundaryCall finds "name(" in text where name starts at a word
601 // boundary (not preceded by an identifier character). Returns the index of
602 // name in text, or -1 if not found.
603 func findWordBoundaryCall(text, name string) int {
604 callPat := name + "("
605 idx := 0
606 for {
607 pos := strings.Index(text[idx:], callPat)
608 if pos < 0 {
609 return -1
610 }
611 absPos := idx + pos
612 if absPos > 0 {
613 prev := text[absPos-1]
614 if (prev >= 'a' && prev <= 'z') || (prev >= 'A' && prev <= 'Z') ||
615 (prev >= '0' && prev <= '9') || prev == '_' {
616 idx = absPos + len(callPat)
617 continue
618 }
619 }
620 return absPos
621 }
622 }
623
624 // isCalledAsFunc checks if name appears as a function call "name(" in the
625 // source, using word-boundary matching to avoid false positives like matching
626 // "runGeneration(" when looking for "gen(".
627 func isCalledAsFunc(source, name string) bool {
628 callPat := name + "("
629 idx := 0
630 for {
631 pos := strings.Index(source[idx:], callPat)
632 if pos < 0 {
633 return false
634 }
635 absPos := idx + pos
636 // Check character before the match — must be a non-identifier char.
637 if absPos > 0 {
638 prev := source[absPos-1]
639 if (prev >= 'a' && prev <= 'z') || (prev >= 'A' && prev <= 'Z') ||
640 (prev >= '0' && prev <= '9') || prev == '_' {
641 idx = absPos + len(callPat)
642 continue // false positive: part of a longer identifier
643 }
644 }
645 return true
646 }
647 }
648
649 // inferReturnSignature tries to determine the return type signature for a
650 // function call by examining assignment context. Looks for patterns like:
651 //
652 // s, err := name(...) → checks if "err" is used with error comparisons
653 // bin, err := name(...) → (string, error) if bin is used in string context
654 // ok := name(...) → bool if "ok" is used as a bool
655 //
656 // Returns a Go return type string like "string", "error", "(string, error)"
657 // or "" if inference fails.
658 func inferReturnSignature(source, name string) string {
659 // Scan for assignment call sites with word-boundary matching.
660 for _, line := range strings.Split(source, "\n") {
661 trimmed := strings.TrimSpace(line)
662 callIdx := findWordBoundaryCall(trimmed, name)
663 if callIdx < 0 {
664 continue
665 }
666 prefix := strings.TrimSpace(trimmed[:callIdx])
667 isAssign := false
668 if strings.HasSuffix(prefix, ":=") {
669 prefix = strings.TrimSuffix(prefix, ":=")
670 isAssign = true
671 } else if strings.HasSuffix(prefix, "=") && !strings.HasSuffix(prefix, "!=") && !strings.HasSuffix(prefix, "==") {
672 prefix = strings.TrimSuffix(prefix, "=")
673 isAssign = true
674 }
675 if !isAssign {
676 continue
677 }
678 prefix = strings.TrimSpace(prefix)
679 if prefix == "" {
680 continue
681 }
682 parts := strings.Split(prefix, ",")
683 var varNames []string
684 for _, p := range parts {
685 p = strings.TrimSpace(p)
686 if p != "" {
687 varNames = append(varNames, p)
688 }
689 }
690 if len(varNames) == 0 {
691 continue
692 }
693 // Infer types from variable names and usage context.
694 types := make([]string, len(varNames))
695 for i, v := range varNames {
696 types[i] = inferVarType(source, v)
697 }
698 if len(types) == 1 {
699 return types[0]
700 }
701 return "(" + strings.Join(types, ", ") + ")"
702 }
703 return ""
704 }
705
706 // inferVarType guesses the type of a variable from its name and how it's
707 // used in the source. Common patterns:
708 //
709 // err → error
710 // ok → bool
711 // _ → interface{}
712 // n, count → int
713 // s, str → string
714 // b, buf → []byte
715 func inferVarType(source, name string) string {
716 if name == "_" {
717 return "interface{}"
718 }
719 if name == "err" {
720 return "error"
721 }
722 if name == "ok" {
723 return "bool"
724 }
725 // Check usage patterns in source.
726 if strings.Contains(source, name+" != nil") || strings.Contains(source, name+" == nil") {
727 // Nil-comparable — could be error, pointer, slice, map, interface.
728 if strings.Contains(source, "return "+name) {
729 // If returned alone or last, likely error.
730 return "error"
731 }
732 return "interface{}"
733 }
734 if strings.Contains(source, "string("+name+")") || strings.Contains(source, name+` + "`) || strings.Contains(source, `" + `+name) {
735 return "string"
736 }
737 if strings.Contains(source, "len("+name+")") {
738 return "string" // could be []byte too, string is safer
739 }
740 // Default to interface{} — most permissive.
741 return "interface{}"
742 }
743
744 // inferReturnArity scans source for assignment patterns that call name()
745 // and returns the number of LHS variables. Handles:
746 // - a, b := name(...) → 2
747 // - a, b, c = name(...) → 3
748 // - a := name(...) → 1
749 // - name(...) (no assignment) → 1
750 func inferReturnArity(source, name string) int {
751 maxArity := 1
752 for _, line := range strings.Split(source, "\n") {
753 trimmed := strings.TrimSpace(line)
754
755 // Find call sites with word-boundary matching.
756 callIdx := findWordBoundaryCall(trimmed, name)
757 if callIdx < 0 {
758 continue
759 }
760
761 // Check if this is part of an assignment (not just a standalone call).
762 // Look for := or = before the call.
763 prefix := trimmed[:callIdx]
764 prefix = strings.TrimSpace(prefix)
765
766 // Strip trailing := or =
767 isAssign := false
768 if strings.HasSuffix(prefix, ":=") {
769 prefix = strings.TrimSuffix(prefix, ":=")
770 isAssign = true
771 } else if strings.HasSuffix(prefix, "=") && !strings.HasSuffix(prefix, "!=") && !strings.HasSuffix(prefix, "==") {
772 prefix = strings.TrimSuffix(prefix, "=")
773 isAssign = true
774 }
775
776 if !isAssign {
777 continue
778 }
779
780 // Count comma-separated LHS variables.
781 prefix = strings.TrimSpace(prefix)
782 if prefix == "" {
783 continue
784 }
785 parts := strings.Split(prefix, ",")
786 arity := 0
787 for _, p := range parts {
788 p = strings.TrimSpace(p)
789 if p != "" && p != "_" || p == "_" {
790 arity++
791 }
792 }
793 if arity > maxArity {
794 maxArity = arity
795 }
796 }
797 return maxArity
798 }
799
800 // nameIsDeclared checks if a name is already declared in the source
801 // as a type, var, const, or function.
802 func nameIsDeclared(source, name string) bool {
803 for _, line := range strings.Split(source, "\n") {
804 trimmed := strings.TrimSpace(line)
805 if strings.HasPrefix(trimmed, "type "+name+" ") ||
806 strings.HasPrefix(trimmed, "type "+name+"=") ||
807 strings.HasPrefix(trimmed, "var "+name+" ") ||
808 strings.HasPrefix(trimmed, "const "+name+" ") ||
809 strings.HasPrefix(trimmed, "func "+name+"(") ||
810 strings.HasPrefix(trimmed, name+" :=") {
811 return true
812 }
813 }
814 return false
815 }
816
817 // insertAfterImports inserts text after the import block (or after package line).
818 func insertAfterImports(source, text string) string {
819 lines := strings.Split(source, "\n")
820 insertIdx := -1
821
822 // Find end of import block.
823 inImport := false
824 for i, line := range lines {
825 trimmed := strings.TrimSpace(line)
826 if trimmed == "import (" {
827 inImport = true
828 }
829 if inImport && trimmed == ")" {
830 insertIdx = i + 1
831 break
832 }
833 // Single-line import.
834 if strings.HasPrefix(trimmed, "import ") && !strings.Contains(trimmed, "(") {
835 insertIdx = i + 1
836 }
837 }
838
839 // Fallback: after package line.
840 if insertIdx < 0 {
841 for i, line := range lines {
842 if strings.HasPrefix(strings.TrimSpace(line), "package ") {
843 insertIdx = i + 1
844 break
845 }
846 }
847 }
848 if insertIdx < 0 {
849 insertIdx = 0
850 }
851
852 // Insert.
853 result := make([]string, 0, len(lines)+2)
854 result = append(result, lines[:insertIdx]...)
855 result = append(result, text)
856 result = append(result, lines[insertIdx:]...)
857 return strings.Join(result, "\n")
858 }
859
860 // removeImport removes an import path from the source.
861 func removeImport(source, importPath string) string {
862 lines := strings.Split(source, "\n")
863 result := make([]string, 0, len(lines))
864
865 for _, line := range lines {
866 trimmed := strings.TrimSpace(line)
867 // Match: "path" or alias "path"
868 if strings.Contains(trimmed, `"`+importPath+`"`) {
869 // Skip single-line import or import block entry.
870 if strings.HasPrefix(trimmed, "import ") || !strings.HasPrefix(trimmed, "import") {
871 continue
872 }
873 }
874 result = append(result, line)
875 }
876
877 return strings.Join(result, "\n")
878 }
879
880 // removeRedeclaration removes the SECOND occurrence of a declaration.
881 func removeRedeclaration(source, name string) string {
882 lines := strings.Split(source, "\n")
883 seen := false
884 result := make([]string, 0, len(lines))
885
886 patterns := []string{
887 "type " + name + " ",
888 "type " + name + "=",
889 "var " + name + " ",
890 "func " + name + "(",
891 }
892
893 for i := 0; i < len(lines); i++ {
894 trimmed := strings.TrimSpace(lines[i])
895 isDecl := false
896 for _, pat := range patterns {
897 if strings.HasPrefix(trimmed, pat) {
898 isDecl = true
899 break
900 }
901 }
902
903 if isDecl {
904 if seen {
905 // Skip this declaration (and its body if it has braces).
906 depth := strings.Count(lines[i], "{") - strings.Count(lines[i], "}")
907 for depth > 0 && i+1 < len(lines) {
908 i++
909 depth += strings.Count(lines[i], "{") - strings.Count(lines[i], "}")
910 }
911 continue
912 }
913 seen = true
914 }
915
916 result = append(result, lines[i])
917 }
918
919 return strings.Join(result, "\n")
920 }
921
922 // removeFuncAtLine removes the function whose closing brace is at the
923 // given line number (as a string). Walks backward to find "func " and
924 // forward to find the matching "}".
925 func removeFuncAtLine(source, lineNumStr string) string {
926 lineNum := 0
927 for _, ch := range lineNumStr {
928 if ch >= '0' && ch <= '9' {
929 lineNum = lineNum*10 + int(ch-'0')
930 }
931 }
932 if lineNum == 0 {
933 return source
934 }
935
936 lines := strings.Split(source, "\n")
937 if lineNum > len(lines) {
938 return source
939 }
940
941 // Walk backward from the error line to find the function declaration.
942 funcStart := -1
943 for i := lineNum - 1; i >= 0; i-- {
944 trimmed := strings.TrimSpace(lines[i])
945 if strings.HasPrefix(trimmed, "func ") {
946 funcStart = i
947 break
948 }
949 }
950 if funcStart < 0 {
951 return source
952 }
953
954 // Walk forward from funcStart to find the matching closing brace.
955 depth := 0
956 funcEnd := -1
957 for i := funcStart; i < len(lines); i++ {
958 depth += strings.Count(lines[i], "{") - strings.Count(lines[i], "}")
959 if depth <= 0 && strings.Count(lines[i], "{") > 0 || (depth == 0 && i > funcStart) {
960 funcEnd = i
961 break
962 }
963 }
964 if funcEnd < 0 {
965 funcEnd = len(lines) - 1
966 }
967
968 // Remove the function.
969 result := make([]string, 0, len(lines)-(funcEnd-funcStart+1))
970 result = append(result, lines[:funcStart]...)
971 result = append(result, lines[funcEnd+1:]...)
972 return strings.Join(result, "\n")
973 }
974
975 // removeErrorFunctions parses compiler output to find which line numbers
976 // have errors, then removes the enclosing functions for those lines.
977 func removeErrorFunctions(source, compileOutput string) string {
978 // Extract error line numbers from compiler output.
979 errorLines := make(map[int]bool)
980 for _, line := range strings.Split(compileOutput, "\n") {
981 if m := reErrorAtLine.FindStringSubmatch(line); m != nil {
982 lineNum := 0
983 for _, ch := range m[1] {
984 lineNum = lineNum*10 + int(ch-'0')
985 }
986 errorLines[lineNum] = true
987 }
988 }
989
990 if len(errorLines) == 0 {
991 return source
992 }
993
994 // Find which functions contain errors and remove them.
995 lines := strings.Split(source, "\n")
996 type funcRange struct {
997 start, end int
998 }
999
1000 // Build a list of all function ranges.
1001 var funcs []funcRange
1002 for i := 0; i < len(lines); i++ {
1003 trimmed := strings.TrimSpace(lines[i])
1004 if !strings.HasPrefix(trimmed, "func ") {
1005 continue
1006 }
1007 start := i
1008 depth := 0
1009 foundBody := false
1010 for j := i; j < len(lines); j++ {
1011 depth += strings.Count(lines[j], "{") - strings.Count(lines[j], "}")
1012 if strings.Contains(lines[j], "{") {
1013 foundBody = true
1014 }
1015 if foundBody && depth <= 0 {
1016 funcs = append(funcs, funcRange{start, j})
1017 i = j // skip past this function
1018 break
1019 }
1020 }
1021 }
1022
1023 // Find functions that contain error lines.
1024 removeFuncs := make(map[int]bool)
1025 for _, fr := range funcs {
1026 for lineNum := range errorLines {
1027 // Line numbers are 1-based; our slice is 0-based.
1028 if lineNum-1 >= fr.start && lineNum-1 <= fr.end {
1029 removeFuncs[fr.start] = true
1030 break
1031 }
1032 }
1033 }
1034
1035 if len(removeFuncs) == 0 {
1036 return source
1037 }
1038
1039 // Rebuild source, replacing error functions with signature-preserving stubs.
1040 // This keeps the function callable (correct name, params, return types)
1041 // while removing the broken body.
1042 result := make([]string, 0, len(lines))
1043 for i := 0; i < len(lines); i++ {
1044 // Check if this is a function we're replacing.
1045 skip := false
1046 for _, fr := range funcs {
1047 if fr.start == i && removeFuncs[fr.start] {
1048 trimmed := strings.TrimSpace(lines[i])
1049 if trimmed == "func main() {" || strings.HasPrefix(trimmed, "func main()") {
1050 // Replace main with empty body.
1051 result = append(result, "func main() {}")
1052 i = fr.end
1053 skip = true
1054 break
1055 }
1056 // Extract the full func signature line and generate a stub
1057 // with the correct return types.
1058 stub := stubFromSignature(trimmed)
1059 if stub != "" {
1060 result = append(result, stub)
1061 }
1062 i = fr.end // skip to end of function
1063 skip = true
1064 break
1065 }
1066 }
1067 if !skip {
1068 result = append(result, lines[i])
1069 }
1070 }
1071
1072 return strings.Join(result, "\n")
1073 }
1074
1075 // neutralizeErrorLines attempts to preserve variable declarations from error
1076 // lines by replacing broken RHS expressions with zero values. For example:
1077 //
1078 // x, err := brokenCall() → var x interface{}; var err error
1079 // s := undefined + "foo" → var s string
1080 //
1081 // This prevents cascading "undefined" errors from variables that were declared
1082 // on the removed line. Returns unchanged source if no neutralization is possible.
1083 func neutralizeErrorLines(source, compileOutput string) string {
1084 errorLines := make(map[int]bool)
1085 for _, line := range strings.Split(compileOutput, "\n") {
1086 if m := reErrorAtLine.FindStringSubmatch(line); m != nil {
1087 lineNum := 0
1088 for _, ch := range m[1] {
1089 lineNum = lineNum*10 + int(ch-'0')
1090 }
1091 if lineNum > 0 {
1092 errorLines[lineNum] = true
1093 }
1094 }
1095 }
1096 if len(errorLines) == 0 {
1097 return source
1098 }
1099
1100 lines := strings.Split(source, "\n")
1101 changed := false
1102 for lineNum := range errorLines {
1103 idx := lineNum - 1
1104 if idx < 0 || idx >= len(lines) {
1105 continue
1106 }
1107 line := lines[idx]
1108 trimmed := strings.TrimSpace(line)
1109
1110 // Only neutralize short-assign statements: "a, b := expr"
1111 assignIdx := strings.Index(trimmed, ":=")
1112 if assignIdx <= 0 {
1113 continue
1114 }
1115
1116 // Don't neutralize structural lines.
1117 if strings.HasPrefix(trimmed, "func ") || strings.HasPrefix(trimmed, "if ") ||
1118 strings.HasPrefix(trimmed, "for ") || strings.HasPrefix(trimmed, "switch ") {
1119 continue
1120 }
1121
1122 lhs := strings.TrimSpace(trimmed[:assignIdx])
1123 parts := strings.Split(lhs, ",")
1124 var decls []string
1125 for _, p := range parts {
1126 p = strings.TrimSpace(p)
1127 if p == "" || p == "_" {
1128 continue
1129 }
1130 typ := inferVarType(source, p)
1131 decls = append(decls, fmt.Sprintf("var %s %s", p, typ))
1132 }
1133 if len(decls) > 0 {
1134 // Preserve indentation.
1135 indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
1136 lines[idx] = indent + strings.Join(decls, "; ")
1137 changed = true
1138 }
1139 }
1140 if !changed {
1141 return source
1142 }
1143 return strings.Join(lines, "\n")
1144 }
1145
1146 // removeErrorLines removes specific lines that the compiler reports errors on,
1147 // then cleans up orphaned syntax (empty var/const/type blocks, etc.).
1148 func removeErrorLines(source, compileOutput string) string {
1149 // Extract error line numbers.
1150 errorLines := make(map[int]bool)
1151 for _, line := range strings.Split(compileOutput, "\n") {
1152 if m := reErrorAtLine.FindStringSubmatch(line); m != nil {
1153 lineNum := 0
1154 for _, ch := range m[1] {
1155 lineNum = lineNum*10 + int(ch-'0')
1156 }
1157 if lineNum > 0 {
1158 errorLines[lineNum] = true
1159 }
1160 }
1161 }
1162
1163 if len(errorLines) == 0 {
1164 return source
1165 }
1166
1167 lines := strings.Split(source, "\n")
1168
1169 // First, find which statements the error lines belong to.
1170 // A "statement" spans from the line with a top-level expression to its
1171 // closing paren/brace. If an error is inside a multi-line call like
1172 // runColony(arg1,\n arg2,\n arg3), remove the entire call.
1173 removeStmts := findStatementsToRemove(lines, errorLines)
1174
1175 result := make([]string, 0, len(lines))
1176 for i, line := range lines {
1177 lineNum := i + 1 // 1-based
1178 if removeStmts[lineNum] || errorLines[lineNum] {
1179 // Don't remove structural lines (func, closing braces, package, import).
1180 trimmed := strings.TrimSpace(line)
1181 if trimmed == "}" || trimmed == "{" ||
1182 strings.HasPrefix(trimmed, "func ") ||
1183 strings.HasPrefix(trimmed, "package ") ||
1184 strings.HasPrefix(trimmed, "import") {
1185 result = append(result, line)
1186 continue
1187 }
1188 // Skip this error line or its containing statement.
1189 continue
1190 }
1191 result = append(result, line)
1192 }
1193
1194 source = strings.Join(result, "\n")
1195
1196 // Clean up orphaned group blocks: var\n), const\n), type\n)
1197 // Also clean up stray "var" or "const" or "type" keywords on their own line.
1198 source = cleanOrphanedBlocks(source)
1199
1200 return source
1201 }
1202
1203 // cleanOrphanedBlocks removes broken group declarations like:
1204 //
1205 // var
1206 // }
1207 //
1208 // or empty groups like:
1209 //
1210 // var (
1211 // )
1212 func cleanOrphanedBlocks(source string) string {
1213 lines := strings.Split(source, "\n")
1214 result := make([]string, 0, len(lines))
1215
1216 for i := 0; i < len(lines); i++ {
1217 trimmed := strings.TrimSpace(lines[i])
1218
1219 // Check for a lone keyword followed by } or ) on the next non-empty line.
1220 if trimmed == "var" || trimmed == "const" || trimmed == "type" {
1221 // Look ahead for closing brace or paren.
1222 j := i + 1
1223 for j < len(lines) && strings.TrimSpace(lines[j]) == "" {
1224 j++
1225 }
1226 if j < len(lines) {
1227 next := strings.TrimSpace(lines[j])
1228 if next == "}" || next == ")" {
1229 i = j // skip both
1230 continue
1231 }
1232 }
1233 }
1234
1235 // Check for empty group: "var (" followed (after blanks) by ")".
1236 if (trimmed == "var (" || trimmed == "const (" || trimmed == "type (") {
1237 j := i + 1
1238 for j < len(lines) && strings.TrimSpace(lines[j]) == "" {
1239 j++
1240 }
1241 if j < len(lines) && strings.TrimSpace(lines[j]) == ")" {
1242 i = j // skip the empty group
1243 continue
1244 }
1245 }
1246
1247 // Check for a "var // comment" pattern (broken var decl).
1248 if strings.HasPrefix(trimmed, "var //") || strings.HasPrefix(trimmed, "const //") || strings.HasPrefix(trimmed, "type //") {
1249 continue // remove broken declaration
1250 }
1251
1252 result = append(result, lines[i])
1253 }
1254
1255 return strings.Join(result, "\n")
1256 }
1257
1258 // removeIncompleteDecls scans top-level declarations and removes those
1259 // with unbalanced braces (e.g., "var X = func() int {" with stripped body).
1260 func removeIncompleteDecls(source string) string {
1261 lines := strings.Split(source, "\n")
1262 result := make([]string, 0, len(lines))
1263
1264 for i := 0; i < len(lines); i++ {
1265 trimmed := strings.TrimSpace(lines[i])
1266
1267 // Detect top-level declaration lines with opening brace.
1268 isDecl := strings.HasPrefix(trimmed, "var ") ||
1269 strings.HasPrefix(trimmed, "const ") ||
1270 strings.HasPrefix(trimmed, "type ") ||
1271 strings.HasPrefix(trimmed, "func ")
1272
1273 if isDecl && strings.Contains(trimmed, "{") {
1274 // Count braces for this declaration.
1275 depth := 0
1276 j := i
1277 for j < len(lines) {
1278 depth += strings.Count(lines[j], "{") - strings.Count(lines[j], "}")
1279 if depth <= 0 {
1280 break
1281 }
1282 j++
1283 }
1284 if j >= len(lines) && depth > 0 {
1285 // Unclosed declaration — remove from i to end of file.
1286 // But don't discard everything; stop at the next top-level decl.
1287 for k := i + 1; k < len(lines); k++ {
1288 next := strings.TrimSpace(lines[k])
1289 if next == "" {
1290 continue
1291 }
1292 nextIsDecl := strings.HasPrefix(next, "var ") ||
1293 strings.HasPrefix(next, "const ") ||
1294 strings.HasPrefix(next, "type ") ||
1295 strings.HasPrefix(next, "func ")
1296 if nextIsDecl {
1297 // Skip lines i through k-1 (the broken decl).
1298 i = k - 1
1299 break
1300 }
1301 }
1302 continue // skip this broken declaration line
1303 }
1304 }
1305
1306 result = append(result, lines[i])
1307 }
1308
1309 return strings.Join(result, "\n")
1310 }
1311
1312 // findStatementsToRemove identifies multi-line statements that contain error
1313 // lines. When an error occurs inside a multi-line function call like
1314 // runColony(arg1,\n arg2,\n arg3), all lines of that call should be removed
1315 // together rather than one at a time (which leaves broken syntax).
1316 func findStatementsToRemove(lines []string, errorLines map[int]bool) map[int]bool {
1317 removeLines := make(map[int]bool)
1318
1319 // For each error line, check if it's inside a multi-line statement
1320 // (unclosed parentheses from above). If so, find the statement boundaries
1321 // and mark all lines for removal.
1322 for errLine := range errorLines {
1323 idx := errLine - 1 // 0-based
1324 if idx < 0 || idx >= len(lines) {
1325 continue
1326 }
1327
1328 // Walk backward to find where the unclosed paren starts.
1329 parenDepth := 0
1330 for i := idx; i >= 0; i-- {
1331 line := lines[i]
1332 for j := len(line) - 1; j >= 0; j-- {
1333 if line[j] == ')' {
1334 parenDepth++
1335 } else if line[j] == '(' {
1336 parenDepth--
1337 }
1338 }
1339 if parenDepth < 0 {
1340 // Found the start of the multi-line expression.
1341 // This line has an unmatched '(' — find the matching ')'.
1342 stmtStart := i + 1 // line AFTER the opening paren
1343 stmtEnd := idx
1344
1345 // Walk forward from the error line to find the closing ')'.
1346 depth := 0
1347 for j := i; j < len(lines); j++ {
1348 for _, c := range lines[j] {
1349 if c == '(' {
1350 depth++
1351 } else if c == ')' {
1352 depth--
1353 }
1354 }
1355 if depth <= 0 {
1356 stmtEnd = j
1357 break
1358 }
1359 }
1360
1361 // Mark ALL lines of this statement for removal.
1362 // Include the statement start line too (the call itself).
1363 for k := stmtStart; k <= stmtEnd; k++ {
1364 removeLines[k+1] = true // 1-based
1365 }
1366 // Also mark the line with the function call.
1367 removeLines[i+1] = true
1368 break
1369 }
1370 }
1371 }
1372
1373 return removeLines
1374 }
1375
1376 // suppressUnusedVar adds "_ = name" after the declaration line of an unused
1377 // variable to suppress the "declared and not used" error without removing it.
1378 func suppressUnusedVar(source, name string) string {
1379 lines := strings.Split(source, "\n")
1380 for i, line := range lines {
1381 trimmed := strings.TrimSpace(line)
1382 // Match ":=" assignments: "name := ..." or "name, other := ..."
1383 if strings.Contains(trimmed, name) && strings.Contains(trimmed, ":=") {
1384 // Check if this line declares the variable.
1385 lhs := trimmed
1386 if eqIdx := strings.Index(lhs, ":="); eqIdx > 0 {
1387 lhs = strings.TrimSpace(lhs[:eqIdx])
1388 }
1389 // Check all LHS parts.
1390 parts := strings.Split(lhs, ",")
1391 for _, p := range parts {
1392 if strings.TrimSpace(p) == name {
1393 // Insert "_ = name" after this line.
1394 suppression := "\t_ = " + name
1395 result := make([]string, 0, len(lines)+1)
1396 result = append(result, lines[:i+1]...)
1397 result = append(result, suppression)
1398 result = append(result, lines[i+1:]...)
1399 return strings.Join(result, "\n")
1400 }
1401 }
1402 }
1403 // Also check "var name ..." declarations.
1404 if strings.HasPrefix(trimmed, "var "+name+" ") || strings.HasPrefix(trimmed, "var "+name+"=") {
1405 suppression := "\t_ = " + name
1406 result := make([]string, 0, len(lines)+1)
1407 result = append(result, lines[:i+1]...)
1408 result = append(result, suppression)
1409 result = append(result, lines[i+1:]...)
1410 return strings.Join(result, "\n")
1411 }
1412 }
1413 return source
1414 }
1415
1416 // stubFromSignature takes a function declaration line (e.g.,
1417 // "func buildSelf() (string, error) {") and returns a stub with an
1418 // empty body and zero-value return. This preserves the function's
1419 // call signature so callers don't break.
1420 func stubFromSignature(funcLine string) string {
1421 // Strip trailing "{" and whitespace.
1422 sig := strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(funcLine), "{"))
1423 if !strings.HasPrefix(sig, "func ") {
1424 return ""
1425 }
1426
1427 // Extract the return signature.
1428 retSig := extractReturnSig(sig)
1429 zr := zeroReturn(retSig)
1430
1431 if zr != "" {
1432 return sig + " { " + zr + " }"
1433 }
1434 return sig + " {}"
1435 }
1436
1437 // truncate shortens a string to maxLen characters.
1438 func truncate(s string, maxLen int) string {
1439 if len(s) <= maxLen {
1440 return s
1441 }
1442 return s[:maxLen] + "..."
1443 }
1444
1445 // StubNames returns the names of all type, var, and func stubs added by CompileAndRepair.
1446 // Detects: "type X struct{}", "var x interface{}", and "func x(args ...interface{}) ..."
1447 func StubNames(source string) []string {
1448 var names []string
1449 for _, line := range strings.Split(source, "\n") {
1450 trimmed := strings.TrimSpace(line)
1451 if strings.HasPrefix(trimmed, "type ") && strings.HasSuffix(trimmed, "struct{}") {
1452 fields := strings.Fields(trimmed)
1453 if len(fields) >= 2 {
1454 names = append(names, fields[1])
1455 }
1456 }
1457 if strings.HasPrefix(trimmed, "var ") && strings.HasSuffix(trimmed, "interface{}") {
1458 fields := strings.Fields(trimmed)
1459 if len(fields) >= 2 {
1460 names = append(names, fields[1])
1461 }
1462 }
1463 // Detect func stubs: "func name(args ...interface{})"
1464 if strings.HasPrefix(trimmed, "func ") && strings.Contains(trimmed, "(args ...interface{})") {
1465 // Extract name from "func name(args ..."
1466 rest := strings.TrimPrefix(trimmed, "func ")
1467 if paren := strings.IndexByte(rest, '('); paren > 0 {
1468 name := rest[:paren]
1469 if name != "" && !strings.Contains(name, " ") {
1470 names = append(names, name)
1471 }
1472 }
1473 }
1474 }
1475 sort.Strings(names)
1476 return names
1477 }
1478