litclaude-ai 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
Typed errors, wrap chains, `errors.Is` / `errors.As`, no panic in libraries, resource cleanup. Go errors look simple and are full of footguns. This document is the canonical set of moves.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The five rules
|
|
8
|
+
|
|
9
|
+
1. **Every error is wrapped on the way up, with `%w`, with context.** Never `return err` from a non-trivial site.
|
|
10
|
+
2. **Compare with `errors.Is`, not `==`.** Wrap chains break `==`. The `errorlint` linter forbids `==` on errors.
|
|
11
|
+
3. **Cast with `errors.As`, not type assertion.** Same reason.
|
|
12
|
+
4. **`panic` is reserved for programmer errors.** Library code never panics on user input or environment failures. Use `(T, error)`.
|
|
13
|
+
5. **Resources released via `defer` immediately after acquisition.** No "I'll add it later".
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Sentinel errors — for invariant programmatic checks
|
|
18
|
+
|
|
19
|
+
```go
|
|
20
|
+
package domain
|
|
21
|
+
|
|
22
|
+
import "errors"
|
|
23
|
+
|
|
24
|
+
var (
|
|
25
|
+
ErrInvalidEmail = errors.New("domain: invalid email")
|
|
26
|
+
ErrInvalidPhone = errors.New("domain: invalid phone")
|
|
27
|
+
ErrInvalidAge = errors.New("domain: invalid age")
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
func NewEmail(s string) (Email, error) {
|
|
31
|
+
if !emailRe.MatchString(s) {
|
|
32
|
+
return Email{}, fmt.Errorf("email %q: %w", s, ErrInvalidEmail)
|
|
33
|
+
}
|
|
34
|
+
return Email{raw: strings.ToLower(s)}, nil
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Caller branches on identity:
|
|
39
|
+
|
|
40
|
+
```go
|
|
41
|
+
email, err := domain.NewEmail(input)
|
|
42
|
+
if errors.Is(err, domain.ErrInvalidEmail) {
|
|
43
|
+
return c.JSON(400, gin.H{"error": "email format"})
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`errors.Is` walks the wrap chain. `err == domain.ErrInvalidEmail` would have failed because `fmt.Errorf` wrapped it.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Typed errors — when you need structured data
|
|
52
|
+
|
|
53
|
+
When callers need fields off the error (the offending value, the failing field name, the upstream HTTP status):
|
|
54
|
+
|
|
55
|
+
```go
|
|
56
|
+
type ValidationError struct {
|
|
57
|
+
Field string
|
|
58
|
+
Value string
|
|
59
|
+
Rule string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func (e *ValidationError) Error() string {
|
|
63
|
+
return fmt.Sprintf("validation: %s=%q failed %s", e.Field, e.Value, e.Rule)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Optional: identity sentinel for errors.Is comparisons
|
|
67
|
+
var ErrValidation = errors.New("validation")
|
|
68
|
+
|
|
69
|
+
func (e *ValidationError) Is(target error) bool {
|
|
70
|
+
return target == ErrValidation
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Caller:
|
|
75
|
+
|
|
76
|
+
```go
|
|
77
|
+
err := svc.Save(ctx, user)
|
|
78
|
+
|
|
79
|
+
var vErr *ValidationError
|
|
80
|
+
if errors.As(err, &vErr) {
|
|
81
|
+
// vErr.Field, vErr.Rule are available
|
|
82
|
+
c.JSON(400, gin.H{"field": vErr.Field, "rule": vErr.Rule})
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**`errors.As` requires a non-nil pointer-to-pointer.** Almost always the type is `*ConcreteError`. Forgetting the leading `*` is the most common bug here.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Wrapping — `%w` is mandatory
|
|
92
|
+
|
|
93
|
+
```go
|
|
94
|
+
// BAD — drops context
|
|
95
|
+
return err
|
|
96
|
+
|
|
97
|
+
// BAD — drops the error chain (errors.Is/As stops working)
|
|
98
|
+
return fmt.Errorf("failed to save user: %v", err)
|
|
99
|
+
|
|
100
|
+
// GOOD — preserves chain via %w
|
|
101
|
+
return fmt.Errorf("save user %s: %w", userID, err)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `errorlint` linter catches `%v` where `%w` was meant. **Wrap once per layer**, with the minimum useful context:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
api/handler: "create user request: %w"
|
|
108
|
+
service: "validate inputs: %w"
|
|
109
|
+
domain: "email %q: %w"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each frame adds one fact, not a duplicate. The top-level error message reads as a path: `create user request: validate inputs: email "foo": domain: invalid email`.
|
|
113
|
+
|
|
114
|
+
### `errors.Join` — multiple errors at once
|
|
115
|
+
|
|
116
|
+
```go
|
|
117
|
+
// Validate all fields, collect all errors
|
|
118
|
+
var errs []error
|
|
119
|
+
if _, err := NewEmail(req.Email); err != nil {
|
|
120
|
+
errs = append(errs, fmt.Errorf("email: %w", err))
|
|
121
|
+
}
|
|
122
|
+
if _, err := NewUsername(req.Username); err != nil {
|
|
123
|
+
errs = append(errs, fmt.Errorf("username: %w", err))
|
|
124
|
+
}
|
|
125
|
+
if len(errs) > 0 {
|
|
126
|
+
return errors.Join(errs...)
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`errors.Is` still walks each joined error. Use when reporting batch validation, not for "wrap two unrelated errors".
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Panics — when allowed, when banned
|
|
135
|
+
|
|
136
|
+
**Banned**:
|
|
137
|
+
|
|
138
|
+
- Anywhere a `(T, error)` could be returned.
|
|
139
|
+
- Inside HTTP handlers (gin's `Recovery` middleware catches them, but you've already lost the error context).
|
|
140
|
+
- Inside any goroutine that survives request lifetime.
|
|
141
|
+
|
|
142
|
+
**Allowed** (with documentation):
|
|
143
|
+
|
|
144
|
+
- Map literal init at package level: `var statusNames = map[Status]string{...}` followed by a `func init()` that panics if a const has no name. Catches the bug at startup, not runtime.
|
|
145
|
+
- The `must*` convention for genuinely unrecoverable startup:
|
|
146
|
+
```go
|
|
147
|
+
func MustParseURL(s string) *url.URL {
|
|
148
|
+
u, err := url.Parse(s)
|
|
149
|
+
if err != nil { panic(err) }
|
|
150
|
+
return u
|
|
151
|
+
}
|
|
152
|
+
// Use only with literals known at compile time:
|
|
153
|
+
var defaultAPI = MustParseURL("https://api.example.com")
|
|
154
|
+
```
|
|
155
|
+
- `default:` case of an exhaustive sealed-interface switch — see `type-patterns.md`.
|
|
156
|
+
|
|
157
|
+
The `revive` linter rule `error-return` will flag suspect panic sites; treat them as bugs.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## `defer` for resources — the only safe pattern
|
|
162
|
+
|
|
163
|
+
```go
|
|
164
|
+
func writeReport(path string) (err error) {
|
|
165
|
+
f, err := os.Create(path)
|
|
166
|
+
if err != nil {
|
|
167
|
+
return fmt.Errorf("create %s: %w", path, err)
|
|
168
|
+
}
|
|
169
|
+
defer func() {
|
|
170
|
+
if cerr := f.Close(); cerr != nil && err == nil {
|
|
171
|
+
err = fmt.Errorf("close %s: %w", path, cerr)
|
|
172
|
+
}
|
|
173
|
+
}()
|
|
174
|
+
|
|
175
|
+
if _, err := f.Write(data); err != nil {
|
|
176
|
+
return fmt.Errorf("write %s: %w", path, err)
|
|
177
|
+
}
|
|
178
|
+
return nil
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Key points:
|
|
183
|
+
|
|
184
|
+
- `defer f.Close()` immediately after `os.Create` — never further down.
|
|
185
|
+
- Named return `(err error)` so the deferred close can mutate it on close failure.
|
|
186
|
+
- `bodyclose` linter catches missed `defer resp.Body.Close()` for HTTP responses.
|
|
187
|
+
- `sqlclosecheck` linter catches missed `defer rows.Close()` for SQL.
|
|
188
|
+
|
|
189
|
+
### `errors.Join` for multi-stage cleanup
|
|
190
|
+
|
|
191
|
+
```go
|
|
192
|
+
func process(path string) (err error) {
|
|
193
|
+
f, err := os.Open(path)
|
|
194
|
+
if err != nil { return err }
|
|
195
|
+
defer func() {
|
|
196
|
+
err = errors.Join(err, f.Close())
|
|
197
|
+
}()
|
|
198
|
+
// ... use f ...
|
|
199
|
+
return nil
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
When both the main operation AND `Close` can fail, `errors.Join` reports both without dropping either.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## HTTP error responses — a single funnel
|
|
208
|
+
|
|
209
|
+
Build one helper, route all handler errors through it:
|
|
210
|
+
|
|
211
|
+
```go
|
|
212
|
+
package httperr
|
|
213
|
+
|
|
214
|
+
type APIError struct {
|
|
215
|
+
Status int `json:"-"`
|
|
216
|
+
Code string `json:"code"`
|
|
217
|
+
Message string `json:"message"`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func (e *APIError) Error() string { return e.Code + ": " + e.Message }
|
|
221
|
+
|
|
222
|
+
var (
|
|
223
|
+
NotFound = &APIError{Status: 404, Code: "not_found", Message: "resource not found"}
|
|
224
|
+
Unauthorized = &APIError{Status: 401, Code: "unauthorized", Message: "unauthorized"}
|
|
225
|
+
BadRequest = &APIError{Status: 400, Code: "bad_request", Message: "bad request"}
|
|
226
|
+
Internal = &APIError{Status: 500, Code: "internal", Message: "internal error"}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// Wrap a domain error into an API error.
|
|
230
|
+
func From(err error) *APIError {
|
|
231
|
+
if err == nil { return nil }
|
|
232
|
+
|
|
233
|
+
var apiErr *APIError
|
|
234
|
+
if errors.As(err, &apiErr) { return apiErr }
|
|
235
|
+
|
|
236
|
+
switch {
|
|
237
|
+
case errors.Is(err, domain.ErrInvalidEmail),
|
|
238
|
+
errors.Is(err, domain.ErrInvalidUsername):
|
|
239
|
+
return &APIError{Status: 400, Code: "validation", Message: err.Error()}
|
|
240
|
+
case errors.Is(err, ErrNotFound):
|
|
241
|
+
return NotFound
|
|
242
|
+
case errors.Is(err, ErrUnauthorized):
|
|
243
|
+
return Unauthorized
|
|
244
|
+
default:
|
|
245
|
+
// unknown — log full chain, return generic
|
|
246
|
+
slog.Error("unmapped error", slog.Any("err", err))
|
|
247
|
+
return Internal
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func Write(c *gin.Context, err error) {
|
|
252
|
+
apiErr := From(err)
|
|
253
|
+
c.JSON(apiErr.Status, apiErr)
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Handlers become trivial:
|
|
258
|
+
|
|
259
|
+
```go
|
|
260
|
+
func (h *Handler) Create(c *gin.Context) {
|
|
261
|
+
user, err := h.svc.Create(c.Request.Context(), req)
|
|
262
|
+
if err != nil {
|
|
263
|
+
httperr.Write(c, err)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
c.JSON(201, user)
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## errgroup — error propagation across goroutines
|
|
273
|
+
|
|
274
|
+
```go
|
|
275
|
+
import "golang.org/x/sync/errgroup"
|
|
276
|
+
|
|
277
|
+
func fetchAll(ctx context.Context, urls []string) ([][]byte, error) {
|
|
278
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
279
|
+
g.SetLimit(8) // concurrency cap
|
|
280
|
+
|
|
281
|
+
results := make([][]byte, len(urls))
|
|
282
|
+
for i, u := range urls {
|
|
283
|
+
g.Go(func() error {
|
|
284
|
+
body, err := fetch(ctx, u)
|
|
285
|
+
if err != nil {
|
|
286
|
+
return fmt.Errorf("fetch %s: %w", u, err)
|
|
287
|
+
}
|
|
288
|
+
results[i] = body
|
|
289
|
+
return nil
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if err := g.Wait(); err != nil {
|
|
294
|
+
return nil, err
|
|
295
|
+
}
|
|
296
|
+
return results, nil
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- `errgroup.WithContext` cancels remaining tasks on first error.
|
|
301
|
+
- `SetLimit` bounds concurrency.
|
|
302
|
+
- First non-nil error is returned; others are discarded — by design.
|
|
303
|
+
|
|
304
|
+
See `concurrency.md` for the full pattern.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Logging errors — structured, once
|
|
309
|
+
|
|
310
|
+
```go
|
|
311
|
+
slog.ErrorContext(ctx, "save user failed",
|
|
312
|
+
slog.String("user_id", string(id)),
|
|
313
|
+
slog.Any("err", err), // %w chain is fully rendered
|
|
314
|
+
)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Log once, at the outermost frame.** Logging at every wrap site produces five log lines for one error.
|
|
318
|
+
|
|
319
|
+
The `sloglint` linter enforces `slog.Any("err", err)` over `slog.String("err", err.Error())` — the former preserves the chain when handlers walk the value.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Antipatterns
|
|
324
|
+
|
|
325
|
+
| Bad | Why | Good |
|
|
326
|
+
|---|---|---|
|
|
327
|
+
| `_ = err` | Silent ignore | Handle, log, or wrap |
|
|
328
|
+
| `if err != nil { return err }` chained 10 deep without wrap | No path info | Add one fact per layer: `fmt.Errorf("step: %w", err)` |
|
|
329
|
+
| `panic(err)` in HTTP handlers | Loses error chain, hits gin Recovery | `httperr.Write(c, err)` |
|
|
330
|
+
| `err.Error() == "some string"` | Brittle, breaks on wrap | Define a sentinel, use `errors.Is` |
|
|
331
|
+
| `if err == sql.ErrNoRows` | Breaks under wrap | `errors.Is(err, sql.ErrNoRows)` |
|
|
332
|
+
| `catch-all log.Fatal(err)` in library code | Crashes the caller's process | Return error, let main decide |
|
|
333
|
+
| Returning a typed nil pointer wrapped in error interface | Classic "nil != nil" bug | Return explicit `nil` for the error |
|
|
334
|
+
|
|
335
|
+
The last bug deserves its own example:
|
|
336
|
+
|
|
337
|
+
```go
|
|
338
|
+
// BUG — returns a non-nil error interface containing a nil concrete type
|
|
339
|
+
func bad() error {
|
|
340
|
+
var e *MyError = nil
|
|
341
|
+
return e // interface wraps nil pointer; errors == nil is FALSE
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Caller
|
|
345
|
+
if err := bad(); err != nil {
|
|
346
|
+
// ← entered, but err.(*MyError) is nil — surprise panic
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Fix: return explicit `nil`, not a typed nil. The `nilnil` linter catches this in `(T, error)` returns.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Sources
|
|
355
|
+
|
|
356
|
+
- Go blog "Working with Errors in Go 1.13+": https://go.dev/blog/go1.13-errors
|
|
357
|
+
- `errors.Join` (Go 1.20+): https://pkg.go.dev/errors#Join
|
|
358
|
+
- errorlint: https://github.com/polyfloyd/go-errorlint
|
|
359
|
+
- nilaway nil-interface check: https://github.com/uber-go/nilaway
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Strict `.golangci.yml` (golangci-lint v2)
|
|
2
|
+
|
|
3
|
+
The single source of truth for "is this Go code acceptable". Drop this in unmodified. **Every linter below is enabled deliberately — read the rationale before disabling one.**
|
|
4
|
+
|
|
5
|
+
`golangci-lint` v2 changed config schema (top-level `version: "2"`). All v1 configs are incompatible. The block below is v2.
|
|
6
|
+
|
|
7
|
+
## `.golangci.yml`
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
version: "2"
|
|
11
|
+
|
|
12
|
+
run:
|
|
13
|
+
timeout: 5m
|
|
14
|
+
tests: true
|
|
15
|
+
modules-download-mode: readonly
|
|
16
|
+
|
|
17
|
+
linters:
|
|
18
|
+
default: none
|
|
19
|
+
enable:
|
|
20
|
+
# ── Correctness — bug catchers ───────────────────────────────
|
|
21
|
+
- govet # stdlib vet, includes shadow, fieldalignment, nilness
|
|
22
|
+
- staticcheck # SA1*-SA9* — the de facto Go correctness linter
|
|
23
|
+
- errcheck # unhandled errors. ZERO tolerance.
|
|
24
|
+
- errorlint # %w wrapping, errors.As vs type-assertion, errors.Is vs ==
|
|
25
|
+
- nilerr # `return nil` after `err != nil` — classic bug
|
|
26
|
+
- nilnil # returning `(nil, nil)` from a (*T, error) function
|
|
27
|
+
- bodyclose # http.Response.Body not closed
|
|
28
|
+
- rowserrcheck # sql.Rows.Err() not checked
|
|
29
|
+
- sqlclosecheck # sql.Rows / sql.Stmt not closed
|
|
30
|
+
- contextcheck # functions taking context.Context don't get context.Background()
|
|
31
|
+
- fatcontext # context.WithValue() in a loop — leaks
|
|
32
|
+
- copyloopvar # Go 1.22 loop-var capture — should now use the new semantics
|
|
33
|
+
- intrange # use `for i := range N` (Go 1.22+) instead of `for i := 0; i < N; i++`
|
|
34
|
+
- usetesting # use t.TempDir/t.Setenv over os.* in tests
|
|
35
|
+
- testifylint # require vs assert correctness, ObjectsAreEqual misuse
|
|
36
|
+
|
|
37
|
+
# ── Style / readability — kept narrow to avoid bikeshedding ─
|
|
38
|
+
- gofumpt # stricter gofmt
|
|
39
|
+
- goimports # import grouping + local prefix
|
|
40
|
+
- whitespace # leading/trailing whitespace
|
|
41
|
+
- misspell # typos in comments and strings
|
|
42
|
+
- unconvert # redundant type conversions
|
|
43
|
+
- unparam # unused function parameters
|
|
44
|
+
- ineffassign # ineffective assignments
|
|
45
|
+
- dupword # duplicate words ("the the")
|
|
46
|
+
|
|
47
|
+
# ── Architecture — file size, complexity, dead code ─────────
|
|
48
|
+
- gocognit # cognitive complexity per function (threshold 25)
|
|
49
|
+
- gocyclo # cyclomatic complexity per function (threshold 15)
|
|
50
|
+
- funlen # function length (90 lines, 60 statements)
|
|
51
|
+
- lll # line length 120
|
|
52
|
+
- nestif # excessive nesting depth (>4)
|
|
53
|
+
- dupl # duplicate code blocks
|
|
54
|
+
- revive # extensible replacement for golint; selected rules below
|
|
55
|
+
- unused # unused vars/funcs/types
|
|
56
|
+
|
|
57
|
+
# ── Exhaustiveness — Go's weakest spot ──────────────────────
|
|
58
|
+
- exhaustive # type switch and enum-like const groups completeness
|
|
59
|
+
|
|
60
|
+
# ── Security ────────────────────────────────────────────────
|
|
61
|
+
- gosec # CWE-aware security scanner
|
|
62
|
+
|
|
63
|
+
# ── Logging ─────────────────────────────────────────────────
|
|
64
|
+
- sloglint # slog attr style + no slog.Any(); enforce structured logs
|
|
65
|
+
|
|
66
|
+
# ── Performance ─────────────────────────────────────────────
|
|
67
|
+
- perfsprint # fmt.Sprintf where strconv suffices
|
|
68
|
+
- prealloc # slice prealloc when length is known
|
|
69
|
+
- makezero # make([]T, n) with non-zero n then append (the classic bug)
|
|
70
|
+
|
|
71
|
+
linters-settings:
|
|
72
|
+
errcheck:
|
|
73
|
+
check-type-assertions: true
|
|
74
|
+
check-blank: true # `_ = err` is a violation
|
|
75
|
+
|
|
76
|
+
govet:
|
|
77
|
+
enable-all: true
|
|
78
|
+
settings:
|
|
79
|
+
shadow:
|
|
80
|
+
strict: true
|
|
81
|
+
fieldalignment:
|
|
82
|
+
# On by default; this catches struct layouts wasting memory.
|
|
83
|
+
# Disable per-file with //nolint:fieldalignment ONLY for boundary types
|
|
84
|
+
# whose JSON tag order matters for OpenAPI doc stability.
|
|
85
|
+
|
|
86
|
+
errorlint:
|
|
87
|
+
errorf: true # %w mandatory for wrapping
|
|
88
|
+
asserts: true # errors.As over type-assertion on `error`
|
|
89
|
+
comparison: true # errors.Is over ==
|
|
90
|
+
|
|
91
|
+
gocognit:
|
|
92
|
+
min-complexity: 25
|
|
93
|
+
|
|
94
|
+
gocyclo:
|
|
95
|
+
min-complexity: 15
|
|
96
|
+
|
|
97
|
+
funlen:
|
|
98
|
+
lines: 90
|
|
99
|
+
statements: 60
|
|
100
|
+
ignore-comments: true
|
|
101
|
+
|
|
102
|
+
lll:
|
|
103
|
+
line-length: 120
|
|
104
|
+
tab-width: 4
|
|
105
|
+
|
|
106
|
+
nestif:
|
|
107
|
+
min-complexity: 4
|
|
108
|
+
|
|
109
|
+
exhaustive:
|
|
110
|
+
default-signifies-exhaustive: false
|
|
111
|
+
check:
|
|
112
|
+
- switch
|
|
113
|
+
- map
|
|
114
|
+
|
|
115
|
+
gosec:
|
|
116
|
+
excludes:
|
|
117
|
+
- G104 # handled by errcheck/errorlint
|
|
118
|
+
- G304 # file path provided as input — too noisy for CLIs
|
|
119
|
+
|
|
120
|
+
sloglint:
|
|
121
|
+
no-mixed-args: true # all attr or all key-value, never mixed
|
|
122
|
+
kv-only: false
|
|
123
|
+
attr-only: true # force slog.String(...) form
|
|
124
|
+
no-global: all # disallow slog.Info; force a logger receiver
|
|
125
|
+
context: scope # require *Context variants where ctx is in scope
|
|
126
|
+
static-msg: true # msg must be a string literal (not fmt.Sprintf)
|
|
127
|
+
no-raw-keys: true # use slog.String("key", ...) not raw "key", "val"
|
|
128
|
+
key-naming-case: snake
|
|
129
|
+
|
|
130
|
+
testifylint:
|
|
131
|
+
enable-all: true
|
|
132
|
+
disable:
|
|
133
|
+
- require-error # We DO use assert.Error in table-driven loops
|
|
134
|
+
|
|
135
|
+
revive:
|
|
136
|
+
severity: warning
|
|
137
|
+
rules:
|
|
138
|
+
- name: var-naming
|
|
139
|
+
- name: package-comments
|
|
140
|
+
- name: exported
|
|
141
|
+
- name: error-return
|
|
142
|
+
- name: error-naming
|
|
143
|
+
- name: errorf # use fmt.Errorf instead of errors.New(fmt.Sprintf)
|
|
144
|
+
- name: if-return
|
|
145
|
+
- name: indent-error-flow
|
|
146
|
+
- name: range-val-in-closure
|
|
147
|
+
- name: redefines-builtin-id
|
|
148
|
+
- name: superfluous-else
|
|
149
|
+
- name: unhandled-error
|
|
150
|
+
arguments:
|
|
151
|
+
- "fmt.Print.*"
|
|
152
|
+
- "fmt.Fprint.*"
|
|
153
|
+
|
|
154
|
+
perfsprint:
|
|
155
|
+
integer-format: true
|
|
156
|
+
error-format: true
|
|
157
|
+
bool-format: true
|
|
158
|
+
string-format: true
|
|
159
|
+
|
|
160
|
+
goimports:
|
|
161
|
+
local-prefixes:
|
|
162
|
+
- github.com/your-org
|
|
163
|
+
|
|
164
|
+
issues:
|
|
165
|
+
max-issues-per-linter: 0
|
|
166
|
+
max-same-issues: 0
|
|
167
|
+
exclude-rules:
|
|
168
|
+
# Tests get a longer leash on funlen + lll
|
|
169
|
+
- path: _test\.go
|
|
170
|
+
linters:
|
|
171
|
+
- funlen
|
|
172
|
+
- lll
|
|
173
|
+
- dupl
|
|
174
|
+
- gosec
|
|
175
|
+
# Generated code never lints
|
|
176
|
+
- path: \.pb\.go$
|
|
177
|
+
linters: [all]
|
|
178
|
+
- path: \.connect\.go$
|
|
179
|
+
linters: [all]
|
|
180
|
+
- path: ^.*sqlc/.*\.sql\.go$
|
|
181
|
+
linters: [all]
|
|
182
|
+
|
|
183
|
+
formatters:
|
|
184
|
+
enable:
|
|
185
|
+
- gofumpt
|
|
186
|
+
- goimports
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Per-linter rationale (why each is on)
|
|
190
|
+
|
|
191
|
+
| Linter | What it catches | Why no compromise |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `errcheck` (incl. `check-blank: true`) | `_ = err`, ignored errors from `Close()`, `Write()`, `json.Marshal()` | Silent error ignore is the #1 Go bug class. Banning `_ = err` forces a decision at every site. |
|
|
194
|
+
| `errorlint` | `err == io.EOF` instead of `errors.Is(err, io.EOF)`; missing `%w` in `fmt.Errorf` | Once you wrap in middleware, `==` checks silently break. `errors.Is/As` is the only safe form. |
|
|
195
|
+
| `nilerr` / `nilnil` | `return nil` after `err != nil`; `return nil, nil` from `(*T, error)` | Classic AI-generated bugs. Linter catches them mechanically. |
|
|
196
|
+
| `bodyclose` | `defer resp.Body.Close()` missed | Single most common Go memory leak. |
|
|
197
|
+
| `contextcheck` | `ctx := context.Background()` inside a function that received `ctx` | Breaks cancellation propagation — the entire reason ctx exists. |
|
|
198
|
+
| `exhaustive` | `switch x.(type)` missing a sealed-interface variant | **Go's weakest type-system spot.** This linter is the closest thing to compiler-enforced exhaustiveness. |
|
|
199
|
+
| `sloglint` | `slog.Info(...)` (global), mixed `Any`/typed attrs | Without this, structured logging silently degrades into string concatenation. |
|
|
200
|
+
| `govet/shadow` strict | `err := ... ; if ... { err := ...; ... }` shadowing | Hides the real error from outer scope — extremely common. |
|
|
201
|
+
| `govet/fieldalignment` | Struct field order wasting memory | Cheap correctness signal. Disable per-file when JSON tag order matters for OpenAPI. |
|
|
202
|
+
| `copyloopvar` + `intrange` | Pre-1.22 loop-var capture and old `for i := 0; i < N; i++` | The language modernized; the lint enforces it. |
|
|
203
|
+
| `usetesting` | `os.Setenv` / `os.Mkdir` in tests instead of `t.Setenv` / `t.TempDir` | Avoids test isolation bugs. |
|
|
204
|
+
| `gocognit` / `gocyclo` / `funlen` | Functions exceeding cognitive thresholds | Direct architectural signal — same purpose as the 250 LOC ceiling, at function granularity. |
|
|
205
|
+
| `gosec` | CWE patterns — SQL injection, weak crypto, path traversal | Production must pass this. |
|
|
206
|
+
| `testifylint` | `assert.Equal` where `require.Equal` was meant; `ObjectsAreEqual` misuse | Subtle test-correctness bugs. |
|
|
207
|
+
| `perfsprint` | `fmt.Sprintf("%d", n)` instead of `strconv.Itoa(n)` | 5–10x faster in tight loops, lints catch the lazy form. |
|
|
208
|
+
|
|
209
|
+
## `nolint` policy
|
|
210
|
+
|
|
211
|
+
`//nolint:linter1,linter2 // <reason>` is permitted with **two hard rules**:
|
|
212
|
+
|
|
213
|
+
1. **One linter at a time per directive.** No `//nolint:all`. No omitting the linter name.
|
|
214
|
+
2. **A reason after `//` is mandatory.** "Generated code", "false positive — protobuf imports", "OpenAPI field order" are acceptable. "Ignore" is not.
|
|
215
|
+
|
|
216
|
+
The skill auto-rejects `//nolint` without a reason. So does `revive` if you enable its `nolint` rule.
|
|
217
|
+
|
|
218
|
+
## CI gate
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
gofumpt -l . | (! grep .) # format
|
|
222
|
+
golangci-lint run --timeout 5m ./... # everything above
|
|
223
|
+
go vet -vettool=$(which fieldalignment) ./... # extra check (also in govet)
|
|
224
|
+
nilaway ./... # nil-deref static analysis
|
|
225
|
+
go test -race -shuffle=on -count=1 ./... # races + ordering
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Any non-zero exit = the change does not ship.
|
|
229
|
+
|
|
230
|
+
## Sources
|
|
231
|
+
|
|
232
|
+
- golangci-lint v2 docs: https://golangci-lint.run/docs/configuration/
|
|
233
|
+
- staticcheck rules: https://staticcheck.dev/docs/checks
|
|
234
|
+
- sloglint: https://github.com/go-simpler/sloglint
|
|
235
|
+
- exhaustive: https://github.com/nishanths/exhaustive
|
|
236
|
+
- nilaway: https://github.com/uber-go/nilaway
|