@bfirestone45/opencode-slop-review 0.5.0
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/README.md +23 -0
- package/commands/slop-review-review.md +9 -0
- package/index.js +57 -0
- package/package.json +42 -0
- package/skills/ai-slop-review/SKILL.md +762 -0
- package/skills/ai-slop-review/references/go.md +401 -0
- package/skills/ai-slop-review/references/python.md +263 -0
- package/skills/ai-slop-review/references/rust.md +296 -0
- package/skills/ai-slop-review/references/svelte-ts.md +384 -0
- package/version.txt +1 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Go AI Slop Signals
|
|
2
|
+
|
|
3
|
+
Language-specific signals for Go codebases. These supplement the universal signals
|
|
4
|
+
in SKILL.md -- apply both.
|
|
5
|
+
|
|
6
|
+
## What idiomatic Go looks like
|
|
7
|
+
|
|
8
|
+
### At version 1.26+
|
|
9
|
+
- `log/slog` for structured logging (stdlib since 1.21)
|
|
10
|
+
- Range-over-func iterators (since 1.23)
|
|
11
|
+
- Generic `slices`/`maps` functions over hand-rolled loops
|
|
12
|
+
- `cmp.Or` for default values
|
|
13
|
+
- Structured concurrency patterns with `errgroup`
|
|
14
|
+
- `maps.Clone`, `slices.Concat`, `slices.Contains` etc.
|
|
15
|
+
- New `http.ServeMux` with method-based routing (1.22+)
|
|
16
|
+
|
|
17
|
+
### Stdlib preferences
|
|
18
|
+
- Logging: `log/slog` over `log` or third-party loggers in new code
|
|
19
|
+
- Slices: `slices` package over manual loops for search/sort/compare
|
|
20
|
+
- Maps: `maps` package for clone/keys/values operations
|
|
21
|
+
- HTTP: `http.NewServeMux` patterns with method routing (1.22+)
|
|
22
|
+
- Testing: `testing.TB` helpers, `t.Cleanup` over deferred cleanup
|
|
23
|
+
|
|
24
|
+
### Error handling convention
|
|
25
|
+
- Always wrap errors with `fmt.Errorf("context: %w", err)`
|
|
26
|
+
- Use `errors.Is`/`errors.As` for comparison, never string matching
|
|
27
|
+
- Return errors to caller, do not `log.Fatal` in library or handler code
|
|
28
|
+
- Sentinel errors for expected conditions, wrapped errors for unexpected
|
|
29
|
+
|
|
30
|
+
### Project adaptation
|
|
31
|
+
Before flagging any idiom violation, check if the project's idiom baseline uses a different convention. The baseline overrides these defaults.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Priority order for Go
|
|
36
|
+
|
|
37
|
+
1. Error handling -- this is where AI Go code fails most visibly
|
|
38
|
+
2. Context propagation -- threading context.Context correctly
|
|
39
|
+
3. Concurrency -- goroutine lifecycle, channel ownership, sync primitives
|
|
40
|
+
4. Interface and type design -- idiomatic Go vs. Java-in-Go
|
|
41
|
+
5. Testing -- table-driven tests, test helpers, subtests
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Error handling (highest priority)
|
|
46
|
+
|
|
47
|
+
This is the single biggest tell in AI-generated Go. Humans who write Go daily internalize
|
|
48
|
+
error handling patterns; AI frequently gets them subtly wrong.
|
|
49
|
+
|
|
50
|
+
**Fatal/print instead of return:**
|
|
51
|
+
```go
|
|
52
|
+
// SLOP -- log.Fatal in a library or handler function
|
|
53
|
+
if err != nil {
|
|
54
|
+
log.Fatal(err)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// SLOP -- fmt.Println for errors
|
|
58
|
+
if err != nil {
|
|
59
|
+
fmt.Println("error:", err)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// IDIOMATIC -- return the error to the caller
|
|
63
|
+
if err != nil {
|
|
64
|
+
return fmt.Errorf("fetching user %d: %w", id, err)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Missing error wrapping:**
|
|
69
|
+
```go
|
|
70
|
+
// SLOP -- no context, breaks error unwrapping
|
|
71
|
+
if err != nil {
|
|
72
|
+
return err
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// SLOP -- wraps but without %w, breaks errors.Is/As
|
|
76
|
+
if err != nil {
|
|
77
|
+
return fmt.Errorf("failed to connect: %v", err)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// IDIOMATIC
|
|
81
|
+
if err != nil {
|
|
82
|
+
return fmt.Errorf("connecting to %s: %w", addr, err)
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**String comparison on errors:**
|
|
87
|
+
```go
|
|
88
|
+
// SLOP
|
|
89
|
+
if err.Error() == "not found" {
|
|
90
|
+
// ...
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// IDIOMATIC
|
|
94
|
+
if errors.Is(err, ErrNotFound) {
|
|
95
|
+
// ...
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Swallowed errors:**
|
|
100
|
+
```go
|
|
101
|
+
// SLOP -- error silently discarded
|
|
102
|
+
result, _ := doSomething()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Context propagation
|
|
108
|
+
|
|
109
|
+
**Missing context.Context in signatures:**
|
|
110
|
+
```go
|
|
111
|
+
// SLOP -- no context parameter
|
|
112
|
+
func FetchUser(id int) (*User, error) {
|
|
113
|
+
|
|
114
|
+
// IDIOMATIC -- context is first parameter
|
|
115
|
+
func FetchUser(ctx context.Context, id int) (*User, error) {
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**context.Background() mid-callstack:**
|
|
119
|
+
```go
|
|
120
|
+
// SLOP -- creates a new root context deep in the call chain
|
|
121
|
+
func (s *Service) Process(data []byte) error {
|
|
122
|
+
ctx := context.Background() // should come from caller
|
|
123
|
+
return s.store.Save(ctx, data)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**No cancellation handling:**
|
|
128
|
+
```go
|
|
129
|
+
// SLOP -- ignores context cancellation
|
|
130
|
+
func worker(ctx context.Context) {
|
|
131
|
+
for {
|
|
132
|
+
doWork()
|
|
133
|
+
time.Sleep(time.Second)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// IDIOMATIC
|
|
138
|
+
func worker(ctx context.Context) {
|
|
139
|
+
for {
|
|
140
|
+
select {
|
|
141
|
+
case <-ctx.Done():
|
|
142
|
+
return
|
|
143
|
+
default:
|
|
144
|
+
doWork()
|
|
145
|
+
}
|
|
146
|
+
select {
|
|
147
|
+
case <-ctx.Done():
|
|
148
|
+
return
|
|
149
|
+
case <-time.After(time.Second):
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Concurrency
|
|
158
|
+
|
|
159
|
+
**Goroutine leaks -- no cancellation path:**
|
|
160
|
+
```go
|
|
161
|
+
// SLOP -- goroutine runs forever, no way to stop it
|
|
162
|
+
go func() {
|
|
163
|
+
for {
|
|
164
|
+
process(queue)
|
|
165
|
+
}
|
|
166
|
+
}()
|
|
167
|
+
|
|
168
|
+
// IDIOMATIC -- cancellable via context
|
|
169
|
+
go func() {
|
|
170
|
+
for {
|
|
171
|
+
select {
|
|
172
|
+
case <-ctx.Done():
|
|
173
|
+
return
|
|
174
|
+
case item := <-queue:
|
|
175
|
+
process(item)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}()
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Channel ownership confusion:**
|
|
182
|
+
- Channel created by one goroutine, closed by another (race condition risk)
|
|
183
|
+
- Channel created but never closed (goroutines blocked on range forever)
|
|
184
|
+
- Unbuffered channel used where buffered is needed (deadlock risk)
|
|
185
|
+
|
|
186
|
+
**sync.Mutex overuse:**
|
|
187
|
+
```go
|
|
188
|
+
// SLOP -- mutex protecting a counter that should use atomic
|
|
189
|
+
var mu sync.Mutex
|
|
190
|
+
var count int
|
|
191
|
+
|
|
192
|
+
// BETTER
|
|
193
|
+
var count atomic.Int64
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**sync.Mutex where single-goroutine ownership works:**
|
|
197
|
+
If a struct is only accessed from one goroutine, it does not need a mutex.
|
|
198
|
+
AI often adds mutexes "just in case" -- a strong slop signal.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Type and interface misuse
|
|
203
|
+
|
|
204
|
+
**Interface defined in the wrong package:**
|
|
205
|
+
```go
|
|
206
|
+
// SLOP -- interface defined next to its implementation
|
|
207
|
+
// (Go convention: interfaces belong in the consuming package)
|
|
208
|
+
type UserStore interface {
|
|
209
|
+
GetUser(ctx context.Context, id int) (*User, error)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
type userStore struct { ... }
|
|
213
|
+
func (s *userStore) GetUser(...) { ... }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Premature interface:**
|
|
217
|
+
```go
|
|
218
|
+
// SLOP -- interface with exactly one implementation and one consumer
|
|
219
|
+
type Processor interface {
|
|
220
|
+
Process(data []byte) error
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**`interface{}` instead of `any`, or `any` instead of generics:**
|
|
225
|
+
```go
|
|
226
|
+
// SLOP -- interface{} is an alias for any since 1.18; using the old syntax is dated
|
|
227
|
+
func Contains(slice []interface{}, item interface{}) bool {
|
|
228
|
+
|
|
229
|
+
// STILL SLOP at 1.26+ -- hand-rolled contains; use slices.Contains
|
|
230
|
+
func Contains[T comparable](slice []T, item T) bool {
|
|
231
|
+
|
|
232
|
+
// IDIOMATIC at 1.26+ -- use the stdlib slices package
|
|
233
|
+
import "slices"
|
|
234
|
+
found := slices.Contains(slice, item)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Value receiver on mutating method:**
|
|
238
|
+
```go
|
|
239
|
+
// SLOP -- s is a copy, mutation is lost
|
|
240
|
+
func (s Service) SetTimeout(d time.Duration) {
|
|
241
|
+
s.timeout = d
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// CORRECT
|
|
245
|
+
func (s *Service) SetTimeout(d time.Duration) {
|
|
246
|
+
s.timeout = d
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Other Go idioms
|
|
253
|
+
|
|
254
|
+
**defer inside a loop:**
|
|
255
|
+
```go
|
|
256
|
+
// SLOP -- defers accumulate until function return, not loop iteration
|
|
257
|
+
for _, f := range files {
|
|
258
|
+
fd, _ := os.Open(f)
|
|
259
|
+
defer fd.Close() // all closes happen at function exit
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// CORRECT -- extract to helper or close explicitly
|
|
263
|
+
for _, f := range files {
|
|
264
|
+
if err := processFile(f); err != nil {
|
|
265
|
+
return err
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Hand-rolled slice/map operations (1.26+):**
|
|
271
|
+
```go
|
|
272
|
+
// SLOP -- manual contains check
|
|
273
|
+
found := false
|
|
274
|
+
for _, v := range items {
|
|
275
|
+
if v == target {
|
|
276
|
+
found = true
|
|
277
|
+
break
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// IDIOMATIC at 1.26+ -- use slices package
|
|
282
|
+
found := slices.Contains(items, target)
|
|
283
|
+
|
|
284
|
+
// SLOP -- manual sort
|
|
285
|
+
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
|
|
286
|
+
|
|
287
|
+
// IDIOMATIC at 1.26+
|
|
288
|
+
slices.Sort(items)
|
|
289
|
+
|
|
290
|
+
// SLOP -- manual map key collection
|
|
291
|
+
keys := make([]string, 0, len(m))
|
|
292
|
+
for k := range m {
|
|
293
|
+
keys = append(keys, k)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// IDIOMATIC at 1.26+
|
|
297
|
+
keys := slices.Collect(maps.Keys(m))
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Old-style logging instead of slog (1.26+):**
|
|
301
|
+
```go
|
|
302
|
+
// SLOP in new code -- unstructured logging
|
|
303
|
+
log.Printf("user %d logged in from %s", id, ip)
|
|
304
|
+
|
|
305
|
+
// IDIOMATIC at 1.26+ -- structured logging with log/slog
|
|
306
|
+
slog.Info("user logged in", "user_id", id, "ip", ip)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Default value chains without cmp.Or (1.26+):**
|
|
310
|
+
```go
|
|
311
|
+
// SLOP -- verbose conditional default
|
|
312
|
+
port := os.Getenv("PORT")
|
|
313
|
+
if port == "" {
|
|
314
|
+
port = "8080"
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// IDIOMATIC at 1.26+
|
|
318
|
+
port := cmp.Or(os.Getenv("PORT"), "8080")
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**HTTP server without timeouts:**
|
|
322
|
+
```go
|
|
323
|
+
// SLOP
|
|
324
|
+
http.ListenAndServe(":8080", handler)
|
|
325
|
+
|
|
326
|
+
// IDIOMATIC at 1.26+ -- use method-based routing on ServeMux
|
|
327
|
+
mux := http.NewServeMux()
|
|
328
|
+
mux.HandleFunc("GET /users/{id}", getUser)
|
|
329
|
+
mux.HandleFunc("POST /users", createUser)
|
|
330
|
+
|
|
331
|
+
srv := &http.Server{
|
|
332
|
+
Addr: ":8080",
|
|
333
|
+
Handler: mux,
|
|
334
|
+
ReadTimeout: 15 * time.Second,
|
|
335
|
+
WriteTimeout: 15 * time.Second,
|
|
336
|
+
IdleTimeout: 60 * time.Second,
|
|
337
|
+
}
|
|
338
|
+
srv.ListenAndServe()
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Global singletons via init():**
|
|
342
|
+
```go
|
|
343
|
+
// SLOP
|
|
344
|
+
var db *sql.DB
|
|
345
|
+
|
|
346
|
+
func init() {
|
|
347
|
+
var err error
|
|
348
|
+
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
|
349
|
+
if err != nil {
|
|
350
|
+
log.Fatal(err)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// BETTER -- dependency injection, initialized in main()
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Security signals
|
|
360
|
+
|
|
361
|
+
- SQL via `fmt.Sprintf` instead of parameterized queries (`db.Query(query, args...)`)
|
|
362
|
+
- `http.ListenAndServe` with no TLS and no reverse proxy documented
|
|
363
|
+
- `os/exec.Command` with user input concatenated into the command string
|
|
364
|
+
- `http.Get` / `http.Post` with no timeout (uses `http.DefaultClient` which has no timeout)
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Go-specific test signals
|
|
369
|
+
|
|
370
|
+
**Not using table-driven tests** -- the canonical Go testing pattern:
|
|
371
|
+
```go
|
|
372
|
+
// SLOP -- separate test functions for each case
|
|
373
|
+
func TestParseValid(t *testing.T) { ... }
|
|
374
|
+
func TestParseEmpty(t *testing.T) { ... }
|
|
375
|
+
func TestParseInvalid(t *testing.T) { ... }
|
|
376
|
+
|
|
377
|
+
// IDIOMATIC
|
|
378
|
+
func TestParse(t *testing.T) {
|
|
379
|
+
tests := []struct {
|
|
380
|
+
name string
|
|
381
|
+
input string
|
|
382
|
+
want Result
|
|
383
|
+
wantErr bool
|
|
384
|
+
}{
|
|
385
|
+
{"valid", "good", Result{...}, false},
|
|
386
|
+
{"empty", "", Result{}, true},
|
|
387
|
+
{"invalid", "bad", Result{}, true},
|
|
388
|
+
}
|
|
389
|
+
for _, tt := range tests {
|
|
390
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
391
|
+
got, err := Parse(tt.input)
|
|
392
|
+
// ...
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
- `t.Log` or `fmt.Println` instead of assertion -- test always passes
|
|
399
|
+
- No `t.Helper()` on test helper functions (error locations point to helper, not caller)
|
|
400
|
+
- No `t.Parallel()` on tests that are safe to parallelize
|
|
401
|
+
- `testify` used in a codebase that uses stdlib `testing` everywhere else (or vice versa)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Python AI Slop Signals
|
|
2
|
+
|
|
3
|
+
Language-specific signals for Python codebases. These supplement the universal signals
|
|
4
|
+
in SKILL.md -- apply both.
|
|
5
|
+
|
|
6
|
+
## What idiomatic Python looks like
|
|
7
|
+
|
|
8
|
+
### At version 3.13+
|
|
9
|
+
|
|
10
|
+
- Union types: `X | None` not `Optional[X]`, `X | Y` not `Union[X, Y]`
|
|
11
|
+
- Built-in generics: `list[str]` not `typing.List[str]`, `dict[str, int]` not `typing.Dict[str, int]`
|
|
12
|
+
- `@deprecated` decorator from `warnings`
|
|
13
|
+
- `StrEnum` for string enumerations (stdlib since 3.11)
|
|
14
|
+
- `tomllib` for TOML parsing (stdlib since 3.11)
|
|
15
|
+
- `ExceptionGroup` and `except*` for concurrent error handling
|
|
16
|
+
- Walrus operator `:=` where it reduces duplication
|
|
17
|
+
- Per-interpreter GIL / free-threaded mode awareness
|
|
18
|
+
|
|
19
|
+
### Stdlib preferences
|
|
20
|
+
|
|
21
|
+
- Filesystem: `pathlib.Path` over `os.path` in new code
|
|
22
|
+
- Caching: `functools.cache` (3.9+) or `lru_cache` over hand-rolled memoization
|
|
23
|
+
- Data containers: `dataclasses` or `NamedTuple` over plain dicts for structured data
|
|
24
|
+
- Iteration: `itertools` (product, chain, groupby) over nested manual loops
|
|
25
|
+
- Context managers: `contextlib.contextmanager` over manual `__enter__`/`__exit__`
|
|
26
|
+
- Temporary files: `tempfile` with context managers, never hardcoded paths
|
|
27
|
+
|
|
28
|
+
### Error handling convention
|
|
29
|
+
|
|
30
|
+
- Specific exceptions over bare `except Exception`
|
|
31
|
+
- `raise X from e` to preserve tracebacks
|
|
32
|
+
- EAFP for duck typing, LBYL for everything else
|
|
33
|
+
- `.get()` over try/except KeyError for dict access
|
|
34
|
+
|
|
35
|
+
### Project adaptation
|
|
36
|
+
|
|
37
|
+
Before flagging any idiom violation, check if the project's idiom baseline (from Step 0)
|
|
38
|
+
uses a different convention. The baseline overrides these defaults.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Priority order for Python
|
|
43
|
+
|
|
44
|
+
1. Error handling -- bare excepts, swallowed errors, exceptions for control flow
|
|
45
|
+
2. Idiomatic Python -- are they writing Python or Java-in-Python?
|
|
46
|
+
3. Type system usage -- modern typing vs. no types vs. over-typed
|
|
47
|
+
4. Async correctness -- if async is used, is it used properly?
|
|
48
|
+
5. Security -- eval, exec, shell injection, pickle
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Error handling
|
|
53
|
+
|
|
54
|
+
**Bare exception swallowing** -- the single most common AI Python tell:
|
|
55
|
+
```python
|
|
56
|
+
# SLOP: silent failure
|
|
57
|
+
try:
|
|
58
|
+
result = do_thing()
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# SLOP: catching too broadly then re-raising generically
|
|
63
|
+
try:
|
|
64
|
+
data = parse(raw)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise ValueError(f"Parse failed: {e}") # loses traceback, catches too much
|
|
67
|
+
|
|
68
|
+
# BETTER: specific exceptions, context preserved
|
|
69
|
+
try:
|
|
70
|
+
data = parse(raw)
|
|
71
|
+
except json.JSONDecodeError as e:
|
|
72
|
+
raise ParseError(f"Invalid JSON at position {e.pos}") from e
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Exceptions for control flow** -- using try/except where a conditional or `.get()` works:
|
|
76
|
+
```python
|
|
77
|
+
# SLOP
|
|
78
|
+
try:
|
|
79
|
+
value = data["key"]
|
|
80
|
+
except KeyError:
|
|
81
|
+
value = default
|
|
82
|
+
|
|
83
|
+
# IDIOMATIC
|
|
84
|
+
value = data.get("key", default)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Over-defensive error handling** -- wrapping operations that cannot fail:
|
|
88
|
+
```python
|
|
89
|
+
# SLOP: len() on a list cannot raise
|
|
90
|
+
try:
|
|
91
|
+
count = len(items)
|
|
92
|
+
except Exception:
|
|
93
|
+
count = 0
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Classic footguns AI still produces
|
|
99
|
+
|
|
100
|
+
**Mutable default arguments:**
|
|
101
|
+
```python
|
|
102
|
+
# SLOP -- the list is shared across all calls
|
|
103
|
+
def append_to(item, target=[]):
|
|
104
|
+
target.append(item)
|
|
105
|
+
return target
|
|
106
|
+
|
|
107
|
+
# CORRECT
|
|
108
|
+
def append_to(item, target=None):
|
|
109
|
+
if target is None:
|
|
110
|
+
target = []
|
|
111
|
+
target.append(item)
|
|
112
|
+
return target
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Global state as a crutch:**
|
|
116
|
+
```python
|
|
117
|
+
# SLOP
|
|
118
|
+
_cache = {}
|
|
119
|
+
def get_user(user_id):
|
|
120
|
+
global _cache
|
|
121
|
+
if user_id not in _cache:
|
|
122
|
+
_cache[user_id] = fetch(user_id)
|
|
123
|
+
return _cache[user_id]
|
|
124
|
+
|
|
125
|
+
# BETTER: encapsulate state, or use functools.lru_cache
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Resources without context managers:**
|
|
129
|
+
```python
|
|
130
|
+
# SLOP
|
|
131
|
+
f = open("data.csv")
|
|
132
|
+
data = f.read()
|
|
133
|
+
f.close()
|
|
134
|
+
|
|
135
|
+
# IDIOMATIC
|
|
136
|
+
with open("data.csv") as f:
|
|
137
|
+
data = f.read()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Type and style signals
|
|
143
|
+
|
|
144
|
+
**Missing or outdated type hints** -- with 3.13+ as the minimum floor, these are all
|
|
145
|
+
non-idiomatic and should be flagged:
|
|
146
|
+
- No type hints at all on function signatures
|
|
147
|
+
- `Optional[X]` instead of `X | None` -- the `Optional` alias is legacy
|
|
148
|
+
- `typing.List`, `typing.Dict`, `typing.Tuple`, `typing.Set` instead of `list`, `dict`, `tuple`, `set` -- built-in generics have been available since 3.9
|
|
149
|
+
- `typing.Union[X, Y]` instead of `X | Y` -- the pipe syntax has been available since 3.10
|
|
150
|
+
- Importing from `typing` for constructs that now live in the stdlib (e.g., `typing.NamedTuple` when `typing` import is the only reason)
|
|
151
|
+
|
|
152
|
+
**isinstance chains instead of structural typing:**
|
|
153
|
+
```python
|
|
154
|
+
# SLOP
|
|
155
|
+
def process(item):
|
|
156
|
+
if isinstance(item, str):
|
|
157
|
+
return handle_str(item)
|
|
158
|
+
elif isinstance(item, int):
|
|
159
|
+
return handle_int(item)
|
|
160
|
+
elif isinstance(item, list):
|
|
161
|
+
return handle_list(item)
|
|
162
|
+
|
|
163
|
+
# BETTER: Protocol, singledispatch, or rethink the interface
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Avoiding the stdlib:**
|
|
167
|
+
- `os.path.join` instead of `pathlib.Path` in new code
|
|
168
|
+
- Manual iteration instead of `itertools` (product, chain, groupby)
|
|
169
|
+
- Hand-rolled memoization instead of `functools.lru_cache` or `cache`
|
|
170
|
+
- Manual context managers instead of `contextlib.contextmanager`
|
|
171
|
+
- Custom data containers instead of `dataclasses` or `NamedTuple`
|
|
172
|
+
- `collections.OrderedDict` in Python 3.7+ (regular dicts are ordered)
|
|
173
|
+
|
|
174
|
+
**`*args, **kwargs` on internal functions:**
|
|
175
|
+
```python
|
|
176
|
+
# SLOP -- lazy interface design, impossible to type-check
|
|
177
|
+
def create_user(*args, **kwargs):
|
|
178
|
+
return User(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
# BETTER: explicit parameters with types
|
|
181
|
+
def create_user(name: str, email: str, role: Role = Role.USER) -> User:
|
|
182
|
+
return User(name=name, email=email, role=role)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Async signals
|
|
188
|
+
|
|
189
|
+
**Blocking calls in async code** -- the most insidious AI async mistake:
|
|
190
|
+
```python
|
|
191
|
+
# SLOP -- requests blocks the event loop
|
|
192
|
+
async def fetch_data(url):
|
|
193
|
+
response = requests.get(url) # BLOCKS
|
|
194
|
+
return response.json()
|
|
195
|
+
|
|
196
|
+
# SLOP -- time.sleep blocks the event loop
|
|
197
|
+
async def poll():
|
|
198
|
+
while True:
|
|
199
|
+
await check()
|
|
200
|
+
time.sleep(5) # BLOCKS -- should be await asyncio.sleep(5)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Deprecated async patterns:**
|
|
204
|
+
```python
|
|
205
|
+
# SLOP (deprecated since 3.10)
|
|
206
|
+
loop = asyncio.get_event_loop()
|
|
207
|
+
loop.run_until_complete(main())
|
|
208
|
+
|
|
209
|
+
# IDIOMATIC
|
|
210
|
+
asyncio.run(main())
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Missing await** -- code runs but the coroutine never executes:
|
|
214
|
+
```python
|
|
215
|
+
# SLOP -- result is a coroutine object, not the actual result
|
|
216
|
+
async def process():
|
|
217
|
+
result = fetch_data() # missing await
|
|
218
|
+
return result
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Security signals
|
|
224
|
+
|
|
225
|
+
These are serious -- flag as Critical regardless of other context:
|
|
226
|
+
|
|
227
|
+
- `eval()` or `exec()` on any external or user-influenced input
|
|
228
|
+
- Shell commands with `shell=True` and string formatting for the command
|
|
229
|
+
- `pickle.loads()` / `pickle.load()` on untrusted data
|
|
230
|
+
- Secrets in default argument values, module-level constants, or committed `.env` files
|
|
231
|
+
- `yaml.load()` without `Loader=SafeLoader` (allows arbitrary code execution)
|
|
232
|
+
- SQL built via f-strings or `.format()` instead of parameterized queries
|
|
233
|
+
- `hashlib.md5()` or `hashlib.sha1()` for security purposes (use sha256+)
|
|
234
|
+
- `random` module for security-sensitive values (use `secrets` module)
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Python-specific test signals
|
|
239
|
+
|
|
240
|
+
- `unittest.TestCase` subclasses in a project that uses `pytest` everywhere else
|
|
241
|
+
- `mock.patch` on everything -- tests that mock the entire world test nothing
|
|
242
|
+
- No `parametrize` / `params` where the function has obvious input variations
|
|
243
|
+
- `assert result is not None` as the only assertion (proves nothing about correctness)
|
|
244
|
+
- Test files that import and call the function but do not assert meaningful behavior
|
|
245
|
+
- `conftest.py` with fixtures that do too much setup (hiding test complexity)
|
|
246
|
+
- No `tmp_path` or `tmp_path_factory` -- tests writing to fixed filesystem paths
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Framework-specific notes
|
|
251
|
+
|
|
252
|
+
Be aware that some frameworks have conventions that look unusual:
|
|
253
|
+
|
|
254
|
+
- **Dagster:** `@asset` decorators, `@op`, resource injection via type annotations,
|
|
255
|
+
`MaterializeResult` returns. These are framework conventions, not slop.
|
|
256
|
+
- **Django:** Class-based views, `Meta` inner classes, `models.Manager` subclasses.
|
|
257
|
+
- **FastAPI:** `Depends()` injection, Pydantic models for validation, `async def` route
|
|
258
|
+
handlers that may legitimately use sync ORM calls via `run_in_executor`.
|
|
259
|
+
- **SQLAlchemy:** `Column()`, `relationship()`, declarative base patterns.
|
|
260
|
+
- **Pydantic:** `model_validator`, `field_validator`, `ConfigDict` -- these are idiomatic.
|
|
261
|
+
|
|
262
|
+
Do not flag framework-conventional patterns. Do flag when framework patterns are mixed
|
|
263
|
+
incorrectly (e.g., raw SQL in a project that uses SQLAlchemy ORM everywhere else).
|