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,118 @@
|
|
|
1
|
+
# Memory reference
|
|
2
|
+
|
|
3
|
+
Memory bugs are quiet. They don't break tests. They get noticed when a process OOMs in production at 3am.
|
|
4
|
+
|
|
5
|
+
## The big classes of leak
|
|
6
|
+
|
|
7
|
+
### 1. Unbounded cache
|
|
8
|
+
```js
|
|
9
|
+
// Bad — grows forever
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
function get(key) {
|
|
12
|
+
if (!cache.has(key)) cache.set(key, expensive(key));
|
|
13
|
+
return cache.get(key);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Good — bounded
|
|
17
|
+
import { LRUCache } from 'lru-cache';
|
|
18
|
+
const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5 });
|
|
19
|
+
```
|
|
20
|
+
Any `Map` / `dict` keyed by user input is a candidate leak.
|
|
21
|
+
|
|
22
|
+
### 2. Event listeners not removed
|
|
23
|
+
```js
|
|
24
|
+
// Bad — every component instance adds a listener; unmount doesn't remove it
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
window.addEventListener('resize', handler);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
// Good
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
window.addEventListener('resize', handler);
|
|
32
|
+
return () => window.removeEventListener('resize', handler);
|
|
33
|
+
}, []);
|
|
34
|
+
```
|
|
35
|
+
Same for: `setInterval`, `setTimeout` (for long timers), `IntersectionObserver`, `MutationObserver`, EventEmitter `.on`, Postgres / Redis client subscriptions, WebSocket.
|
|
36
|
+
|
|
37
|
+
### 3. Closures retaining large parents
|
|
38
|
+
```js
|
|
39
|
+
// Bad — handler closes over `largeData` even though it only uses `id`
|
|
40
|
+
function makeHandler(largeData) {
|
|
41
|
+
const id = largeData.id;
|
|
42
|
+
return () => doStuff(id, largeData); // largeData stays alive as long as handler exists
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Better — destructure what you need
|
|
46
|
+
function makeHandler({ id }) {
|
|
47
|
+
return () => doStuff(id);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
Watch for: handlers attached to long-lived emitters, cached partial applications, React `useCallback` deps that include big objects.
|
|
51
|
+
|
|
52
|
+
### 4. Detached DOM
|
|
53
|
+
```js
|
|
54
|
+
// Bad — node removed from DOM, but JS still references it
|
|
55
|
+
const cache = { lastTooltip: tooltipEl };
|
|
56
|
+
tooltipEl.remove(); // removed from DOM but cache still holds → not GC'd
|
|
57
|
+
```
|
|
58
|
+
Null out references when you're done.
|
|
59
|
+
|
|
60
|
+
### 5. Streams not consumed / not closed
|
|
61
|
+
```js
|
|
62
|
+
// Bad — leaks file handle if `process` throws
|
|
63
|
+
const stream = fs.createReadStream(path);
|
|
64
|
+
await process(stream);
|
|
65
|
+
|
|
66
|
+
// Good
|
|
67
|
+
const stream = fs.createReadStream(path);
|
|
68
|
+
try { await process(stream); } finally { stream.destroy(); }
|
|
69
|
+
```
|
|
70
|
+
Same for: DB cursors, HTTP responses (in Node, `res.on('data')` without consuming), child processes.
|
|
71
|
+
|
|
72
|
+
### 6. Globals and module-level state
|
|
73
|
+
A module-level `Map`, `Array`, or counter that's appended to from request handlers but never trimmed is a leak.
|
|
74
|
+
|
|
75
|
+
## Buffer vs stream
|
|
76
|
+
|
|
77
|
+
If the input can be unbounded (user upload, log file, ML response), **stream** it:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
// Bad — loads full body into memory
|
|
81
|
+
const body = await req.text();
|
|
82
|
+
const data = JSON.parse(body);
|
|
83
|
+
|
|
84
|
+
// Good — streaming JSON parser, or process line-by-line
|
|
85
|
+
import readline from 'readline';
|
|
86
|
+
const rl = readline.createInterface({ input: req });
|
|
87
|
+
for await (const line of rl) handle(JSON.parse(line));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
In Python: read `chunk = f.read(64 * 1024)` in a loop, or use generators. Don't `f.read()` an arbitrary file.
|
|
91
|
+
|
|
92
|
+
## Large objects in request scope
|
|
93
|
+
|
|
94
|
+
Holding a 50 MB result set in memory per concurrent request → 50 reqs = 2.5 GB. Patterns:
|
|
95
|
+
|
|
96
|
+
- Paginate. Don't return "all".
|
|
97
|
+
- Project (`SELECT id, name`) — don't `SELECT *`.
|
|
98
|
+
- Stream the response. In Node, write chunks to `res`. In Python/Django, `StreamingHttpResponse`.
|
|
99
|
+
|
|
100
|
+
## "Deep clone to be safe" anti-pattern
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const copy = JSON.parse(JSON.stringify(huge)); // O(n) time + O(n) memory + breaks Dates, Maps, etc.
|
|
104
|
+
```
|
|
105
|
+
Almost always wrong. Either you need a true clone (use `structuredClone`) or you don't (just spread the keys you need).
|
|
106
|
+
|
|
107
|
+
## Observable signs in prod
|
|
108
|
+
|
|
109
|
+
- RSS / heap grows monotonically over hours and doesn't drop after low traffic → cache or listener leak.
|
|
110
|
+
- Pods OOM-killed during long-running migrations → buffer instead of stream.
|
|
111
|
+
- Latency P99 climbs slowly during a deploy's lifetime, drops at restart → GC pressure from leak.
|
|
112
|
+
|
|
113
|
+
## Tools
|
|
114
|
+
|
|
115
|
+
- Node: `node --inspect`, Chrome DevTools "Memory" → take heap snapshots over time, diff them.
|
|
116
|
+
- Browser: same DevTools panel, "Detached elements" view for DOM leaks.
|
|
117
|
+
- Python: `tracemalloc`, `objgraph` for retention paths.
|
|
118
|
+
- Production: track RSS over time; alert on monotonic growth.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# N+1 query reference
|
|
2
|
+
|
|
3
|
+
The single most common performance bug shipped by AI assistants. One outer query, then one query per row.
|
|
4
|
+
|
|
5
|
+
## How to spot it
|
|
6
|
+
|
|
7
|
+
Any of these is N+1 until proven otherwise:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
// JS / TS
|
|
11
|
+
for (const user of users) {
|
|
12
|
+
const posts = await db.post.findMany({ where: { userId: user.id } });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
users.map(async u => await fetchProfile(u.id)); // hidden — runs in parallel but still N calls
|
|
16
|
+
await Promise.all(users.map(u => fetchProfile(u.id))); // parallel — still N HTTP calls
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# Python / Django
|
|
21
|
+
for user in users:
|
|
22
|
+
posts = Post.objects.filter(user=user) # N+1
|
|
23
|
+
profile = user.profile # N+1 if not select_related
|
|
24
|
+
|
|
25
|
+
# SQLAlchemy
|
|
26
|
+
for user in session.query(User).all():
|
|
27
|
+
print(user.posts) # lazy load = N+1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Active Record
|
|
32
|
+
@users.each do |u|
|
|
33
|
+
puts u.posts.count # N+1 + extra count query
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Heuristic: if the loop body calls anything that hits the DB, network, or filesystem — stop and batch.
|
|
38
|
+
|
|
39
|
+
## Fixes — by ORM
|
|
40
|
+
|
|
41
|
+
### Prisma (TS)
|
|
42
|
+
```ts
|
|
43
|
+
// Bad
|
|
44
|
+
for (const u of users) { u.posts = await db.post.findMany({ where: { userId: u.id } }); }
|
|
45
|
+
|
|
46
|
+
// Good — eager via include
|
|
47
|
+
const users = await db.user.findMany({ include: { posts: true } });
|
|
48
|
+
|
|
49
|
+
// Good — explicit batch
|
|
50
|
+
const posts = await db.post.findMany({ where: { userId: { in: users.map(u => u.id) } } });
|
|
51
|
+
const byUser = Map.groupBy(posts, p => p.userId); // or reduce
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Drizzle (TS)
|
|
55
|
+
```ts
|
|
56
|
+
// Bad
|
|
57
|
+
for (const u of users) await db.select().from(posts).where(eq(posts.userId, u.id));
|
|
58
|
+
|
|
59
|
+
// Good
|
|
60
|
+
const all = await db.select().from(posts).where(inArray(posts.userId, users.map(u => u.id)));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Django
|
|
64
|
+
```python
|
|
65
|
+
# Bad
|
|
66
|
+
for u in User.objects.all():
|
|
67
|
+
print(u.profile.bio) # N+1
|
|
68
|
+
|
|
69
|
+
# Good
|
|
70
|
+
for u in User.objects.select_related('profile'): # FK / OneToOne — JOIN
|
|
71
|
+
print(u.profile.bio)
|
|
72
|
+
|
|
73
|
+
for u in User.objects.prefetch_related('posts'): # reverse FK / M2M — batched IN query
|
|
74
|
+
print(list(u.posts.all()))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### SQLAlchemy
|
|
78
|
+
```python
|
|
79
|
+
# Bad: default lazy
|
|
80
|
+
users = session.query(User).all()
|
|
81
|
+
for u in users: u.posts # N+1
|
|
82
|
+
|
|
83
|
+
# Good
|
|
84
|
+
from sqlalchemy.orm import selectinload, joinedload
|
|
85
|
+
users = session.query(User).options(selectinload(User.posts)).all()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Active Record (Ruby)
|
|
89
|
+
```ruby
|
|
90
|
+
# Bad
|
|
91
|
+
@users.each { |u| u.posts.each { |p| ... } }
|
|
92
|
+
|
|
93
|
+
# Good
|
|
94
|
+
@users = User.includes(:posts)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Raw SQL — the underlying fix
|
|
98
|
+
|
|
99
|
+
All ORMs are doing one of these under the hood:
|
|
100
|
+
|
|
101
|
+
```sql
|
|
102
|
+
-- JOIN (good for 1:1 and small 1:many)
|
|
103
|
+
SELECT u.*, p.*
|
|
104
|
+
FROM users u
|
|
105
|
+
LEFT JOIN posts p ON p.user_id = u.id
|
|
106
|
+
WHERE u.tenant_id = $1;
|
|
107
|
+
|
|
108
|
+
-- Two queries with IN (better when "many" is large — avoids row duplication)
|
|
109
|
+
SELECT * FROM users WHERE tenant_id = $1;
|
|
110
|
+
SELECT * FROM posts WHERE user_id IN ($1, $2, ..., $N);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Prefer the second form when the "many" side is wide. JOIN-then-group inflates payload by `O(parents * children)`.
|
|
114
|
+
|
|
115
|
+
## DataLoader pattern (when you can't restructure)
|
|
116
|
+
|
|
117
|
+
In GraphQL / per-request batching, where requests come in one at a time but you want to coalesce:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const userLoader = new DataLoader(async (ids) => {
|
|
121
|
+
const users = await db.user.findMany({ where: { id: { in: ids } } });
|
|
122
|
+
return ids.map(id => users.find(u => u.id === id));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Now N parallel resolver calls become 1 batched query within the request tick.
|
|
126
|
+
await Promise.all(userIds.map(id => userLoader.load(id)));
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## When N+1 is actually fine (rare)
|
|
130
|
+
|
|
131
|
+
- N is bounded and tiny (`n <= 5` forever, e.g. enum lookup).
|
|
132
|
+
- The "loop" is async-streaming over an unbounded source (e.g. event-by-event processing where batching would break correctness).
|
|
133
|
+
- Each call is to a different sharded service and cannot be batched.
|
|
134
|
+
|
|
135
|
+
In all these cases, **leave a comment** stating why this is intentional, otherwise the next pass will "fix" it back into an N+1.
|
|
136
|
+
|
|
137
|
+
## Quick mental test
|
|
138
|
+
|
|
139
|
+
Read the code aloud: "for each user, fetch its posts". If you say "for each X, fetch Y" — it's N+1. Rephrase as: "fetch all posts for these users."
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cpp-map-double-lookup
|
|
3
|
+
title: map.count(k) then map[k] — two lookups; use find()
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: cpp
|
|
8
|
+
tags: cpp, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# map.count(k) then map[k] — two lookups; use find()
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `auto it = m.find(k); if (it != m.end()) use(it->second);` — one lookup.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```cpp
|
|
22
|
+
if (m.count(k)) {
|
|
23
|
+
use(m[k]); // second lookup
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```cpp
|
|
30
|
+
auto it = m.find(k);
|
|
31
|
+
if (it != m.end()) {
|
|
32
|
+
use(it->second);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Escalate to
|
|
37
|
+
|
|
38
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cpp-range-loop-copy
|
|
3
|
+
title: Range-for `auto x` copies each element — use `const auto&` for non-trivial types
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: cpp
|
|
8
|
+
tags: cpp, info
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Range-for `auto x` copies each element — use `const auto&` for non-trivial types
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Prefer `for (const auto& x : container)` unless you intentionally need a copy.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```cpp
|
|
22
|
+
for (auto s : large_strings) {
|
|
23
|
+
process(s); // s is a fresh copy each iteration
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```cpp
|
|
30
|
+
for (const auto& s : large_strings) {
|
|
31
|
+
process(s); // reference, no copy
|
|
32
|
+
}
|
|
33
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cpp-raw-new
|
|
3
|
+
title: Raw `new` outside of smart-pointer ctor — manual delete, exception-unsafe
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: cpp
|
|
8
|
+
tags: cpp, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Raw `new` outside of smart-pointer ctor — manual delete, exception-unsafe
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `std::make_unique<T>(...)` or `std::make_shared<T>(...)`. Reserve raw `new` for placement-new or interop with C APIs.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```cpp
|
|
22
|
+
Widget* w = new Widget(42);
|
|
23
|
+
configure(w); // if this throws, w leaks
|
|
24
|
+
delete w;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```cpp
|
|
30
|
+
auto w = std::make_unique<Widget>(42);
|
|
31
|
+
configure(w.get()); // destructs on every exit path, including throw
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Escalate to
|
|
35
|
+
|
|
36
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cpp-string-concat-in-loop
|
|
3
|
+
title: std::string += inside loop — O(n^2) without reserve()
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: cpp
|
|
8
|
+
tags: cpp, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# std::string += inside loop — O(n^2) without reserve()
|
|
12
|
+
|
|
13
|
+
`std::string::operator+=` past capacity reallocates. Without a `reserve`, repeated concatenation is O(n²). `std::ostringstream` or one `reserve` fixes it.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Call `s.reserve(total)` once if size known, or accumulate into a `std::ostringstream` and call `.str()` at the end.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```cpp
|
|
22
|
+
std::string s;
|
|
23
|
+
for (const auto& part : parts) {
|
|
24
|
+
s += part;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```cpp
|
|
31
|
+
std::ostringstream os;
|
|
32
|
+
for (const auto& part : parts) {
|
|
33
|
+
os << part;
|
|
34
|
+
}
|
|
35
|
+
auto s = os.str();
|
|
36
|
+
|
|
37
|
+
// Or, if size known:
|
|
38
|
+
std::string s;
|
|
39
|
+
s.reserve(total_size);
|
|
40
|
+
for (const auto& part : parts) s += part;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Escalate to
|
|
44
|
+
|
|
45
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cpp-vector-push-no-reserve
|
|
3
|
+
title: vector push_back in loop without reserve() — log-amortized reallocation
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: cpp
|
|
8
|
+
tags: cpp, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# vector push_back in loop without reserve() — log-amortized reallocation
|
|
12
|
+
|
|
13
|
+
`std::vector::push_back` past the current capacity doubles the backing array and copies/moves every element. Calling `reserve(n)` first does one allocation.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Call `v.reserve(n)` before the loop when n is known.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```cpp
|
|
22
|
+
std::vector<int> v;
|
|
23
|
+
for (int i = 0; i < n; ++i) {
|
|
24
|
+
v.push_back(compute(i)); // reallocates log(n) times
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```cpp
|
|
31
|
+
std::vector<int> v;
|
|
32
|
+
v.reserve(n);
|
|
33
|
+
for (int i = 0; i < n; ++i) {
|
|
34
|
+
v.push_back(compute(i));
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Escalate to
|
|
39
|
+
|
|
40
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cs-async-void
|
|
3
|
+
title: async void — exceptions are unobserved and crash the process
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: csharp
|
|
8
|
+
tags: csharp, error, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# async void — exceptions are unobserved and crash the process
|
|
12
|
+
|
|
13
|
+
`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`).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Return Task. async void is only acceptable for true event handlers (OnClick, etc.).
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```csharp
|
|
22
|
+
public async void DoWork() {
|
|
23
|
+
await Task.Delay(100);
|
|
24
|
+
throw new Exception("boom"); // crashes the process
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```csharp
|
|
31
|
+
public async Task DoWork() {
|
|
32
|
+
await Task.Delay(100);
|
|
33
|
+
throw new Exception("boom"); // caller can await + catch
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Event handlers stay async void with a try/catch at the boundary:
|
|
37
|
+
private async void OnClick(object s, EventArgs e) {
|
|
38
|
+
try { await DoWorkAsync(); }
|
|
39
|
+
catch (Exception ex) { _log.LogError(ex, "OnClick failed"); }
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Escalate to
|
|
44
|
+
|
|
45
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cs-disposable-no-using
|
|
3
|
+
title: IDisposable allocated without using — leak on exception
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: csharp
|
|
8
|
+
tags: csharp, warning
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# IDisposable allocated without using — leak on exception
|
|
12
|
+
|
|
13
|
+
`IDisposable` resources allocated without `using` leak if an exception fires before `Dispose()`. `using var` ensures cleanup even on throw.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Wrap in `using var x = new ...;` or `using (var x = ...) { }`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```csharp
|
|
22
|
+
var stream = new FileStream(path, FileMode.Open);
|
|
23
|
+
var data = ReadAll(stream); // if this throws, stream is never disposed
|
|
24
|
+
stream.Dispose();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```csharp
|
|
30
|
+
using var stream = new FileStream(path, FileMode.Open);
|
|
31
|
+
var data = ReadAll(stream); // disposed on every exit path
|
|
32
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cs-list-contains-in-loop
|
|
3
|
+
title: List.Contains inside LINQ/loop — O(n*m); use HashSet<T>
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: csharp
|
|
8
|
+
tags: csharp, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# List.Contains inside LINQ/loop — O(n*m); use HashSet<T>
|
|
12
|
+
|
|
13
|
+
`List<T>.Contains` is O(n). Inside LINQ over m items it is O(n·m). `HashSet<T>` is O(1).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Convert to HashSet once: `var s = new HashSet<T>(list); s.Contains(x);`
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```csharp
|
|
22
|
+
// O(n·m)
|
|
23
|
+
var active = users.Where(u => !banned.Contains(u.Id)).ToList();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```csharp
|
|
29
|
+
// O(n+m)
|
|
30
|
+
var bannedSet = new HashSet<int>(banned);
|
|
31
|
+
var active = users.Where(u => !bannedSet.Contains(u.Id)).ToList();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Escalate to
|
|
35
|
+
|
|
36
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cs-string-concat-in-loop
|
|
3
|
+
title: string += inside loop — O(n^2) on immutable string
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: csharp
|
|
8
|
+
tags: csharp, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# string += inside loop — O(n^2) on immutable string
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use StringBuilder: `var sb = new StringBuilder(); foreach (...) sb.Append(x); sb.ToString();`
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```csharp
|
|
22
|
+
// O(n²)
|
|
23
|
+
string s = "";
|
|
24
|
+
foreach (var line in lines) {
|
|
25
|
+
s += line + "\n";
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
// O(n)
|
|
33
|
+
var sb = new StringBuilder(lines.Count * 80);
|
|
34
|
+
foreach (var line in lines) {
|
|
35
|
+
sb.Append(line).Append('\n');
|
|
36
|
+
}
|
|
37
|
+
var s = sb.ToString();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Escalate to
|
|
41
|
+
|
|
42
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: go-defer-in-loop
|
|
3
|
+
title: defer inside loop — defers accumulate until function returns
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: go
|
|
8
|
+
tags: go, warning
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# defer inside loop — defers accumulate until function returns
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Wrap the body in a function so each iteration's defer fires at iteration end, or close resources explicitly.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```go
|
|
22
|
+
for _, f := range files {
|
|
23
|
+
fp, _ := os.Open(f)
|
|
24
|
+
defer fp.Close() // accumulates; nothing closes until the outer function returns
|
|
25
|
+
process(fp)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```go
|
|
32
|
+
for _, f := range files {
|
|
33
|
+
func() {
|
|
34
|
+
fp, _ := os.Open(f)
|
|
35
|
+
defer fp.Close() // closes at end of this iteration
|
|
36
|
+
process(fp)
|
|
37
|
+
}()
|
|
38
|
+
}
|
|
39
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: go-err-not-checked
|
|
3
|
+
title: Error return value discarded — silent failures
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: go
|
|
8
|
+
tags: go, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Error return value discarded — silent failures
|
|
12
|
+
|
|
13
|
+
Discarding the error return with `_` is silent failure. The function looks like it succeeded; downstream code sees zero values and behaves unpredictably.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Handle the error or comment why it is safe to ignore (`_ = err // <reason>`).
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```go
|
|
22
|
+
data, _ := os.ReadFile(path)
|
|
23
|
+
return parse(data) // parse on empty buffer if file missing
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```go
|
|
29
|
+
data, err := os.ReadFile(path)
|
|
30
|
+
if err != nil {
|
|
31
|
+
return nil, fmt.Errorf("read %s: %w", path, err)
|
|
32
|
+
}
|
|
33
|
+
return parse(data), nil
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Escalate to
|
|
37
|
+
|
|
38
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|