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,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: go-loop-var-capture
|
|
3
|
+
title: Loop variable captured by goroutine — pre-1.22 races on the last value
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: go
|
|
8
|
+
tags: go, error, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Loop variable captured by goroutine — pre-1.22 races on the last value
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
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+.)
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```go
|
|
22
|
+
// Pre-1.22: all goroutines print the last item
|
|
23
|
+
for _, v := range items {
|
|
24
|
+
go func() {
|
|
25
|
+
fmt.Println(v)
|
|
26
|
+
}()
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Correct
|
|
31
|
+
|
|
32
|
+
```go
|
|
33
|
+
// Pin the variable explicitly
|
|
34
|
+
for _, v := range items {
|
|
35
|
+
v := v
|
|
36
|
+
go func() { fmt.Println(v) }()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Or pass as a parameter
|
|
40
|
+
for _, v := range items {
|
|
41
|
+
go func(v string) { fmt.Println(v) }(v)
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Escalate to
|
|
46
|
+
|
|
47
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: go-slice-append-no-cap
|
|
3
|
+
title: append in loop without preallocated capacity — repeated reallocation
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: go
|
|
8
|
+
tags: go, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# append in loop without preallocated capacity — repeated reallocation
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Preallocate: `out := make([]T, 0, len(in))` then `append`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```go
|
|
22
|
+
var out []int
|
|
23
|
+
for _, x := range in {
|
|
24
|
+
out = append(out, x*2) // grows; reallocates log(n) times
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```go
|
|
31
|
+
out := make([]int, 0, len(in))
|
|
32
|
+
for _, x := range in {
|
|
33
|
+
out = append(out, x*2)
|
|
34
|
+
}
|
|
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,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: go-string-concat-in-loop
|
|
3
|
+
title: string += inside loop — O(n^2); use strings.Builder
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: go
|
|
8
|
+
tags: go, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# string += inside loop — O(n^2); use strings.Builder
|
|
12
|
+
|
|
13
|
+
Go strings are immutable. `s += x` allocates each iteration — O(n²). `strings.Builder` reuses one backing array.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use strings.Builder: `var sb strings.Builder; for ... { sb.WriteString(x) }; sb.String()`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```go
|
|
22
|
+
// O(n²)
|
|
23
|
+
var s string
|
|
24
|
+
for _, line := range lines {
|
|
25
|
+
s += line + "\n"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```go
|
|
32
|
+
// O(n)
|
|
33
|
+
var sb strings.Builder
|
|
34
|
+
sb.Grow(len(lines) * 80)
|
|
35
|
+
for _, line := range lines {
|
|
36
|
+
sb.WriteString(line)
|
|
37
|
+
sb.WriteByte('\n')
|
|
38
|
+
}
|
|
39
|
+
s := sb.String()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Escalate to
|
|
43
|
+
|
|
44
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: java-arraylist-remove-in-for-i
|
|
3
|
+
title: list.remove(i) inside for-i — index shifts; ConcurrentModification risk
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: java
|
|
8
|
+
tags: java, error, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# list.remove(i) inside for-i — index shifts; ConcurrentModification risk
|
|
12
|
+
|
|
13
|
+
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`.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Iterate backwards, use Iterator.remove(), or collect indexes and remove in one removeIf().
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```java
|
|
22
|
+
for (int i = 0; i < list.size(); i++) {
|
|
23
|
+
if (shouldRemove(list.get(i))) {
|
|
24
|
+
list.remove(i); // shifts everything; next element is skipped
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```java
|
|
32
|
+
// Idiomatic and correct
|
|
33
|
+
list.removeIf(this::shouldRemove);
|
|
34
|
+
|
|
35
|
+
// Or, with an explicit iterator:
|
|
36
|
+
var it = list.iterator();
|
|
37
|
+
while (it.hasNext()) {
|
|
38
|
+
if (shouldRemove(it.next())) it.remove();
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Escalate to
|
|
43
|
+
|
|
44
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: java-bare-catch-exception
|
|
3
|
+
title: catch (Exception) with empty body or log-only — swallows root cause
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: java
|
|
8
|
+
tags: java, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# catch (Exception) with empty body or log-only — swallows root cause
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Catch the narrowest exception, rethrow as a domain exception, or handle with a documented fallback. Never swallow silently.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```java
|
|
22
|
+
try {
|
|
23
|
+
riskyCall();
|
|
24
|
+
} catch (Exception e) {
|
|
25
|
+
e.printStackTrace(); // swallowed; caller thinks everything is fine
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```java
|
|
32
|
+
try {
|
|
33
|
+
riskyCall();
|
|
34
|
+
} catch (IOException e) {
|
|
35
|
+
// narrow exception, rethrow as domain error with cause preserved
|
|
36
|
+
throw new ReadFailedException("riskyCall failed for " + ctx, e);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Escalate to
|
|
41
|
+
|
|
42
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: java-list-contains-in-loop
|
|
3
|
+
title: List.contains inside iterator — O(n*m); use HashSet
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: java
|
|
8
|
+
tags: java, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# List.contains inside iterator — O(n*m); use HashSet
|
|
12
|
+
|
|
13
|
+
`List.contains` is O(n). Used inside a stream/iterator over m items it is O(n·m). A `HashSet` lookup is O(1).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Build a HashSet outside: `Set<T> s = new HashSet<>(list); s.contains(x);`
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```java
|
|
22
|
+
// O(n·m)
|
|
23
|
+
var active = users.stream()
|
|
24
|
+
.filter(u -> !banned.contains(u.id))
|
|
25
|
+
.toList();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```java
|
|
31
|
+
// O(n+m)
|
|
32
|
+
var bannedSet = new HashSet<>(banned);
|
|
33
|
+
var active = users.stream()
|
|
34
|
+
.filter(u -> !bannedSet.contains(u.id))
|
|
35
|
+
.toList();
|
|
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,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: java-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: java
|
|
8
|
+
tags: java, warning, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# String += inside loop — O(n^2) on immutable String
|
|
12
|
+
|
|
13
|
+
Java `String` is immutable. `s += x` allocates a fresh `String` (and an underlying `char[]`) each iteration — O(n²) total work. `StringBuilder` reuses one buffer.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use StringBuilder: `var sb = new StringBuilder(); for (...) sb.append(x); sb.toString();`
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```java
|
|
22
|
+
// O(n²)
|
|
23
|
+
String s = "";
|
|
24
|
+
for (var line : lines) {
|
|
25
|
+
s += line + "\n";
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```java
|
|
32
|
+
// O(n)
|
|
33
|
+
var sb = new StringBuilder(lines.size() * 80);
|
|
34
|
+
for (var line : lines) {
|
|
35
|
+
sb.append(line).append('\n');
|
|
36
|
+
}
|
|
37
|
+
String 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,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-anonymous-handler-jsx
|
|
3
|
+
title: Anonymous arrow handler in JSX — breaks memoized-child equality
|
|
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
|
+
# Anonymous arrow handler in JSX — breaks memoized-child equality
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Wrap with useCallback if the child is memoized. Otherwise ignore.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// Child is React.memo — this defeats it
|
|
23
|
+
<MemoButton onClick={() => save(id)} />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
const onClick = useCallback(() => save(id), [id]);
|
|
30
|
+
<MemoButton onClick={onClick} />
|
|
31
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-array-key-index
|
|
3
|
+
title: key={index} on a list — breaks identity for reorderable items
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: javascript
|
|
8
|
+
tags: javascript, info
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# key={index} on a list — breaks identity for reorderable items
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use a stable id when the list can reorder, insert, or delete in the middle.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
{items.map((item, i) => <Row key={i} item={item} />)}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Correct
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
{items.map((item) => <Row key={item.id} item={item} />)}
|
|
29
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-async-in-foreach
|
|
3
|
+
title: async passed to .forEach — returned promises are dropped
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: javascript
|
|
8
|
+
tags: javascript, error, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# async passed to .forEach — returned promises are dropped
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `for (const x of arr) await fn(x)` or `await Promise.all(arr.map(fn))`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// Promises dropped; errors silently swallowed
|
|
23
|
+
items.forEach(async (item) => {
|
|
24
|
+
await save(item);
|
|
25
|
+
});
|
|
26
|
+
console.log('done'); // logs before any save completes
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// Sequential, with errors propagated
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
await save(item);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Parallel
|
|
38
|
+
await Promise.all(items.map((item) => save(item)));
|
|
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: js-await-in-for-loop
|
|
3
|
+
title: await inside for-loop — likely N+1 / serialized I/O
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: javascript
|
|
8
|
+
tags: javascript, error, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# await inside for-loop — likely N+1 / serialized I/O
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Collect promises into an array and use Promise.all(...), or batch with a bulk query (WHERE id IN (...)).
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// Wall-clock: O(n × latency)
|
|
23
|
+
for (const id of ids) {
|
|
24
|
+
const user = await db.users.findById(id);
|
|
25
|
+
results.push(user);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// Wall-clock: O(latency); one bulk query
|
|
33
|
+
const users = await db.users.findMany({ where: { id: { in: ids } } });
|
|
34
|
+
|
|
35
|
+
// Or, if calls are truly independent:
|
|
36
|
+
const results = await Promise.all(ids.map(id => db.users.findById(id)));
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Escalate to
|
|
40
|
+
|
|
41
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-deep-clone-via-json
|
|
3
|
+
title: JSON.parse(JSON.stringify(x)) — slow clone; loses Dates/Maps/undefined
|
|
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
|
+
# JSON.parse(JSON.stringify(x)) — slow clone; loses Dates/Maps/undefined
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use structuredClone(x), or copy only the fields you actually need.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const copy = JSON.parse(JSON.stringify(state));
|
|
23
|
+
// state.createdAt was a Date — now a string
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const copy = structuredClone(state); // preserves Date, Map, Set, cycles
|
|
30
|
+
|
|
31
|
+
// Or, when you only need a few fields:
|
|
32
|
+
const copy = { id: state.id, name: state.name };
|
|
33
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-helper-call-in-iterator
|
|
3
|
+
title: get*/find*/fetch* helper called inside iterator — likely N round-trips
|
|
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
|
+
# get*/find*/fetch* helper called inside iterator — likely N round-trips
|
|
12
|
+
|
|
13
|
+
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).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Hoist the helper out of the loop and pass a precomputed lookup (Map/Set) into the iterator, or batch with a single bulk query.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// N round trips
|
|
23
|
+
const enriched = orders.map((o) => ({
|
|
24
|
+
...o,
|
|
25
|
+
user: getUserById(o.userId),
|
|
26
|
+
}));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Correct
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// 1 round trip
|
|
33
|
+
const userIds = [...new Set(orders.map((o) => o.userId))];
|
|
34
|
+
const users = await db.users.findMany({ where: { id: { in: userIds } } });
|
|
35
|
+
const userById = new Map(users.map((u) => [u.id, u]));
|
|
36
|
+
const enriched = orders.map((o) => ({ ...o, user: userById.get(o.userId) }));
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Escalate to
|
|
40
|
+
|
|
41
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-includes-in-iterator
|
|
3
|
+
title: Array.includes inside .map/.filter/.forEach — O(n*m); use a Set
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: javascript
|
|
8
|
+
tags: javascript, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Array.includes inside .map/.filter/.forEach — O(n*m); use a Set
|
|
12
|
+
|
|
13
|
+
`.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).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Build a Set once outside the iterator, then `set.has(x)` inside.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// O(n × m)
|
|
23
|
+
const allowed = ['admin', 'editor', 'owner'];
|
|
24
|
+
const result = users.filter((u) => allowed.includes(u.role));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// O(n + m)
|
|
31
|
+
const allowed = new Set(['admin', 'editor', 'owner']);
|
|
32
|
+
const result = users.filter((u) => allowed.has(u.role));
|
|
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,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-inline-object-jsx-prop
|
|
3
|
+
title: Inline object literal as JSX prop — new reference 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
|
+
# Inline object literal as JSX prop — new reference every render
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Hoist outside render, or wrap in useMemo if the child is React.memo.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<Chart options={{ animated: true, color: 'red' }} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Correct
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// Hoist if static
|
|
29
|
+
const CHART_OPTIONS = { animated: true, color: 'red' };
|
|
30
|
+
<Chart options={CHART_OPTIONS} />
|
|
31
|
+
|
|
32
|
+
// Memoize if derived
|
|
33
|
+
const options = useMemo(() => ({ animated, color }), [animated, color]);
|
|
34
|
+
<Chart options={options} />
|
|
35
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-nested-for-loops
|
|
3
|
+
title: Nested for-loops — O(n*m); consider hashing one side
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: javascript
|
|
8
|
+
tags: javascript, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Nested for-loops — O(n*m); consider hashing one side
|
|
12
|
+
|
|
13
|
+
Two `for` loops that check membership between arrays are O(n × m). Hashing one side into a `Set` makes it O(n + m).
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
If looking up between the two arrays, put one into a Set/Map first → O(n+m).
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// O(n × m)
|
|
23
|
+
const matches = [];
|
|
24
|
+
for (const a of left) {
|
|
25
|
+
for (const b of right) {
|
|
26
|
+
if (a.id === b.id) matches.push([a, b]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Correct
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// O(n + m)
|
|
35
|
+
const rightById = new Map(right.map((b) => [b.id, b]));
|
|
36
|
+
const matches = [];
|
|
37
|
+
for (const a of left) {
|
|
38
|
+
const b = rightById.get(a.id);
|
|
39
|
+
if (b) matches.push([a, b]);
|
|
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,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: js-spread-in-reduce
|
|
3
|
+
title: Object spread inside reduce — O(n^2)
|
|
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
|
+
# Object spread inside reduce — O(n^2)
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Mutate the accumulator (`acc[k] = v; return acc`) or use Object.fromEntries(arr.map(...)).
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// O(n²)
|
|
23
|
+
const byId = items.reduce((acc, x) => ({ ...acc, [x.id]: x }), {});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// O(n)
|
|
30
|
+
const byId = Object.fromEntries(items.map((x) => [x.id, x]));
|
|
31
|
+
|
|
32
|
+
// Or mutate the accumulator
|
|
33
|
+
const byId = items.reduce((acc, x) => { acc[x.id] = x; return acc; }, {});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Escalate to
|
|
37
|
+
|
|
38
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|