From 55517108c0f81a95350db46cba2a3182d27bdb05 Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 6 Dec 2025 09:00:11 +0100 Subject: [PATCH 01/11] Handle parse-only agent blocks in client migrator --- cmd/internal/migrations/v3/client_usage.go | 9 ++- .../migrations/v3/client_usage_test.go | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index 649d8e9..b854e25 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -327,7 +327,14 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue default: - out = append(out, line) + respLine := fmt.Sprintf("%s_, err := client.%s(%s%s)", indent, methodName, uriExpr, configLine) + out = append(out, respLine) + out = append(out, parseIndent+"if err != nil {") + out = append(out, parseBody[:len(parseBody)-1]...) + out = append(out, parseIndent+"}") + + i = parseEnd + changed = true continue } } diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index 6621a5d..ab90ac2 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -929,6 +929,67 @@ func handler(code string) { assert.Equal(t, expected, content) } +func Test_MigrateClientUsage_AcquireAgentOnlyParse(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientparseonly") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" +) + +var ( + ClientID = "id" + ClientSecret = "secret" +) + +func handler(code string) { + a := fiber.AcquireAgent() + req := a.Request() + req.Header.SetMethod(fiber.MethodPost) + req.Header.Set("accept", "application/json") + req.SetRequestURI(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + if err := a.Parse(); err != nil { + log.Errorf("could not create HTTP request: %v", err) + } +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/client" + "github.com/gofiber/fiber/v3/log" +) + +var ( + ClientID = "id" + ClientSecret = "secret" +) + +func handler(code string) { + _, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + if err != nil { + log.Errorf("could not create HTTP request: %v", err) + } +}`) + + assert.Equal(t, expected, content) +} + func Test_MigrateClientUsage_RemovesSingleLineImport(t *testing.T) { t.Parallel() From eaf8f1dabdf4e244c063327f9a5a3b776f61142b Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 6 Dec 2025 11:09:05 +0100 Subject: [PATCH 02/11] Improve client migrator err assignment handling --- cmd/internal/migrations/v3/client_usage.go | 108 ++++++++++++++++-- .../migrations/v3/client_usage_test.go | 61 ++++++++++ 2 files changed, 160 insertions(+), 9 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index b854e25..a479d7a 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -22,6 +22,8 @@ var ( clientErrMapPattern = regexp.MustCompile(`"errs"\s*:\s*errs`) clientErrVarPattern = regexp.MustCompile(`\berrs\b`) clientErrsDeclPattern = regexp.MustCompile(`\berrs\s+\[]error\b`) + errShortDeclPattern = regexp.MustCompile(`(^|[\s\(\),])err\s*:=`) + errVarDeclPattern = regexp.MustCompile(`^var\s+err\b`) // Non-alias-dependent patterns requestFromAgent = regexp.MustCompile(`^([ \t]*)(\w+)\s*:=\s*(\w+)\.Request\(\)\s*$`) @@ -237,6 +239,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { switch { case len(structMatch) > 0 && structMatch[5] == agentVar: + errAssign := errAssignmentOperator(out, lines, i, "resp") statusVar := strings.TrimSpace(structMatch[2]) bodyVar := strings.TrimSpace(structMatch[3]) structTarget := strings.TrimSpace(structMatch[6]) @@ -257,7 +260,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { continue } - respLine := fmt.Sprintf("%sresp, err := client.%s(%s%s)", indent, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, err %s client.%s(%s%s)", indent, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) out = append(out, parseIndent+"if err != nil {") @@ -287,10 +290,11 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue case len(bytesMatch) > 0 && bytesMatch[5] == agentVar: + errAssign := errAssignmentOperator(out, lines, i, "resp") statusVar := strings.TrimSpace(bytesMatch[2]) bodyVar := strings.TrimSpace(bytesMatch[3]) - respLine := fmt.Sprintf("%sresp, err := client.%s(%s%s)", indent, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, err %s client.%s(%s%s)", indent, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) out = append(out, parseIndent+"if err != nil {") out = append(out, parseBody[:len(parseBody)-1]...) @@ -307,10 +311,11 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue case len(stringMatch) > 0 && stringMatch[5] == agentVar: + errAssign := errAssignmentOperator(out, lines, i, "resp") statusVar := strings.TrimSpace(stringMatch[2]) bodyVar := strings.TrimSpace(stringMatch[3]) - respLine := fmt.Sprintf("%sresp, err := client.%s(%s%s)", indent, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, err %s client.%s(%s%s)", indent, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) out = append(out, parseIndent+"if err != nil {") out = append(out, parseBody[:len(parseBody)-1]...) @@ -327,7 +332,8 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue default: - respLine := fmt.Sprintf("%s_, err := client.%s(%s%s)", indent, methodName, uriExpr, configLine) + errAssign := errAssignmentOperator(out, lines, i) + respLine := fmt.Sprintf("%s_, err %s client.%s(%s%s)", indent, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) out = append(out, parseIndent+"if err != nil {") out = append(out, parseBody[:len(parseBody)-1]...) @@ -347,6 +353,75 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { return strings.Join(out, "\n"), false } +func errAssignmentOperator(existing, original []string, idx int, newVars ...string) string { + errInScope := errDeclared(existing) || errDeclared(original[:idx]) + hasNewVar := false + + for _, name := range newVars { + if name == "" || name == "_" { + continue + } + + if identifierDeclared(existing, original, idx, name) { + continue + } + + hasNewVar = true + break + } + + if errInScope && !hasNewVar { + return "=" + } + + return ":=" +} + +func errDeclared(lines []string) bool { + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "//") { + continue + } + + if errShortDeclPattern.MatchString(trimmed) || errVarDeclPattern.MatchString(trimmed) { + return true + } + } + + return false +} + +func identifierDeclared(existing, original []string, idx int, name string) bool { + if identifierDeclaredInLines(existing, name) { + return true + } + + return identifierDeclaredInLines(original[:idx], name) +} + +func identifierDeclaredInLines(lines []string, name string) bool { + if len(lines) == 0 { + return false + } + + shortPattern := regexp.MustCompile(fmt.Sprintf(`(^|[\s\(\),])%s\s*:=`, regexp.QuoteMeta(name))) + varPattern := regexp.MustCompile(fmt.Sprintf(`^var\s+%s\b`, regexp.QuoteMeta(name))) + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "//") { + continue + } + + if shortPattern.MatchString(trimmed) || varPattern.MatchString(trimmed) { + return true + } + } + + return false +} + func methodFromExpr(expr string) string { lower := strings.ToLower(expr) switch { @@ -532,19 +607,34 @@ func rewriteSimpleAgentBlocksWithAlias(content, alias string) (string, bool) { continue } if m := agentBytesCallPattern.FindStringSubmatch(l); len(m) > 0 && m[5] == varName { - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "bytes", "", m[1]) + newVar := "" + if m[4] == ":=" { + newVar = varName + } + errAssign := errAssignmentOperator(out, lines, i, newVar) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "bytes", "", m[1], errAssign) callFound = true callIndex = j break } if m := agentStringCallPattern.FindStringSubmatch(l); len(m) > 0 && m[5] == varName { - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "string", "", m[1]) + newVar := "" + if m[4] == ":=" { + newVar = varName + } + errAssign := errAssignmentOperator(out, lines, i, newVar) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "string", "", m[1], errAssign) callFound = true callIndex = j break } if m := agentStructCallPattern.FindStringSubmatch(l); len(m) > 0 && m[5] == varName { - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "struct", strings.TrimSpace(m[6]), m[1]) + newVar := "" + if m[4] == ":=" { + newVar = varName + } + errAssign := errAssignmentOperator(out, lines, i, newVar) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "struct", strings.TrimSpace(m[6]), m[1], errAssign) callFound = true callIndex = j break @@ -568,11 +658,11 @@ func rewriteSimpleAgentBlocksWithAlias(content, alias string) (string, bool) { return strings.Join(out, "\n"), changed } -func buildSimpleAgentReplacement(indent, urlExpr, method string, cfg simpleAgentConfig, statusVar, bodyVar, assignOp, varName, callType, structTarget, callIndent string) []string { +func buildSimpleAgentReplacement(indent, urlExpr, method string, cfg simpleAgentConfig, statusVar, bodyVar, assignOp, varName, callType, structTarget, callIndent, errAssign string) []string { config := buildSimpleConfig(cfg) var lines []string - respLine := fmt.Sprintf("%s%s, err := client.%s(%s%s)", indent, varName, method, urlExpr, config) + respLine := fmt.Sprintf("%s%s, err %s client.%s(%s%s)", indent, varName, errAssign, method, urlExpr, config) lines = append(lines, respLine) statusName := strings.TrimSpace(statusVar) diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index ab90ac2..f1a7155 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -990,6 +990,67 @@ func handler(code string) { assert.Equal(t, expected, content) } +func Test_MigrateClientUsage_AcquireAgentOnlyParseWithExistingErr(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientparsewitherr") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + +var ( + ClientID = "id" + ClientSecret = "secret" +) + +func handler(code string) { + var err error + a := fiber.AcquireAgent() + req := a.Request() + req.Header.SetMethod(fiber.MethodPost) + req.Header.Set("accept", "application/json") + req.SetRequestURI(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + if err := a.Parse(); err != nil { + panic(err) + } +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/client" +) + +var ( + ClientID = "id" + ClientSecret = "secret" +) + +func handler(code string) { + var err error + _, err = client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + if err != nil { + panic(err) + } +}`) + + assert.Equal(t, expected, content) +} + func Test_MigrateClientUsage_RemovesSingleLineImport(t *testing.T) { t.Parallel() From a30e352d4192cac68c879e302c2decae24681441 Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 6 Dec 2025 12:19:53 +0100 Subject: [PATCH 03/11] Handle client migrator err name conflicts --- cmd/internal/migrations/v3/client_usage.go | 124 ++++++++++++------ .../migrations/v3/client_usage_test.go | 65 +++++++++ 2 files changed, 150 insertions(+), 39 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index a479d7a..92a7c57 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -22,8 +22,6 @@ var ( clientErrMapPattern = regexp.MustCompile(`"errs"\s*:\s*errs`) clientErrVarPattern = regexp.MustCompile(`\berrs\b`) clientErrsDeclPattern = regexp.MustCompile(`\berrs\s+\[]error\b`) - errShortDeclPattern = regexp.MustCompile(`(^|[\s\(\),])err\s*:=`) - errVarDeclPattern = regexp.MustCompile(`^var\s+err\b`) // Non-alias-dependent patterns requestFromAgent = regexp.MustCompile(`^([ \t]*)(\w+)\s*:=\s*(\w+)\.Request\(\)\s*$`) @@ -68,7 +66,10 @@ func buildAliasPatterns(alias string) map[string]*regexp.Regexp { } } -const callTypeString = "string" +const ( + callTypeString = "string" + defaultErrName = "err" +) func MigrateClientUsage(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { changed, err := internal.ChangeFileContent(cwd, func(content string) string { @@ -236,10 +237,11 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { stringMatch := stringAssignPattern.FindStringSubmatch(lines[structStart]) methodName := methodFromExpr(methodExpr) configLine := buildConfig(headers) + errName := chooseErrName(out, lines, i) switch { case len(structMatch) > 0 && structMatch[5] == agentVar: - errAssign := errAssignmentOperator(out, lines, i, "resp") + errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(structMatch[2]) bodyVar := strings.TrimSpace(structMatch[3]) structTarget := strings.TrimSpace(structMatch[6]) @@ -260,11 +262,11 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { continue } - respLine := fmt.Sprintf("%sresp, err %s client.%s(%s%s)", indent, errAssign, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) - out = append(out, parseIndent+"if err != nil {") - out = append(out, parseBody[:len(parseBody)-1]...) + out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) + out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName)...) out = append(out, parseIndent+"}") // Declare variables and assign only if err == nil to avoid nil pointer dereference if statusVar != "" { @@ -273,36 +275,36 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { if bodyVar != "" { out = append(out, fmt.Sprintf("%svar %s []byte", indent, bodyVar)) } - out = append(out, indent+"if err == nil {") + out = append(out, fmt.Sprintf("%sif %s == nil {", indent, errName)) if statusVar != "" { out = append(out, fmt.Sprintf("%s\t%s = resp.StatusCode()", indent, statusVar)) } if bodyVar != "" { out = append(out, fmt.Sprintf("%s\t%s = resp.Body()", indent, bodyVar)) } - out = append(out, fmt.Sprintf("%s\terr = resp.JSON(%s)", indent, structTarget)) + out = append(out, fmt.Sprintf("%s\t%s = resp.JSON(%s)", indent, errName, structTarget)) out = append(out, indent+"}") - out = append(out, structMatch[1]+"if err != nil {") - out = append(out, structBody[:len(structBody)-1]...) + out = append(out, fmt.Sprintf("%sif %s != nil {", structMatch[1], errName)) + out = append(out, replaceErrIdentifier(structBody[:len(structBody)-1], errName)...) out = append(out, structMatch[1]+"}") i = structStart changed = true continue case len(bytesMatch) > 0 && bytesMatch[5] == agentVar: - errAssign := errAssignmentOperator(out, lines, i, "resp") + errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(bytesMatch[2]) bodyVar := strings.TrimSpace(bytesMatch[3]) - respLine := fmt.Sprintf("%sresp, err %s client.%s(%s%s)", indent, errAssign, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) - out = append(out, parseIndent+"if err != nil {") - out = append(out, parseBody[:len(parseBody)-1]...) + out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) + out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName)...) out = append(out, parseIndent+"}") // Declare variables and assign only if err == nil to avoid nil pointer dereference out = append(out, fmt.Sprintf("%svar %s int", indent, statusVar)) out = append(out, fmt.Sprintf("%svar %s []byte", indent, bodyVar)) - out = append(out, indent+"if err == nil {") + out = append(out, fmt.Sprintf("%sif %s == nil {", indent, errName)) out = append(out, fmt.Sprintf("%s\t%s = resp.StatusCode()", indent, statusVar)) out = append(out, fmt.Sprintf("%s\t%s = resp.Body()", indent, bodyVar)) out = append(out, indent+"}") @@ -311,19 +313,19 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue case len(stringMatch) > 0 && stringMatch[5] == agentVar: - errAssign := errAssignmentOperator(out, lines, i, "resp") + errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(stringMatch[2]) bodyVar := strings.TrimSpace(stringMatch[3]) - respLine := fmt.Sprintf("%sresp, err %s client.%s(%s%s)", indent, errAssign, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) - out = append(out, parseIndent+"if err != nil {") - out = append(out, parseBody[:len(parseBody)-1]...) + out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) + out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName)...) out = append(out, parseIndent+"}") // Declare variables and assign only if err == nil to avoid nil pointer dereference out = append(out, fmt.Sprintf("%svar %s int", indent, statusVar)) out = append(out, fmt.Sprintf("%svar %s string", indent, bodyVar)) - out = append(out, indent+"if err == nil {") + out = append(out, fmt.Sprintf("%sif %s == nil {", indent, errName)) out = append(out, fmt.Sprintf("%s\t%s = resp.StatusCode()", indent, statusVar)) out = append(out, fmt.Sprintf("%s\t%s = resp.String()", indent, bodyVar)) out = append(out, indent+"}") @@ -332,11 +334,11 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue default: - errAssign := errAssignmentOperator(out, lines, i) - respLine := fmt.Sprintf("%s_, err %s client.%s(%s%s)", indent, errAssign, methodName, uriExpr, configLine) + errAssign := errAssignmentOperator(errName, out, lines, i) + respLine := fmt.Sprintf("%s_, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) - out = append(out, parseIndent+"if err != nil {") - out = append(out, parseBody[:len(parseBody)-1]...) + out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) + out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName)...) out = append(out, parseIndent+"}") i = parseEnd @@ -353,8 +355,8 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { return strings.Join(out, "\n"), false } -func errAssignmentOperator(existing, original []string, idx int, newVars ...string) string { - errInScope := errDeclared(existing) || errDeclared(original[:idx]) +func errAssignmentOperator(errName string, existing, original []string, idx int, newVars ...string) string { + errInScope := identifierDeclared(existing, original, idx, errName) hasNewVar := false for _, name := range newVars { @@ -377,14 +379,42 @@ func errAssignmentOperator(existing, original []string, idx int, newVars ...stri return ":=" } -func errDeclared(lines []string) bool { +func chooseErrName(existing, original []string, idx int) string { + if identifierDeclared(existing, original, idx, defaultErrName) { + return defaultErrName + } + + if !futureErrConflictExists(original[idx:]) { + return defaultErrName + } + + candidates := []string{"clientErr", "agentErr", "parseErr"} + for i := 0; ; i++ { + if i >= len(candidates) { + candidates = append(candidates, fmt.Sprintf("err%d", i-len(candidates)+1)) + } + + name := candidates[i] + if identifierDeclared(existing, original, idx, name) || identifierDeclaredInLines(original[idx:], name) { + continue + } + + return name + } +} + +func futureErrConflictExists(lines []string) bool { for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "//") { continue } - if errShortDeclPattern.MatchString(trimmed) || errVarDeclPattern.MatchString(trimmed) { + if strings.HasPrefix(trimmed, "if ") || strings.HasPrefix(trimmed, "for ") || strings.HasPrefix(trimmed, "switch ") { + continue + } + + if strings.HasPrefix(trimmed, "var err") || strings.HasPrefix(trimmed, "err :=") { return true } } @@ -392,6 +422,21 @@ func errDeclared(lines []string) bool { return false } +func replaceErrIdentifier(lines []string, errName string) []string { + if errName == defaultErrName { + return lines + } + + replaced := make([]string, len(lines)) + pattern := regexp.MustCompile(`\berr\b`) + + for i, line := range lines { + replaced[i] = pattern.ReplaceAllString(line, errName) + } + + return replaced +} + func identifierDeclared(existing, original []string, idx int, name string) bool { if identifierDeclaredInLines(existing, name) { return true @@ -526,6 +571,7 @@ func rewriteSimpleAgentBlocksWithAlias(content, alias string) (string, bool) { varName := match[2] method := match[3] urlExpr := strings.TrimSpace(match[4]) + errName := chooseErrName(out, lines, i) cfg := simpleAgentConfig{headers: map[string]headerValue{}, params: map[string]string{}} callFound := false @@ -611,8 +657,8 @@ func rewriteSimpleAgentBlocksWithAlias(content, alias string) (string, bool) { if m[4] == ":=" { newVar = varName } - errAssign := errAssignmentOperator(out, lines, i, newVar) - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "bytes", "", m[1], errAssign) + errAssign := errAssignmentOperator(errName, out, lines, i, newVar) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "bytes", "", m[1], errAssign, errName) callFound = true callIndex = j break @@ -622,8 +668,8 @@ func rewriteSimpleAgentBlocksWithAlias(content, alias string) (string, bool) { if m[4] == ":=" { newVar = varName } - errAssign := errAssignmentOperator(out, lines, i, newVar) - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "string", "", m[1], errAssign) + errAssign := errAssignmentOperator(errName, out, lines, i, newVar) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "string", "", m[1], errAssign, errName) callFound = true callIndex = j break @@ -633,8 +679,8 @@ func rewriteSimpleAgentBlocksWithAlias(content, alias string) (string, bool) { if m[4] == ":=" { newVar = varName } - errAssign := errAssignmentOperator(out, lines, i, newVar) - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "struct", strings.TrimSpace(m[6]), m[1], errAssign) + errAssign := errAssignmentOperator(errName, out, lines, i, newVar) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "struct", strings.TrimSpace(m[6]), m[1], errAssign, errName) callFound = true callIndex = j break @@ -658,11 +704,11 @@ func rewriteSimpleAgentBlocksWithAlias(content, alias string) (string, bool) { return strings.Join(out, "\n"), changed } -func buildSimpleAgentReplacement(indent, urlExpr, method string, cfg simpleAgentConfig, statusVar, bodyVar, assignOp, varName, callType, structTarget, callIndent, errAssign string) []string { +func buildSimpleAgentReplacement(indent, urlExpr, method string, cfg simpleAgentConfig, statusVar, bodyVar, assignOp, varName, callType, structTarget, callIndent, errAssign, errName string) []string { config := buildSimpleConfig(cfg) var lines []string - respLine := fmt.Sprintf("%s%s, err %s client.%s(%s%s)", indent, varName, errAssign, method, urlExpr, config) + respLine := fmt.Sprintf("%s%s, %s %s client.%s(%s%s)", indent, varName, errName, errAssign, method, urlExpr, config) lines = append(lines, respLine) statusName := strings.TrimSpace(statusVar) @@ -681,7 +727,7 @@ func buildSimpleAgentReplacement(indent, urlExpr, method string, cfg simpleAgent } } lines = append(lines, statusInit, bodyInit) - lines = append(lines, callIndent+"if err == nil {") + lines = append(lines, fmt.Sprintf("%sif %s == nil {", callIndent, errName)) status := fmt.Sprintf("%s\t%s = %s.StatusCode()", callIndent, strings.TrimSpace(statusVar), varName) bodyCall := "Body()" if callType == callTypeString { @@ -690,7 +736,7 @@ func buildSimpleAgentReplacement(indent, urlExpr, method string, cfg simpleAgent body := fmt.Sprintf("%s\t%s = %s.%s", callIndent, strings.TrimSpace(bodyVar), varName, bodyCall) lines = append(lines, status, body) if callType == "struct" { - lines = append(lines, fmt.Sprintf("%s\terr = %s.JSON(%s)", callIndent, varName, structTarget)) + lines = append(lines, fmt.Sprintf("%s\t%s = %s.JSON(%s)", callIndent, errName, varName, structTarget)) } lines = append(lines, callIndent+"}") diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index f1a7155..9b2de8e 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -1051,6 +1051,71 @@ func handler(code string) { assert.Equal(t, expected, content) } +func Test_MigrateClientUsage_AcquireAgentOnlyParseAvoidsErrCollisions(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientparsecollision") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" +) + +var ( + ClientID = "id" + ClientSecret = "secret" +) + +func handler(code string) { + a := fiber.AcquireAgent() + req := a.Request() + req.Header.SetMethod(fiber.MethodPost) + req.Header.Set("accept", "application/json") + req.SetRequestURI(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + if err := a.Parse(); err != nil { + log.Errorf("could not create HTTP request: %v", err) + } + var err error + _ = err +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/client" + "github.com/gofiber/fiber/v3/log" +) + +var ( + ClientID = "id" + ClientSecret = "secret" +) + +func handler(code string) { + _, clientErr := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + if clientErr != nil { + log.Errorf("could not create HTTP request: %v", clientErr) + } + var err error + _ = err +}`) + + assert.Equal(t, expected, content) +} + func Test_MigrateClientUsage_RemovesSingleLineImport(t *testing.T) { t.Parallel() From 8fdabd1b6d9ac50f4025c5b13723d37fd2b1ccbe Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 6 Dec 2025 15:33:17 +0100 Subject: [PATCH 04/11] Handle additional client migrator cases --- cmd/internal/migrations/v3/client_usage.go | 56 ++++--- .../migrations/v3/client_usage_test.go | 138 ++++++++++++++++++ 2 files changed, 175 insertions(+), 19 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index 92a7c57..ec5db94 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -29,6 +29,7 @@ var ( headerSetPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Header\.Set\(([^,]+),\s*([^)]*)\)\s*$`) requestURIPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.SetRequestURI\((.*)\)\s*$`) parseCallPattern = regexp.MustCompile(`^([ \t]*)if\s+err\s*:=\s*(\w+)\.Parse\(\);\s*err\s*!=\s*nil\s*{\s*$`) + requestBodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.SetBody(?:String)?\((.*)\)\s*$`) structAssignPattern = regexp.MustCompile(`^([ \t]*)if\s+([^,]+?)\s*,\s*([^,]+?)\s*,\s*errs\s*([:=]?)=\s*(\w+)\.Struct\((.*)\);\s*len\(errs\)\s*>\s*0\s*{\s*$`) bytesAssignPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Bytes\(\)\s*$`) stringAssignPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.String\(\)\s*$`) @@ -36,18 +37,19 @@ var ( var ( // Agent method patterns (non-alias-dependent) - headerSetSimplePattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Set\(([^,]+),\s*([^)]*)\)\s*$`) - queryStringPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.QueryString\(([^)]*)\)\s*$`) - timeoutPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Timeout\(([^)]*)\)\s*$`) - jsonBodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.JSON\(([^)]*)\)\s*$`) - bodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.(Body|BodyString)\(([^)]*)\)\s*$`) - basicAuthPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.BasicAuth\(([^,]+),\s*([^)]*)\)\s*$`) - tlsConfigPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.TLSConfig\(([^)]*)\)\s*$`) - debugPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Debug\(([^)]*)\)\s*$`) - reusePattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Reuse\(\)\s*$`) - agentBytesCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Bytes\(\)\s*$`) - agentStringCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.String\(\)\s*$`) - agentStructCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Struct\((.+)\)\s*$`) + headerSetSimplePattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Set\(([^,]+),\s*([^)]*)\)\s*$`) + queryStringPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.QueryString\(([^)]*)\)\s*$`) + timeoutPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Timeout\(([^)]*)\)\s*$`) + jsonBodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.JSON\(([^)]*)\)\s*$`) + bodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.(Body|BodyString)\(([^)]*)\)\s*$`) + basicAuthPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.BasicAuth\(([^,]+),\s*([^)]*)\)\s*$`) + tlsConfigPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.TLSConfig\(([^)]*)\)\s*$`) + debugPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Debug\(([^)]*)\)\s*$`) + reusePattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Reuse\(\)\s*$`) + agentBytesCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Bytes\(\)\s*$`) + agentStringCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.String\(\)\s*$`) + agentStructCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Struct\((.+)\)\s*$`) + futureErrConflictPattern = regexp.MustCompile(`^(?:var\s+err\b|err\s*:=)`) // detects future err declarations that would clash with injected err ) // buildAliasPatterns creates regex patterns for fiber package calls with given alias @@ -171,6 +173,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { methodExpr := "" uriExpr := "" headers := make(map[string]string) + bodyExpr := "" j := reqLine + 1 for ; j < len(lines); j++ { @@ -190,6 +193,10 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { uriExpr = strings.TrimSpace(m[3]) continue } + if m := requestBodyPattern.FindStringSubmatch(l); len(m) > 0 && m[2] == reqVar { + bodyExpr = strings.TrimSpace(m[3]) + continue + } if parseCallPattern.MatchString(l) { break } @@ -236,7 +243,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { bytesMatch := bytesAssignPattern.FindStringSubmatch(lines[structStart]) stringMatch := stringAssignPattern.FindStringSubmatch(lines[structStart]) methodName := methodFromExpr(methodExpr) - configLine := buildConfig(headers) + configLine := buildConfig(headers, bodyExpr) errName := chooseErrName(out, lines, i) switch { @@ -414,7 +421,7 @@ func futureErrConflictExists(lines []string) bool { continue } - if strings.HasPrefix(trimmed, "var err") || strings.HasPrefix(trimmed, "err :=") { + if futureErrConflictPattern.MatchString(trimmed) { return true } } @@ -485,20 +492,31 @@ func methodFromExpr(expr string) string { } } -func buildConfig(headers map[string]string) string { - if len(headers) == 0 { +func buildConfig(headers map[string]string, body string) string { + if len(headers) == 0 && body == "" { return "" } + var keys []string for k := range headers { keys = append(keys, k) } sort.Strings(keys) + var parts []string - for _, k := range keys { - parts = append(parts, fmt.Sprintf("%s: %s", k, headers[k])) + if len(keys) > 0 { + var headerParts []string + for _, k := range keys { + headerParts = append(headerParts, fmt.Sprintf("%s: %s", k, headers[k])) + } + parts = append(parts, fmt.Sprintf("Header: map[string]string{%s}", strings.Join(headerParts, ", "))) + } + + if body != "" { + parts = append(parts, "Body: "+body) } - return fmt.Sprintf(", client.Config{Header: map[string]string{%s}}", strings.Join(parts, ", ")) + + return fmt.Sprintf(", client.Config{%s}", strings.Join(parts, ", ")) } func rewriteClientExamplesWithAlias(content, alias string) (string, bool) { diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index 9b2de8e..492685f 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -535,6 +535,79 @@ func main() { assert.Equal(t, expected, content) } +func Test_MigrateClientUsage_RewritesParseBytesFlowWithBody(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientparsebody") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + +func main() { + a := fiber.AcquireAgent() + defer fiber.ReleaseAgent(a) + + req := a.Request() + req.Header.SetMethod(fiber.MethodPost) + req.Header.Set("Content-Type", "application/json") + req.SetBodyString("{\"demo\":true}") + req.SetRequestURI("https://httpbin.org/post") + + if err := a.Parse(); err != nil { + panic(err) + } + + status, body, errs := a.Bytes() + if len(errs) > 0 { + panic(errs) + } + + fmt.Println("Status:", status) + fmt.Println("Body:", string(body)) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/client" +) + +func main() { + resp, err := client.Post("https://httpbin.org/post", client.Config{Header: map[string]string{"Content-Type": "application/json"}, Body: "{\"demo\":true}"}) + if err != nil { + panic(err) + } + var status int + var body []byte + if err == nil { + status = resp.StatusCode() + body = resp.Body() + } + if err != nil { + panic(err) + } + + fmt.Println("Status:", status) + fmt.Println("Body:", string(body)) +}`) + + assert.Equal(t, expected, content) +} + func Test_MigrateClientUsage_RewritesBasicAuth(t *testing.T) { t.Parallel() @@ -1116,6 +1189,71 @@ func handler(code string) { assert.Equal(t, expected, content) } +func Test_MigrateClientUsage_AcquireAgentOnlyParseIgnoresErrLikeNames(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientparsealiases") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + +var ( + ClientID = "id" + ClientSecret = "secret" +) + +func handler(code string) { + a := fiber.AcquireAgent() + req := a.Request() + req.Header.SetMethod(fiber.MethodPost) + req.Header.Set("accept", "application/json") + req.SetRequestURI(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + if err := a.Parse(); err != nil { + fmt.Printf("could not create HTTP request: %v", err) + } + + var errMsg string + _ = errMsg +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/client" +) + +var ( + ClientID = "id" + ClientSecret = "secret" +) + +func handler(code string) { + _, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + if err != nil { + fmt.Printf("could not create HTTP request: %v", err) + } + + var errMsg string + _ = errMsg +}`) + + assert.Equal(t, expected, content) +} + func Test_MigrateClientUsage_RemovesSingleLineImport(t *testing.T) { t.Parallel() From 3956cfb39310a97e6b22b255c73a6535c7653fe5 Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 6 Dec 2025 16:08:31 +0100 Subject: [PATCH 05/11] Improve client error conflict detection --- cmd/internal/migrations/v3/client_usage.go | 72 +++++++++++++++---- .../migrations/v3/client_usage_test.go | 56 +++++++++++++++ 2 files changed, 113 insertions(+), 15 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index ec5db94..9b7b30d 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -37,19 +37,20 @@ var ( var ( // Agent method patterns (non-alias-dependent) - headerSetSimplePattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Set\(([^,]+),\s*([^)]*)\)\s*$`) - queryStringPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.QueryString\(([^)]*)\)\s*$`) - timeoutPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Timeout\(([^)]*)\)\s*$`) - jsonBodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.JSON\(([^)]*)\)\s*$`) - bodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.(Body|BodyString)\(([^)]*)\)\s*$`) - basicAuthPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.BasicAuth\(([^,]+),\s*([^)]*)\)\s*$`) - tlsConfigPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.TLSConfig\(([^)]*)\)\s*$`) - debugPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Debug\(([^)]*)\)\s*$`) - reusePattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Reuse\(\)\s*$`) - agentBytesCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Bytes\(\)\s*$`) - agentStringCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.String\(\)\s*$`) - agentStructCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Struct\((.+)\)\s*$`) - futureErrConflictPattern = regexp.MustCompile(`^(?:var\s+err\b|err\s*:=)`) // detects future err declarations that would clash with injected err + headerSetSimplePattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Set\(([^,]+),\s*([^)]*)\)\s*$`) + queryStringPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.QueryString\(([^)]*)\)\s*$`) + timeoutPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Timeout\(([^)]*)\)\s*$`) + jsonBodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.JSON\(([^)]*)\)\s*$`) + bodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.(Body|BodyString)\(([^)]*)\)\s*$`) + basicAuthPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.BasicAuth\(([^,]+),\s*([^)]*)\)\s*$`) + tlsConfigPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.TLSConfig\(([^)]*)\)\s*$`) + debugPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Debug\(([^)]*)\)\s*$`) + reusePattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Reuse\(\)\s*$`) + agentBytesCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Bytes\(\)\s*$`) + agentStringCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.String\(\)\s*$`) + agentStructCallPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Struct\((.+)\)\s*$`) + futureErrConflictPattern = regexp.MustCompile(`\b(?:var\s+err\b|err\s*:=)`) // detects future err declarations that would clash with injected err + futureErrsConflictPattern = regexp.MustCompile(`\b(?:var\s+errs\b|errs\s*:=)`) // errs will be renamed to err during rewrite ) // buildAliasPatterns creates regex patterns for fiber package calls with given alias @@ -417,11 +418,23 @@ func futureErrConflictExists(lines []string) bool { continue } - if strings.HasPrefix(trimmed, "if ") || strings.HasPrefix(trimmed, "for ") || strings.HasPrefix(trimmed, "switch ") { + if declaredInVarBlockLine(defaultErrName, trimmed) || declaredInVarBlockLine("errs", trimmed) { + return true + } + + if parseCallPattern.MatchString(trimmed) { + continue + } + + if strings.HasPrefix(trimmed, "if ") || strings.HasPrefix(trimmed, "for ") || strings.HasPrefix(trimmed, "switch ") || strings.HasPrefix(trimmed, "else if ") { continue } - if futureErrConflictPattern.MatchString(trimmed) { + if agentBytesCallPattern.MatchString(trimmed) || agentStringCallPattern.MatchString(trimmed) || agentStructCallPattern.MatchString(trimmed) { + continue + } + + if futureErrConflictPattern.MatchString(trimmed) || futureErrsConflictPattern.MatchString(trimmed) { return true } } @@ -469,11 +482,40 @@ func identifierDeclaredInLines(lines []string, name string) bool { if shortPattern.MatchString(trimmed) || varPattern.MatchString(trimmed) { return true } + + if name == defaultErrName && futureErrsConflictPattern.MatchString(trimmed) { + return true + } + + if declaredInVarBlockLine(name, trimmed) { + return true + } + + if name == defaultErrName && declaredInVarBlockLine("errs", trimmed) { + return true + } } return false } +func declaredInVarBlockLine(name, trimmed string) bool { + if !strings.HasPrefix(trimmed, name) { + return false + } + + remainder := strings.TrimSpace(strings.TrimPrefix(trimmed, name)) + if remainder == "" { + return false + } + + if strings.Contains(remainder, ":=") || strings.Contains(remainder, "=") { + return false + } + + return true +} + func methodFromExpr(expr string) string { lower := strings.ToLower(expr) switch { diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index 492685f..876a5cf 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -1254,6 +1254,62 @@ func handler(code string) { assert.Equal(t, expected, content) } +func Test_MigrateClientUsage_AcquireAgentOnlyParseRespectsErrsSlice(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientparseerrs") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + +func handler(url string) { + var errs []error + + a := fiber.AcquireAgent() + req := a.Request() + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI(url) + if err := a.Parse(); err != nil { + errs = append(errs, err) + } + + fmt.Println(errs) +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/client" +) + +func handler(url string) { + var err error + + _, err = client.Get(url) + if err != nil { + err = append(err, err) + } + + fmt.Println(err) +}`) + + assert.Equal(t, expected, content) +} + func Test_MigrateClientUsage_RemovesSingleLineImport(t *testing.T) { t.Parallel() From be2484fb8b0e751bb191f4b3efaa98d8180246e0 Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 6 Dec 2025 16:31:52 +0100 Subject: [PATCH 06/11] Improve client err handling in migrator --- cmd/internal/migrations/v3/client_usage.go | 36 +++++++------------ .../migrations/v3/client_usage_test.go | 8 ++--- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index 9b7b30d..4b9c5f2 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -483,10 +483,6 @@ func identifierDeclaredInLines(lines []string, name string) bool { return true } - if name == defaultErrName && futureErrsConflictPattern.MatchString(trimmed) { - return true - } - if declaredInVarBlockLine(name, trimmed) { return true } @@ -713,34 +709,19 @@ func rewriteSimpleAgentBlocksWithAlias(content, alias string) (string, bool) { continue } if m := agentBytesCallPattern.FindStringSubmatch(l); len(m) > 0 && m[5] == varName { - newVar := "" - if m[4] == ":=" { - newVar = varName - } - errAssign := errAssignmentOperator(errName, out, lines, i, newVar) - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "bytes", "", m[1], errAssign, errName) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "bytes", "", m[1], ":=", errName) callFound = true callIndex = j break } if m := agentStringCallPattern.FindStringSubmatch(l); len(m) > 0 && m[5] == varName { - newVar := "" - if m[4] == ":=" { - newVar = varName - } - errAssign := errAssignmentOperator(errName, out, lines, i, newVar) - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "string", "", m[1], errAssign, errName) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "string", "", m[1], ":=", errName) callFound = true callIndex = j break } if m := agentStructCallPattern.FindStringSubmatch(l); len(m) > 0 && m[5] == varName { - newVar := "" - if m[4] == ":=" { - newVar = varName - } - errAssign := errAssignmentOperator(errName, out, lines, i, newVar) - replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "struct", strings.TrimSpace(m[6]), m[1], errAssign, errName) + replacement = buildSimpleAgentReplacement(indent, urlExpr, method, cfg, m[2], m[3], m[4], varName, "struct", strings.TrimSpace(m[6]), m[1], ":=", errName) callFound = true callIndex = j break @@ -1011,11 +992,18 @@ func ensureClientImport(content string) string { } func rewriteClientErrorHandling(content string) string { - updated := clientErrsDeclPattern.ReplaceAllString(content, "err error") - updated = clientErrIfPattern.ReplaceAllString(updated, "if err != nil {") + // Only rewrite when we see the legacy multi-error usage patterns. This avoids + // mutating unrelated identifiers such as custom "errs" slices. + hasLegacyErrs := clientErrIfPattern.MatchString(content) || clientErrLenPattern.MatchString(content) || clientErrComparePattern.MatchString(content) || clientErrMapPattern.MatchString(content) + if !hasLegacyErrs { + return content + } + + updated := clientErrIfPattern.ReplaceAllString(content, "if err != nil {") updated = clientErrLenPattern.ReplaceAllString(updated, "err != nil") updated = clientErrComparePattern.ReplaceAllString(updated, "err != nil") updated = clientErrMapPattern.ReplaceAllString(updated, `"err": err`) + updated = clientErrsDeclPattern.ReplaceAllString(updated, "err error") updated = clientErrVarPattern.ReplaceAllString(updated, "err") return updated } diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index 876a5cf..279f0d0 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -1297,14 +1297,14 @@ import ( ) func handler(url string) { - var err error + var errs []error - _, err = client.Get(url) + _, err := client.Get(url) if err != nil { - err = append(err, err) + errs = append(errs, err) } - fmt.Println(err) + fmt.Println(errs) }`) assert.Equal(t, expected, content) From b582a77c19b9881beb9602c4be4793bef46d0c5c Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 7 Dec 2025 14:39:58 +0100 Subject: [PATCH 07/11] Improve client migrator coverage for agent flows --- cmd/internal/migrations/v3/client_usage.go | 152 +++++++++++++++--- .../v3/client_usage_internal_test.go | 30 ++-- .../migrations/v3/client_usage_test.go | 94 ++++++++++- 3 files changed, 235 insertions(+), 41 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index 4b9c5f2..041b30a 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -231,11 +231,43 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { } parseEnd := j - structStart := parseEnd + 1 - for structStart < len(lines) && strings.TrimSpace(lines[structStart]) == "" { - structStart++ + structStart := -1 + preservedLines := []string{} + preservedIdx := []int{} + for k := parseEnd + 1; k < len(lines); k++ { + if skipLines[k] { + continue + } + + trimmed := strings.TrimSpace(lines[k]) + if trimmed == "" { + continue + } + if strings.HasPrefix(trimmed, "//") { + preservedLines = append(preservedLines, lines[k]) + preservedIdx = append(preservedIdx, k) + continue + } + + if strings.HasPrefix(trimmed, "var ") { + fields := strings.Fields(trimmed) + if len(fields) >= 2 { + name := fields[1] + if name == defaultErrName || name == "errs" { + continue + } + } + + preservedLines = append(preservedLines, lines[k]) + preservedIdx = append(preservedIdx, k) + continue + } + + structStart = k + break } - if structStart >= len(lines) { + + if structStart == -1 { out = append(out, line) continue } @@ -245,14 +277,29 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { stringMatch := stringAssignPattern.FindStringSubmatch(lines[structStart]) methodName := methodFromExpr(methodExpr) configLine := buildConfig(headers, bodyExpr) + + if len(structMatch) == 0 && len(bytesMatch) == 0 && len(stringMatch) == 0 { + structStart = parseEnd + preservedLines = nil + preservedIdx = nil + } + errName := chooseErrName(out, lines, i) switch { case len(structMatch) > 0 && structMatch[5] == agentVar: + if len(preservedLines) > 0 { + out = append(out, preservedLines...) + for _, idx := range preservedIdx { + skipLines[idx] = true + } + } errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(structMatch[2]) bodyVar := strings.TrimSpace(structMatch[3]) structTarget := strings.TrimSpace(structMatch[6]) + statusDeclared := statusVar != "" && (identifierDeclaredInLines(preservedLines, statusVar) || identifierDeclared(out, lines, i, statusVar)) + bodyDeclared := bodyVar != "" && (identifierDeclaredInLines(preservedLines, bodyVar) || identifierDeclared(out, lines, i, bodyVar)) structBody := []string{} braceDepth = 0 @@ -274,13 +321,13 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) - out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName)...) + out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName, false)...) out = append(out, parseIndent+"}") // Declare variables and assign only if err == nil to avoid nil pointer dereference - if statusVar != "" { + if statusVar != "" && !statusDeclared { out = append(out, fmt.Sprintf("%svar %s int", indent, statusVar)) } - if bodyVar != "" { + if bodyVar != "" && !bodyDeclared { out = append(out, fmt.Sprintf("%svar %s []byte", indent, bodyVar)) } out = append(out, fmt.Sprintf("%sif %s == nil {", indent, errName)) @@ -293,50 +340,92 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { out = append(out, fmt.Sprintf("%s\t%s = resp.JSON(%s)", indent, errName, structTarget)) out = append(out, indent+"}") out = append(out, fmt.Sprintf("%sif %s != nil {", structMatch[1], errName)) - out = append(out, replaceErrIdentifier(structBody[:len(structBody)-1], errName)...) + out = append(out, replaceErrIdentifier(structBody[:len(structBody)-1], errName, true)...) out = append(out, structMatch[1]+"}") + if statusVar != "" && !hasBlankAssignment(lines, statusVar) { + out = append(out, fmt.Sprintf("%s_ = %s", indent, statusVar)) + } + if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) { + out = append(out, fmt.Sprintf("%s_ = %s", indent, bodyVar)) + } i = structStart changed = true continue case len(bytesMatch) > 0 && bytesMatch[5] == agentVar: + if len(preservedLines) > 0 { + out = append(out, preservedLines...) + for _, idx := range preservedIdx { + skipLines[idx] = true + } + } errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(bytesMatch[2]) bodyVar := strings.TrimSpace(bytesMatch[3]) + statusDeclared := statusVar != "" && (identifierDeclaredInLines(preservedLines, statusVar) || identifierDeclared(out, lines, i, statusVar)) + bodyDeclared := bodyVar != "" && (identifierDeclaredInLines(preservedLines, bodyVar) || identifierDeclared(out, lines, i, bodyVar)) respLine := fmt.Sprintf("%sresp, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) - out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName)...) + out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName, false)...) out = append(out, parseIndent+"}") // Declare variables and assign only if err == nil to avoid nil pointer dereference - out = append(out, fmt.Sprintf("%svar %s int", indent, statusVar)) - out = append(out, fmt.Sprintf("%svar %s []byte", indent, bodyVar)) + if statusVar != "" && !statusDeclared { + out = append(out, fmt.Sprintf("%svar %s int", indent, statusVar)) + } + if bodyVar != "" && !bodyDeclared { + out = append(out, fmt.Sprintf("%svar %s []byte", indent, bodyVar)) + } out = append(out, fmt.Sprintf("%sif %s == nil {", indent, errName)) out = append(out, fmt.Sprintf("%s\t%s = resp.StatusCode()", indent, statusVar)) out = append(out, fmt.Sprintf("%s\t%s = resp.Body()", indent, bodyVar)) out = append(out, indent+"}") + if statusVar != "" && !hasBlankAssignment(lines, statusVar) { + out = append(out, fmt.Sprintf("%s_ = %s", indent, statusVar)) + } + if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) { + out = append(out, fmt.Sprintf("%s_ = %s", indent, bodyVar)) + } i = structStart changed = true continue case len(stringMatch) > 0 && stringMatch[5] == agentVar: + if len(preservedLines) > 0 { + out = append(out, preservedLines...) + for _, idx := range preservedIdx { + skipLines[idx] = true + } + } errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(stringMatch[2]) bodyVar := strings.TrimSpace(stringMatch[3]) + statusDeclared := statusVar != "" && (identifierDeclaredInLines(preservedLines, statusVar) || identifierDeclared(out, lines, i, statusVar)) + bodyDeclared := bodyVar != "" && (identifierDeclaredInLines(preservedLines, bodyVar) || identifierDeclared(out, lines, i, bodyVar)) respLine := fmt.Sprintf("%sresp, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) - out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName)...) + out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName, false)...) out = append(out, parseIndent+"}") // Declare variables and assign only if err == nil to avoid nil pointer dereference - out = append(out, fmt.Sprintf("%svar %s int", indent, statusVar)) - out = append(out, fmt.Sprintf("%svar %s string", indent, bodyVar)) + if statusVar != "" && !statusDeclared { + out = append(out, fmt.Sprintf("%svar %s int", indent, statusVar)) + } + if bodyVar != "" && !bodyDeclared { + out = append(out, fmt.Sprintf("%svar %s string", indent, bodyVar)) + } out = append(out, fmt.Sprintf("%sif %s == nil {", indent, errName)) out = append(out, fmt.Sprintf("%s\t%s = resp.StatusCode()", indent, statusVar)) out = append(out, fmt.Sprintf("%s\t%s = resp.String()", indent, bodyVar)) out = append(out, indent+"}") + if statusVar != "" && !hasBlankAssignment(lines, statusVar) { + out = append(out, fmt.Sprintf("%s_ = %s", indent, statusVar)) + } + if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) { + out = append(out, fmt.Sprintf("%s_ = %s", indent, bodyVar)) + } i = structStart changed = true @@ -346,7 +435,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { respLine := fmt.Sprintf("%s_, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) - out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName)...) + out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName, false)...) out = append(out, parseIndent+"}") i = parseEnd @@ -442,21 +531,40 @@ func futureErrConflictExists(lines []string) bool { return false } -func replaceErrIdentifier(lines []string, errName string) []string { - if errName == defaultErrName { - return lines - } - +func replaceErrIdentifier(lines []string, errName string, replaceErrs bool) []string { replaced := make([]string, len(lines)) - pattern := regexp.MustCompile(`\berr\b`) + errPattern := regexp.MustCompile(`\berr\b`) + errsPattern := regexp.MustCompile(`\berrs\b`) for i, line := range lines { - replaced[i] = pattern.ReplaceAllString(line, errName) + updated := errPattern.ReplaceAllString(line, errName) + if replaceErrs { + replaced[i] = errsPattern.ReplaceAllString(updated, errName) + continue + } + + replaced[i] = updated } return replaced } +func hasBlankAssignment(lines []string, name string) bool { + if name == "" { + return false + } + + pattern := regexp.MustCompile(fmt.Sprintf(`\b_\s*=\s*%s\b`, regexp.QuoteMeta(name))) + + for _, line := range lines { + if pattern.MatchString(strings.TrimSpace(line)) { + return true + } + } + + return false +} + func identifierDeclared(existing, original []string, idx int, name string) bool { if identifierDeclaredInLines(existing, name) { return true diff --git a/cmd/internal/migrations/v3/client_usage_internal_test.go b/cmd/internal/migrations/v3/client_usage_internal_test.go index 340e9ae..df62861 100644 --- a/cmd/internal/migrations/v3/client_usage_internal_test.go +++ b/cmd/internal/migrations/v3/client_usage_internal_test.go @@ -65,22 +65,20 @@ func handler(ctx *fiber.Ctx, code string) error { t map[string]any ) - resp, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code), client.Config{Header: map[string]string{"accept": "application/json"}}) - if err != nil { - return err - } - var retCode int - var retBody []byte - if err == nil { - retCode = resp.StatusCode() - retBody = resp.Body() - err = resp.JSON(&t) - } - if err != nil { - return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "errs": errs, - }) - } +resp, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code), client.Config{Header: map[string]string{"accept": "application/json"}}) +if err != nil { +return err +} +if err == nil { +retCode = resp.StatusCode() +retBody = resp.Body() +err = resp.JSON(&t) +} +if err != nil { +return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ +"err": err, +}) +} _ = retCode _ = retBody diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index 279f0d0..395e576 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -314,7 +314,7 @@ func handler(ctx *fiber.Ctx, code string) error { var ( retCode int retBody []byte - err error + errs []error t map[string]any ) @@ -322,8 +322,6 @@ func handler(ctx *fiber.Ctx, code string) error { if err != nil { return err } - var retCode int - var retBody []byte if err == nil { retCode = resp.StatusCode() retBody = resp.Body() @@ -343,6 +341,90 @@ func handler(ctx *fiber.Ctx, code string) error { assert.Equal(t, expected, content) } +func Test_MigrateClientUsage_RewritesAcquireAgentStructWithVarsBetweenParseAndCall(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientagentvars") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + +func handler(ctx *fiber.Ctx, code string) error { + a := fiber.AcquireAgent() + req := a.Request() + req.Header.SetMethod(fiber.MethodPost) + req.Header.Set("accept", "application/json") + req.SetRequestURI(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code)) + if err := a.Parse(); err != nil { + return err + } + + var retCode int + var retBody []byte + var errs []error + var t map[string]any + + if retCode, retBody, errs = a.Struct(&t); len(errs) > 0 { + return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "errs": errs, + }) + } + + var err error + _ = err + return nil +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/client" +) + +func handler(ctx *fiber.Ctx, code string) error { + var retCode int + var retBody []byte + var t map[string]any + resp, clientErr := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code), client.Config{Header: map[string]string{"accept": "application/json"}}) + if clientErr != nil { + return clientErr + } + if clientErr == nil { + retCode = resp.StatusCode() + retBody = resp.Body() + clientErr = resp.JSON(&t) + } + if clientErr != nil { + return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "clientErr": clientErr, + }) + } + _ = retCode + _ = retBody + + var err error + _ = err + return nil +}`) + + assert.Equal(t, expected, content) +} + func Test_MigrateClientUsage_RewritesHeadersQueriesAndTimeout(t *testing.T) { t.Parallel() @@ -519,6 +601,8 @@ func main() { status = resp.StatusCode() body = resp.Body() } + _ = status + _ = body if err != nil { panic(err) } @@ -597,6 +681,8 @@ func main() { status = resp.StatusCode() body = resp.Body() } + _ = status + _ = body if err != nil { panic(err) } @@ -991,6 +1077,8 @@ func handler(code string) { status = resp.StatusCode() body = resp.Body() } + _ = status + _ = body if err != nil { panic(err) } From 016685dac077033014ac2b1e2925de22709fa611 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 7 Dec 2025 15:05:29 +0100 Subject: [PATCH 08/11] Update client agent rewrites to request API --- cmd/internal/migrations/v3/client_usage.go | 87 ++++++++++--------- .../v3/client_usage_internal_test.go | 33 ++++--- .../migrations/v3/client_usage_test.go | 69 ++++++++++++--- 3 files changed, 123 insertions(+), 66 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index 041b30a..275fff0 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -29,7 +29,7 @@ var ( headerSetPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.Header\.Set\(([^,]+),\s*([^)]*)\)\s*$`) requestURIPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.SetRequestURI\((.*)\)\s*$`) parseCallPattern = regexp.MustCompile(`^([ \t]*)if\s+err\s*:=\s*(\w+)\.Parse\(\);\s*err\s*!=\s*nil\s*{\s*$`) - requestBodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.SetBody(?:String)?\((.*)\)\s*$`) + requestBodyPattern = regexp.MustCompile(`^([ \t]*)(\w+)\.(SetBody(?:String)?)\((.*)\)\s*$`) structAssignPattern = regexp.MustCompile(`^([ \t]*)if\s+([^,]+?)\s*,\s*([^,]+?)\s*,\s*errs\s*([:=]?)=\s*(\w+)\.Struct\((.*)\);\s*len\(errs\)\s*>\s*0\s*{\s*$`) bytesAssignPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.Bytes\(\)\s*$`) stringAssignPattern = regexp.MustCompile(`^([ \t]*)([^,]+),\s*([^,]+),\s*errs\s*(=|:=)\s*(\w+)\.String\(\)\s*$`) @@ -175,6 +175,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { uriExpr := "" headers := make(map[string]string) bodyExpr := "" + bodyIsString := false j := reqLine + 1 for ; j < len(lines); j++ { @@ -195,7 +196,8 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { continue } if m := requestBodyPattern.FindStringSubmatch(l); len(m) > 0 && m[2] == reqVar { - bodyExpr = strings.TrimSpace(m[3]) + bodyExpr = strings.TrimSpace(m[4]) + bodyIsString = m[3] == "SetBodyString" continue } if parseCallPattern.MatchString(l) { @@ -275,8 +277,6 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { structMatch := structAssignPattern.FindStringSubmatch(lines[structStart]) bytesMatch := bytesAssignPattern.FindStringSubmatch(lines[structStart]) stringMatch := stringAssignPattern.FindStringSubmatch(lines[structStart]) - methodName := methodFromExpr(methodExpr) - configLine := buildConfig(headers, bodyExpr) if len(structMatch) == 0 && len(bytesMatch) == 0 && len(stringMatch) == 0 { structStart = parseEnd @@ -286,6 +286,34 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { errName := chooseErrName(out, lines, i) + out = append(out, fmt.Sprintf("%s%s := client.New()", indent, agentVar)) + out = append(out, fmt.Sprintf("%s%s := %s.R()", indent, reqVar, agentVar)) + if methodExpr != "" { + out = append(out, fmt.Sprintf("%s%s.SetMethod(%s)", indent, reqVar, methodLiteral(methodExpr))) + } + if uriExpr != "" { + out = append(out, fmt.Sprintf("%s%s.SetURL(%s)", indent, reqVar, uriExpr)) + } + + if len(headers) > 0 { + keys := make([]string, 0, len(headers)) + for k := range headers { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + out = append(out, fmt.Sprintf("%s%s.SetHeader(%s, %s)", indent, reqVar, k, headers[k])) + } + } + + if bodyExpr != "" { + rawBody := bodyExpr + if bodyIsString { + rawBody = fmt.Sprintf("[]byte(%s)", bodyExpr) + } + out = append(out, fmt.Sprintf("%s%s.SetRawBody(%s)", indent, reqVar, rawBody)) + } + switch { case len(structMatch) > 0 && structMatch[5] == agentVar: if len(preservedLines) > 0 { @@ -317,7 +345,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { continue } - respLine := fmt.Sprintf("%sresp, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, %s %s %s.Send()", indent, errName, errAssign, reqVar) out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) @@ -365,7 +393,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { statusDeclared := statusVar != "" && (identifierDeclaredInLines(preservedLines, statusVar) || identifierDeclared(out, lines, i, statusVar)) bodyDeclared := bodyVar != "" && (identifierDeclaredInLines(preservedLines, bodyVar) || identifierDeclared(out, lines, i, bodyVar)) - respLine := fmt.Sprintf("%sresp, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, %s %s %s.Send()", indent, errName, errAssign, reqVar) out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName, false)...) @@ -404,7 +432,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { statusDeclared := statusVar != "" && (identifierDeclaredInLines(preservedLines, statusVar) || identifierDeclared(out, lines, i, statusVar)) bodyDeclared := bodyVar != "" && (identifierDeclaredInLines(preservedLines, bodyVar) || identifierDeclared(out, lines, i, bodyVar)) - respLine := fmt.Sprintf("%sresp, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%sresp, %s %s %s.Send()", indent, errName, errAssign, reqVar) out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName, false)...) @@ -432,7 +460,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { continue default: errAssign := errAssignmentOperator(errName, out, lines, i) - respLine := fmt.Sprintf("%s_, %s %s client.%s(%s%s)", indent, errName, errAssign, methodName, uriExpr, configLine) + respLine := fmt.Sprintf("%s_, %s %s %s.Send()", indent, errName, errAssign, reqVar) out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName, false)...) @@ -620,51 +648,26 @@ func declaredInVarBlockLine(name, trimmed string) bool { return true } -func methodFromExpr(expr string) string { +func methodLiteral(expr string) string { lower := strings.ToLower(expr) switch { + case strings.Contains(lower, "methodget"): + return "\"GET\"" case strings.Contains(lower, "methodpost"): - return "Post" + return "\"POST\"" case strings.Contains(lower, "methodput"): - return "Put" + return "\"PUT\"" case strings.Contains(lower, "methodpatch"): - return "Patch" + return "\"PATCH\"" case strings.Contains(lower, "methoddelete"): - return "Delete" + return "\"DELETE\"" case strings.Contains(lower, "methodhead"): - return "Head" + return "\"HEAD\"" default: - return "Get" + return expr } } -func buildConfig(headers map[string]string, body string) string { - if len(headers) == 0 && body == "" { - return "" - } - - var keys []string - for k := range headers { - keys = append(keys, k) - } - sort.Strings(keys) - - var parts []string - if len(keys) > 0 { - var headerParts []string - for _, k := range keys { - headerParts = append(headerParts, fmt.Sprintf("%s: %s", k, headers[k])) - } - parts = append(parts, fmt.Sprintf("Header: map[string]string{%s}", strings.Join(headerParts, ", "))) - } - - if body != "" { - parts = append(parts, "Body: "+body) - } - - return fmt.Sprintf(", client.Config{%s}", strings.Join(parts, ", ")) -} - func rewriteClientExamplesWithAlias(content, alias string) (string, bool) { patterns := buildAliasPatterns(alias) diff --git a/cmd/internal/migrations/v3/client_usage_internal_test.go b/cmd/internal/migrations/v3/client_usage_internal_test.go index df62861..7824846 100644 --- a/cmd/internal/migrations/v3/client_usage_internal_test.go +++ b/cmd/internal/migrations/v3/client_usage_internal_test.go @@ -65,20 +65,25 @@ func handler(ctx *fiber.Ctx, code string) error { t map[string]any ) -resp, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code), client.Config{Header: map[string]string{"accept": "application/json"}}) -if err != nil { -return err -} -if err == nil { -retCode = resp.StatusCode() -retBody = resp.Body() -err = resp.JSON(&t) -} -if err != nil { -return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ -"err": err, -}) -} + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code)) + req.SetHeader("accept", "application/json") + resp, err := req.Send() + if err != nil { + return err + } + if err == nil { + retCode = resp.StatusCode() + retBody = resp.Body() + err = resp.JSON(&t) + } + if err != nil { + return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "err": err, + }) + } _ = retCode _ = retBody diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index 395e576..3763e60 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -318,7 +318,12 @@ func handler(ctx *fiber.Ctx, code string) error { t map[string]any ) - resp, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code), client.Config{Header: map[string]string{"accept": "application/json"}}) + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code)) + req.SetHeader("accept", "application/json") + resp, err := req.Send() if err != nil { return err } @@ -397,10 +402,15 @@ import ( ) func handler(ctx *fiber.Ctx, code string) error { + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code)) + req.SetHeader("accept", "application/json") var retCode int var retBody []byte var t map[string]any - resp, clientErr := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?code=%s", code), client.Config{Header: map[string]string{"accept": "application/json"}}) + resp, clientErr := req.Send() if clientErr != nil { return clientErr } @@ -591,7 +601,11 @@ import ( ) func main() { - resp, err := client.Get("https://httpbin.org/json") + a := client.New() + req := a.R() + req.SetMethod("GET") + req.SetURL("https://httpbin.org/json") + resp, err := req.Send() if err != nil { panic(err) } @@ -671,7 +685,13 @@ import ( ) func main() { - resp, err := client.Post("https://httpbin.org/post", client.Config{Header: map[string]string{"Content-Type": "application/json"}, Body: "{\"demo\":true}"}) + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL("https://httpbin.org/post") + req.SetHeader("Content-Type", "application/json") + req.SetRawBody([]byte("{\"demo\":true}")) + resp, err := req.Send() if err != nil { panic(err) } @@ -1067,7 +1087,12 @@ var ( ) func handler(code string) { - resp, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + req.SetHeader("accept", "application/json") + resp, err := req.Send() if err != nil { fmt.Printf("could not create HTTP request: %v", err) } @@ -1142,7 +1167,12 @@ var ( ) func handler(code string) { - _, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + req.SetHeader("accept", "application/json") + _, err := req.Send() if err != nil { log.Errorf("could not create HTTP request: %v", err) } @@ -1203,7 +1233,12 @@ var ( func handler(code string) { var err error - _, err = client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + req.SetHeader("accept", "application/json") + _, err = req.Send() if err != nil { panic(err) } @@ -1266,7 +1301,12 @@ var ( ) func handler(code string) { - _, clientErr := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + req.SetHeader("accept", "application/json") + _, clientErr := req.Send() if clientErr != nil { log.Errorf("could not create HTTP request: %v", clientErr) } @@ -1330,7 +1370,12 @@ var ( ) func handler(code string) { - _, err := client.Post(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code), client.Config{Header: map[string]string{"accept": "application/json"}}) + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", ClientID, ClientSecret, code)) + req.SetHeader("accept", "application/json") + _, err := req.Send() if err != nil { fmt.Printf("could not create HTTP request: %v", err) } @@ -1387,7 +1432,11 @@ import ( func handler(url string) { var errs []error - _, err := client.Get(url) + a := client.New() + req := a.R() + req.SetMethod("GET") + req.SetURL(url) + _, err := req.Send() if err != nil { errs = append(errs, err) } From 46205dfa7525ee738de5198414bfb258f60220e2 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 7 Dec 2025 15:20:07 +0100 Subject: [PATCH 09/11] Avoid counting control-scope err declarations as existing --- cmd/internal/migrations/v3/client_usage.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index 275fff0..ee9bfb3 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -615,6 +615,21 @@ func identifierDeclaredInLines(lines []string, name string) bool { continue } + // Declarations inside control statement initializers (e.g., + // "if err := ...") are scoped to that statement and should not be + // treated as function-scope declarations. + if strings.HasPrefix(trimmed, "if ") || strings.HasPrefix(trimmed, "for ") || strings.HasPrefix(trimmed, "switch ") || strings.HasPrefix(trimmed, "else if ") { + if declaredInVarBlockLine(name, trimmed) { + return true + } + + if name == defaultErrName && declaredInVarBlockLine("errs", trimmed) { + return true + } + + continue + } + if shortPattern.MatchString(trimmed) || varPattern.MatchString(trimmed) { return true } From d04112f36ea06f70ad97128ace430efdbcbd091a Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 7 Dec 2025 15:39:16 +0100 Subject: [PATCH 10/11] Refine client migration error handling --- cmd/internal/migrations/v3/client_usage.go | 77 +++++++++++++++++-- .../migrations/v3/client_usage_test.go | 6 -- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index ee9bfb3..8d526d7 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -285,6 +285,12 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { } errName := chooseErrName(out, lines, i) + errBlockEnd := blockEndIndex(lines, structStart) + if errBlockEnd < structStart { + errBlockEnd = structStart + } + errsNeeded := identifierUsedAfter(lines[errBlockEnd+1:], "errs") + errsAlreadyDeclared := identifierDeclared(out, lines, i, "errs") || identifierDeclaredInLines(preservedLines, "errs") out = append(out, fmt.Sprintf("%s%s := client.New()", indent, agentVar)) out = append(out, fmt.Sprintf("%s%s := %s.R()", indent, reqVar, agentVar)) @@ -370,10 +376,18 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { out = append(out, fmt.Sprintf("%sif %s != nil {", structMatch[1], errName)) out = append(out, replaceErrIdentifier(structBody[:len(structBody)-1], errName, true)...) out = append(out, structMatch[1]+"}") - if statusVar != "" && !hasBlankAssignment(lines, statusVar) { + if errsNeeded && !errsAlreadyDeclared { + out = append(out, indent+"var errs []error") + } + if errsNeeded { + out = append(out, fmt.Sprintf("%sif %s != nil {", indent, errName)) + out = append(out, fmt.Sprintf("%s\terrs = append(errs, %s)", indent, errName)) + out = append(out, indent+"}") + } + if statusVar != "" && !hasBlankAssignment(lines, statusVar) && !identifierUsedAfter(lines[structStart+1:], statusVar) { out = append(out, fmt.Sprintf("%s_ = %s", indent, statusVar)) } - if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) { + if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) && !identifierUsedAfter(lines[structStart+1:], bodyVar) { out = append(out, fmt.Sprintf("%s_ = %s", indent, bodyVar)) } @@ -409,10 +423,18 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { out = append(out, fmt.Sprintf("%s\t%s = resp.StatusCode()", indent, statusVar)) out = append(out, fmt.Sprintf("%s\t%s = resp.Body()", indent, bodyVar)) out = append(out, indent+"}") - if statusVar != "" && !hasBlankAssignment(lines, statusVar) { + if errsNeeded && !errsAlreadyDeclared { + out = append(out, indent+"var errs []error") + } + if errsNeeded { + out = append(out, fmt.Sprintf("%sif %s != nil {", indent, errName)) + out = append(out, fmt.Sprintf("%s\terrs = append(errs, %s)", indent, errName)) + out = append(out, indent+"}") + } + if statusVar != "" && !hasBlankAssignment(lines, statusVar) && !identifierUsedAfter(lines[structStart+1:], statusVar) { out = append(out, fmt.Sprintf("%s_ = %s", indent, statusVar)) } - if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) { + if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) && !identifierUsedAfter(lines[structStart+1:], bodyVar) { out = append(out, fmt.Sprintf("%s_ = %s", indent, bodyVar)) } @@ -448,10 +470,18 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { out = append(out, fmt.Sprintf("%s\t%s = resp.StatusCode()", indent, statusVar)) out = append(out, fmt.Sprintf("%s\t%s = resp.String()", indent, bodyVar)) out = append(out, indent+"}") - if statusVar != "" && !hasBlankAssignment(lines, statusVar) { + if errsNeeded && !errsAlreadyDeclared { + out = append(out, indent+"var errs []error") + } + if errsNeeded { + out = append(out, fmt.Sprintf("%sif %s != nil {", indent, errName)) + out = append(out, fmt.Sprintf("%s\terrs = append(errs, %s)", indent, errName)) + out = append(out, indent+"}") + } + if statusVar != "" && !hasBlankAssignment(lines, statusVar) && !identifierUsedAfter(lines[structStart+1:], statusVar) { out = append(out, fmt.Sprintf("%s_ = %s", indent, statusVar)) } - if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) { + if bodyVar != "" && !hasBlankAssignment(lines, bodyVar) && !identifierUsedAfter(lines[structStart+1:], bodyVar) { out = append(out, fmt.Sprintf("%s_ = %s", indent, bodyVar)) } @@ -593,6 +623,41 @@ func hasBlankAssignment(lines []string, name string) bool { return false } +func identifierUsedAfter(lines []string, name string) bool { + if name == "" { + return false + } + + pattern := regexp.MustCompile(fmt.Sprintf(`\b%s\b`, regexp.QuoteMeta(name))) + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "//") { + continue + } + + if pattern.MatchString(trimmed) { + return true + } + } + + return false +} + +func blockEndIndex(lines []string, start int) int { + depth := 0 + + for i := start + 1; i < len(lines); i++ { + depth += strings.Count(lines[i], "{") + depth -= strings.Count(lines[i], "}") + if depth < 0 { + return i + } + } + + return len(lines) - 1 +} + func identifierDeclared(existing, original []string, idx int, name string) bool { if identifierDeclaredInLines(existing, name) { return true diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index 3763e60..37c9788 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -615,8 +615,6 @@ func main() { status = resp.StatusCode() body = resp.Body() } - _ = status - _ = body if err != nil { panic(err) } @@ -701,8 +699,6 @@ func main() { status = resp.StatusCode() body = resp.Body() } - _ = status - _ = body if err != nil { panic(err) } @@ -1102,8 +1098,6 @@ func handler(code string) { status = resp.StatusCode() body = resp.Body() } - _ = status - _ = body if err != nil { panic(err) } From bf9f607a628fdb3a18327994f0c473f540b8c846 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 7 Dec 2025 16:18:06 +0100 Subject: [PATCH 11/11] Improve client migration spacing and errs handling --- cmd/internal/migrations/v3/client_usage.go | 103 +++++++++++----- .../migrations/v3/client_usage_test.go | 114 ++++++++++++++++++ 2 files changed, 184 insertions(+), 33 deletions(-) diff --git a/cmd/internal/migrations/v3/client_usage.go b/cmd/internal/migrations/v3/client_usage.go index 8d526d7..c5be3f6 100644 --- a/cmd/internal/migrations/v3/client_usage.go +++ b/cmd/internal/migrations/v3/client_usage.go @@ -20,8 +20,7 @@ var ( clientErrLenPattern = regexp.MustCompile(`\blen\(errs\)`) clientErrComparePattern = regexp.MustCompile(`err\s*!=\s*nil\s*>\s*0`) clientErrMapPattern = regexp.MustCompile(`"errs"\s*:\s*errs`) - clientErrVarPattern = regexp.MustCompile(`\berrs\b`) - clientErrsDeclPattern = regexp.MustCompile(`\berrs\s+\[]error\b`) + clientErrsDeclPattern = regexp.MustCompile(`(?m)^\s*var\s+errs\b`) // Non-alias-dependent patterns requestFromAgent = regexp.MustCompile(`^([ \t]*)(\w+)\s*:=\s*(\w+)\.Request\(\)\s*$`) @@ -236,6 +235,8 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { structStart := -1 preservedLines := []string{} preservedIdx := []int{} + blankIdx := []int{} + blankAfterParse := false for k := parseEnd + 1; k < len(lines); k++ { if skipLines[k] { continue @@ -243,6 +244,8 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { trimmed := strings.TrimSpace(lines[k]) if trimmed == "" { + blankAfterParse = true + blankIdx = append(blankIdx, k) continue } if strings.HasPrefix(trimmed, "//") { @@ -270,18 +273,41 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { } if structStart == -1 { - out = append(out, line) - continue + structStart = parseEnd } structMatch := structAssignPattern.FindStringSubmatch(lines[structStart]) bytesMatch := bytesAssignPattern.FindStringSubmatch(lines[structStart]) stringMatch := stringAssignPattern.FindStringSubmatch(lines[structStart]) + parseOnly := len(structMatch) == 0 && len(bytesMatch) == 0 && len(stringMatch) == 0 + + addPreserved := func() { + if len(preservedLines) == 0 { + return + } + if blankAfterParse && parseOnly { + out = append(out, "") + for _, idx := range blankIdx { + skipLines[idx] = true + } + blankAfterParse = false + } + + out = append(out, preservedLines...) + for _, idx := range preservedIdx { + skipLines[idx] = true + } + } + + if len(preservedLines) == 0 && !parseOnly { + for _, idx := range blankIdx { + skipLines[idx] = true + } + blankAfterParse = false + } if len(structMatch) == 0 && len(bytesMatch) == 0 && len(stringMatch) == 0 { structStart = parseEnd - preservedLines = nil - preservedIdx = nil } errName := chooseErrName(out, lines, i) @@ -322,12 +348,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { switch { case len(structMatch) > 0 && structMatch[5] == agentVar: - if len(preservedLines) > 0 { - out = append(out, preservedLines...) - for _, idx := range preservedIdx { - skipLines[idx] = true - } - } + addPreserved() errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(structMatch[2]) bodyVar := strings.TrimSpace(structMatch[3]) @@ -395,12 +416,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue case len(bytesMatch) > 0 && bytesMatch[5] == agentVar: - if len(preservedLines) > 0 { - out = append(out, preservedLines...) - for _, idx := range preservedIdx { - skipLines[idx] = true - } - } + addPreserved() errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(bytesMatch[2]) bodyVar := strings.TrimSpace(bytesMatch[3]) @@ -442,12 +458,7 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue case len(stringMatch) > 0 && stringMatch[5] == agentVar: - if len(preservedLines) > 0 { - out = append(out, preservedLines...) - for _, idx := range preservedIdx { - skipLines[idx] = true - } - } + addPreserved() errAssign := errAssignmentOperator(errName, out, lines, i, "resp") statusVar := strings.TrimSpace(stringMatch[2]) bodyVar := strings.TrimSpace(stringMatch[3]) @@ -489,12 +500,18 @@ func rewriteAcquireAgentBlocksWithAlias(content, alias string) (string, bool) { changed = true continue default: + if !parseOnly { + addPreserved() + } errAssign := errAssignmentOperator(errName, out, lines, i) respLine := fmt.Sprintf("%s_, %s %s %s.Send()", indent, errName, errAssign, reqVar) out = append(out, respLine) out = append(out, fmt.Sprintf("%sif %s != nil {", parseIndent, errName)) out = append(out, replaceErrIdentifier(parseBody[:len(parseBody)-1], errName, false)...) out = append(out, parseIndent+"}") + if parseOnly { + addPreserved() + } i = parseEnd changed = true @@ -712,17 +729,18 @@ func identifierDeclaredInLines(lines []string, name string) bool { } func declaredInVarBlockLine(name, trimmed string) bool { - if !strings.HasPrefix(trimmed, name) { + fields := strings.Fields(trimmed) + if len(fields) < 2 { return false } - - remainder := strings.TrimSpace(strings.TrimPrefix(trimmed, name)) - if remainder == "" { + if fields[0] != name { return false } - if strings.Contains(remainder, ":=") || strings.Contains(remainder, "=") { - return false + for _, f := range fields[1:] { + if strings.Contains(f, ":=") || strings.Contains(f, "=") { + return false + } } return true @@ -1185,7 +1203,14 @@ func ensureClientImport(content string) string { func rewriteClientErrorHandling(content string) string { // Only rewrite when we see the legacy multi-error usage patterns. This avoids // mutating unrelated identifiers such as custom "errs" slices. - hasLegacyErrs := clientErrIfPattern.MatchString(content) || clientErrLenPattern.MatchString(content) || clientErrComparePattern.MatchString(content) || clientErrMapPattern.MatchString(content) + baseLegacy := clientErrIfPattern.MatchString(content) || clientErrLenPattern.MatchString(content) || clientErrComparePattern.MatchString(content) || clientErrMapPattern.MatchString(content) + declaredErrs := clientErrsDeclPattern.MatchString(content) + if !baseLegacy { + // Declaration-only cases keep errs slices intact. + return content + } + + hasLegacyErrs := baseLegacy || declaredErrs if !hasLegacyErrs { return content } @@ -1194,8 +1219,20 @@ func rewriteClientErrorHandling(content string) string { updated = clientErrLenPattern.ReplaceAllString(updated, "err != nil") updated = clientErrComparePattern.ReplaceAllString(updated, "err != nil") updated = clientErrMapPattern.ReplaceAllString(updated, `"err": err`) - updated = clientErrsDeclPattern.ReplaceAllString(updated, "err error") - updated = clientErrVarPattern.ReplaceAllString(updated, "err") + + errToken := regexp.MustCompile(`\berrs\b`) + lines := strings.Split(updated, "\n") + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "var errs") || strings.HasPrefix(trimmed, "errs") { + continue + } + + lines[i] = errToken.ReplaceAllString(line, "err") + } + + updated = strings.Join(lines, "\n") + return updated } diff --git a/cmd/internal/migrations/v3/client_usage_test.go b/cmd/internal/migrations/v3/client_usage_test.go index 37c9788..957583a 100644 --- a/cmd/internal/migrations/v3/client_usage_test.go +++ b/cmd/internal/migrations/v3/client_usage_test.go @@ -1311,6 +1311,120 @@ func handler(code string) { assert.Equal(t, expected, content) } +func Test_MigrateClientUsage_AcquireAgentOnlyParseKeepsTrailingCode(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientparsepreserve") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" +) + +func handler(code string) { + a := fiber.AcquireAgent() + req := a.Request() + req.Header.SetMethod(fiber.MethodPost) + req.SetRequestURI(fmt.Sprintf("https://example.com/auth?code=%s", code)) + if err := a.Parse(); err != nil { + fmt.Println(err) + } + // keep + value := 123 + _ = value +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v3/client" +) + +func handler(code string) { + a := client.New() + req := a.R() + req.SetMethod("POST") + req.SetURL(fmt.Sprintf("https://example.com/auth?code=%s", code)) + _, err := req.Send() + if err != nil { + fmt.Println(err) + } + // keep + value := 123 + _ = value +}`) + + assert.Equal(t, expected, content) +} + +func Test_MigrateClientUsage_AcquireAgentOnlyParseIgnoresErrLikeVarNames(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mclientparsevarblock") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main + +import ( + "github.com/gofiber/fiber/v3" +) + +var ( + errMsg string +) + +func handler() { + a := fiber.AcquireAgent() + req := a.Request() + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("https://example.com") + if err := a.Parse(); err != nil { + println(err) + } +}`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateClientUsage(cmd, dir, nil, nil)) + + content := gofmtSource(t, readFile(t, file)) + expected := gofmtSource(t, `package main + +import ( + "github.com/gofiber/fiber/v3/client" +) + +var ( + errMsg string +) + +func handler() { + a := client.New() + req := a.R() + req.SetMethod("GET") + req.SetURL("https://example.com") + _, err := req.Send() + if err != nil { + println(err) + } +}`) + + assert.Equal(t, expected, content) +} + func Test_MigrateClientUsage_AcquireAgentOnlyParseIgnoresErrLikeNames(t *testing.T) { t.Parallel()