diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1fa8c7f..a02d896 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Coding guidelines, code review commands, and quality agents for Go and Python development", - "version": "0.9.12" + "version": "0.11.0" }, "plugins": [ { "name": "coding", "description": "Coding guidelines, code review commands, and quality agents for Go and Python development", "source": "./", - "version": "0.9.12", + "version": "0.11.0", "strict": true } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 70bc536..b8125d4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "coding", "description": "Coding guidelines, code review commands, and quality agents for Go and Python development", - "version": "0.9.12", + "version": "0.11.0", "author": { "name": "Benjamin Borbe" }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77610cd..211127e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,7 @@ on: branches: [master] jobs: - precommit: - name: Precommit + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/docs/go-error-wrapping-guide.md b/docs/go-error-wrapping-guide.md index 7c688f1..c052650 100644 --- a/docs/go-error-wrapping-guide.md +++ b/docs/go-error-wrapping-guide.md @@ -238,3 +238,131 @@ It("returns ErrNotFound for missing item", func() { Expect(err).To(MatchError(ErrNotFound)) }) ``` + +### RULE go-errors/no-fmt-errorf (MUST) + +**Owner**: go-error-assistant +**Applies when**: any `*.go` file outside `main.go`, `*_test.go`, `vendor/` calls `fmt.Errorf(...)`. +**Enforcement**: `rules/go/no-fmt-errorf.yml` +**Why**: `fmt.Errorf` loses ctx-derived structured data and stack traces. Use `errors.Wrapf(ctx, err, "...")` (wrapping) or `errors.Errorf(ctx, "...", args...)` (new error) from `github.com/bborbe/errors` instead. + +#### Bad + +```go +return fmt.Errorf("fetch failed: %w", err) +``` + +#### Good + +```go +return errors.Wrapf(ctx, err, "fetch failed") +``` + +### RULE go-errors/no-bare-return-err (MUST) + +**Owner**: go-error-assistant +**Applies when**: a Go `return err` statement appears inside an `if err != nil { ... }` block, outside `*_test.go` and `vendor/`. Inner closures where the outer scope already wraps are an exception — see RULE `go-errors/inner-closure-no-double-wrap`. +**Enforcement**: `rules/go/no-bare-return-err.yml` +**Why**: bare `return err` propagates errors without context or stack trace. Wrap with `errors.Wrapf(ctx, err, "operation description")` at every layer that adds meaning. + +#### Bad + +```go +if err != nil { + return err // No context, no stack trace +} +``` + +#### Good + +```go +result, err := s.repo.Fetch(ctx, id) +if err != nil { + return nil, errors.Wrapf(ctx, err, "fetch account %s", id) +} +``` + +### RULE go-errors/no-context-background-in-business-logic (MUST) + +**Owner**: go-error-assistant +**Applies when**: a Go `context.Background()` call appears outside `main.go`, `cmd/**`, `*_test.go`, `vendor/`. Top-level goroutine spawners in `main` are exempt by path filter. +**Enforcement**: `rules/go/no-context-background-in-business-logic.yml` +**Why**: `context.Background()` discards any context data the caller added via `errors.AddToContext`, making subsequent wrapping pointless. Add `ctx context.Context` as a function parameter and propagate from callers. + +#### Bad + +```go +return errors.Wrapf(context.Background(), err, "failed") +// Loses all structured data from caller's context +``` + +#### Good + +```go +func (s *svc) validate(ctx context.Context, input string) error { + return errors.Errorf(ctx, "invalid: %s", input) +} +``` + +### RULE go-errors/inner-closure-no-double-wrap (SHOULD) + +**Owner**: go-error-assistant +**Applies when**: an inner closure (passed to `db.Update`, `filepath.WalkDir`, or similar callback APIs) calls `errors.Wrap`/`errors.Wrapf` while the surrounding function ALSO wraps the closure's return value. +**Enforcement**: judgment +**Why**: double-wrapping inflates error messages with redundant prefixes (`save data X: update: put: bolt: connection refused`) and doesn't add new information. The outer wrap is enough. + +#### Bad + +```go +err := s.db.Update(func(tx *bolt.Tx) error { + if err := put(tx); err != nil { + return errors.Wrapf(ctx, err, "put") // Redundant — outer wraps too + } + return nil +}) +return errors.Wrapf(ctx, err, "update") // Double-wrapped +``` + +#### Good + +```go +func (s *svc) Save(ctx context.Context, data Data) error { + err := s.db.Update(func(tx *bolt.Tx) error { + // Inner closure: bare return is OK here + // The outer Wrapf below will add context + return tx.Bucket(key).Put(id, encoded) + }) + if err != nil { + return errors.Wrapf(ctx, err, "save data %s", data.ID) + } + return nil +} +``` + +### RULE go-errors/sentinel-err-prefix-naming (SHOULD) + +**Owner**: go-error-assistant +**Applies when**: a package-level sentinel error variable uses the legacy `XxxError`/`XxxErr` naming convention (e.g. `BucketNotFoundErr`, `ConnectionError`) instead of the stdlib-style `ErrXxx` prefix (`ErrBucketNotFound`, `ErrConnection`). +**Enforcement**: judgment +**Why**: stdlib uses `Err` prefix (`io.EOF`, `sql.ErrNoRows`); matching it makes the convention discoverable and consistent. Legacy projects may keep the old name as a `Deprecated:` alias during transition. + +#### Bad + +```go +var BucketNotFoundErr = stderrors.New("bucket not found") +``` + +#### Good + +```go +var ErrNotFound = stderrors.New("not found") +``` + +And during a rename transition: + +```go +var ErrBucketNotFound = stderrors.New("bucket not found") + +// Deprecated: use ErrBucketNotFound. +var BucketNotFoundErr = ErrBucketNotFound +``` diff --git a/prompts/completed/007-bootstrap-rules-go-error-wrapping.md b/prompts/completed/007-bootstrap-rules-go-error-wrapping.md new file mode 100644 index 0000000..e7870b0 --- /dev/null +++ b/prompts/completed/007-bootstrap-rules-go-error-wrapping.md @@ -0,0 +1,225 @@ +--- +status: completed +summary: Migrated docs/go-error-wrapping-guide.md with five inline RULE blocks (3 mechanical + 2 judgment) and created three ast-grep YAML detectors; rules/index.json grew from 4 to 9 entries. +container: coding-bootstrap-error-wrapping-exec-007-bootstrap-rules-go-error-wrapping +dark-factory-version: v0.173.0 +created: "2026-05-31T22:00:56Z" +queued: "2026-05-31T22:06:08Z" +started: "2026-05-31T22:06:13Z" +completed: "2026-05-31T22:08:17Z" +--- + + +- Second bootstrap prompt — migrates `docs/go-error-wrapping-guide.md` from prose-only to rule-blocks-inline (Model A: hand-author the contract once) +- Appends 5 `### RULE` blocks (3 MUST mechanical + 2 SHOULD judgment) corresponding to the doc's `## Key Rules` + `## Anti-Patterns` +- Adds 3 `rules/go/*.yml` ast-grep YAMLs for the mechanical rules; the 2 judgment rules carry `Enforcement: judgment` +- Runs `make build-index` so `rules/index.json` grows from 4 entries to 9 +- Mirrors the proven `bootstrap-rules-go-time-injection.md` shape; this is the second pass of the mirror pattern +- Scope note: `## Key Rules` lists 7 items; this prompt extracts 5. Skipped: #4 "Multi-return wrapping" (snippet variation of rules 1+2, not a distinct rule) and #6 "Remove unused imports" (goimports' job, not a content rule) + + + +Following the schema in `docs/rule-block-schema.md` and the ast-grep conventions in `docs/ast-grep-rule-writing-guide.md`, extract five rules from `docs/go-error-wrapping-guide.md` as inline `### RULE` blocks, write ast-grep YAML detectors for the three MUST mechanical rules, and refresh `rules/index.json` via the walker. Every block must conform to the schema (Owner, Applies when, Enforcement fields, ID format `//`, anchor = id verbatim). + + + +Read `CLAUDE.md` for project conventions, including the doc-agent alignment table (`go-error-wrapping-guide.md` → `go-error-assistant`). +Read `docs/rule-block-schema.md` for the rule-block contract — required fields, level tokens, ID format, anchor rule (`anchor == id` verbatim). +Read `docs/ast-grep-rule-writing-guide.md` for the YAML conventions — frontmatter shape, pattern strategies, pitfalls (especially the `main.go` + `**/main.go` dual-ignore), smoke testing, when NOT to write a YAML. +Read `docs/go-error-wrapping-guide.md` — the doc to migrate. The `## Key Rules` section (around line 97) and `## Anti-Patterns` section (around line 172) list the rules to extract; the rest of the doc provides Bad/Good code snippets to cite. +Read `docs/go-context-cancellation-in-loops.md` lines 160-192 — the canonical pilot `### RULE` block. The new blocks MUST match its formatting exactly: heading line `### RULE (LEVEL)`, then bolded fields `**Owner**:`, `**Applies when**:`, `**Enforcement**:`, then `**Why**:`, then `#### Bad` / `#### Good` code blocks. +Read `rules/go/cancel-check-in-loop.yml`, `rules/go/no-time-now-direct.yml`, `rules/go/no-time-time-in-fields.yml` — canonical ast-grep YAMLs. Match their shape (frontmatter keys: `id`, `language`, `severity`, `message`, `rule`, `ignores`). +Read `scripts/build-index.py` and run `python3 scripts/build-index.py` once to see the current `rules/index.json` shape (4 entries) before changes. + + + +1. Append five `### RULE` blocks at the END of `docs/go-error-wrapping-guide.md` (after the existing `## Testing` section). Do NOT modify any prose above. The five blocks, in order: + +**Rule 1 — MUST mechanical** +- Heading: `### RULE go-errors/no-fmt-errorf (MUST)` +- Owner: `go-error-assistant` +- Applies when: any `*.go` file outside `main.go`, `*_test.go`, `vendor/` calls `fmt.Errorf(...)`. +- Enforcement: `rules/go/no-fmt-errorf.yml` +- Why: `fmt.Errorf` loses ctx-derived structured data and stack traces. Use `errors.Wrapf(ctx, err, "...")` (wrapping) or `errors.Errorf(ctx, "...", args...)` (new error) from `github.com/bborbe/errors` instead. +- Bad/Good snippets: reuse `## Examples > Fix: Replace fmt.Errorf` and `## Examples > Fix: New Error Without Cause` blocks from the doc itself. + +**Rule 2 — MUST mechanical** +- Heading: `### RULE go-errors/no-bare-return-err (MUST)` +- Owner: `go-error-assistant` +- Applies when: a Go `return err` statement appears inside an `if err != nil { ... }` block, outside `*_test.go` and `vendor/`. Inner closures (e.g. `db.Update(func(tx *bolt.Tx) error { ... })`) where the outer scope already wraps are an exception — see RULE `go-errors/inner-closure-no-double-wrap` below; ast-grep flags them and the judgment tier drops false positives. +- Enforcement: `rules/go/no-bare-return-err.yml` +- Why: bare `return err` propagates errors without context or stack trace. Wrap with `errors.Wrapf(ctx, err, "operation description")` at every layer that adds meaning. +- Bad/Good snippets: reuse `## Anti-Patterns > Bare return` block + the corresponding `### Wrapping an Existing Error` block from `## Core Patterns`. + +**Rule 3 — MUST mechanical** +- Heading: `### RULE go-errors/no-context-background-in-business-logic (MUST)` +- Owner: `go-error-assistant` +- Applies when: a Go `context.Background()` call appears outside `main.go`, `cmd/**`, `*_test.go`, `vendor/`. Top-level goroutine spawners in `main` are exempt by path filter; non-business-logic uses get adjudicated by the judgment tier. +- Enforcement: `rules/go/no-context-background-in-business-logic.yml` +- Why: `context.Background()` discards any context data the caller added via `errors.AddToContext`, making subsequent wrapping pointless. Add `ctx context.Context` as a function parameter and propagate from callers. +- Bad/Good snippets: reuse `## Anti-Patterns > context.Background() in business logic` block + the `### Fix: Function Missing ctx` block from `## Examples`. + +**Rule 4 — SHOULD judgment** +- Heading: `### RULE go-errors/inner-closure-no-double-wrap (SHOULD)` +- Owner: `go-error-assistant` +- Applies when: an inner closure (passed to `db.Update`, `filepath.WalkDir`, or similar callback APIs) calls `errors.Wrap`/`errors.Wrapf` while the surrounding function ALSO wraps the closure's return value. Distinguishing this case from "the outer doesn't wrap" requires whole-function reasoning. +- Enforcement: `judgment` (no ast-grep — closure double-wrap detection requires reading both the closure body AND the calling function's wrap layer). +- Why: double-wrapping inflates error messages with redundant prefixes (`save data X: update: put: bolt: connection refused`) and doesn't add new information. The outer wrap is enough. +- Bad/Good snippets: reuse `## Anti-Patterns > Double-wrapping inner closures` block + the `### Inner Closures — Do Not Double-Wrap` block from `## Examples`. + +**Rule 5 — SHOULD judgment** +- Heading: `### RULE go-errors/sentinel-err-prefix-naming (SHOULD)` +- Owner: `go-error-assistant` +- Applies when: a package-level sentinel error variable uses the legacy `XxxError` / `XxxErr` naming convention (e.g. `BucketNotFoundErr`, `ConnectionError`) instead of the stdlib-style `ErrXxx` prefix (`ErrBucketNotFound`, `ErrConnection`). Detection is semantic — distinguishing a sentinel error variable from any other `ErrXxx`-shaped identifier requires understanding the value's type. +- Enforcement: `judgment` (no ast-grep — naming conventions are pattern-recognition for the LLM, not syntactic). +- Why: stdlib uses `Err` prefix (`io.EOF`, `sql.ErrNoRows`); matching it makes the convention discoverable and consistent. Legacy projects may keep the old name as a `Deprecated:` alias during transition. +- Bad/Good snippets: reuse the `### Sentinel Errors` block from `## Core Patterns` (showing `var ErrNotFound = stderrors.New(...)`) + the `### Renaming Sentinel Errors (Backwards-Compat Alias)` block. + +Each block must follow the canonical pilot's exact formatting (`docs/go-context-cancellation-in-loops.md:160-192`) — verbatim field labels with `**` bolding, blank line between fields, `#### Bad` / `#### Good` H4 headings for the example code blocks. + +2. Create `rules/go/no-fmt-errorf.yml` matching the shape of `rules/go/no-time-now-direct.yml`: + - `id: go-errors/no-fmt-errorf` + - `language: go` + - `severity: error` + - `message`: 3-line block — short summary, blank line, then `See docs/go-error-wrapping-guide.md (RULE go-errors/no-fmt-errorf).` + - `rule`: `pattern: fmt.Errorf($$$ARGS)` + - `ignores`: `main.go`, `**/main.go`, `**/*_test.go`, `vendor/**`, `**/vendor/**`, `**/mocks/**` + +3. Create `rules/go/no-bare-return-err.yml`: + - `id: go-errors/no-bare-return-err` + - `language: go` + - `severity: error` + - `message`: 3-line block referencing the doc + - `rule`: detect `return err` inside an `if err != nil { ... }` block. Pattern guidance: + ```yaml + rule: + pattern: return err + inside: + kind: if_statement + has: + pattern: err != nil + ``` + Alternative shape using `any:` for multi-line bodies is acceptable as long as a synthetic `if err != nil { return err }` matches and a `return errors.Wrapf(ctx, err, "...")` inside the same shape does NOT match. + - `ignores`: same as rule 1 + +4. Create `rules/go/no-context-background-in-business-logic.yml`: + - `id: go-errors/no-context-background-in-business-logic` + - `language: go` + - `severity: error` + - `message`: 3-line block referencing the doc + - `rule`: `pattern: context.Background()` + - `ignores`: `main.go`, `**/main.go`, `cmd/**`, `**/cmd/**`, `**/*_test.go`, `vendor/**`, `**/vendor/**`, `**/mocks/**` — the `cmd/**` additions are critical because top-level goroutine spawners in `main` are exempt per the doc's `## Where context.Background() Is Allowed` section. + +5. The two judgment rules (rule 4, rule 5) have NO ast-grep YAMLs. Their `### RULE` blocks carry `Enforcement: judgment` (literal string, no path). + +6. There is NO ast-grep smoke inside this prompt — operator runs `scripts/scan.sh ` post-merge to confirm the detectors fire correctly on real code. YAML validity is verified mechanically by `make build-index` reading the index and the file existence/content checks below. + +7. Run `make build-index` to refresh `rules/index.json`. It must grow from 4 entries to **9 entries** (pilot + 3 go-time + 5 new go-errors rules), with entries sorted by `id` and each entry's keys alphabetically sorted. + +8. Verify each new index entry has: + - `owner: "go-error-assistant"` + - `doc_path: "docs/go-error-wrapping-guide.md"` + - `anchor == id` (the rule ID verbatim) + - `level` in `("MUST","SHOULD","MAY")` + - non-empty `applies_when` and `enforcement` + - For rules 4 + 5: `enforcement: "judgment"` (literal string) + +9. Run `make precommit` — must pass (link check + JSON validity unchanged). + + + +- Do NOT commit — dark-factory handles git +- Do NOT modify the existing pilot rule (`docs/go-context-cancellation-in-loops.md` rule block or `rules/go/cancel-check-in-loop.yml`) +- Do NOT modify the existing go-time rule blocks in `docs/go-time-injection.md` or `rules/go/no-time-*.yml` +- Do NOT modify the schema doc (`docs/rule-block-schema.md`) or the ast-grep guide (`docs/ast-grep-rule-writing-guide.md`) +- Do NOT rewrite `docs/go-error-wrapping-guide.md`'s existing prose — only APPEND the five `### RULE` blocks at the END of the file (after the `## Testing` section) +- Do NOT wire ast-grep into `make precommit` — that is Phase 1 territory and out of scope +- Do NOT attempt `ast-grep scan` from inside the container (binary may be absent until claude-yolo:v0.9.0 propagates to dark-factory's default image) +- Use the same Bad/Good code snippets shown in `docs/go-error-wrapping-guide.md` itself; do NOT invent new ones +- Generic examples only (User, Order, Product, Customer) — no Candle/Epic/Broker/SignalStore +- No personal paths (`~/Documents/`, `/Users/`) anywhere +- No `Co-Authored-By:` or attribution trailers (project convention) + + + +Run from repo root: +```bash +# Five new ### RULE blocks added to the doc +grep -c '^### RULE go-errors/' docs/go-error-wrapping-guide.md +# Must return: 5 + +# Three ast-grep YAMLs created +test -f rules/go/no-fmt-errorf.yml && \ +test -f rules/go/no-bare-return-err.yml && \ +test -f rules/go/no-context-background-in-business-logic.yml && \ +echo "yamls present: ok" + +# YAMLs reference the correct ids on their first line +head -1 rules/go/no-fmt-errorf.yml | grep -q '^id: go-errors/no-fmt-errorf' && echo "yaml1 id: ok" +head -1 rules/go/no-bare-return-err.yml | grep -q '^id: go-errors/no-bare-return-err' && echo "yaml2 id: ok" +head -1 rules/go/no-context-background-in-business-logic.yml | grep -q '^id: go-errors/no-context-background-in-business-logic' && echo "yaml3 id: ok" + +# All three YAMLs use severity: error (MUST rules → mechanical layer → error severity per the ast-grep guide) +for f in rules/go/no-fmt-errorf.yml rules/go/no-bare-return-err.yml rules/go/no-context-background-in-business-logic.yml; do + head -5 "$f" | grep -q '^severity: error' || { echo "FAIL: $f missing severity: error"; exit 1; } +done +echo "severity: ok" + +# No YAML for the judgment rules (rule 4, rule 5) +test ! -f rules/go/inner-closure-no-double-wrap.yml && echo "rule 4 has no YAML: ok" +test ! -f rules/go/sentinel-err-prefix-naming.yml && echo "rule 5 has no YAML: ok" + +# rules/index.json now has 9 entries +python3 -c " +import json +d = json.load(open('rules/index.json')) +assert isinstance(d, list) and len(d) == 9, f'expected 9 entries, got {len(d)}' +print(f'entries: {len(d)} ok') +" + +# All nine expected ids present and sorted +python3 -c " +import json +ids = [e['id'] for e in json.load(open('rules/index.json'))] +expected = [ + 'go-context/cancel-check-in-loop', + 'go-errors/inner-closure-no-double-wrap', + 'go-errors/no-bare-return-err', + 'go-errors/no-context-background-in-business-logic', + 'go-errors/no-fmt-errorf', + 'go-errors/sentinel-err-prefix-naming', + 'go-time/inject-getter-not-create', + 'go-time/no-time-now-direct', + 'go-time/no-time-time-in-fields', +] +assert ids == expected, f'ids mismatch:\n got: {ids}\n expected: {expected}' +print('ids sorted: ok') +" + +# Five new entries owned by go-error-assistant + point at the right doc +python3 -c " +import json +go_errors = [e for e in json.load(open('rules/index.json')) if e['id'].startswith('go-errors/')] +assert len(go_errors) == 5, f'expected 5 go-errors/* entries, got {len(go_errors)}' +for e in go_errors: + assert e['owner'] == 'go-error-assistant', f\"owner mismatch for {e['id']}: {e['owner']}\" + assert e['doc_path'] == 'docs/go-error-wrapping-guide.md', f\"doc_path mismatch for {e['id']}\" + assert e['anchor'] == e['id'], f\"anchor != id for {e['id']}: {e['anchor']}\" + assert e['level'] in ('MUST','SHOULD','MAY'), f\"invalid level for {e['id']}: {e['level']}\" + assert e['applies_when'], f\"empty applies_when for {e['id']}\" + assert e['enforcement'], f\"empty enforcement for {e['id']}\" +judgment_ids = {'go-errors/inner-closure-no-double-wrap', 'go-errors/sentinel-err-prefix-naming'} +for e in go_errors: + if e['id'] in judgment_ids: + assert e['enforcement'] == 'judgment', f\"{e['id']} enforcement must be literal 'judgment', got: {e['enforcement']}\" +print('go-errors entries: ok') +" + +# Determinism — running build-index again produces identical bytes +make build-index +git diff --exit-code rules/index.json && echo "deterministic: ok" + +# make precommit clean +make precommit +``` + diff --git a/rules/go/no-bare-return-err.yml b/rules/go/no-bare-return-err.yml new file mode 100644 index 0000000..bd8890d --- /dev/null +++ b/rules/go/no-bare-return-err.yml @@ -0,0 +1,20 @@ +id: go-errors/no-bare-return-err +language: go +severity: error +message: | + Bare `return err` must not appear inside an `if err != nil { ... }` block. + Wrap with errors.Wrapf(ctx, err, "operation description") to add context and stack trace. + See docs/go-error-wrapping-guide.md (RULE go-errors/no-bare-return-err). +rule: + pattern: return err + inside: + kind: if_statement + has: + pattern: err != nil +ignores: + - "main.go" + - "**/main.go" + - "**/*_test.go" + - "vendor/**" + - "**/vendor/**" + - "**/mocks/**" diff --git a/rules/go/no-context-background-in-business-logic.yml b/rules/go/no-context-background-in-business-logic.yml new file mode 100644 index 0000000..99c2f85 --- /dev/null +++ b/rules/go/no-context-background-in-business-logic.yml @@ -0,0 +1,18 @@ +id: go-errors/no-context-background-in-business-logic +language: go +severity: error +message: | + context.Background() must not appear in business logic. + Add ctx context.Context as a function parameter and propagate from callers. + See docs/go-error-wrapping-guide.md (RULE go-errors/no-context-background-in-business-logic). +rule: + pattern: context.Background() +ignores: + - "main.go" + - "**/main.go" + - "cmd/**" + - "**/cmd/**" + - "**/*_test.go" + - "vendor/**" + - "**/vendor/**" + - "**/mocks/**" diff --git a/rules/go/no-fmt-errorf.yml b/rules/go/no-fmt-errorf.yml new file mode 100644 index 0000000..be0c946 --- /dev/null +++ b/rules/go/no-fmt-errorf.yml @@ -0,0 +1,16 @@ +id: go-errors/no-fmt-errorf +language: go +severity: error +message: | + fmt.Errorf must not be used in production code. + Use errors.Wrapf (wrapping) or errors.Errorf (new error) from github.com/bborbe/errors instead. + See docs/go-error-wrapping-guide.md (RULE go-errors/no-fmt-errorf). +rule: + pattern: fmt.Errorf($$$ARGS) +ignores: + - "main.go" + - "**/main.go" + - "**/*_test.go" + - "vendor/**" + - "**/vendor/**" + - "**/mocks/**" diff --git a/rules/index.json b/rules/index.json index 2a5bbac..c85143a 100644 --- a/rules/index.json +++ b/rules/index.json @@ -8,6 +8,51 @@ "level": "SHOULD", "owner": "go-context-assistant" }, + { + "anchor": "go-errors/inner-closure-no-double-wrap", + "applies_when": "an inner closure (passed to `db.Update`, `filepath.WalkDir`, or similar callback APIs) calls `errors.Wrap`/`errors.Wrapf` while the surrounding function ALSO wraps the closure's return value.", + "doc_path": "docs/go-error-wrapping-guide.md", + "enforcement": "judgment", + "id": "go-errors/inner-closure-no-double-wrap", + "level": "SHOULD", + "owner": "go-error-assistant" + }, + { + "anchor": "go-errors/no-bare-return-err", + "applies_when": "a Go `return err` statement appears inside an `if err != nil { ... }` block, outside `*_test.go` and `vendor/`. Inner closures where the outer scope already wraps are an exception — see RULE `go-errors/inner-closure-no-double-wrap`.", + "doc_path": "docs/go-error-wrapping-guide.md", + "enforcement": "`rules/go/no-bare-return-err.yml`", + "id": "go-errors/no-bare-return-err", + "level": "MUST", + "owner": "go-error-assistant" + }, + { + "anchor": "go-errors/no-context-background-in-business-logic", + "applies_when": "a Go `context.Background()` call appears outside `main.go`, `cmd/**`, `*_test.go`, `vendor/`. Top-level goroutine spawners in `main` are exempt by path filter.", + "doc_path": "docs/go-error-wrapping-guide.md", + "enforcement": "`rules/go/no-context-background-in-business-logic.yml`", + "id": "go-errors/no-context-background-in-business-logic", + "level": "MUST", + "owner": "go-error-assistant" + }, + { + "anchor": "go-errors/no-fmt-errorf", + "applies_when": "any `*.go` file outside `main.go`, `*_test.go`, `vendor/` calls `fmt.Errorf(...)`.", + "doc_path": "docs/go-error-wrapping-guide.md", + "enforcement": "`rules/go/no-fmt-errorf.yml`", + "id": "go-errors/no-fmt-errorf", + "level": "MUST", + "owner": "go-error-assistant" + }, + { + "anchor": "go-errors/sentinel-err-prefix-naming", + "applies_when": "a package-level sentinel error variable uses the legacy `XxxError`/`XxxErr` naming convention (e.g. `BucketNotFoundErr`, `ConnectionError`) instead of the stdlib-style `ErrXxx` prefix (`ErrBucketNotFound`, `ErrConnection`).", + "doc_path": "docs/go-error-wrapping-guide.md", + "enforcement": "judgment", + "id": "go-errors/sentinel-err-prefix-naming", + "level": "SHOULD", + "owner": "go-error-assistant" + }, { "anchor": "go-time/inject-getter-not-create", "applies_when": "a factory or constructor file outside `main.go` calls `libtime.NewCurrentDateTime()`.",