lemmaly 0.1.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/LICENSE +201 -0
- package/README.md +238 -0
- package/cli/gen-agents-md.js +60 -0
- package/cli/gen-rule-docs.js +885 -0
- package/cli/lemmaly.js +162 -0
- package/commands/benchmark.md +40 -0
- package/commands/budget.md +53 -0
- package/commands/complexity.md +26 -0
- package/commands/cut.md +27 -0
- package/commands/hotpath.md +22 -0
- package/commands/invariant.md +22 -0
- package/commands/n-plus-one.md +20 -0
- package/commands/profile.md +34 -0
- package/commands/regress.md +43 -0
- package/commands/scale-check.md +37 -0
- package/commands/ship-check.md +26 -0
- package/package.json +48 -0
- package/rules/cpp.json +46 -0
- package/rules/csharp.json +38 -0
- package/rules/go.json +46 -0
- package/rules/java.json +38 -0
- package/rules/javascript.json +102 -0
- package/rules/php.json +38 -0
- package/rules/python.json +62 -0
- package/rules/ruby.json +38 -0
- package/rules/rust.json +38 -0
- package/rules/shell.json +38 -0
- package/rules/sql.json +54 -0
- package/skills/complexity-cuts/SKILL.md +259 -0
- package/skills/invariant-guard/SKILL.md +310 -0
- package/skills/lemmaly/AGENTS.md +1869 -0
- package/skills/lemmaly/SKILL.md +365 -0
- package/skills/lemmaly/references/async.md +135 -0
- package/skills/lemmaly/references/complexity.md +66 -0
- package/skills/lemmaly/references/hot-paths.md +87 -0
- package/skills/lemmaly/references/memory.md +118 -0
- package/skills/lemmaly/references/n-plus-one.md +139 -0
- package/skills/lemmaly/rules/cpp-map-double-lookup.md +38 -0
- package/skills/lemmaly/rules/cpp-range-loop-copy.md +33 -0
- package/skills/lemmaly/rules/cpp-raw-new.md +36 -0
- package/skills/lemmaly/rules/cpp-string-concat-in-loop.md +45 -0
- package/skills/lemmaly/rules/cpp-vector-push-no-reserve.md +40 -0
- package/skills/lemmaly/rules/cs-async-void.md +45 -0
- package/skills/lemmaly/rules/cs-disposable-no-using.md +32 -0
- package/skills/lemmaly/rules/cs-list-contains-in-loop.md +36 -0
- package/skills/lemmaly/rules/cs-string-concat-in-loop.md +42 -0
- package/skills/lemmaly/rules/go-defer-in-loop.md +39 -0
- package/skills/lemmaly/rules/go-err-not-checked.md +38 -0
- package/skills/lemmaly/rules/go-loop-var-capture.md +47 -0
- package/skills/lemmaly/rules/go-slice-append-no-cap.md +39 -0
- package/skills/lemmaly/rules/go-string-concat-in-loop.md +44 -0
- package/skills/lemmaly/rules/java-arraylist-remove-in-for-i.md +44 -0
- package/skills/lemmaly/rules/java-bare-catch-exception.md +42 -0
- package/skills/lemmaly/rules/java-list-contains-in-loop.md +40 -0
- package/skills/lemmaly/rules/java-string-concat-in-loop.md +42 -0
- package/skills/lemmaly/rules/js-anonymous-handler-jsx.md +31 -0
- package/skills/lemmaly/rules/js-array-key-index.md +29 -0
- package/skills/lemmaly/rules/js-async-in-foreach.md +43 -0
- package/skills/lemmaly/rules/js-await-in-for-loop.md +41 -0
- package/skills/lemmaly/rules/js-deep-clone-via-json.md +33 -0
- package/skills/lemmaly/rules/js-helper-call-in-iterator.md +41 -0
- package/skills/lemmaly/rules/js-includes-in-iterator.md +37 -0
- package/skills/lemmaly/rules/js-inline-object-jsx-prop.md +35 -0
- package/skills/lemmaly/rules/js-nested-for-loops.md +45 -0
- package/skills/lemmaly/rules/js-spread-in-reduce.md +38 -0
- package/skills/lemmaly/rules/js-unique-via-indexof.md +35 -0
- package/skills/lemmaly/rules/js-useeffect-missing-deps.md +33 -0
- package/skills/lemmaly/rules/php-count-in-for-condition.md +45 -0
- package/skills/lemmaly/rules/php-in-array-in-loop.md +42 -0
- package/skills/lemmaly/rules/php-loose-equality.md +35 -0
- package/skills/lemmaly/rules/php-query-in-loop.md +47 -0
- package/skills/lemmaly/rules/py-bare-except.md +39 -0
- package/skills/lemmaly/rules/py-django-loop-without-eager.md +42 -0
- package/skills/lemmaly/rules/py-in-list-literal.md +37 -0
- package/skills/lemmaly/rules/py-mutable-default-arg.md +39 -0
- package/skills/lemmaly/rules/py-open-without-with.md +33 -0
- package/skills/lemmaly/rules/py-range-len.md +35 -0
- package/skills/lemmaly/rules/py-string-concat-in-loop.md +43 -0
- package/skills/lemmaly/rules/rb-bare-rescue.md +41 -0
- package/skills/lemmaly/rules/rb-include-in-iterator.md +37 -0
- package/skills/lemmaly/rules/rb-n-plus-one-activerecord.md +39 -0
- package/skills/lemmaly/rules/rb-string-concat-in-loop.md +39 -0
- package/skills/lemmaly/rules/rs-clone-in-loop.md +38 -0
- package/skills/lemmaly/rules/rs-string-push-no-capacity.md +43 -0
- package/skills/lemmaly/rules/rs-unwrap-in-prod.md +36 -0
- package/skills/lemmaly/rules/rs-vec-push-no-capacity.md +42 -0
- package/skills/lemmaly/rules/sh-for-ls.md +41 -0
- package/skills/lemmaly/rules/sh-set-e-no-pipefail.md +37 -0
- package/skills/lemmaly/rules/sh-unquoted-var.md +35 -0
- package/skills/lemmaly/rules/sh-useless-cat-pipe.md +32 -0
- package/skills/lemmaly/rules/sql-leading-wildcard-like.md +34 -0
- package/skills/lemmaly/rules/sql-not-in-subquery.md +38 -0
- package/skills/lemmaly/rules/sql-or-in-where.md +35 -0
- package/skills/lemmaly/rules/sql-select-no-limit.md +37 -0
- package/skills/lemmaly/rules/sql-select-star.md +29 -0
- package/skills/lemmaly/rules/sql-update-no-where.md +35 -0
- package/skills/mathguard/SKILL.md +277 -0
|
@@ -0,0 +1,1869 @@
|
|
|
1
|
+
# Lemmaly Rule Catalog (compiled)
|
|
2
|
+
|
|
3
|
+
Full compiled view of every CLI rule with hand-authored Incorrect/Correct examples and escalation path. Generated from `rules/*.json` and `skills/lemmaly/rules/*.md`.
|
|
4
|
+
|
|
5
|
+
For per-rule documents, load `skills/lemmaly/rules/<rule-id>.md`. For the deterministic CLI, run `node cli/lemmaly.js scan <path>`.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## CRITICAL — will break or scale-fail in production
|
|
9
|
+
|
|
10
|
+
### `cs-async-void` — csharp
|
|
11
|
+
|
|
12
|
+
# async void — exceptions are unobserved and crash the process
|
|
13
|
+
|
|
14
|
+
`async void` cannot be awaited. Exceptions raised inside are unobserved and crash the process — they bypass `try/catch` at the call site. The only legitimate use is true event handlers (e.g. `OnClick`).
|
|
15
|
+
|
|
16
|
+
## Fix
|
|
17
|
+
|
|
18
|
+
Return Task. async void is only acceptable for true event handlers (OnClick, etc.).
|
|
19
|
+
|
|
20
|
+
## Incorrect
|
|
21
|
+
|
|
22
|
+
```csharp
|
|
23
|
+
public async void DoWork() {
|
|
24
|
+
await Task.Delay(100);
|
|
25
|
+
throw new Exception("boom"); // crashes the process
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
public async Task DoWork() {
|
|
33
|
+
await Task.Delay(100);
|
|
34
|
+
throw new Exception("boom"); // caller can await + catch
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Event handlers stay async void with a try/catch at the boundary:
|
|
38
|
+
private async void OnClick(object s, EventArgs e) {
|
|
39
|
+
try { await DoWorkAsync(); }
|
|
40
|
+
catch (Exception ex) { _log.LogError(ex, "OnClick failed"); }
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Escalate to
|
|
45
|
+
|
|
46
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
47
|
+
|
|
48
|
+
### `go-loop-var-capture` — go
|
|
49
|
+
|
|
50
|
+
# Loop variable captured by goroutine — pre-1.22 races on the last value
|
|
51
|
+
|
|
52
|
+
Before Go 1.22, the loop variable in `for ... := range` was reused across iterations. Goroutines that closed over it all read the *same* (final) value. Go 1.22+ fixes this at the language level, but the pattern still appears in libraries that target older versions.
|
|
53
|
+
|
|
54
|
+
## Fix
|
|
55
|
+
|
|
56
|
+
Pin the variable inside the loop: `i := i` before `go func() { use(i) }()`, or pass as argument: `go func(i int) { ... }(i)`. (Fixed automatically in Go 1.22+.)
|
|
57
|
+
|
|
58
|
+
## Incorrect
|
|
59
|
+
|
|
60
|
+
```go
|
|
61
|
+
// Pre-1.22: all goroutines print the last item
|
|
62
|
+
for _, v := range items {
|
|
63
|
+
go func() {
|
|
64
|
+
fmt.Println(v)
|
|
65
|
+
}()
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Correct
|
|
70
|
+
|
|
71
|
+
```go
|
|
72
|
+
// Pin the variable explicitly
|
|
73
|
+
for _, v := range items {
|
|
74
|
+
v := v
|
|
75
|
+
go func() { fmt.Println(v) }()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Or pass as a parameter
|
|
79
|
+
for _, v := range items {
|
|
80
|
+
go func(v string) { fmt.Println(v) }(v)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Escalate to
|
|
85
|
+
|
|
86
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
87
|
+
|
|
88
|
+
### `java-arraylist-remove-in-for-i` — java
|
|
89
|
+
|
|
90
|
+
# list.remove(i) inside for-i — index shifts; ConcurrentModification risk
|
|
91
|
+
|
|
92
|
+
Removing from an `ArrayList` inside a `for (int i = 0; i < list.size(); i++)` shifts indices and skips elements — and on a `Collections.synchronizedList` or in concurrent code, it throws `ConcurrentModificationException`.
|
|
93
|
+
|
|
94
|
+
## Fix
|
|
95
|
+
|
|
96
|
+
Iterate backwards, use Iterator.remove(), or collect indexes and remove in one removeIf().
|
|
97
|
+
|
|
98
|
+
## Incorrect
|
|
99
|
+
|
|
100
|
+
```java
|
|
101
|
+
for (int i = 0; i < list.size(); i++) {
|
|
102
|
+
if (shouldRemove(list.get(i))) {
|
|
103
|
+
list.remove(i); // shifts everything; next element is skipped
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Correct
|
|
109
|
+
|
|
110
|
+
```java
|
|
111
|
+
// Idiomatic and correct
|
|
112
|
+
list.removeIf(this::shouldRemove);
|
|
113
|
+
|
|
114
|
+
// Or, with an explicit iterator:
|
|
115
|
+
var it = list.iterator();
|
|
116
|
+
while (it.hasNext()) {
|
|
117
|
+
if (shouldRemove(it.next())) it.remove();
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Escalate to
|
|
122
|
+
|
|
123
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
124
|
+
|
|
125
|
+
### `js-async-in-foreach` — javascript
|
|
126
|
+
|
|
127
|
+
# async passed to .forEach — returned promises are dropped
|
|
128
|
+
|
|
129
|
+
`Array.prototype.forEach` ignores return values. Passing an `async` function returns promises that are dropped — errors are swallowed and the caller continues before the work finishes.
|
|
130
|
+
|
|
131
|
+
## Fix
|
|
132
|
+
|
|
133
|
+
Use `for (const x of arr) await fn(x)` or `await Promise.all(arr.map(fn))`.
|
|
134
|
+
|
|
135
|
+
## Incorrect
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// Promises dropped; errors silently swallowed
|
|
139
|
+
items.forEach(async (item) => {
|
|
140
|
+
await save(item);
|
|
141
|
+
});
|
|
142
|
+
console.log('done'); // logs before any save completes
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Correct
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Sequential, with errors propagated
|
|
149
|
+
for (const item of items) {
|
|
150
|
+
await save(item);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parallel
|
|
154
|
+
await Promise.all(items.map((item) => save(item)));
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Escalate to
|
|
158
|
+
|
|
159
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
160
|
+
|
|
161
|
+
### `js-await-in-for-loop` — javascript
|
|
162
|
+
|
|
163
|
+
# await inside for-loop — likely N+1 / serialized I/O
|
|
164
|
+
|
|
165
|
+
Awaiting inside a `for` over independent items serializes wall-clock work into O(n × latency). For network or DB calls this is the N+1 problem.
|
|
166
|
+
|
|
167
|
+
## Fix
|
|
168
|
+
|
|
169
|
+
Collect promises into an array and use Promise.all(...), or batch with a bulk query (WHERE id IN (...)).
|
|
170
|
+
|
|
171
|
+
## Incorrect
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// Wall-clock: O(n × latency)
|
|
175
|
+
for (const id of ids) {
|
|
176
|
+
const user = await db.users.findById(id);
|
|
177
|
+
results.push(user);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Correct
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// Wall-clock: O(latency); one bulk query
|
|
185
|
+
const users = await db.users.findMany({ where: { id: { in: ids } } });
|
|
186
|
+
|
|
187
|
+
// Or, if calls are truly independent:
|
|
188
|
+
const results = await Promise.all(ids.map(id => db.users.findById(id)));
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Escalate to
|
|
192
|
+
|
|
193
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
194
|
+
|
|
195
|
+
### `php-query-in-loop` — php
|
|
196
|
+
|
|
197
|
+
# DB query inside loop — N+1
|
|
198
|
+
|
|
199
|
+
A SQL query inside a loop is the textbook N+1 problem. 1000 orders, 1000 round-trips to the DB. Batch with `WHERE id IN (...)` or eager-load in your ORM.
|
|
200
|
+
|
|
201
|
+
## Fix
|
|
202
|
+
|
|
203
|
+
Collect the ids, run ONE `WHERE id IN (...)` query, index by id, then loop. In Eloquent use eager loading: `Model::with('relation')`.
|
|
204
|
+
|
|
205
|
+
## Incorrect
|
|
206
|
+
|
|
207
|
+
```php
|
|
208
|
+
<?php
|
|
209
|
+
foreach ($orderIds as $id) {
|
|
210
|
+
$row = $db->query("SELECT * FROM orders WHERE id = $id"); // 1 query per id
|
|
211
|
+
process($row);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Correct
|
|
216
|
+
|
|
217
|
+
```php
|
|
218
|
+
<?php
|
|
219
|
+
// One bulk query
|
|
220
|
+
$placeholders = implode(',', array_fill(0, count($orderIds), '?'));
|
|
221
|
+
$stmt = $db->prepare("SELECT * FROM orders WHERE id IN ($placeholders)");
|
|
222
|
+
$stmt->execute($orderIds);
|
|
223
|
+
foreach ($stmt->fetchAll() as $row) {
|
|
224
|
+
process($row);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Eloquent / Doctrine: use eager loading
|
|
228
|
+
$orders = Order::with('items')->whereIn('id', $orderIds)->get();
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Escalate to
|
|
232
|
+
|
|
233
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
234
|
+
|
|
235
|
+
### `py-mutable-default-arg` — python
|
|
236
|
+
|
|
237
|
+
# Mutable default argument — shared across calls
|
|
238
|
+
|
|
239
|
+
Python evaluates default arguments once, at function definition. A mutable default (list, dict, set) is **shared across every call** — calls accumulate state from previous calls.
|
|
240
|
+
|
|
241
|
+
## Fix
|
|
242
|
+
|
|
243
|
+
Default to None; create inside: `def f(x=None): x = x or []`.
|
|
244
|
+
|
|
245
|
+
## Incorrect
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
def collect(item, bucket=[]): # shared across all calls!
|
|
249
|
+
bucket.append(item)
|
|
250
|
+
return bucket
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Correct
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
def collect(item, bucket=None):
|
|
257
|
+
if bucket is None:
|
|
258
|
+
bucket = []
|
|
259
|
+
bucket.append(item)
|
|
260
|
+
return bucket
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Escalate to
|
|
264
|
+
|
|
265
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
266
|
+
|
|
267
|
+
### `rb-n-plus-one-activerecord` — ruby
|
|
268
|
+
|
|
269
|
+
# ActiveRecord iteration touching an association — N+1 without includes
|
|
270
|
+
|
|
271
|
+
Iterating an ActiveRecord scope and touching an association fires one extra query per row. `includes` (preload or eager_load) fetches everything in a constant number of queries.
|
|
272
|
+
|
|
273
|
+
## Fix
|
|
274
|
+
|
|
275
|
+
Use `Model.includes(:assoc)` or `:preload` / `:eager_load` before iterating.
|
|
276
|
+
|
|
277
|
+
## Incorrect
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# N+1 queries
|
|
281
|
+
Post.all.each do |p|
|
|
282
|
+
puts p.author.name # 1 extra query per post
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Correct
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
# 2 queries total (or 1 with eager_load)
|
|
290
|
+
Post.includes(:author).each do |p|
|
|
291
|
+
puts p.author.name
|
|
292
|
+
end
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Escalate to
|
|
296
|
+
|
|
297
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
298
|
+
|
|
299
|
+
### `sql-update-no-where` — sql
|
|
300
|
+
|
|
301
|
+
# UPDATE / DELETE without WHERE — touches every row
|
|
302
|
+
|
|
303
|
+
An `UPDATE` or `DELETE` without a `WHERE` clause rewrites every row in the table. In production this is an incident, not a bug.
|
|
304
|
+
|
|
305
|
+
## Fix
|
|
306
|
+
|
|
307
|
+
Add a WHERE clause, or confirm in a comment that touching all rows is intentional.
|
|
308
|
+
|
|
309
|
+
## Incorrect
|
|
310
|
+
|
|
311
|
+
```sql
|
|
312
|
+
UPDATE users SET active = false;
|
|
313
|
+
DELETE FROM sessions;
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Correct
|
|
317
|
+
|
|
318
|
+
```sql
|
|
319
|
+
UPDATE users SET active = false WHERE last_seen_at < NOW() - INTERVAL '90 days';
|
|
320
|
+
DELETE FROM sessions WHERE expires_at < NOW();
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Escalate to
|
|
324
|
+
|
|
325
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
## HIGH — hot-path or correctness risk at realistic n
|
|
329
|
+
|
|
330
|
+
### `cpp-raw-new` — cpp
|
|
331
|
+
|
|
332
|
+
# Raw `new` outside of smart-pointer ctor — manual delete, exception-unsafe
|
|
333
|
+
|
|
334
|
+
Raw `new` requires a matching `delete` on every exit path. If an exception fires between `new` and `delete`, the memory leaks. Smart pointers (`unique_ptr`, `shared_ptr`) destruct automatically.
|
|
335
|
+
|
|
336
|
+
## Fix
|
|
337
|
+
|
|
338
|
+
Use `std::make_unique<T>(...)` or `std::make_shared<T>(...)`. Reserve raw `new` for placement-new or interop with C APIs.
|
|
339
|
+
|
|
340
|
+
## Incorrect
|
|
341
|
+
|
|
342
|
+
```cpp
|
|
343
|
+
Widget* w = new Widget(42);
|
|
344
|
+
configure(w); // if this throws, w leaks
|
|
345
|
+
delete w;
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Correct
|
|
349
|
+
|
|
350
|
+
```cpp
|
|
351
|
+
auto w = std::make_unique<Widget>(42);
|
|
352
|
+
configure(w.get()); // destructs on every exit path, including throw
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Escalate to
|
|
356
|
+
|
|
357
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
358
|
+
|
|
359
|
+
### `cpp-string-concat-in-loop` — cpp
|
|
360
|
+
|
|
361
|
+
# std::string += inside loop — O(n^2) without reserve()
|
|
362
|
+
|
|
363
|
+
`std::string::operator+=` past capacity reallocates. Without a `reserve`, repeated concatenation is O(n²). `std::ostringstream` or one `reserve` fixes it.
|
|
364
|
+
|
|
365
|
+
## Fix
|
|
366
|
+
|
|
367
|
+
Call `s.reserve(total)` once if size known, or accumulate into a `std::ostringstream` and call `.str()` at the end.
|
|
368
|
+
|
|
369
|
+
## Incorrect
|
|
370
|
+
|
|
371
|
+
```cpp
|
|
372
|
+
std::string s;
|
|
373
|
+
for (const auto& part : parts) {
|
|
374
|
+
s += part;
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Correct
|
|
379
|
+
|
|
380
|
+
```cpp
|
|
381
|
+
std::ostringstream os;
|
|
382
|
+
for (const auto& part : parts) {
|
|
383
|
+
os << part;
|
|
384
|
+
}
|
|
385
|
+
auto s = os.str();
|
|
386
|
+
|
|
387
|
+
// Or, if size known:
|
|
388
|
+
std::string s;
|
|
389
|
+
s.reserve(total_size);
|
|
390
|
+
for (const auto& part : parts) s += part;
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Escalate to
|
|
394
|
+
|
|
395
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
396
|
+
|
|
397
|
+
### `cs-disposable-no-using` — csharp
|
|
398
|
+
|
|
399
|
+
# IDisposable allocated without using — leak on exception
|
|
400
|
+
|
|
401
|
+
`IDisposable` resources allocated without `using` leak if an exception fires before `Dispose()`. `using var` ensures cleanup even on throw.
|
|
402
|
+
|
|
403
|
+
## Fix
|
|
404
|
+
|
|
405
|
+
Wrap in `using var x = new ...;` or `using (var x = ...) { }`.
|
|
406
|
+
|
|
407
|
+
## Incorrect
|
|
408
|
+
|
|
409
|
+
```csharp
|
|
410
|
+
var stream = new FileStream(path, FileMode.Open);
|
|
411
|
+
var data = ReadAll(stream); // if this throws, stream is never disposed
|
|
412
|
+
stream.Dispose();
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Correct
|
|
416
|
+
|
|
417
|
+
```csharp
|
|
418
|
+
using var stream = new FileStream(path, FileMode.Open);
|
|
419
|
+
var data = ReadAll(stream); // disposed on every exit path
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### `cs-list-contains-in-loop` — csharp
|
|
423
|
+
|
|
424
|
+
# List.Contains inside LINQ/loop — O(n*m); use HashSet<T>
|
|
425
|
+
|
|
426
|
+
`List<T>.Contains` is O(n). Inside LINQ over m items it is O(n·m). `HashSet<T>` is O(1).
|
|
427
|
+
|
|
428
|
+
## Fix
|
|
429
|
+
|
|
430
|
+
Convert to HashSet once: `var s = new HashSet<T>(list); s.Contains(x);`
|
|
431
|
+
|
|
432
|
+
## Incorrect
|
|
433
|
+
|
|
434
|
+
```csharp
|
|
435
|
+
// O(n·m)
|
|
436
|
+
var active = users.Where(u => !banned.Contains(u.Id)).ToList();
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Correct
|
|
440
|
+
|
|
441
|
+
```csharp
|
|
442
|
+
// O(n+m)
|
|
443
|
+
var bannedSet = new HashSet<int>(banned);
|
|
444
|
+
var active = users.Where(u => !bannedSet.Contains(u.Id)).ToList();
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Escalate to
|
|
448
|
+
|
|
449
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
450
|
+
|
|
451
|
+
### `cs-string-concat-in-loop` — csharp
|
|
452
|
+
|
|
453
|
+
# string += inside loop — O(n^2) on immutable string
|
|
454
|
+
|
|
455
|
+
C# `string` is immutable. `s += x` allocates a new string each iteration — O(n²). `StringBuilder` reuses one buffer; or use `string.Concat` / `string.Join` for known parts.
|
|
456
|
+
|
|
457
|
+
## Fix
|
|
458
|
+
|
|
459
|
+
Use StringBuilder: `var sb = new StringBuilder(); foreach (...) sb.Append(x); sb.ToString();`
|
|
460
|
+
|
|
461
|
+
## Incorrect
|
|
462
|
+
|
|
463
|
+
```csharp
|
|
464
|
+
// O(n²)
|
|
465
|
+
string s = "";
|
|
466
|
+
foreach (var line in lines) {
|
|
467
|
+
s += line + "\n";
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Correct
|
|
472
|
+
|
|
473
|
+
```csharp
|
|
474
|
+
// O(n)
|
|
475
|
+
var sb = new StringBuilder(lines.Count * 80);
|
|
476
|
+
foreach (var line in lines) {
|
|
477
|
+
sb.Append(line).Append('\n');
|
|
478
|
+
}
|
|
479
|
+
var s = sb.ToString();
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Escalate to
|
|
483
|
+
|
|
484
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
485
|
+
|
|
486
|
+
### `go-defer-in-loop` — go
|
|
487
|
+
|
|
488
|
+
# defer inside loop — defers accumulate until function returns
|
|
489
|
+
|
|
490
|
+
`defer` fires when the enclosing *function* returns, not when the loop body ends. Deferring inside a loop over N files holds N open handles until the function exits — easy way to exhaust file descriptors.
|
|
491
|
+
|
|
492
|
+
## Fix
|
|
493
|
+
|
|
494
|
+
Wrap the body in a function so each iteration's defer fires at iteration end, or close resources explicitly.
|
|
495
|
+
|
|
496
|
+
## Incorrect
|
|
497
|
+
|
|
498
|
+
```go
|
|
499
|
+
for _, f := range files {
|
|
500
|
+
fp, _ := os.Open(f)
|
|
501
|
+
defer fp.Close() // accumulates; nothing closes until the outer function returns
|
|
502
|
+
process(fp)
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## Correct
|
|
507
|
+
|
|
508
|
+
```go
|
|
509
|
+
for _, f := range files {
|
|
510
|
+
func() {
|
|
511
|
+
fp, _ := os.Open(f)
|
|
512
|
+
defer fp.Close() // closes at end of this iteration
|
|
513
|
+
process(fp)
|
|
514
|
+
}()
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### `go-err-not-checked` — go
|
|
519
|
+
|
|
520
|
+
# Error return value discarded — silent failures
|
|
521
|
+
|
|
522
|
+
Discarding the error return with `_` is silent failure. The function looks like it succeeded; downstream code sees zero values and behaves unpredictably.
|
|
523
|
+
|
|
524
|
+
## Fix
|
|
525
|
+
|
|
526
|
+
Handle the error or comment why it is safe to ignore (`_ = err // <reason>`).
|
|
527
|
+
|
|
528
|
+
## Incorrect
|
|
529
|
+
|
|
530
|
+
```go
|
|
531
|
+
data, _ := os.ReadFile(path)
|
|
532
|
+
return parse(data) // parse on empty buffer if file missing
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Correct
|
|
536
|
+
|
|
537
|
+
```go
|
|
538
|
+
data, err := os.ReadFile(path)
|
|
539
|
+
if err != nil {
|
|
540
|
+
return nil, fmt.Errorf("read %s: %w", path, err)
|
|
541
|
+
}
|
|
542
|
+
return parse(data), nil
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
## Escalate to
|
|
546
|
+
|
|
547
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
548
|
+
|
|
549
|
+
### `go-string-concat-in-loop` — go
|
|
550
|
+
|
|
551
|
+
# string += inside loop — O(n^2); use strings.Builder
|
|
552
|
+
|
|
553
|
+
Go strings are immutable. `s += x` allocates each iteration — O(n²). `strings.Builder` reuses one backing array.
|
|
554
|
+
|
|
555
|
+
## Fix
|
|
556
|
+
|
|
557
|
+
Use strings.Builder: `var sb strings.Builder; for ... { sb.WriteString(x) }; sb.String()`.
|
|
558
|
+
|
|
559
|
+
## Incorrect
|
|
560
|
+
|
|
561
|
+
```go
|
|
562
|
+
// O(n²)
|
|
563
|
+
var s string
|
|
564
|
+
for _, line := range lines {
|
|
565
|
+
s += line + "\n"
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
## Correct
|
|
570
|
+
|
|
571
|
+
```go
|
|
572
|
+
// O(n)
|
|
573
|
+
var sb strings.Builder
|
|
574
|
+
sb.Grow(len(lines) * 80)
|
|
575
|
+
for _, line := range lines {
|
|
576
|
+
sb.WriteString(line)
|
|
577
|
+
sb.WriteByte('\n')
|
|
578
|
+
}
|
|
579
|
+
s := sb.String()
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
## Escalate to
|
|
583
|
+
|
|
584
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
585
|
+
|
|
586
|
+
### `java-bare-catch-exception` — java
|
|
587
|
+
|
|
588
|
+
# catch (Exception) with empty body or log-only — swallows root cause
|
|
589
|
+
|
|
590
|
+
`catch (Exception e)` with an empty body or just `printStackTrace()` swallows the root cause. The bug lives on, the stack trace evaporates, the production incident has no breadcrumbs.
|
|
591
|
+
|
|
592
|
+
## Fix
|
|
593
|
+
|
|
594
|
+
Catch the narrowest exception, rethrow as a domain exception, or handle with a documented fallback. Never swallow silently.
|
|
595
|
+
|
|
596
|
+
## Incorrect
|
|
597
|
+
|
|
598
|
+
```java
|
|
599
|
+
try {
|
|
600
|
+
riskyCall();
|
|
601
|
+
} catch (Exception e) {
|
|
602
|
+
e.printStackTrace(); // swallowed; caller thinks everything is fine
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## Correct
|
|
607
|
+
|
|
608
|
+
```java
|
|
609
|
+
try {
|
|
610
|
+
riskyCall();
|
|
611
|
+
} catch (IOException e) {
|
|
612
|
+
// narrow exception, rethrow as domain error with cause preserved
|
|
613
|
+
throw new ReadFailedException("riskyCall failed for " + ctx, e);
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
## Escalate to
|
|
618
|
+
|
|
619
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
620
|
+
|
|
621
|
+
### `java-list-contains-in-loop` — java
|
|
622
|
+
|
|
623
|
+
# List.contains inside iterator — O(n*m); use HashSet
|
|
624
|
+
|
|
625
|
+
`List.contains` is O(n). Used inside a stream/iterator over m items it is O(n·m). A `HashSet` lookup is O(1).
|
|
626
|
+
|
|
627
|
+
## Fix
|
|
628
|
+
|
|
629
|
+
Build a HashSet outside: `Set<T> s = new HashSet<>(list); s.contains(x);`
|
|
630
|
+
|
|
631
|
+
## Incorrect
|
|
632
|
+
|
|
633
|
+
```java
|
|
634
|
+
// O(n·m)
|
|
635
|
+
var active = users.stream()
|
|
636
|
+
.filter(u -> !banned.contains(u.id))
|
|
637
|
+
.toList();
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## Correct
|
|
641
|
+
|
|
642
|
+
```java
|
|
643
|
+
// O(n+m)
|
|
644
|
+
var bannedSet = new HashSet<>(banned);
|
|
645
|
+
var active = users.stream()
|
|
646
|
+
.filter(u -> !bannedSet.contains(u.id))
|
|
647
|
+
.toList();
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## Escalate to
|
|
651
|
+
|
|
652
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
653
|
+
|
|
654
|
+
### `java-string-concat-in-loop` — java
|
|
655
|
+
|
|
656
|
+
# String += inside loop — O(n^2) on immutable String
|
|
657
|
+
|
|
658
|
+
Java `String` is immutable. `s += x` allocates a fresh `String` (and an underlying `char[]`) each iteration — O(n²) total work. `StringBuilder` reuses one buffer.
|
|
659
|
+
|
|
660
|
+
## Fix
|
|
661
|
+
|
|
662
|
+
Use StringBuilder: `var sb = new StringBuilder(); for (...) sb.append(x); sb.toString();`
|
|
663
|
+
|
|
664
|
+
## Incorrect
|
|
665
|
+
|
|
666
|
+
```java
|
|
667
|
+
// O(n²)
|
|
668
|
+
String s = "";
|
|
669
|
+
for (var line : lines) {
|
|
670
|
+
s += line + "\n";
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
## Correct
|
|
675
|
+
|
|
676
|
+
```java
|
|
677
|
+
// O(n)
|
|
678
|
+
var sb = new StringBuilder(lines.size() * 80);
|
|
679
|
+
for (var line : lines) {
|
|
680
|
+
sb.append(line).append('\n');
|
|
681
|
+
}
|
|
682
|
+
String s = sb.toString();
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
## Escalate to
|
|
686
|
+
|
|
687
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
688
|
+
|
|
689
|
+
### `js-anonymous-handler-jsx` — javascript
|
|
690
|
+
|
|
691
|
+
# Anonymous arrow handler in JSX — breaks memoized-child equality
|
|
692
|
+
|
|
693
|
+
An anonymous arrow handler is a new function every render. For an ordinary child, this is fine. For a `React.memo` child, it breaks equality and forces a re-render every time.
|
|
694
|
+
|
|
695
|
+
## Fix
|
|
696
|
+
|
|
697
|
+
Wrap with useCallback if the child is memoized. Otherwise ignore.
|
|
698
|
+
|
|
699
|
+
## Incorrect
|
|
700
|
+
|
|
701
|
+
```tsx
|
|
702
|
+
// Child is React.memo — this defeats it
|
|
703
|
+
<MemoButton onClick={() => save(id)} />
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
## Correct
|
|
707
|
+
|
|
708
|
+
```tsx
|
|
709
|
+
const onClick = useCallback(() => save(id), [id]);
|
|
710
|
+
<MemoButton onClick={onClick} />
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### `js-deep-clone-via-json` — javascript
|
|
714
|
+
|
|
715
|
+
# JSON.parse(JSON.stringify(x)) — slow clone; loses Dates/Maps/undefined
|
|
716
|
+
|
|
717
|
+
`JSON.parse(JSON.stringify(x))` is slow, allocates twice, and silently loses `Date`, `Map`, `Set`, `undefined`, `BigInt`, `RegExp`, and any non-enumerable property. It is also unsafe on cyclic structures.
|
|
718
|
+
|
|
719
|
+
## Fix
|
|
720
|
+
|
|
721
|
+
Use structuredClone(x), or copy only the fields you actually need.
|
|
722
|
+
|
|
723
|
+
## Incorrect
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
const copy = JSON.parse(JSON.stringify(state));
|
|
727
|
+
// state.createdAt was a Date — now a string
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
## Correct
|
|
731
|
+
|
|
732
|
+
```ts
|
|
733
|
+
const copy = structuredClone(state); // preserves Date, Map, Set, cycles
|
|
734
|
+
|
|
735
|
+
// Or, when you only need a few fields:
|
|
736
|
+
const copy = { id: state.id, name: state.name };
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### `js-helper-call-in-iterator` — javascript
|
|
740
|
+
|
|
741
|
+
# get*/find*/fetch* helper called inside iterator — likely N round-trips
|
|
742
|
+
|
|
743
|
+
A `get*`/`find*`/`fetch*` helper inside an iterator usually means N independent lookups. If the helper hits a DB or network, this is N+1. If it scans an array, it is O(n × m).
|
|
744
|
+
|
|
745
|
+
## Fix
|
|
746
|
+
|
|
747
|
+
Hoist the helper out of the loop and pass a precomputed lookup (Map/Set) into the iterator, or batch with a single bulk query.
|
|
748
|
+
|
|
749
|
+
## Incorrect
|
|
750
|
+
|
|
751
|
+
```ts
|
|
752
|
+
// N round trips
|
|
753
|
+
const enriched = orders.map((o) => ({
|
|
754
|
+
...o,
|
|
755
|
+
user: getUserById(o.userId),
|
|
756
|
+
}));
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
## Correct
|
|
760
|
+
|
|
761
|
+
```ts
|
|
762
|
+
// 1 round trip
|
|
763
|
+
const userIds = [...new Set(orders.map((o) => o.userId))];
|
|
764
|
+
const users = await db.users.findMany({ where: { id: { in: userIds } } });
|
|
765
|
+
const userById = new Map(users.map((u) => [u.id, u]));
|
|
766
|
+
const enriched = orders.map((o) => ({ ...o, user: userById.get(o.userId) }));
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
## Escalate to
|
|
770
|
+
|
|
771
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
772
|
+
|
|
773
|
+
### `js-inline-object-jsx-prop` — javascript
|
|
774
|
+
|
|
775
|
+
# Inline object literal as JSX prop — new reference every render
|
|
776
|
+
|
|
777
|
+
An inline object literal in JSX creates a new reference every render. If the child is `React.memo`, this defeats the memoization. If it is a dependency of a hook in the child, it re-fires that hook every render.
|
|
778
|
+
|
|
779
|
+
## Fix
|
|
780
|
+
|
|
781
|
+
Hoist outside render, or wrap in useMemo if the child is React.memo.
|
|
782
|
+
|
|
783
|
+
## Incorrect
|
|
784
|
+
|
|
785
|
+
```tsx
|
|
786
|
+
<Chart options={{ animated: true, color: 'red' }} />
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
## Correct
|
|
790
|
+
|
|
791
|
+
```tsx
|
|
792
|
+
// Hoist if static
|
|
793
|
+
const CHART_OPTIONS = { animated: true, color: 'red' };
|
|
794
|
+
<Chart options={CHART_OPTIONS} />
|
|
795
|
+
|
|
796
|
+
// Memoize if derived
|
|
797
|
+
const options = useMemo(() => ({ animated, color }), [animated, color]);
|
|
798
|
+
<Chart options={options} />
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### `js-spread-in-reduce` — javascript
|
|
802
|
+
|
|
803
|
+
# Object spread inside reduce — O(n^2)
|
|
804
|
+
|
|
805
|
+
Object-spread in a reducer copies the accumulator on every iteration — O(n²) work and O(n²) allocation. The result is the same as mutating once.
|
|
806
|
+
|
|
807
|
+
## Fix
|
|
808
|
+
|
|
809
|
+
Mutate the accumulator (`acc[k] = v; return acc`) or use Object.fromEntries(arr.map(...)).
|
|
810
|
+
|
|
811
|
+
## Incorrect
|
|
812
|
+
|
|
813
|
+
```ts
|
|
814
|
+
// O(n²)
|
|
815
|
+
const byId = items.reduce((acc, x) => ({ ...acc, [x.id]: x }), {});
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
## Correct
|
|
819
|
+
|
|
820
|
+
```ts
|
|
821
|
+
// O(n)
|
|
822
|
+
const byId = Object.fromEntries(items.map((x) => [x.id, x]));
|
|
823
|
+
|
|
824
|
+
// Or mutate the accumulator
|
|
825
|
+
const byId = items.reduce((acc, x) => { acc[x.id] = x; return acc; }, {});
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
## Escalate to
|
|
829
|
+
|
|
830
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
831
|
+
|
|
832
|
+
### `js-unique-via-indexof` — javascript
|
|
833
|
+
|
|
834
|
+
# Unique-by-indexOf — O(n^2) dedupe; use `Array.from(new Set(arr))`
|
|
835
|
+
|
|
836
|
+
`.filter((x, i, a) => a.indexOf(x) === i)` is the textbook O(n²) dedupe. A `Set` does it in O(n).
|
|
837
|
+
|
|
838
|
+
## Fix
|
|
839
|
+
|
|
840
|
+
Use `Array.from(new Set(arr))`, or build a Set inline. O(n) instead of O(n^2).
|
|
841
|
+
|
|
842
|
+
## Incorrect
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
// O(n²)
|
|
846
|
+
const unique = arr.filter((x, i, a) => a.indexOf(x) === i);
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
## Correct
|
|
850
|
+
|
|
851
|
+
```ts
|
|
852
|
+
// O(n)
|
|
853
|
+
const unique = Array.from(new Set(arr));
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
## Escalate to
|
|
857
|
+
|
|
858
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
859
|
+
|
|
860
|
+
### `js-useeffect-missing-deps` — javascript
|
|
861
|
+
|
|
862
|
+
# useEffect without a deps array — runs after every render
|
|
863
|
+
|
|
864
|
+
A `useEffect` with no dependency array runs after every render. If the effect updates state, you can get a render loop or wasted work each frame.
|
|
865
|
+
|
|
866
|
+
## Fix
|
|
867
|
+
|
|
868
|
+
Add a deps array — `[]` for mount-only, `[a, b]` to track changes.
|
|
869
|
+
|
|
870
|
+
## Incorrect
|
|
871
|
+
|
|
872
|
+
```tsx
|
|
873
|
+
useEffect(() => {
|
|
874
|
+
setUser(fetchUser(id));
|
|
875
|
+
}); // no deps — runs every render
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
## Correct
|
|
879
|
+
|
|
880
|
+
```tsx
|
|
881
|
+
useEffect(() => {
|
|
882
|
+
setUser(fetchUser(id));
|
|
883
|
+
}, [id]); // runs when id changes
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### `php-count-in-for-condition` — php
|
|
887
|
+
|
|
888
|
+
# count() in for-condition — recomputed every iteration
|
|
889
|
+
|
|
890
|
+
PHP recomputes the loop condition every iteration. `count($a)` on a 100k-element array, called 100k times, is 10B element traversals. Hoist it.
|
|
891
|
+
|
|
892
|
+
## Fix
|
|
893
|
+
|
|
894
|
+
Hoist: `for ($i = 0, $n = count($a); $i < $n; $i++)`. Or use `foreach`.
|
|
895
|
+
|
|
896
|
+
## Incorrect
|
|
897
|
+
|
|
898
|
+
```php
|
|
899
|
+
<?php
|
|
900
|
+
for ($i = 0; $i < count($a); $i++) {
|
|
901
|
+
echo $a[$i];
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
## Correct
|
|
906
|
+
|
|
907
|
+
```php
|
|
908
|
+
<?php
|
|
909
|
+
// Hoist the length
|
|
910
|
+
for ($i = 0, $n = count($a); $i < $n; $i++) {
|
|
911
|
+
echo $a[$i];
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Or, idiomatic
|
|
915
|
+
foreach ($a as $x) {
|
|
916
|
+
echo $x;
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
## Escalate to
|
|
921
|
+
|
|
922
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
923
|
+
|
|
924
|
+
### `php-in-array-in-loop` — php
|
|
925
|
+
|
|
926
|
+
# in_array inside loop — O(n*m); use array_flip + isset
|
|
927
|
+
|
|
928
|
+
`in_array` is a linear scan. Used inside a loop over m items it is O(n·m). `array_flip + isset` is O(1) per check.
|
|
929
|
+
|
|
930
|
+
## Fix
|
|
931
|
+
|
|
932
|
+
`$set = array_flip($haystack); ... isset($set[$x])` is O(1) per check.
|
|
933
|
+
|
|
934
|
+
## Incorrect
|
|
935
|
+
|
|
936
|
+
```php
|
|
937
|
+
<?php
|
|
938
|
+
foreach ($users as $u) {
|
|
939
|
+
if (in_array($u->id, $banned)) continue;
|
|
940
|
+
ship($u);
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
## Correct
|
|
945
|
+
|
|
946
|
+
```php
|
|
947
|
+
<?php
|
|
948
|
+
$bannedSet = array_flip($banned); // O(n) once
|
|
949
|
+
foreach ($users as $u) {
|
|
950
|
+
if (isset($bannedSet[$u->id])) continue; // O(1)
|
|
951
|
+
ship($u);
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
## Escalate to
|
|
956
|
+
|
|
957
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
958
|
+
|
|
959
|
+
### `py-django-loop-without-eager` — python
|
|
960
|
+
|
|
961
|
+
# Django ORM iteration — verify select_related/prefetch_related to avoid N+1
|
|
962
|
+
|
|
963
|
+
Iterating a Django QuerySet that touches a related model triggers one extra query per row — classic N+1. `select_related` (foreign key, one query with JOIN) or `prefetch_related` (reverse / many-to-many, two queries) fixes it.
|
|
964
|
+
|
|
965
|
+
## Fix
|
|
966
|
+
|
|
967
|
+
Add .select_related('fk') for FK/OneToOne, .prefetch_related('rel') for reverse FK / M2M.
|
|
968
|
+
|
|
969
|
+
## Incorrect
|
|
970
|
+
|
|
971
|
+
```python
|
|
972
|
+
# N+1 queries
|
|
973
|
+
for order in Order.objects.all():
|
|
974
|
+
print(order.user.email) # extra query per order
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
## Correct
|
|
978
|
+
|
|
979
|
+
```python
|
|
980
|
+
# 1 query with JOIN
|
|
981
|
+
for order in Order.objects.select_related("user"):
|
|
982
|
+
print(order.user.email)
|
|
983
|
+
|
|
984
|
+
# For reverse FK / M2M:
|
|
985
|
+
for user in User.objects.prefetch_related("orders"):
|
|
986
|
+
for order in user.orders.all():
|
|
987
|
+
...
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
## Escalate to
|
|
991
|
+
|
|
992
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
993
|
+
|
|
994
|
+
### `py-string-concat-in-loop` — python
|
|
995
|
+
|
|
996
|
+
# String += inside loop — O(n^2)
|
|
997
|
+
|
|
998
|
+
Python strings are immutable. `s += x` in a loop allocates and copies the whole `s` each iteration — O(n²) total work. A list join is O(n).
|
|
999
|
+
|
|
1000
|
+
## Fix
|
|
1001
|
+
|
|
1002
|
+
Append to a list, then ''.join(list). Or use io.StringIO.
|
|
1003
|
+
|
|
1004
|
+
## Incorrect
|
|
1005
|
+
|
|
1006
|
+
```python
|
|
1007
|
+
# O(n²)
|
|
1008
|
+
s = ""
|
|
1009
|
+
for line in lines:
|
|
1010
|
+
s += line + "\n"
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
## Correct
|
|
1014
|
+
|
|
1015
|
+
```python
|
|
1016
|
+
# O(n)
|
|
1017
|
+
s = "\n".join(lines) + "\n"
|
|
1018
|
+
|
|
1019
|
+
# Or, for incremental building:
|
|
1020
|
+
parts = []
|
|
1021
|
+
for line in lines:
|
|
1022
|
+
parts.append(line)
|
|
1023
|
+
s = "\n".join(parts)
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
## Escalate to
|
|
1027
|
+
|
|
1028
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1029
|
+
|
|
1030
|
+
### `rb-bare-rescue` — ruby
|
|
1031
|
+
|
|
1032
|
+
# Bare rescue — catches StandardError, hides bugs
|
|
1033
|
+
|
|
1034
|
+
A bare `rescue` (without a class) catches `StandardError` — including `NoMethodError`, `ArgumentError`, and other bugs you almost always want to surface. Catch the specific class.
|
|
1035
|
+
|
|
1036
|
+
## Fix
|
|
1037
|
+
|
|
1038
|
+
Catch a specific class: `rescue Net::ReadTimeout` etc. Bare rescue swallows too much.
|
|
1039
|
+
|
|
1040
|
+
## Incorrect
|
|
1041
|
+
|
|
1042
|
+
```ruby
|
|
1043
|
+
begin
|
|
1044
|
+
fetch_remote
|
|
1045
|
+
rescue
|
|
1046
|
+
retry # also catches typos and bugs in fetch_remote
|
|
1047
|
+
end
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
## Correct
|
|
1051
|
+
|
|
1052
|
+
```ruby
|
|
1053
|
+
begin
|
|
1054
|
+
fetch_remote
|
|
1055
|
+
rescue Net::ReadTimeout, Net::OpenTimeout => e
|
|
1056
|
+
retry
|
|
1057
|
+
end
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
## Escalate to
|
|
1061
|
+
|
|
1062
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
1063
|
+
|
|
1064
|
+
### `rb-include-in-iterator` — ruby
|
|
1065
|
+
|
|
1066
|
+
# Array#include? inside iterator — O(n*m); use Set
|
|
1067
|
+
|
|
1068
|
+
`Array#include?` is O(n). Used inside an iterator over m items it is O(n·m). `Set#include?` is O(1).
|
|
1069
|
+
|
|
1070
|
+
## Fix
|
|
1071
|
+
|
|
1072
|
+
`require 'set'; allowed = Set.new(list); ... allowed.include?(x)`.
|
|
1073
|
+
|
|
1074
|
+
## Incorrect
|
|
1075
|
+
|
|
1076
|
+
```ruby
|
|
1077
|
+
# O(n·m)
|
|
1078
|
+
users.select { |u| banned.include?(u.id) }
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
## Correct
|
|
1082
|
+
|
|
1083
|
+
```ruby
|
|
1084
|
+
# O(n+m)
|
|
1085
|
+
require 'set'
|
|
1086
|
+
banned_set = Set.new(banned)
|
|
1087
|
+
users.select { |u| banned_set.include?(u.id) }
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
## Escalate to
|
|
1091
|
+
|
|
1092
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1093
|
+
|
|
1094
|
+
### `rs-unwrap-in-prod` — rust
|
|
1095
|
+
|
|
1096
|
+
# .unwrap() / .expect() panics on None/Err — surface the error
|
|
1097
|
+
|
|
1098
|
+
`.unwrap()` and `.expect()` panic on `None`/`Err`. In production code they crash the process and lose the structured error. Rust gives you `?`, `match`, and `ok_or` to surface the error to the caller.
|
|
1099
|
+
|
|
1100
|
+
## Fix
|
|
1101
|
+
|
|
1102
|
+
Use `?`, `match`, `unwrap_or`, `unwrap_or_else`, or `ok_or(...)?`. Reserve unwrap for tests and provably infallible paths.
|
|
1103
|
+
|
|
1104
|
+
## Incorrect
|
|
1105
|
+
|
|
1106
|
+
```rust
|
|
1107
|
+
let value = map.get(&key).unwrap(); // panics if key missing
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
## Correct
|
|
1111
|
+
|
|
1112
|
+
```rust
|
|
1113
|
+
let value = map.get(&key).ok_or(Error::MissingKey)?;
|
|
1114
|
+
|
|
1115
|
+
// Or, when None has a meaningful default:
|
|
1116
|
+
let value = map.get(&key).copied().unwrap_or_default();
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
## Escalate to
|
|
1120
|
+
|
|
1121
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
1122
|
+
|
|
1123
|
+
### `sh-for-ls` — shell
|
|
1124
|
+
|
|
1125
|
+
# for f in $(ls ...) — breaks on filenames with spaces / newlines
|
|
1126
|
+
|
|
1127
|
+
`for f in $(ls ...)` breaks on filenames with spaces, tabs, newlines, or globs. The output of `ls` is meant for humans, not programs. Use a glob or `find -print0 | xargs -0`.
|
|
1128
|
+
|
|
1129
|
+
## Fix
|
|
1130
|
+
|
|
1131
|
+
Use glob `for f in *.txt` or `find ... -print0 | xargs -0`. Never parse `ls`.
|
|
1132
|
+
|
|
1133
|
+
## Incorrect
|
|
1134
|
+
|
|
1135
|
+
```shell
|
|
1136
|
+
for f in $(ls *.txt); do
|
|
1137
|
+
process "$f" # breaks on "my file.txt"
|
|
1138
|
+
done
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
## Correct
|
|
1142
|
+
|
|
1143
|
+
```shell
|
|
1144
|
+
# Glob directly
|
|
1145
|
+
for f in *.txt; do
|
|
1146
|
+
process "$f"
|
|
1147
|
+
done
|
|
1148
|
+
|
|
1149
|
+
# Or, when find is necessary
|
|
1150
|
+
find . -name '*.txt' -print0 | xargs -0 -n1 process
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
## Escalate to
|
|
1154
|
+
|
|
1155
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
1156
|
+
|
|
1157
|
+
### `sh-set-e-no-pipefail` — shell
|
|
1158
|
+
|
|
1159
|
+
# set -e without set -o pipefail — failures inside pipes are masked
|
|
1160
|
+
|
|
1161
|
+
`set -e` exits on a failed command, but a failure in the middle of a pipe is masked — only the *last* command's exit status counts. `set -o pipefail` fixes it. `set -u` catches unset variables.
|
|
1162
|
+
|
|
1163
|
+
## Fix
|
|
1164
|
+
|
|
1165
|
+
Use `set -euo pipefail` so the script also fails when an earlier stage in a pipe fails.
|
|
1166
|
+
|
|
1167
|
+
## Incorrect
|
|
1168
|
+
|
|
1169
|
+
```shell
|
|
1170
|
+
#!/bin/bash
|
|
1171
|
+
set -e
|
|
1172
|
+
some_failing_step | grep needle # if some_failing_step fails, script keeps going
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
## Correct
|
|
1176
|
+
|
|
1177
|
+
```shell
|
|
1178
|
+
#!/bin/bash
|
|
1179
|
+
set -euo pipefail
|
|
1180
|
+
some_failing_step | grep needle # any failure in the pipe aborts the script
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
## Escalate to
|
|
1184
|
+
|
|
1185
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
1186
|
+
|
|
1187
|
+
### `sh-unquoted-var` — shell
|
|
1188
|
+
|
|
1189
|
+
# Unquoted $var — word splitting and glob expansion
|
|
1190
|
+
|
|
1191
|
+
An unquoted `$var` is subject to word splitting (on `$IFS`) and glob expansion. A path with a space, a tab, or a `*` will silently do the wrong thing.
|
|
1192
|
+
|
|
1193
|
+
## Fix
|
|
1194
|
+
|
|
1195
|
+
Quote: `"$var"`. Use `"${arr[@]}"` for arrays. Required even for paths you trust.
|
|
1196
|
+
|
|
1197
|
+
## Incorrect
|
|
1198
|
+
|
|
1199
|
+
```shell
|
|
1200
|
+
if [ -d $dir ]; then echo yes; fi
|
|
1201
|
+
# If $dir = "/tmp/with space", expands to: [ -d /tmp/with space ] — syntax error
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
## Correct
|
|
1205
|
+
|
|
1206
|
+
```shell
|
|
1207
|
+
if [ -d "$dir" ]; then echo yes; fi
|
|
1208
|
+
# Always quote. For arrays: "${arr[@]}".
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
## Escalate to
|
|
1212
|
+
|
|
1213
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
1214
|
+
|
|
1215
|
+
### `sql-leading-wildcard-like` — sql
|
|
1216
|
+
|
|
1217
|
+
# LIKE with leading wildcard — cannot use a B-tree index
|
|
1218
|
+
|
|
1219
|
+
A B-tree index sorts by prefix. `LIKE '%foo'` cannot use it — the query scans the table. Use a trigram index (Postgres `pg_trgm`), reverse the column for suffix search, or a full-text search index.
|
|
1220
|
+
|
|
1221
|
+
## Fix
|
|
1222
|
+
|
|
1223
|
+
Use a trigram index (pg_trgm), full-text search, or a reversed-column index.
|
|
1224
|
+
|
|
1225
|
+
## Incorrect
|
|
1226
|
+
|
|
1227
|
+
```sql
|
|
1228
|
+
SELECT * FROM products WHERE name LIKE '%phone';
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
## Correct
|
|
1232
|
+
|
|
1233
|
+
```sql
|
|
1234
|
+
-- Postgres: GIN index on trigrams
|
|
1235
|
+
CREATE INDEX products_name_trgm ON products USING gin (name gin_trgm_ops);
|
|
1236
|
+
SELECT * FROM products WHERE name ILIKE '%phone%';
|
|
1237
|
+
|
|
1238
|
+
-- Or full-text:
|
|
1239
|
+
SELECT * FROM products WHERE to_tsvector(name) @@ to_tsquery('phone');
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
### `sql-not-in-subquery` — sql
|
|
1243
|
+
|
|
1244
|
+
# NOT IN (subquery) — null-unsafe; usually slower than NOT EXISTS
|
|
1245
|
+
|
|
1246
|
+
`NOT IN (subquery)` is null-unsafe: if any row in the subquery is NULL, the whole predicate is NULL (not TRUE), and the outer row is dropped. `NOT EXISTS` is null-safe and usually has a better plan.
|
|
1247
|
+
|
|
1248
|
+
## Fix
|
|
1249
|
+
|
|
1250
|
+
Rewrite as `WHERE NOT EXISTS (SELECT 1 FROM ... WHERE ...)`.
|
|
1251
|
+
|
|
1252
|
+
## Incorrect
|
|
1253
|
+
|
|
1254
|
+
```sql
|
|
1255
|
+
SELECT * FROM orders
|
|
1256
|
+
WHERE user_id NOT IN (SELECT id FROM banned_users);
|
|
1257
|
+
-- One NULL in banned_users.id → returns zero rows
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
## Correct
|
|
1261
|
+
|
|
1262
|
+
```sql
|
|
1263
|
+
SELECT o.* FROM orders o
|
|
1264
|
+
WHERE NOT EXISTS (
|
|
1265
|
+
SELECT 1 FROM banned_users b WHERE b.id = o.user_id
|
|
1266
|
+
);
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
## Escalate to
|
|
1270
|
+
|
|
1271
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
1272
|
+
|
|
1273
|
+
### `sql-select-star` — sql
|
|
1274
|
+
|
|
1275
|
+
# SELECT * — fetches unused columns; blocks index-only scans
|
|
1276
|
+
|
|
1277
|
+
`SELECT *` fetches every column, defeating index-only scans, inflating wire traffic, and breaking downstream code when a column is added/renamed. Project only what you need.
|
|
1278
|
+
|
|
1279
|
+
## Fix
|
|
1280
|
+
|
|
1281
|
+
Name the columns. Smaller payload and more covering-index opportunities.
|
|
1282
|
+
|
|
1283
|
+
## Incorrect
|
|
1284
|
+
|
|
1285
|
+
```sql
|
|
1286
|
+
SELECT * FROM users WHERE id = $1;
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
## Correct
|
|
1290
|
+
|
|
1291
|
+
```sql
|
|
1292
|
+
SELECT id, email, created_at FROM users WHERE id = $1;
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
## MEDIUM — suboptimal; flag when n is large or hot path
|
|
1297
|
+
|
|
1298
|
+
### `cpp-map-double-lookup` — cpp
|
|
1299
|
+
|
|
1300
|
+
# map.count(k) then map[k] — two lookups; use find()
|
|
1301
|
+
|
|
1302
|
+
`m.count(k)` then `m[k]` does two hash lookups (and for `std::map`, two binary searches). `find` returns an iterator that gives both presence and value in one lookup.
|
|
1303
|
+
|
|
1304
|
+
## Fix
|
|
1305
|
+
|
|
1306
|
+
Use `auto it = m.find(k); if (it != m.end()) use(it->second);` — one lookup.
|
|
1307
|
+
|
|
1308
|
+
## Incorrect
|
|
1309
|
+
|
|
1310
|
+
```cpp
|
|
1311
|
+
if (m.count(k)) {
|
|
1312
|
+
use(m[k]); // second lookup
|
|
1313
|
+
}
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
## Correct
|
|
1317
|
+
|
|
1318
|
+
```cpp
|
|
1319
|
+
auto it = m.find(k);
|
|
1320
|
+
if (it != m.end()) {
|
|
1321
|
+
use(it->second);
|
|
1322
|
+
}
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
## Escalate to
|
|
1326
|
+
|
|
1327
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1328
|
+
|
|
1329
|
+
### `cpp-range-loop-copy` — cpp
|
|
1330
|
+
|
|
1331
|
+
# Range-for `auto x` copies each element — use `const auto&` for non-trivial types
|
|
1332
|
+
|
|
1333
|
+
`for (auto x : container)` copies each element into `x`. For non-trivial types (`std::string`, `std::vector<…>`, custom types) this is expensive. `const auto&` borrows, `auto&` mutates in place.
|
|
1334
|
+
|
|
1335
|
+
## Fix
|
|
1336
|
+
|
|
1337
|
+
Prefer `for (const auto& x : container)` unless you intentionally need a copy.
|
|
1338
|
+
|
|
1339
|
+
## Incorrect
|
|
1340
|
+
|
|
1341
|
+
```cpp
|
|
1342
|
+
for (auto s : large_strings) {
|
|
1343
|
+
process(s); // s is a fresh copy each iteration
|
|
1344
|
+
}
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
## Correct
|
|
1348
|
+
|
|
1349
|
+
```cpp
|
|
1350
|
+
for (const auto& s : large_strings) {
|
|
1351
|
+
process(s); // reference, no copy
|
|
1352
|
+
}
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
### `cpp-vector-push-no-reserve` — cpp
|
|
1356
|
+
|
|
1357
|
+
# vector push_back in loop without reserve() — log-amortized reallocation
|
|
1358
|
+
|
|
1359
|
+
`std::vector::push_back` past the current capacity doubles the backing array and copies/moves every element. Calling `reserve(n)` first does one allocation.
|
|
1360
|
+
|
|
1361
|
+
## Fix
|
|
1362
|
+
|
|
1363
|
+
Call `v.reserve(n)` before the loop when n is known.
|
|
1364
|
+
|
|
1365
|
+
## Incorrect
|
|
1366
|
+
|
|
1367
|
+
```cpp
|
|
1368
|
+
std::vector<int> v;
|
|
1369
|
+
for (int i = 0; i < n; ++i) {
|
|
1370
|
+
v.push_back(compute(i)); // reallocates log(n) times
|
|
1371
|
+
}
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
## Correct
|
|
1375
|
+
|
|
1376
|
+
```cpp
|
|
1377
|
+
std::vector<int> v;
|
|
1378
|
+
v.reserve(n);
|
|
1379
|
+
for (int i = 0; i < n; ++i) {
|
|
1380
|
+
v.push_back(compute(i));
|
|
1381
|
+
}
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
## Escalate to
|
|
1385
|
+
|
|
1386
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1387
|
+
|
|
1388
|
+
### `go-slice-append-no-cap` — go
|
|
1389
|
+
|
|
1390
|
+
# append in loop without preallocated capacity — repeated reallocation
|
|
1391
|
+
|
|
1392
|
+
A slice grown by repeated `append` reallocates and copies its backing array each time it crosses a capacity boundary. Preallocating with `make([]T, 0, n)` does one allocation total.
|
|
1393
|
+
|
|
1394
|
+
## Fix
|
|
1395
|
+
|
|
1396
|
+
Preallocate: `out := make([]T, 0, len(in))` then `append`.
|
|
1397
|
+
|
|
1398
|
+
## Incorrect
|
|
1399
|
+
|
|
1400
|
+
```go
|
|
1401
|
+
var out []int
|
|
1402
|
+
for _, x := range in {
|
|
1403
|
+
out = append(out, x*2) // grows; reallocates log(n) times
|
|
1404
|
+
}
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
## Correct
|
|
1408
|
+
|
|
1409
|
+
```go
|
|
1410
|
+
out := make([]int, 0, len(in))
|
|
1411
|
+
for _, x := range in {
|
|
1412
|
+
out = append(out, x*2)
|
|
1413
|
+
}
|
|
1414
|
+
```
|
|
1415
|
+
|
|
1416
|
+
## Escalate to
|
|
1417
|
+
|
|
1418
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1419
|
+
|
|
1420
|
+
### `js-array-key-index` — javascript
|
|
1421
|
+
|
|
1422
|
+
# key={index} on a list — breaks identity for reorderable items
|
|
1423
|
+
|
|
1424
|
+
`key={index}` is fine for a static list. For a list that reorders, inserts, or deletes in the middle, it forces React to mismatch component state with the wrong row — losing input focus, animation, and local state.
|
|
1425
|
+
|
|
1426
|
+
## Fix
|
|
1427
|
+
|
|
1428
|
+
Use a stable id when the list can reorder, insert, or delete in the middle.
|
|
1429
|
+
|
|
1430
|
+
## Incorrect
|
|
1431
|
+
|
|
1432
|
+
```tsx
|
|
1433
|
+
{items.map((item, i) => <Row key={i} item={item} />)}
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
## Correct
|
|
1437
|
+
|
|
1438
|
+
```tsx
|
|
1439
|
+
{items.map((item) => <Row key={item.id} item={item} />)}
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
### `js-includes-in-iterator` — javascript
|
|
1443
|
+
|
|
1444
|
+
# Array.includes inside .map/.filter/.forEach — O(n*m); use a Set
|
|
1445
|
+
|
|
1446
|
+
`.includes` on an array is O(n). Calling it inside `.map`/`.filter`/`.forEach` over m items is O(n × m). A `Set` lookup is O(1).
|
|
1447
|
+
|
|
1448
|
+
## Fix
|
|
1449
|
+
|
|
1450
|
+
Build a Set once outside the iterator, then `set.has(x)` inside.
|
|
1451
|
+
|
|
1452
|
+
## Incorrect
|
|
1453
|
+
|
|
1454
|
+
```ts
|
|
1455
|
+
// O(n × m)
|
|
1456
|
+
const allowed = ['admin', 'editor', 'owner'];
|
|
1457
|
+
const result = users.filter((u) => allowed.includes(u.role));
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
## Correct
|
|
1461
|
+
|
|
1462
|
+
```ts
|
|
1463
|
+
// O(n + m)
|
|
1464
|
+
const allowed = new Set(['admin', 'editor', 'owner']);
|
|
1465
|
+
const result = users.filter((u) => allowed.has(u.role));
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
## Escalate to
|
|
1469
|
+
|
|
1470
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1471
|
+
|
|
1472
|
+
### `js-nested-for-loops` — javascript
|
|
1473
|
+
|
|
1474
|
+
# Nested for-loops — O(n*m); consider hashing one side
|
|
1475
|
+
|
|
1476
|
+
Two `for` loops that check membership between arrays are O(n × m). Hashing one side into a `Set` makes it O(n + m).
|
|
1477
|
+
|
|
1478
|
+
## Fix
|
|
1479
|
+
|
|
1480
|
+
If looking up between the two arrays, put one into a Set/Map first → O(n+m).
|
|
1481
|
+
|
|
1482
|
+
## Incorrect
|
|
1483
|
+
|
|
1484
|
+
```ts
|
|
1485
|
+
// O(n × m)
|
|
1486
|
+
const matches = [];
|
|
1487
|
+
for (const a of left) {
|
|
1488
|
+
for (const b of right) {
|
|
1489
|
+
if (a.id === b.id) matches.push([a, b]);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
## Correct
|
|
1495
|
+
|
|
1496
|
+
```ts
|
|
1497
|
+
// O(n + m)
|
|
1498
|
+
const rightById = new Map(right.map((b) => [b.id, b]));
|
|
1499
|
+
const matches = [];
|
|
1500
|
+
for (const a of left) {
|
|
1501
|
+
const b = rightById.get(a.id);
|
|
1502
|
+
if (b) matches.push([a, b]);
|
|
1503
|
+
}
|
|
1504
|
+
```
|
|
1505
|
+
|
|
1506
|
+
## Escalate to
|
|
1507
|
+
|
|
1508
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1509
|
+
|
|
1510
|
+
### `php-loose-equality` — php
|
|
1511
|
+
|
|
1512
|
+
# == / != — loose equality has surprising coercions
|
|
1513
|
+
|
|
1514
|
+
PHP `==` does type coercion in surprising ways (`"0" == false` is `true`, `"abc" == 0` was `true` before PHP 8). `===` compares value AND type — no surprises.
|
|
1515
|
+
|
|
1516
|
+
## Fix
|
|
1517
|
+
|
|
1518
|
+
Use `===` / `!==` unless type-juggling is explicitly intended (with a comment).
|
|
1519
|
+
|
|
1520
|
+
## Incorrect
|
|
1521
|
+
|
|
1522
|
+
```php
|
|
1523
|
+
<?php
|
|
1524
|
+
if ($status == 0) { /* matches "", "0", false, null, 0, "abc" pre-PHP 8 */ }
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
## Correct
|
|
1528
|
+
|
|
1529
|
+
```php
|
|
1530
|
+
<?php
|
|
1531
|
+
if ($status === 0) { /* matches only int 0 */ }
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
## Escalate to
|
|
1535
|
+
|
|
1536
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
1537
|
+
|
|
1538
|
+
### `py-bare-except` — python
|
|
1539
|
+
|
|
1540
|
+
# Bare except — hides timeouts, OOM, KeyboardInterrupt
|
|
1541
|
+
|
|
1542
|
+
Bare `except:` catches `SystemExit`, `KeyboardInterrupt`, `MemoryError`, and `GeneratorExit` — things you almost never want to swallow. It hides timeouts, OOM, and Ctrl-C.
|
|
1543
|
+
|
|
1544
|
+
## Fix
|
|
1545
|
+
|
|
1546
|
+
Catch specific exceptions: `except (TimeoutError, ConnectionError):`.
|
|
1547
|
+
|
|
1548
|
+
## Incorrect
|
|
1549
|
+
|
|
1550
|
+
```python
|
|
1551
|
+
try:
|
|
1552
|
+
do_work()
|
|
1553
|
+
except:
|
|
1554
|
+
log("failed") # also swallows Ctrl-C, OOM, timeouts
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
## Correct
|
|
1558
|
+
|
|
1559
|
+
```python
|
|
1560
|
+
try:
|
|
1561
|
+
do_work()
|
|
1562
|
+
except Exception as e: # excludes SystemExit, KeyboardInterrupt
|
|
1563
|
+
log(f"failed: {e}")
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
## Escalate to
|
|
1567
|
+
|
|
1568
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
1569
|
+
|
|
1570
|
+
### `py-in-list-literal` — python
|
|
1571
|
+
|
|
1572
|
+
# Membership against a list literal — O(n) per check; use a set
|
|
1573
|
+
|
|
1574
|
+
`x in [a, b, c, ...]` is an O(n) linear scan. Inside a loop over m items, this is O(n × m). A `set` (or a literal `{a, b, c}` for membership) is O(1).
|
|
1575
|
+
|
|
1576
|
+
## Fix
|
|
1577
|
+
|
|
1578
|
+
Hoist to a module-level frozenset({...}).
|
|
1579
|
+
|
|
1580
|
+
## Incorrect
|
|
1581
|
+
|
|
1582
|
+
```python
|
|
1583
|
+
# O(n × m)
|
|
1584
|
+
roles = ["admin", "editor", "owner"]
|
|
1585
|
+
result = [u for u in users if u.role in roles]
|
|
1586
|
+
```
|
|
1587
|
+
|
|
1588
|
+
## Correct
|
|
1589
|
+
|
|
1590
|
+
```python
|
|
1591
|
+
# O(n + m)
|
|
1592
|
+
roles = {"admin", "editor", "owner"}
|
|
1593
|
+
result = [u for u in users if u.role in roles]
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
## Escalate to
|
|
1597
|
+
|
|
1598
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1599
|
+
|
|
1600
|
+
### `py-open-without-with` — python
|
|
1601
|
+
|
|
1602
|
+
# open() without `with` — risk of leaked file descriptors
|
|
1603
|
+
|
|
1604
|
+
`open()` returns a file object. Without `with`, an exception between open and close leaks the file descriptor. Long-running processes (servers, workers) eventually exhaust the OS limit.
|
|
1605
|
+
|
|
1606
|
+
## Fix
|
|
1607
|
+
|
|
1608
|
+
Use `with open(path) as f:` to ensure the handle is released.
|
|
1609
|
+
|
|
1610
|
+
## Incorrect
|
|
1611
|
+
|
|
1612
|
+
```python
|
|
1613
|
+
f = open(path)
|
|
1614
|
+
data = f.read()
|
|
1615
|
+
f.close() # skipped if read() raises
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
## Correct
|
|
1619
|
+
|
|
1620
|
+
```python
|
|
1621
|
+
with open(path) as f:
|
|
1622
|
+
data = f.read()
|
|
1623
|
+
# f is closed even if read() raises
|
|
1624
|
+
```
|
|
1625
|
+
|
|
1626
|
+
### `py-range-len` — python
|
|
1627
|
+
|
|
1628
|
+
# range(len(x)) — un-Pythonic; use enumerate(x)
|
|
1629
|
+
|
|
1630
|
+
`for i in range(len(xs))` then indexing `xs[i]` is un-Pythonic, slower, and forces you to think about indices when you usually want the items. `enumerate` is idiomatic and faster.
|
|
1631
|
+
|
|
1632
|
+
## Fix
|
|
1633
|
+
|
|
1634
|
+
Use `for i, item in enumerate(x):` when you need the index too.
|
|
1635
|
+
|
|
1636
|
+
## Incorrect
|
|
1637
|
+
|
|
1638
|
+
```python
|
|
1639
|
+
for i in range(len(xs)):
|
|
1640
|
+
process(xs[i])
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
## Correct
|
|
1644
|
+
|
|
1645
|
+
```python
|
|
1646
|
+
for x in xs:
|
|
1647
|
+
process(x)
|
|
1648
|
+
|
|
1649
|
+
# When you actually need the index
|
|
1650
|
+
for i, x in enumerate(xs):
|
|
1651
|
+
process(i, x)
|
|
1652
|
+
```
|
|
1653
|
+
|
|
1654
|
+
### `rb-string-concat-in-loop` — ruby
|
|
1655
|
+
|
|
1656
|
+
# String += in loop — O(n^2) since += creates a new string each iteration
|
|
1657
|
+
|
|
1658
|
+
`s += x` creates a new string each iteration — O(n²). `<<` mutates in place (O(n) amortized). `Array#join` is O(n) and idiomatic.
|
|
1659
|
+
|
|
1660
|
+
## Fix
|
|
1661
|
+
|
|
1662
|
+
Use `<<` (mutates) or `parts.join` if building from an array.
|
|
1663
|
+
|
|
1664
|
+
## Incorrect
|
|
1665
|
+
|
|
1666
|
+
```ruby
|
|
1667
|
+
s = ""
|
|
1668
|
+
parts.each { |p| s += p } # O(n²)
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
## Correct
|
|
1672
|
+
|
|
1673
|
+
```ruby
|
|
1674
|
+
# Mutating concat
|
|
1675
|
+
s = String.new
|
|
1676
|
+
parts.each { |p| s << p }
|
|
1677
|
+
|
|
1678
|
+
# Idiomatic
|
|
1679
|
+
s = parts.join
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
## Escalate to
|
|
1683
|
+
|
|
1684
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1685
|
+
|
|
1686
|
+
### `rs-clone-in-loop` — rust
|
|
1687
|
+
|
|
1688
|
+
# .clone() inside iterator — avoid if a borrow works
|
|
1689
|
+
|
|
1690
|
+
A `.clone()` inside an iterator allocates a fresh copy per element. If a borrow (`&x`) or `Rc::clone` (cheap atomic increment for shared ownership) would do, the deep clone is wasted work.
|
|
1691
|
+
|
|
1692
|
+
## Fix
|
|
1693
|
+
|
|
1694
|
+
Borrow with &, use Rc/Arc for shared ownership, or move once outside the loop.
|
|
1695
|
+
|
|
1696
|
+
## Incorrect
|
|
1697
|
+
|
|
1698
|
+
```rust
|
|
1699
|
+
// Deep clones every element
|
|
1700
|
+
let names: Vec<String> = users.iter().map(|u| u.name.clone()).collect();
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
## Correct
|
|
1704
|
+
|
|
1705
|
+
```rust
|
|
1706
|
+
// Borrow when possible
|
|
1707
|
+
let names: Vec<&str> = users.iter().map(|u| u.name.as_str()).collect();
|
|
1708
|
+
|
|
1709
|
+
// Cheap reference-counted clone when shared ownership is needed
|
|
1710
|
+
let shared: Vec<Rc<String>> = users.iter().map(|u| Rc::clone(&u.name)).collect();
|
|
1711
|
+
```
|
|
1712
|
+
|
|
1713
|
+
## Escalate to
|
|
1714
|
+
|
|
1715
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1716
|
+
|
|
1717
|
+
### `rs-string-push-no-capacity` — rust
|
|
1718
|
+
|
|
1719
|
+
# String::new() + push_str in loop — repeated reallocation
|
|
1720
|
+
|
|
1721
|
+
`String::new()` starts at capacity 0. Each `push_str` past capacity reallocates. `with_capacity` or `join` avoids it.
|
|
1722
|
+
|
|
1723
|
+
## Fix
|
|
1724
|
+
|
|
1725
|
+
Preallocate: `String::with_capacity(n)`, or `parts.join(sep)` for known parts.
|
|
1726
|
+
|
|
1727
|
+
## Incorrect
|
|
1728
|
+
|
|
1729
|
+
```rust
|
|
1730
|
+
let mut s = String::new();
|
|
1731
|
+
for part in parts.iter() {
|
|
1732
|
+
s.push_str(part); // reallocates as it grows
|
|
1733
|
+
}
|
|
1734
|
+
```
|
|
1735
|
+
|
|
1736
|
+
## Correct
|
|
1737
|
+
|
|
1738
|
+
```rust
|
|
1739
|
+
let total: usize = parts.iter().map(|p| p.len()).sum();
|
|
1740
|
+
let mut s = String::with_capacity(total);
|
|
1741
|
+
for part in parts.iter() {
|
|
1742
|
+
s.push_str(part);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// Or, simplest:
|
|
1746
|
+
let s = parts.join("");
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
## Escalate to
|
|
1750
|
+
|
|
1751
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1752
|
+
|
|
1753
|
+
### `rs-vec-push-no-capacity` — rust
|
|
1754
|
+
|
|
1755
|
+
# Vec::new() + push in loop — repeated reallocation
|
|
1756
|
+
|
|
1757
|
+
`Vec::new()` starts with capacity 0; each `push` past the current capacity reallocates and copies. Preallocating with `Vec::with_capacity(n)` does one allocation.
|
|
1758
|
+
|
|
1759
|
+
## Fix
|
|
1760
|
+
|
|
1761
|
+
Preallocate: `Vec::with_capacity(n)`, or use `.collect::<Vec<_>>()` over an iterator that has a size hint.
|
|
1762
|
+
|
|
1763
|
+
## Incorrect
|
|
1764
|
+
|
|
1765
|
+
```rust
|
|
1766
|
+
let mut out = Vec::new();
|
|
1767
|
+
for x in input.iter() {
|
|
1768
|
+
out.push(transform(x)); // grows; reallocates log(n) times
|
|
1769
|
+
}
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
## Correct
|
|
1773
|
+
|
|
1774
|
+
```rust
|
|
1775
|
+
let mut out = Vec::with_capacity(input.len());
|
|
1776
|
+
for x in input.iter() {
|
|
1777
|
+
out.push(transform(x));
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Or, when transform is pure:
|
|
1781
|
+
let out: Vec<_> = input.iter().map(transform).collect();
|
|
1782
|
+
```
|
|
1783
|
+
|
|
1784
|
+
## Escalate to
|
|
1785
|
+
|
|
1786
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
1787
|
+
|
|
1788
|
+
### `sh-useless-cat-pipe` — shell
|
|
1789
|
+
|
|
1790
|
+
# cat file | cmd — useless use of cat (UUOC)
|
|
1791
|
+
|
|
1792
|
+
`cat file | cmd` reads the file and pipes through `cat` just to feed `cmd`. Every command that takes a file argument can read it directly — one fewer process, clearer intent.
|
|
1793
|
+
|
|
1794
|
+
## Fix
|
|
1795
|
+
|
|
1796
|
+
Run the command on the file directly: `grep ... file` instead of `cat file | grep ...`.
|
|
1797
|
+
|
|
1798
|
+
## Incorrect
|
|
1799
|
+
|
|
1800
|
+
```shell
|
|
1801
|
+
cat access.log | grep "500"
|
|
1802
|
+
```
|
|
1803
|
+
|
|
1804
|
+
## Correct
|
|
1805
|
+
|
|
1806
|
+
```shell
|
|
1807
|
+
grep "500" access.log
|
|
1808
|
+
|
|
1809
|
+
# Or stdin redirection if the command takes only stdin:
|
|
1810
|
+
cmd < access.log
|
|
1811
|
+
```
|
|
1812
|
+
|
|
1813
|
+
### `sql-or-in-where` — sql
|
|
1814
|
+
|
|
1815
|
+
# OR in WHERE — can prevent index use
|
|
1816
|
+
|
|
1817
|
+
A query planner can use an index on one side of an `OR` but often not both, falling back to a sequential scan. `UNION ALL` or `IN (...)` (when both sides are equality on the same column) usually wins.
|
|
1818
|
+
|
|
1819
|
+
## Fix
|
|
1820
|
+
|
|
1821
|
+
Equality on same column → `WHERE col IN (...)`. Otherwise consider UNION ALL.
|
|
1822
|
+
|
|
1823
|
+
## Incorrect
|
|
1824
|
+
|
|
1825
|
+
```sql
|
|
1826
|
+
SELECT * FROM events
|
|
1827
|
+
WHERE user_id = $1 OR account_id = $1;
|
|
1828
|
+
```
|
|
1829
|
+
|
|
1830
|
+
## Correct
|
|
1831
|
+
|
|
1832
|
+
```sql
|
|
1833
|
+
SELECT * FROM events WHERE user_id = $1
|
|
1834
|
+
UNION ALL
|
|
1835
|
+
SELECT * FROM events WHERE account_id = $1;
|
|
1836
|
+
|
|
1837
|
+
-- Or, when both sides are the same column:
|
|
1838
|
+
SELECT * FROM events WHERE user_id IN ($1, $2, $3);
|
|
1839
|
+
```
|
|
1840
|
+
|
|
1841
|
+
### `sql-select-no-limit` — sql
|
|
1842
|
+
|
|
1843
|
+
# SELECT without LIMIT — unbounded result set
|
|
1844
|
+
|
|
1845
|
+
A query with no `LIMIT` returns however many rows match. On a small table this is fine; on a growing one it eventually OOMs the client or the page. Add a bound, or paginate.
|
|
1846
|
+
|
|
1847
|
+
## Fix
|
|
1848
|
+
|
|
1849
|
+
Add LIMIT, or paginate by id range. Unbounded reads OOM under growth.
|
|
1850
|
+
|
|
1851
|
+
## Incorrect
|
|
1852
|
+
|
|
1853
|
+
```sql
|
|
1854
|
+
SELECT id, email FROM users ORDER BY created_at DESC;
|
|
1855
|
+
```
|
|
1856
|
+
|
|
1857
|
+
## Correct
|
|
1858
|
+
|
|
1859
|
+
```sql
|
|
1860
|
+
SELECT id, email FROM users
|
|
1861
|
+
ORDER BY created_at DESC
|
|
1862
|
+
LIMIT 100;
|
|
1863
|
+
|
|
1864
|
+
-- Keyset pagination for next page:
|
|
1865
|
+
SELECT id, email FROM users
|
|
1866
|
+
WHERE created_at < $1
|
|
1867
|
+
ORDER BY created_at DESC
|
|
1868
|
+
LIMIT 100;
|
|
1869
|
+
```
|