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.
Files changed (97) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +238 -0
  3. package/cli/gen-agents-md.js +60 -0
  4. package/cli/gen-rule-docs.js +885 -0
  5. package/cli/lemmaly.js +162 -0
  6. package/commands/benchmark.md +40 -0
  7. package/commands/budget.md +53 -0
  8. package/commands/complexity.md +26 -0
  9. package/commands/cut.md +27 -0
  10. package/commands/hotpath.md +22 -0
  11. package/commands/invariant.md +22 -0
  12. package/commands/n-plus-one.md +20 -0
  13. package/commands/profile.md +34 -0
  14. package/commands/regress.md +43 -0
  15. package/commands/scale-check.md +37 -0
  16. package/commands/ship-check.md +26 -0
  17. package/package.json +48 -0
  18. package/rules/cpp.json +46 -0
  19. package/rules/csharp.json +38 -0
  20. package/rules/go.json +46 -0
  21. package/rules/java.json +38 -0
  22. package/rules/javascript.json +102 -0
  23. package/rules/php.json +38 -0
  24. package/rules/python.json +62 -0
  25. package/rules/ruby.json +38 -0
  26. package/rules/rust.json +38 -0
  27. package/rules/shell.json +38 -0
  28. package/rules/sql.json +54 -0
  29. package/skills/complexity-cuts/SKILL.md +259 -0
  30. package/skills/invariant-guard/SKILL.md +310 -0
  31. package/skills/lemmaly/AGENTS.md +1869 -0
  32. package/skills/lemmaly/SKILL.md +365 -0
  33. package/skills/lemmaly/references/async.md +135 -0
  34. package/skills/lemmaly/references/complexity.md +66 -0
  35. package/skills/lemmaly/references/hot-paths.md +87 -0
  36. package/skills/lemmaly/references/memory.md +118 -0
  37. package/skills/lemmaly/references/n-plus-one.md +139 -0
  38. package/skills/lemmaly/rules/cpp-map-double-lookup.md +38 -0
  39. package/skills/lemmaly/rules/cpp-range-loop-copy.md +33 -0
  40. package/skills/lemmaly/rules/cpp-raw-new.md +36 -0
  41. package/skills/lemmaly/rules/cpp-string-concat-in-loop.md +45 -0
  42. package/skills/lemmaly/rules/cpp-vector-push-no-reserve.md +40 -0
  43. package/skills/lemmaly/rules/cs-async-void.md +45 -0
  44. package/skills/lemmaly/rules/cs-disposable-no-using.md +32 -0
  45. package/skills/lemmaly/rules/cs-list-contains-in-loop.md +36 -0
  46. package/skills/lemmaly/rules/cs-string-concat-in-loop.md +42 -0
  47. package/skills/lemmaly/rules/go-defer-in-loop.md +39 -0
  48. package/skills/lemmaly/rules/go-err-not-checked.md +38 -0
  49. package/skills/lemmaly/rules/go-loop-var-capture.md +47 -0
  50. package/skills/lemmaly/rules/go-slice-append-no-cap.md +39 -0
  51. package/skills/lemmaly/rules/go-string-concat-in-loop.md +44 -0
  52. package/skills/lemmaly/rules/java-arraylist-remove-in-for-i.md +44 -0
  53. package/skills/lemmaly/rules/java-bare-catch-exception.md +42 -0
  54. package/skills/lemmaly/rules/java-list-contains-in-loop.md +40 -0
  55. package/skills/lemmaly/rules/java-string-concat-in-loop.md +42 -0
  56. package/skills/lemmaly/rules/js-anonymous-handler-jsx.md +31 -0
  57. package/skills/lemmaly/rules/js-array-key-index.md +29 -0
  58. package/skills/lemmaly/rules/js-async-in-foreach.md +43 -0
  59. package/skills/lemmaly/rules/js-await-in-for-loop.md +41 -0
  60. package/skills/lemmaly/rules/js-deep-clone-via-json.md +33 -0
  61. package/skills/lemmaly/rules/js-helper-call-in-iterator.md +41 -0
  62. package/skills/lemmaly/rules/js-includes-in-iterator.md +37 -0
  63. package/skills/lemmaly/rules/js-inline-object-jsx-prop.md +35 -0
  64. package/skills/lemmaly/rules/js-nested-for-loops.md +45 -0
  65. package/skills/lemmaly/rules/js-spread-in-reduce.md +38 -0
  66. package/skills/lemmaly/rules/js-unique-via-indexof.md +35 -0
  67. package/skills/lemmaly/rules/js-useeffect-missing-deps.md +33 -0
  68. package/skills/lemmaly/rules/php-count-in-for-condition.md +45 -0
  69. package/skills/lemmaly/rules/php-in-array-in-loop.md +42 -0
  70. package/skills/lemmaly/rules/php-loose-equality.md +35 -0
  71. package/skills/lemmaly/rules/php-query-in-loop.md +47 -0
  72. package/skills/lemmaly/rules/py-bare-except.md +39 -0
  73. package/skills/lemmaly/rules/py-django-loop-without-eager.md +42 -0
  74. package/skills/lemmaly/rules/py-in-list-literal.md +37 -0
  75. package/skills/lemmaly/rules/py-mutable-default-arg.md +39 -0
  76. package/skills/lemmaly/rules/py-open-without-with.md +33 -0
  77. package/skills/lemmaly/rules/py-range-len.md +35 -0
  78. package/skills/lemmaly/rules/py-string-concat-in-loop.md +43 -0
  79. package/skills/lemmaly/rules/rb-bare-rescue.md +41 -0
  80. package/skills/lemmaly/rules/rb-include-in-iterator.md +37 -0
  81. package/skills/lemmaly/rules/rb-n-plus-one-activerecord.md +39 -0
  82. package/skills/lemmaly/rules/rb-string-concat-in-loop.md +39 -0
  83. package/skills/lemmaly/rules/rs-clone-in-loop.md +38 -0
  84. package/skills/lemmaly/rules/rs-string-push-no-capacity.md +43 -0
  85. package/skills/lemmaly/rules/rs-unwrap-in-prod.md +36 -0
  86. package/skills/lemmaly/rules/rs-vec-push-no-capacity.md +42 -0
  87. package/skills/lemmaly/rules/sh-for-ls.md +41 -0
  88. package/skills/lemmaly/rules/sh-set-e-no-pipefail.md +37 -0
  89. package/skills/lemmaly/rules/sh-unquoted-var.md +35 -0
  90. package/skills/lemmaly/rules/sh-useless-cat-pipe.md +32 -0
  91. package/skills/lemmaly/rules/sql-leading-wildcard-like.md +34 -0
  92. package/skills/lemmaly/rules/sql-not-in-subquery.md +38 -0
  93. package/skills/lemmaly/rules/sql-or-in-where.md +35 -0
  94. package/skills/lemmaly/rules/sql-select-no-limit.md +37 -0
  95. package/skills/lemmaly/rules/sql-select-star.md +29 -0
  96. package/skills/lemmaly/rules/sql-update-no-where.md +35 -0
  97. package/skills/mathguard/SKILL.md +277 -0
