◐ Off-By-One · answer catalog

go-golangci-lint-slice-regression

1 answer(s)godocker

golangci-lint run --new-from-rev=origin/main

📦 Source in repository (JSON)

Answer

1. goconstfix rewriter: fmt.Errorf(literal) → errors.New(Str*)

The root cause is confirmed empirically: staticcheck's SA1006 fires for any non-literal first argument to a printf-style function with no further args — even a same-file const:

main.go:9:9: printf-style function with dynamic format string and no further arguments
            should use print-style function instead (SA1006)   // fmt.Errorf(StrItemNotFound)

While the pre-rewrite fmt.Errorf("item not found") is clean. So when goconstfix extracts a repeated literal, it must rewrite fmt.Errorf call sites to errors.New — and only when the literal is a plain message (no format verbs), since errors.New does no formatting:

// goconstfix rewriter — call-site classification
func containsVerb(lit string) bool {
    for i := 0; i+1 < len(lit); i++ {
        if lit[i] != '%' {
            continue
        }
        switch lit[i+1] {
        case '%', 'd', 's', 'v', 'q', 'x', 'X', 'o', 'b', 'c', 'f', 'e', 'E', 'g', 'G', 't', 'T', 'U', 'p', 'w':
            return true
        }
    }
    return false
}

// extractConst replaces repeated literals with a Str* constant
func extractConst(lit string) string { /* existing goconstfix logic */ }

func rewriteCall(fn string, lit string, argCount int, edits *astutil.Applier) {
    switch fn {
    case "fmt.Errorf":
        if containsVerb(lit) || argCount > 1 {
            return // keep fmt.Errorf(StrFmt, args...) — args present, no SA1006; never change format semantics
        }
        // plain message: fmt.Errorf(StrX) -> errors.New(StrX), import "errors"
        edits.Replace(fn, "errors.New")
        edits.Replace(lit, extractConst(lit))
    case "errors.New", "fmt.Sprintf", "fmt.Fprintf", "log.Printf", "log.Fatalf":
        return // never rewrite — these are not affected by SA1006
    }
}

Before/after on a touched line:

// before goconstfix (clean)
return fmt.Errorf("item not found")

// naive rewrite (CI regression: SA1006, dynamic format string)
return fmt.Errorf(StrItemNotFound)

// fixed
const StrItemNotFound = "item not found"   // added const block
return errors.New(StrItemNotFound)          // import "errors" added

Verbs with args stay untouched and lint-clean: fmt.Errorf("user %s not found", name) → fmt.Errorf(StrUserFmt, name) (SA1006 requires no further args; the const is resolvable for arg-count checking).

2. CI gate: --new-from-rev=HEAD vs --new-from-rev=origin/main

Reproduced with a real git simulation (base commit → goconstfix commit → fix commit):

Command Broken state exit Issues
golangci-lint run --new-from-rev=HEAD 0 (passes — the trap) none
golangci-lint run --new-from-rev=origin/main 1 (CI fails) SA1006 + noctx×2 + ineffassign

Why: --new-from-rev=HEAD diffs against the current HEAD — but the goconstfix commit is already in HEAD, so nothing is "new" and every issue (even the freshly-introduced SA1006) is filtered out. CI diffs against the merge base origin/main, so every line the rewriter touched counts as new: SA1006 shows up as genuinely new, and the pre-existing noctx/ineffassign on those rewritten lines surface too (an unchanged line, e.g. the dead baseQuery, correctly stays hidden).

Fix: replicate CI locally and/or use a merge-base:

# .golangci.yml (CI)
issues:
  only-new-issues: true
# local, matches CI exactly
golangci-lint run --new-from-rev=origin/main
# or robust for PRs (equivalent):
golangci-lint run --new-from-rev="$(git merge-base origin/main HEAD)"

3. Fix the surfaced pre-existing issues (they're now gate-failing)

// noctx fix: context-aware request
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, StrAPIBase, nil)
if err != nil { return err }
resp, err := http.DefaultClient.Do(req)

// ineffassign fix: drop the dead reassignment
label := StrItemNotFound
_ = label

4. VecKnnSearch: wire the dead baseQuery into the final query

// before: baseQuery built, overwritten before read -> filters silently dropped
func (s *VecKnnSearch) Search(filters []string) knnQuery {
    baseQuery := buildBaseQuery(filters)
    baseQuery = StrMatchAll
    return knnQuery{base: baseQuery}
}

// after: filters flow into the final query
func (s *VecKnnSearch) Search(filters []string) knnQuery {
    baseQuery := buildBaseQuery(filters)
    return knnQuery{filters: filters, base: baseQuery}
}

Evidence & signatures

All claims verified empirically in this environment (Go 1.26, staticcheck from `honnef.co/go/tools`, golangci-lint v1.64.8):

1. **SA1006 mechanics** (`/tmp/lintrep`): `fmt.Errorf(StrConst)` with same-file `const` → SA1006 fires; `var` form → fires; literal form → clean. After fix: `errors.New(StrItemNotFound)`, `fmt.Errorf(StrUserFmt, name)` (verb+args), and untouched literals all produce **zero** staticcheck findings (`exit 0`).

2. **Local vs CI divergence** (`/tmp/demo`, 3-commit git repo with `refs/remotes/origin/main` simulated): broken goconstfix commit → `--new-from-rev=HEAD` exit 0, `--new-from-rev=origin/main` exit 1 reporting exactly `SA1006` (new) plus surfaced `noctx`×2 and `ineffassign` on rewritten lines, while the unrewritten `baseQuery` line stayed hidden. After the fix commit → both CI-mode and full lint exit **0**.

3. **Behavioral regression test** (`/tmp/demo`): `TestVecKnnSearchCarriesFilters` fails before the fix (filters dropped, `base` lacks the filter terms) and passes after; `TestSearchNoFilters` covers the empty-filter edge.

4. **Edge cases tested** (unit tests, `/tmp/goconstfix_demo`, 14 subtests):
   - plain message → `errors.New`; trailing `%` ("100% done") → not a verb, safe
   - verb + args → keep `fmt.Errorf(StrFmt, args...)`
   - verb, no args → keep (never worsen pre-existing behavior)
   - escaped `%%` → keep (converting would change output text: percent-unescaping)
   - `errors.New`/`Sprintf`/`log.*` call sites → never rewritten

Totals: 16 tests (14 rewrite-decision + 2 VecKnnSearch behavioral), all passing.

---
{"model": "deepseek-v4-flash", "problem_class": "go-golangci-lint-slice-regression", "result": "passed", "tests": 16}
Generated from the verified corpus · MIT licensedBack to the catalog