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,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-unique-via-indexof
|
|
3
|
+
title: Unique-by-indexOf — O(n^2) dedupe; use `Array.from(new Set(arr))`
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: javascript
|
|
8
|
+
tags: javascript, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Unique-by-indexOf — O(n^2) dedupe; use `Array.from(new Set(arr))`
|
|
12
|
+
|
|
13
|
+
`.filter((x, i, a) => a.indexOf(x) === i)` is the textbook O(n²) dedupe. A `Set` does it in O(n).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `Array.from(new Set(arr))`, or build a Set inline. O(n) instead of O(n^2).
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// O(n²)
|
|
23
|
+
const unique = arr.filter((x, i, a) => a.indexOf(x) === i);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// O(n)
|
|
30
|
+
const unique = Array.from(new Set(arr));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Escalate to
|
|
34
|
+
|
|
35
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-useeffect-missing-deps
|
|
3
|
+
title: useEffect without a deps array — runs after every render
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: javascript
|
|
8
|
+
tags: javascript, warning
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# useEffect without a deps array — runs after every render
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Add a deps array — `[]` for mount-only, `[a, b]` to track changes.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setUser(fetchUser(id));
|
|
24
|
+
}); // no deps — runs every render
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setUser(fetchUser(id));
|
|
32
|
+
}, [id]); // runs when id changes
|
|
33
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: php-count-in-for-condition
|
|
3
|
+
title: count() in for-condition — recomputed every iteration
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: php
|
|
8
|
+
tags: php, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# count() in for-condition — recomputed every iteration
|
|
12
|
+
|
|
13
|
+
PHP recomputes the loop condition every iteration. `count($a)` on a 100k-element array, called 100k times, is 10B element traversals. Hoist it.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Hoist: `for ($i = 0, $n = count($a); $i < $n; $i++)`. Or use `foreach`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```php
|
|
22
|
+
<?php
|
|
23
|
+
for ($i = 0; $i < count($a); $i++) {
|
|
24
|
+
echo $a[$i];
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```php
|
|
31
|
+
<?php
|
|
32
|
+
// Hoist the length
|
|
33
|
+
for ($i = 0, $n = count($a); $i < $n; $i++) {
|
|
34
|
+
echo $a[$i];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Or, idiomatic
|
|
38
|
+
foreach ($a as $x) {
|
|
39
|
+
echo $x;
|
|
40
|
+
}
|
|
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,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: php-in-array-in-loop
|
|
3
|
+
title: in_array inside loop — O(n*m); use array_flip + isset
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: php
|
|
8
|
+
tags: php, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# in_array inside loop — O(n*m); use array_flip + isset
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
`$set = array_flip($haystack); ... isset($set[$x])` is O(1) per check.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```php
|
|
22
|
+
<?php
|
|
23
|
+
foreach ($users as $u) {
|
|
24
|
+
if (in_array($u->id, $banned)) continue;
|
|
25
|
+
ship($u);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```php
|
|
32
|
+
<?php
|
|
33
|
+
$bannedSet = array_flip($banned); // O(n) once
|
|
34
|
+
foreach ($users as $u) {
|
|
35
|
+
if (isset($bannedSet[$u->id])) continue; // O(1)
|
|
36
|
+
ship($u);
|
|
37
|
+
}
|
|
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,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: php-loose-equality
|
|
3
|
+
title: == / != — loose equality has surprising coercions
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: php
|
|
8
|
+
tags: php, info, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# == / != — loose equality has surprising coercions
|
|
12
|
+
|
|
13
|
+
PHP `==` does type coercion in surprising ways (`"0" == false` is `true`, `"abc" == 0` was `true` before PHP 8). `===` compares value AND type — no surprises.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `===` / `!==` unless type-juggling is explicitly intended (with a comment).
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```php
|
|
22
|
+
<?php
|
|
23
|
+
if ($status == 0) { /* matches "", "0", false, null, 0, "abc" pre-PHP 8 */ }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```php
|
|
29
|
+
<?php
|
|
30
|
+
if ($status === 0) { /* matches only int 0 */ }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Escalate to
|
|
34
|
+
|
|
35
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: php-query-in-loop
|
|
3
|
+
title: DB query inside loop — N+1
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: php
|
|
8
|
+
tags: php, error, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# DB query inside loop — N+1
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Collect the ids, run ONE `WHERE id IN (...)` query, index by id, then loop. In Eloquent use eager loading: `Model::with('relation')`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```php
|
|
22
|
+
<?php
|
|
23
|
+
foreach ($orderIds as $id) {
|
|
24
|
+
$row = $db->query("SELECT * FROM orders WHERE id = $id"); // 1 query per id
|
|
25
|
+
process($row);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```php
|
|
32
|
+
<?php
|
|
33
|
+
// One bulk query
|
|
34
|
+
$placeholders = implode(',', array_fill(0, count($orderIds), '?'));
|
|
35
|
+
$stmt = $db->prepare("SELECT * FROM orders WHERE id IN ($placeholders)");
|
|
36
|
+
$stmt->execute($orderIds);
|
|
37
|
+
foreach ($stmt->fetchAll() as $row) {
|
|
38
|
+
process($row);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Eloquent / Doctrine: use eager loading
|
|
42
|
+
$orders = Order::with('items')->whereIn('id', $orderIds)->get();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Escalate to
|
|
46
|
+
|
|
47
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: py-bare-except
|
|
3
|
+
title: Bare except — hides timeouts, OOM, KeyboardInterrupt
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: python
|
|
8
|
+
tags: python, info, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Bare except — hides timeouts, OOM, KeyboardInterrupt
|
|
12
|
+
|
|
13
|
+
Bare `except:` catches `SystemExit`, `KeyboardInterrupt`, `MemoryError`, and `GeneratorExit` — things you almost never want to swallow. It hides timeouts, OOM, and Ctrl-C.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Catch specific exceptions: `except (TimeoutError, ConnectionError):`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
try:
|
|
23
|
+
do_work()
|
|
24
|
+
except:
|
|
25
|
+
log("failed") # also swallows Ctrl-C, OOM, timeouts
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
try:
|
|
32
|
+
do_work()
|
|
33
|
+
except Exception as e: # excludes SystemExit, KeyboardInterrupt
|
|
34
|
+
log(f"failed: {e}")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Escalate to
|
|
38
|
+
|
|
39
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: py-django-loop-without-eager
|
|
3
|
+
title: Django ORM iteration — verify select_related/prefetch_related to avoid N+1
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: python
|
|
8
|
+
tags: python, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Django ORM iteration — verify select_related/prefetch_related to avoid N+1
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Add .select_related('fk') for FK/OneToOne, .prefetch_related('rel') for reverse FK / M2M.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# N+1 queries
|
|
23
|
+
for order in Order.objects.all():
|
|
24
|
+
print(order.user.email) # extra query per order
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# 1 query with JOIN
|
|
31
|
+
for order in Order.objects.select_related("user"):
|
|
32
|
+
print(order.user.email)
|
|
33
|
+
|
|
34
|
+
# For reverse FK / M2M:
|
|
35
|
+
for user in User.objects.prefetch_related("orders"):
|
|
36
|
+
for order in user.orders.all():
|
|
37
|
+
...
|
|
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,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: py-in-list-literal
|
|
3
|
+
title: Membership against a list literal — O(n) per check; use a set
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: python
|
|
8
|
+
tags: python, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Membership against a list literal — O(n) per check; use a set
|
|
12
|
+
|
|
13
|
+
`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).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Hoist to a module-level frozenset({...}).
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# O(n × m)
|
|
23
|
+
roles = ["admin", "editor", "owner"]
|
|
24
|
+
result = [u for u in users if u.role in roles]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# O(n + m)
|
|
31
|
+
roles = {"admin", "editor", "owner"}
|
|
32
|
+
result = [u for u in users if u.role in roles]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Escalate to
|
|
36
|
+
|
|
37
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: py-mutable-default-arg
|
|
3
|
+
title: Mutable default argument — shared across calls
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: python
|
|
8
|
+
tags: python, error, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Mutable default argument — shared across calls
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Default to None; create inside: `def f(x=None): x = x or []`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
def collect(item, bucket=[]): # shared across all calls!
|
|
23
|
+
bucket.append(item)
|
|
24
|
+
return bucket
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
def collect(item, bucket=None):
|
|
31
|
+
if bucket is None:
|
|
32
|
+
bucket = []
|
|
33
|
+
bucket.append(item)
|
|
34
|
+
return bucket
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Escalate to
|
|
38
|
+
|
|
39
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: py-open-without-with
|
|
3
|
+
title: open() without `with` — risk of leaked file descriptors
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: python
|
|
8
|
+
tags: python, info
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# open() without `with` — risk of leaked file descriptors
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `with open(path) as f:` to ensure the handle is released.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
f = open(path)
|
|
23
|
+
data = f.read()
|
|
24
|
+
f.close() # skipped if read() raises
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
with open(path) as f:
|
|
31
|
+
data = f.read()
|
|
32
|
+
# f is closed even if read() raises
|
|
33
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: py-range-len
|
|
3
|
+
title: range(len(x)) — un-Pythonic; use enumerate(x)
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: python
|
|
8
|
+
tags: python, info
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# range(len(x)) — un-Pythonic; use enumerate(x)
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `for i, item in enumerate(x):` when you need the index too.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
for i in range(len(xs)):
|
|
23
|
+
process(xs[i])
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
for x in xs:
|
|
30
|
+
process(x)
|
|
31
|
+
|
|
32
|
+
# When you actually need the index
|
|
33
|
+
for i, x in enumerate(xs):
|
|
34
|
+
process(i, x)
|
|
35
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: py-string-concat-in-loop
|
|
3
|
+
title: String += inside loop — O(n^2)
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: python
|
|
8
|
+
tags: python, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# String += inside loop — O(n^2)
|
|
12
|
+
|
|
13
|
+
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).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Append to a list, then ''.join(list). Or use io.StringIO.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# O(n²)
|
|
23
|
+
s = ""
|
|
24
|
+
for line in lines:
|
|
25
|
+
s += line + "\n"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# O(n)
|
|
32
|
+
s = "\n".join(lines) + "\n"
|
|
33
|
+
|
|
34
|
+
# Or, for incremental building:
|
|
35
|
+
parts = []
|
|
36
|
+
for line in lines:
|
|
37
|
+
parts.append(line)
|
|
38
|
+
s = "\n".join(parts)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Escalate to
|
|
42
|
+
|
|
43
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rb-bare-rescue
|
|
3
|
+
title: Bare rescue — catches StandardError, hides bugs
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: ruby
|
|
8
|
+
tags: ruby, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Bare rescue — catches StandardError, hides bugs
|
|
12
|
+
|
|
13
|
+
A bare `rescue` (without a class) catches `StandardError` — including `NoMethodError`, `ArgumentError`, and other bugs you almost always want to surface. Catch the specific class.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Catch a specific class: `rescue Net::ReadTimeout` etc. Bare rescue swallows too much.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
begin
|
|
23
|
+
fetch_remote
|
|
24
|
+
rescue
|
|
25
|
+
retry # also catches typos and bugs in fetch_remote
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
begin
|
|
33
|
+
fetch_remote
|
|
34
|
+
rescue Net::ReadTimeout, Net::OpenTimeout => e
|
|
35
|
+
retry
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Escalate to
|
|
40
|
+
|
|
41
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rb-include-in-iterator
|
|
3
|
+
title: Array#include? inside iterator — O(n*m); use Set
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: ruby
|
|
8
|
+
tags: ruby, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Array#include? inside iterator — O(n*m); use Set
|
|
12
|
+
|
|
13
|
+
`Array#include?` is O(n). Used inside an iterator over m items it is O(n·m). `Set#include?` is O(1).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
`require 'set'; allowed = Set.new(list); ... allowed.include?(x)`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# O(n·m)
|
|
23
|
+
users.select { |u| banned.include?(u.id) }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# O(n+m)
|
|
30
|
+
require 'set'
|
|
31
|
+
banned_set = Set.new(banned)
|
|
32
|
+
users.select { |u| banned_set.include?(u.id) }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Escalate to
|
|
36
|
+
|
|
37
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rb-n-plus-one-activerecord
|
|
3
|
+
title: ActiveRecord iteration touching an association — N+1 without includes
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: ruby
|
|
8
|
+
tags: ruby, error, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ActiveRecord iteration touching an association — N+1 without includes
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `Model.includes(:assoc)` or `:preload` / `:eager_load` before iterating.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# N+1 queries
|
|
23
|
+
Post.all.each do |p|
|
|
24
|
+
puts p.author.name # 1 extra query per post
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# 2 queries total (or 1 with eager_load)
|
|
32
|
+
Post.includes(:author).each do |p|
|
|
33
|
+
puts p.author.name
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Escalate to
|
|
38
|
+
|
|
39
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rb-string-concat-in-loop
|
|
3
|
+
title: String += in loop — O(n^2) since += creates a new string each iteration
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: ruby
|
|
8
|
+
tags: ruby, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# String += in loop — O(n^2) since += creates a new string each iteration
|
|
12
|
+
|
|
13
|
+
`s += x` creates a new string each iteration — O(n²). `<<` mutates in place (O(n) amortized). `Array#join` is O(n) and idiomatic.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `<<` (mutates) or `parts.join` if building from an array.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
s = ""
|
|
23
|
+
parts.each { |p| s += p } # O(n²)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Mutating concat
|
|
30
|
+
s = String.new
|
|
31
|
+
parts.each { |p| s << p }
|
|
32
|
+
|
|
33
|
+
# Idiomatic
|
|
34
|
+
s = parts.join
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Escalate to
|
|
38
|
+
|
|
39
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|