package/rules/go.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "language": "go",
3
+ "extensions": [".go"],
4
+ "rules": [
5
+ {
6
+ "id": "go-loop-var-capture",
7
+ "severity": "error",
8
+ "title": "Loop variable captured by goroutine — pre-1.22 races on the last value",
9
+ "pattern": "for\\s+[\\w,\\s]+:?=\\s*range\\s+\\w+\\s*\\{[\\s\\S]{0,200}?go\\s+func\\s*\\(\\s*\\)\\s*\\{",
10
+ "flags": "g",
11
+ "fix": "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+.)"
12
+ },
13
+ {
14
+ "id": "go-string-concat-in-loop",
15
+ "severity": "warning",
16
+ "title": "string += inside loop — O(n^2); use strings.Builder",
17
+ "pattern": "for\\s+[^{]*\\{[\\s\\S]{0,300}?\\b\\w+\\s*\\+=\\s*[\"'\\w]",
18
+ "flags": "g",
19
+ "fix": "Use strings.Builder: `var sb strings.Builder; for ... { sb.WriteString(x) }; sb.String()`."
20
+ },
21
+ {
22
+ "id": "go-defer-in-loop",
23
+ "severity": "warning",
24
+ "title": "defer inside loop — defers accumulate until function returns",
25
+ "pattern": "for\\s+[^{]*\\{[\\s\\S]{0,300}?\\bdefer\\s",
26
+ "flags": "g",
27
+ "fix": "Wrap the body in a function so each iteration's defer fires at iteration end, or close resources explicitly."
28
+ },
29
+ {
30
+ "id": "go-err-not-checked",
31
+ "severity": "warning",
32
+ "title": "Error return value discarded — silent failures",
33
+ "pattern": "^\\s*\\w+\\s*,\\s*_\\s*:?=\\s*\\w+\\.[A-Za-z]\\w*\\s*\\(",
34
+ "flags": "gm",
35
+ "fix": "Handle the error or comment why it is safe to ignore (`_ = err // <reason>`)."
36
+ },
37
+ {
38
+ "id": "go-slice-append-no-cap",
39
+ "severity": "info",
40
+ "title": "append in loop without preallocated capacity — repeated reallocation",
41
+ "pattern": "var\\s+\\w+\\s+\\[\\]\\w+\\s*\\n[\\s\\S]{0,200}?for\\s+[^{]*\\{[\\s\\S]{0,200}?\\w+\\s*=\\s*append\\s*\\(\\s*\\w+\\s*,",
42
+ "flags": "g",
43
+ "fix": "Preallocate: `out := make([]T, 0, len(in))` then `append`."
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "language": "java",
3
+ "extensions": [".java"],
4
+ "rules": [
5
+ {
6
+ "id": "java-string-concat-in-loop",
7
+ "severity": "warning",
8
+ "title": "String += inside loop — O(n^2) on immutable String",
9
+ "pattern": "\\bfor\\s*\\([^{]*\\)\\s*\\{[\\s\\S]{0,300}?\\b\\w+\\s*\\+=\\s*[\"'\\w]",
10
+ "flags": "g",
11
+ "fix": "Use StringBuilder: `var sb = new StringBuilder(); for (...) sb.append(x); sb.toString();`"
12
+ },
13
+ {
14
+ "id": "java-list-contains-in-loop",
15
+ "severity": "warning",
16
+ "title": "List.contains inside iterator — O(n*m); use HashSet",
17
+ "pattern": "\\.(stream|forEach|map|filter)\\s*\\([\\s\\S]{0,200}?\\.contains\\s*\\(",
18
+ "flags": "g",
19
+ "fix": "Build a HashSet outside: `Set<T> s = new HashSet<>(list); s.contains(x);`"
20
+ },
21
+ {
22
+ "id": "java-arraylist-remove-in-for-i",
23
+ "severity": "error",
24
+ "title": "list.remove(i) inside for-i — index shifts; ConcurrentModification risk",
25
+ "pattern": "\\bfor\\s*\\(\\s*int\\s+\\w+\\s*=[\\s\\S]{0,80}?\\w+\\.size\\s*\\(\\s*\\)[\\s\\S]{0,200}?\\.remove\\s*\\(",
26
+ "flags": "g",
27
+ "fix": "Iterate backwards, use Iterator.remove(), or collect indexes and remove in one removeIf()."
28
+ },
29
+ {
30
+ "id": "java-bare-catch-exception",
31
+ "severity": "warning",
32
+ "title": "catch (Exception) with empty body or log-only — swallows root cause",
33
+ "pattern": "catch\\s*\\(\\s*Exception\\s+\\w+\\s*\\)\\s*\\{\\s*(?:\\}|\\w+\\.(printStackTrace|log|warn|info)\\s*\\([^)]*\\)\\s*;\\s*\\})",
34
+ "flags": "g",
35
+ "fix": "Catch the narrowest exception, rethrow as a domain exception, or handle with a documented fallback. Never swallow silently."
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,102 @@
1
+ {
2
+ "language": "javascript",
3
+ "extensions": [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"],
4
+ "rules": [
5
+ {
6
+ "id": "js-await-in-for-loop",
7
+ "severity": "error",
8
+ "title": "await inside for-loop — likely N+1 / serialized I/O",
9
+ "pattern": "for\\s*\\([^)]*\\)\\s*\\{[\\s\\S]{0,400}?\\bawait\\s",
10
+ "flags": "g",
11
+ "fix": "Collect promises into an array and use Promise.all(...), or batch with a bulk query (WHERE id IN (...))."
12
+ },
13
+ {
14
+ "id": "js-async-in-foreach",
15
+ "severity": "error",
16
+ "title": "async passed to .forEach — returned promises are dropped",
17
+ "pattern": "\\.forEach\\s*\\(\\s*async\\b",
18
+ "flags": "g",
19
+ "fix": "Use `for (const x of arr) await fn(x)` or `await Promise.all(arr.map(fn))`."
20
+ },
21
+ {
22
+ "id": "js-deep-clone-via-json",
23
+ "severity": "warning",
24
+ "title": "JSON.parse(JSON.stringify(x)) — slow clone; loses Dates/Maps/undefined",
25
+ "pattern": "JSON\\.parse\\s*\\(\\s*JSON\\.stringify\\s*\\(",
26
+ "flags": "g",
27
+ "fix": "Use structuredClone(x), or copy only the fields you actually need."
28
+ },
29
+ {
30
+ "id": "js-useeffect-missing-deps",
31
+ "severity": "warning",
32
+ "title": "useEffect without a deps array — runs after every render",
33
+ "pattern": "useEffect\\s*\\(\\s*(?:async\\s*)?\\(\\s*\\)\\s*=>\\s*\\{[\\s\\S]*?^\\s*\\}\\s*\\)\\s*;",
34
+ "flags": "gm",
35
+ "fix": "Add a deps array — `[]` for mount-only, `[a, b]` to track changes."
36
+ },
37
+ {
38
+ "id": "js-inline-object-jsx-prop",
39
+ "severity": "warning",
40
+ "title": "Inline object literal as JSX prop — new reference every render",
41
+ "pattern": "\\s[a-zA-Z][a-zA-Z0-9]*=\\{\\{[^{}]+\\}\\}",
42
+ "flags": "g",
43
+ "fix": "Hoist outside render, or wrap in useMemo if the child is React.memo."
44
+ },
45
+ {
46
+ "id": "js-anonymous-handler-jsx",
47
+ "severity": "warning",
48
+ "title": "Anonymous arrow handler in JSX — breaks memoized-child equality",
49
+ "pattern": "\\son[A-Z][A-Za-z]+=\\{\\s*\\(",
50
+ "flags": "g",
51
+ "fix": "Wrap with useCallback if the child is memoized. Otherwise ignore."
52
+ },
53
+ {
54
+ "id": "js-nested-for-loops",
55
+ "severity": "info",
56
+ "title": "Nested for-loops — O(n*m); consider hashing one side",
57
+ "pattern": "for\\s*\\([^)]*\\)\\s*\\{[\\s\\S]{0,300}?for\\s*\\(",
58
+ "flags": "g",
59
+ "fix": "If looking up between the two arrays, put one into a Set/Map first → O(n+m)."
60
+ },
61
+ {
62
+ "id": "js-spread-in-reduce",
63
+ "severity": "warning",
64
+ "title": "Object spread inside reduce — O(n^2)",
65
+ "pattern": "\\.reduce\\s*\\([\\s\\S]{0,80}?=>\\s*\\(\\s*\\{\\s*\\.\\.\\.",
66
+ "flags": "g",
67
+ "fix": "Mutate the accumulator (`acc[k] = v; return acc`) or use Object.fromEntries(arr.map(...))."
68
+ },
69
+ {
70
+ "id": "js-includes-in-iterator",
71
+ "severity": "info",
72
+ "title": "Array.includes inside .map/.filter/.forEach — O(n*m); use a Set",
73
+ "pattern": "\\.(map|forEach|filter|reduce|find|some|every)\\s*\\([\\s\\S]{0,80}?=>[\\s\\S]{0,150}?\\.includes\\s*\\(",
74
+ "flags": "g",
75
+ "fix": "Build a Set once outside the iterator, then `set.has(x)` inside."
76
+ },
77
+ {
78
+ "id": "js-unique-via-indexof",
79
+ "severity": "warning",
80
+ "title": "Unique-by-indexOf — O(n^2) dedupe; use `Array.from(new Set(arr))`",
81
+ "pattern": "\\.filter\\s*\\(\\s*\\([^)]*\\)\\s*=>\\s*\\w+\\.indexOf\\s*\\([^)]+\\)\\s*===\\s*\\w+",
82
+ "flags": "g",
83
+ "fix": "Use `Array.from(new Set(arr))`, or build a Set inline. O(n) instead of O(n^2)."
84
+ },
85
+ {
86
+ "id": "js-helper-call-in-iterator",
87
+ "severity": "warning",
88
+ "title": "get*/find*/fetch* helper called inside iterator — likely N round-trips",
89
+ "pattern": "\\.(map|forEach|filter|reduce|find|some|every)\\s*\\([\\s\\S]{0,80}?=>[\\s\\S]{0,100}?\\b(get|find|fetch|load|query|select)[A-Z]\\w*\\s*\\(",
90
+ "flags": "g",
91
+ "fix": "Hoist the helper out of the loop and pass a precomputed lookup (Map/Set) into the iterator, or batch with a single bulk query."
92
+ },
93
+ {
94
+ "id": "js-array-key-index",
95
+ "severity": "info",
96
+ "title": "key={index} on a list — breaks identity for reorderable items",
97
+ "pattern": "key=\\{(i|idx|index)\\}",
98
+ "flags": "g",
99
+ "fix": "Use a stable id when the list can reorder, insert, or delete in the middle."
100
+ }
101
+ ]
102
+ }
package/rules/php.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "language": "php",
3
+ "extensions": [".php"],
4
+ "rules": [
5
+ {
6
+ "id": "php-count-in-for-condition",
7
+ "severity": "warning",
8
+ "title": "count() in for-condition — recomputed every iteration",
9
+ "pattern": "for\\s*\\(\\s*\\$\\w+\\s*=\\s*\\d+\\s*;\\s*\\$\\w+\\s*<\\s*count\\s*\\(",
10
+ "flags": "g",
11
+ "fix": "Hoist: `for ($i = 0, $n = count($a); $i < $n; $i++)`. Or use `foreach`."
12
+ },
13
+ {
14
+ "id": "php-in-array-in-loop",
15
+ "severity": "warning",
16
+ "title": "in_array inside loop — O(n*m); use array_flip + isset",
17
+ "pattern": "\\b(?:foreach|for|while)\\s*\\([^{]*\\)\\s*\\{[\\s\\S]{0,200}?in_array\\s*\\(",
18
+ "flags": "g",
19
+ "fix": "`$set = array_flip($haystack); ... isset($set[$x])` is O(1) per check."
20
+ },
21
+ {
22
+ "id": "php-query-in-loop",
23
+ "severity": "error",
24
+ "title": "DB query inside loop — N+1",
25
+ "pattern": "\\b(?:foreach|for|while)\\s*\\([^{]*\\)\\s*\\{[\\s\\S]{0,300}?(?:\\$\\w+->query|mysqli_query|->prepare|->execute|DB::|->where\\b)",
26
+ "flags": "g",
27
+ "fix": "Collect the ids, run ONE `WHERE id IN (...)` query, index by id, then loop. In Eloquent use eager loading: `Model::with('relation')`."
28
+ },
29
+ {
30
+ "id": "php-loose-equality",
31
+ "severity": "info",
32
+ "title": "== / != — loose equality has surprising coercions",
33
+ "pattern": "(?<![=!<>])[!=]=(?!=)",
34
+ "flags": "g",
35
+ "fix": "Use `===` / `!==` unless type-juggling is explicitly intended (with a comment)."
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "language": "python",
3
+ "extensions": [".py"],
4
+ "rules": [
5
+ {
6
+ "id": "py-mutable-default-arg",
7
+ "severity": "error",
8
+ "title": "Mutable default argument — shared across calls",
9
+ "pattern": "def\\s+\\w+\\s*\\([^)]*=\\s*(\\[\\s*\\]|\\{\\s*\\}|set\\s*\\(\\s*\\))",
10
+ "flags": "g",
11
+ "fix": "Default to None; create inside: `def f(x=None): x = x or []`."
12
+ },
13
+ {
14
+ "id": "py-string-concat-in-loop",
15
+ "severity": "warning",
16
+ "title": "String += inside loop — O(n^2)",
17
+ "pattern": "for\\s+\\w+\\s+in[\\s\\S]{0,300}?\\b\\w+\\s*\\+=\\s*['\"f]",
18
+ "flags": "g",
19
+ "fix": "Append to a list, then ''.join(list). Or use io.StringIO."
20
+ },
21
+ {
22
+ "id": "py-range-len",
23
+ "severity": "info",
24
+ "title": "range(len(x)) — un-Pythonic; use enumerate(x)",
25
+ "pattern": "range\\s*\\(\\s*len\\s*\\(",
26
+ "flags": "g",
27
+ "fix": "Use `for i, item in enumerate(x):` when you need the index too."
28
+ },
29
+ {
30
+ "id": "py-in-list-literal",
31
+ "severity": "info",
32
+ "title": "Membership against a list literal — O(n) per check; use a set",
33
+ "pattern": "\\bin\\s+\\[[^\\]]{12,}\\]",
34
+ "flags": "g",
35
+ "fix": "Hoist to a module-level frozenset({...})."
36
+ },
37
+ {
38
+ "id": "py-django-loop-without-eager",
39
+ "severity": "warning",
40
+ "title": "Django ORM iteration — verify select_related/prefetch_related to avoid N+1",
41
+ "pattern": "for\\s+\\w+\\s+in\\s+[A-Za-z_][A-Za-z0-9_]*\\.objects\\.(all|filter|exclude)\\s*\\(",
42
+ "flags": "g",
43
+ "fix": "Add .select_related('fk') for FK/OneToOne, .prefetch_related('rel') for reverse FK / M2M."
44
+ },
45
+ {
46
+ "id": "py-bare-except",
47
+ "severity": "info",
48
+ "title": "Bare except — hides timeouts, OOM, KeyboardInterrupt",
49
+ "pattern": "except\\s*:",
50
+ "flags": "g",
51
+ "fix": "Catch specific exceptions: `except (TimeoutError, ConnectionError):`."
52
+ },
53
+ {
54
+ "id": "py-open-without-with",
55
+ "severity": "info",
56
+ "title": "open() without `with` — risk of leaked file descriptors",
57
+ "pattern": "(?<!with\\s)\\bopen\\s*\\([^)]*\\)(?!\\s*as\\b)",
58
+ "flags": "g",
59
+ "fix": "Use `with open(path) as f:` to ensure the handle is released."
60
+ }
61
+ ]
62
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "language": "ruby",
3
+ "extensions": [".rb"],
4
+ "rules": [
5
+ {
6
+ "id": "rb-include-in-iterator",
7
+ "severity": "warning",
8
+ "title": "Array#include? inside iterator — O(n*m); use Set",
9
+ "pattern": "\\.(each|map|select|reject|find|any\\?|all\\?|count|filter)\\s*\\{[\\s\\S]{0,150}?\\.include\\?\\s*\\(",
10
+ "flags": "g",
11
+ "fix": "`require 'set'; allowed = Set.new(list); ... allowed.include?(x)`."
12
+ },
13
+ {
14
+ "id": "rb-n-plus-one-activerecord",
15
+ "severity": "error",
16
+ "title": "ActiveRecord iteration touching an association — N+1 without includes",
17
+ "pattern": "\\b\\w+(?:\\.all|\\.where\\s*\\([^)]+\\))(?!\\.includes)[\\s\\S]{0,80}?\\.(each|map|find_each|pluck)\\s*(?:do|\\{)",
18
+ "flags": "g",
19
+ "fix": "Use `Model.includes(:assoc)` or `:preload` / `:eager_load` before iterating."
20
+ },
21
+ {
22
+ "id": "rb-bare-rescue",
23
+ "severity": "warning",
24
+ "title": "Bare rescue — catches StandardError, hides bugs",
25
+ "pattern": "\\brescue\\s*\\n",
26
+ "flags": "g",
27
+ "fix": "Catch a specific class: `rescue Net::ReadTimeout` etc. Bare rescue swallows too much."
28
+ },
29
+ {
30
+ "id": "rb-string-concat-in-loop",
31
+ "severity": "info",
32
+ "title": "String += in loop — O(n^2) since += creates a new string each iteration",
33
+ "pattern": "\\.each\\s*(?:\\{|do)[\\s\\S]{0,150}?\\w+\\s*\\+=\\s*[\"'\\w]",
34
+ "flags": "g",
35
+ "fix": "Use `<<` (mutates) or `parts.join` if building from an array."
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "language": "rust",
3
+ "extensions": [".rs"],
4
+ "rules": [
5
+ {
6
+ "id": "rs-unwrap-in-prod",
7
+ "severity": "warning",
8
+ "title": ".unwrap() / .expect() panics on None/Err — surface the error",
9
+ "pattern": "\\.(unwrap|expect)\\s*\\(",
10
+ "flags": "g",
11
+ "fix": "Use `?`, `match`, `unwrap_or`, `unwrap_or_else`, or `ok_or(...)?`. Reserve unwrap for tests and provably infallible paths."
12
+ },
13
+ {
14
+ "id": "rs-clone-in-loop",
15
+ "severity": "info",
16
+ "title": ".clone() inside iterator — avoid if a borrow works",
17
+ "pattern": "\\.(iter|into_iter|map|filter)\\s*\\([\\s\\S]{0,150}?\\.clone\\s*\\(\\s*\\)",
18
+ "flags": "g",
19
+ "fix": "Borrow with &, use Rc/Arc for shared ownership, or move once outside the loop."
20
+ },
21
+ {
22
+ "id": "rs-vec-push-no-capacity",
23
+ "severity": "info",
24
+ "title": "Vec::new() + push in loop — repeated reallocation",
25
+ "pattern": "let\\s+mut\\s+\\w+\\s*=\\s*Vec::new\\s*\\(\\s*\\)\\s*;[\\s\\S]{0,300}?for\\s+[^{]*\\{[\\s\\S]{0,200}?\\.push\\s*\\(",
26
+ "flags": "g",
27
+ "fix": "Preallocate: `Vec::with_capacity(n)`, or use `.collect::<Vec<_>>()` over an iterator that has a size hint."
28
+ },
29
+ {
30
+ "id": "rs-string-push-no-capacity",
31
+ "severity": "info",
32
+ "title": "String::new() + push_str in loop — repeated reallocation",
33
+ "pattern": "let\\s+mut\\s+\\w+\\s*=\\s*String::new\\s*\\(\\s*\\)\\s*;[\\s\\S]{0,300}?for\\s+[^{]*\\{[\\s\\S]{0,200}?\\.(push_str|push)\\s*\\(",
34
+ "flags": "g",
35
+ "fix": "Preallocate: `String::with_capacity(n)`, or `parts.join(sep)` for known parts."
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "language": "shell",
3
+ "extensions": [".sh", ".bash"],
4
+ "rules": [
5
+ {
6
+ "id": "sh-set-e-no-pipefail",
7
+ "severity": "warning",
8
+ "title": "set -e without set -o pipefail — failures inside pipes are masked",
9
+ "pattern": "^\\s*set\\s+-e(?:uo?)?\\s*$",
10
+ "flags": "gm",
11
+ "fix": "Use `set -euo pipefail` so the script also fails when an earlier stage in a pipe fails."
12
+ },
13
+ {
14
+ "id": "sh-unquoted-var",
15
+ "severity": "warning",
16
+ "title": "Unquoted $var — word splitting and glob expansion",
17
+ "pattern": "(?:\\b(?:if|while|case)\\s+[^\\n]{0,40}|=)\\s*\\$\\{?[A-Za-z_]\\w*\\}?(?![\\w\\\"])",
18
+ "flags": "g",
19
+ "fix": "Quote: `\"$var\"`. Use `\"${arr[@]}\"` for arrays. Required even for paths you trust."
20
+ },
21
+ {
22
+ "id": "sh-useless-cat-pipe",
23
+ "severity": "info",
24
+ "title": "cat file | cmd — useless use of cat (UUOC)",
25
+ "pattern": "\\bcat\\s+[^|\\n]+\\|\\s*(grep|awk|sed|wc|sort|head|tail)\\b",
26
+ "flags": "g",
27
+ "fix": "Run the command on the file directly: `grep ... file` instead of `cat file | grep ...`."
28
+ },
29
+ {
30
+ "id": "sh-for-ls",
31
+ "severity": "warning",
32
+ "title": "for f in $(ls ...) — breaks on filenames with spaces / newlines",
33
+ "pattern": "for\\s+\\w+\\s+in\\s+\\$\\(\\s*ls\\b",
34
+ "flags": "g",
35
+ "fix": "Use glob `for f in *.txt` or `find ... -print0 | xargs -0`. Never parse `ls`."
36
+ }
37
+ ]
38
+ }
package/rules/sql.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "language": "sql",
3
+ "extensions": [".sql"],
4
+ "rules": [
5
+ {
6
+ "id": "sql-select-star",
7
+ "severity": "warning",
8
+ "title": "SELECT * — fetches unused columns; blocks index-only scans",
9
+ "pattern": "SELECT\\s+\\*",
10
+ "flags": "gi",
11
+ "fix": "Name the columns. Smaller payload and more covering-index opportunities."
12
+ },
13
+ {
14
+ "id": "sql-leading-wildcard-like",
15
+ "severity": "warning",
16
+ "title": "LIKE with leading wildcard — cannot use a B-tree index",
17
+ "pattern": "LIKE\\s+['\"]\\s*%",
18
+ "flags": "gi",
19
+ "fix": "Use a trigram index (pg_trgm), full-text search, or a reversed-column index."
20
+ },
21
+ {
22
+ "id": "sql-not-in-subquery",
23
+ "severity": "warning",
24
+ "title": "NOT IN (subquery) — null-unsafe; usually slower than NOT EXISTS",
25
+ "pattern": "NOT\\s+IN\\s*\\(\\s*SELECT",
26
+ "flags": "gi",
27
+ "fix": "Rewrite as `WHERE NOT EXISTS (SELECT 1 FROM ... WHERE ...)`."
28
+ },
29
+ {
30
+ "id": "sql-select-no-limit",
31
+ "severity": "info",
32
+ "title": "SELECT without LIMIT — unbounded result set",
33
+ "pattern": "SELECT\\s+(?!.*\\bLIMIT\\b)[\\s\\S]+?FROM[\\s\\S]+?;",
34
+ "flags": "gi",
35
+ "fix": "Add LIMIT, or paginate by id range. Unbounded reads OOM under growth."
36
+ },
37
+ {
38
+ "id": "sql-or-in-where",
39
+ "severity": "info",
40
+ "title": "OR in WHERE — can prevent index use",
41
+ "pattern": "WHERE[\\s\\S]{0,200}?\\sOR\\s",
42
+ "flags": "gi",
43
+ "fix": "Equality on same column → `WHERE col IN (...)`. Otherwise consider UNION ALL."
44
+ },
45
+ {
46
+ "id": "sql-update-no-where",
47
+ "severity": "error",
48
+ "title": "UPDATE / DELETE without WHERE — touches every row",
49
+ "pattern": "(?:UPDATE|DELETE\\s+FROM)\\s+[A-Za-z_][A-Za-z0-9_.\"]*\\s+(?!.*\\bWHERE\\b)[\\s\\S]*?;",
50
+ "flags": "gi",
51
+ "fix": "Add a WHERE clause, or confirm in a comment that touching all rows is intentional."
52
+ }
53
+ ]
54
+ }