@aarock1234/slopscan 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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/SKILL.md +149 -0
  4. package/dist/analyzer.d.ts +5 -0
  5. package/dist/analyzer.js +13 -0
  6. package/dist/analyzer.js.map +1 -0
  7. package/dist/analyzers/ast.d.ts +12 -0
  8. package/dist/analyzers/ast.js +73 -0
  9. package/dist/analyzers/ast.js.map +1 -0
  10. package/dist/analyzers/judge-chunks.d.ts +13 -0
  11. package/dist/analyzers/judge-chunks.js +87 -0
  12. package/dist/analyzers/judge-chunks.js.map +1 -0
  13. package/dist/analyzers/judge-validate.d.ts +19 -0
  14. package/dist/analyzers/judge-validate.js +81 -0
  15. package/dist/analyzers/judge-validate.js.map +1 -0
  16. package/dist/analyzers/judge.d.ts +44 -0
  17. package/dist/analyzers/judge.js +177 -0
  18. package/dist/analyzers/judge.js.map +1 -0
  19. package/dist/change.d.ts +19 -0
  20. package/dist/change.js +87 -0
  21. package/dist/change.js.map +1 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.js +125 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/config.d.ts +91 -0
  26. package/dist/config.js +61 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/confirm.d.ts +11 -0
  29. package/dist/confirm.js +100 -0
  30. package/dist/confirm.js.map +1 -0
  31. package/dist/finding.d.ts +13 -0
  32. package/dist/finding.js +18 -0
  33. package/dist/finding.js.map +1 -0
  34. package/dist/lang.d.ts +16 -0
  35. package/dist/lang.js +37 -0
  36. package/dist/lang.js.map +1 -0
  37. package/dist/model.d.ts +2 -0
  38. package/dist/model.js +34 -0
  39. package/dist/model.js.map +1 -0
  40. package/dist/report.d.ts +124 -0
  41. package/dist/report.js +112 -0
  42. package/dist/report.js.map +1 -0
  43. package/dist/rule.d.ts +66 -0
  44. package/dist/rule.js +259 -0
  45. package/dist/rule.js.map +1 -0
  46. package/dist/scan.d.ts +14 -0
  47. package/dist/scan.js +33 -0
  48. package/dist/scan.js.map +1 -0
  49. package/dist/score.d.ts +147 -0
  50. package/dist/score.js +142 -0
  51. package/dist/score.js.map +1 -0
  52. package/dist/shared/concurrency.d.ts +5 -0
  53. package/dist/shared/concurrency.js +17 -0
  54. package/dist/shared/concurrency.js.map +1 -0
  55. package/dist/shared/env.d.ts +27 -0
  56. package/dist/shared/env.js +29 -0
  57. package/dist/shared/env.js.map +1 -0
  58. package/dist/shared/errors.d.ts +18 -0
  59. package/dist/shared/errors.js +26 -0
  60. package/dist/shared/errors.js.map +1 -0
  61. package/dist/shared/log.d.ts +2 -0
  62. package/dist/shared/log.js +18 -0
  63. package/dist/shared/log.js.map +1 -0
  64. package/dist/shared/paths.d.ts +3 -0
  65. package/dist/shared/paths.js +6 -0
  66. package/dist/shared/paths.js.map +1 -0
  67. package/dist/shared/prompts.d.ts +3 -0
  68. package/dist/shared/prompts.js +34 -0
  69. package/dist/shared/prompts.js.map +1 -0
  70. package/package.json +79 -0
  71. package/prompts/judge.md +37 -0
  72. package/rules/any/futureproof/abstraction/pass-through-wrapper.md +70 -0
  73. package/rules/any/futureproof/abstraction/single-caller-helper.md +38 -0
  74. package/rules/any/futureproof/abstraction/single-impl-interface.md +83 -0
  75. package/rules/any/futureproof/exports/dead-export.md +62 -0
  76. package/rules/any/futureproof/layering/framework-type-in-domain.md +83 -0
  77. package/rules/any/futureproof/layering/logic-in-handler.md +94 -0
  78. package/rules/any/futureproof/params/boolean-positional-param.md +128 -0
  79. package/rules/any/futureproof/params/positional-config-args.md +135 -0
  80. package/rules/any/futureproof/structure/wide-function.md +138 -0
  81. package/rules/any/futureproof/testing/test-asserts-implementation.md +70 -0
  82. package/rules/any/hacky/comments/comment-restates-code.md +50 -0
  83. package/rules/any/hacky/comments/shipped-todo-comment.md +55 -0
  84. package/rules/any/hacky/concurrency/sleep-based-sync.md +95 -0
  85. package/rules/any/hacky/config/hardcoded-url.md +88 -0
  86. package/rules/any/hacky/constants/magic-number.md +94 -0
  87. package/rules/any/hacky/debugging/debug-print.md +73 -0
  88. package/rules/any/hacky/duplication/copy-paste-block.md +117 -0
  89. package/rules/any/hacky/types/stringly-typed-enum.md +86 -0
  90. package/rules/any/idiom/comments/no-section-banners.md +47 -0
  91. package/rules/go/futureproof/constants/string-enums-when-serialized.md +47 -0
  92. package/rules/go/futureproof/constants/typed-constants-for-enums.md +45 -0
  93. package/rules/go/futureproof/interfaces/compile-time-impl-assertion.md +43 -0
  94. package/rules/go/futureproof/interfaces/define-interfaces-at-consumer.md +47 -0
  95. package/rules/go/futureproof/interfaces/keep-interfaces-small.md +53 -0
  96. package/rules/go/futureproof/naming/no-utils-helpers-common-package.md +34 -0
  97. package/rules/go/futureproof/structs/zero-value-usable.md +50 -0
  98. package/rules/go/futureproof/types/generics-over-any.md +55 -0
  99. package/rules/go/hacky/concurrency/no-goroutine-without-wait.md +44 -0
  100. package/rules/go/hacky/context/never-pass-nil-context.md +64 -0
  101. package/rules/go/hacky/context/no-context-in-struct.md +44 -0
  102. package/rules/go/hacky/errors/check-close-error-on-writable.md +53 -0
  103. package/rules/go/hacky/errors/custom-error-type-errors-as.md +54 -0
  104. package/rules/go/hacky/errors/ignored-error-blank.md +84 -0
  105. package/rules/go/hacky/errors/panic-only-unrecoverable.md +77 -0
  106. package/rules/go/hacky/errors/sentinel-errors-with-errors-is.md +63 -0
  107. package/rules/go/hacky/errors/wrap-errors-with-w.md +71 -0
  108. package/rules/go/hacky/state/package-level-mutable-var.md +95 -0
  109. package/rules/go/hacky/structure/os-exit-only-in-main.md +61 -0
  110. package/rules/go/hacky/type-safety/no-map-string-any.md +49 -0
  111. package/rules/go/hacky/types/json-into-struct-not-map.md +48 -0
  112. package/rules/go/hacky/types/no-any.md +71 -0
  113. package/rules/go/hacky/types/no-map-any-any.md +35 -0
  114. package/rules/go/idiom/comments/doc-comment-on-exported.md +81 -0
  115. package/rules/go/idiom/comments/doc-comment-starts-with-name.md +36 -0
  116. package/rules/go/idiom/concurrency/mutex-over-channels-for-state.md +47 -0
  117. package/rules/go/idiom/constructors/constructor-named-new.md +37 -0
  118. package/rules/go/idiom/context/context-is-first-param.md +47 -0
  119. package/rules/go/idiom/context/io-takes-context.md +50 -0
  120. package/rules/go/idiom/errors/capitalized-error-message.md +52 -0
  121. package/rules/go/idiom/errors/check-error-immediately.md +45 -0
  122. package/rules/go/idiom/errors/defer-close-explicit-discard.md +65 -0
  123. package/rules/go/idiom/errors/error-is-last-return-value.md +53 -0
  124. package/rules/go/idiom/errors/no-log-and-return.md +45 -0
  125. package/rules/go/idiom/formatting/blank-line-before-return.md +48 -0
  126. package/rules/go/idiom/formatting/multiline-struct-literals.md +56 -0
  127. package/rules/go/idiom/imports/import-order-groups.md +42 -0
  128. package/rules/go/idiom/imports/side-effect-imports-own-group.md +51 -0
  129. package/rules/go/idiom/interfaces/accept-interfaces-return-concrete.md +39 -0
  130. package/rules/go/idiom/iterators/prefer-iter-seq.md +47 -0
  131. package/rules/go/idiom/logging/lowercase-log-messages.md +80 -0
  132. package/rules/go/idiom/logging/use-slog.md +46 -0
  133. package/rules/go/idiom/naming/acronyms-consistent-case.md +97 -0
  134. package/rules/go/idiom/naming/interface-er-suffix.md +33 -0
  135. package/rules/go/idiom/naming/no-package-name-stutter.md +37 -0
  136. package/rules/go/idiom/naming/package-name-single-lowercase-word.md +40 -0
  137. package/rules/go/idiom/receivers/consistent-receiver-kind-per-type.md +40 -0
  138. package/rules/go/idiom/receivers/no-this-receiver.md +37 -0
  139. package/rules/go/idiom/receivers/pointer-vs-value-receiver-choice.md +49 -0
  140. package/rules/go/idiom/receivers/short-receiver-names.md +43 -0
  141. package/rules/go/idiom/structs/new-expr-for-pointer-fields.md +40 -0
  142. package/rules/go/idiom/structure/main-delegates-to-run.md +54 -0
  143. package/rules/ts/futureproof/classes/composition-over-abstract-base.md +73 -0
  144. package/rules/ts/futureproof/functions/explicit-return-type-on-exports.md +50 -0
  145. package/rules/ts/futureproof/structure/colocate-zod-schemas.md +46 -0
  146. package/rules/ts/futureproof/types/exhaustive-switch-never-check.md +52 -0
  147. package/rules/ts/futureproof/types/readonly-for-immutable-data.md +46 -0
  148. package/rules/ts/futureproof/zod/enum-values-from-const-object.md +37 -0
  149. package/rules/ts/futureproof/zod/no-z-native-enum.md +36 -0
  150. package/rules/ts/futureproof/zod/schema-first-infer-type.md +46 -0
  151. package/rules/ts/hacky/async/no-floating-promises.md +52 -0
  152. package/rules/ts/hacky/config/env-read-outside-config.md +56 -0
  153. package/rules/ts/hacky/errors/catch-param-typed-unknown.md +67 -0
  154. package/rules/ts/hacky/errors/custom-error-class-instanceof.md +66 -0
  155. package/rules/ts/hacky/errors/empty-catch.md +52 -0
  156. package/rules/ts/hacky/errors/log-and-swallow.md +65 -0
  157. package/rules/ts/hacky/errors/throw-typed-error-with-context.md +64 -0
  158. package/rules/ts/hacky/operators/nullish-coalescing-over-or.md +50 -0
  159. package/rules/ts/hacky/state/exported-let.md +60 -0
  160. package/rules/ts/hacky/type-safety/no-double-assertion.md +34 -0
  161. package/rules/ts/hacky/type-safety/no-explicit-any.md +48 -0
  162. package/rules/ts/hacky/type-safety/no-non-null-assertion.md +43 -0
  163. package/rules/ts/hacky/type-safety/no-unchecked-type-assertion.md +54 -0
  164. package/rules/ts/hacky/type-safety/validate-parsed-json.md +48 -0
  165. package/rules/ts/idiom/async/no-promise-chains.md +45 -0
  166. package/rules/ts/idiom/async/parallelize-independent-awaits.md +46 -0
  167. package/rules/ts/idiom/comments/doc-comment-above-declaration.md +43 -0
  168. package/rules/ts/idiom/constants/as-const-for-literal-config.md +65 -0
  169. package/rules/ts/idiom/constants/no-enum.md +37 -0
  170. package/rules/ts/idiom/control-flow/always-brace-if.md +56 -0
  171. package/rules/ts/idiom/control-flow/guard-clauses-early-return.md +52 -0
  172. package/rules/ts/idiom/control-flow/no-nested-ternary.md +54 -0
  173. package/rules/ts/idiom/errors/error-message-lowercase.md +56 -0
  174. package/rules/ts/idiom/errors/log-at-boundary-not-every-layer.md +66 -0
  175. package/rules/ts/idiom/exports/inline-export-at-declaration.md +49 -0
  176. package/rules/ts/idiom/exports/no-default-export.md +37 -0
  177. package/rules/ts/idiom/formatting/multiline-object-literals.md +46 -0
  178. package/rules/ts/idiom/functions/arrow-for-callbacks.md +44 -0
  179. package/rules/ts/idiom/functions/function-declaration-for-top-level.md +56 -0
  180. package/rules/ts/idiom/imports/export-type-for-types.md +34 -0
  181. package/rules/ts/idiom/imports/import-order.md +66 -0
  182. package/rules/ts/idiom/imports/import-type-for-types.md +50 -0
  183. package/rules/ts/idiom/imports/side-effect-imports-last.md +42 -0
  184. package/rules/ts/idiom/logging/structured-logging-lowercase.md +45 -0
  185. package/rules/ts/idiom/naming/boolean-name-question-prefix.md +42 -0
  186. package/rules/ts/idiom/naming/descriptive-identifier-quality.md +48 -0
  187. package/rules/ts/idiom/naming/screaming-snake-module-constants.md +58 -0
  188. package/rules/ts/idiom/strings/template-literals-over-concat.md +51 -0
  189. package/rules/ts/idiom/types/interface-extends-over-intersection.md +44 -0
  190. package/rules/ts/idiom/types/null-vs-undefined-convention.md +43 -0
  191. package/rules/ts/idiom/types/prefer-type-over-interface.md +64 -0
  192. package/rules/ts/idiom/types/satisfies-over-annotation.md +43 -0
  193. package/rules/ts/idiom/types/type-predicates-for-narrowing.md +53 -0
  194. package/rules/ts/idiom/variables/const-by-default.md +41 -0
  195. package/rules/ts/idiom/variables/no-var.md +36 -0
@@ -0,0 +1,52 @@
1
+ ---
2
+ severity: major
3
+ detect: judge
4
+ falsePositives:
5
+ - a promise explicitly discarded with `void` and a comment saying why, such as fire-and-forget telemetry
6
+ - a promise stored in a variable or array and awaited later, including through `Promise.all`
7
+ - a call that returns something other than a promise, when the return type is visible in the diff or obvious from the name
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A promise nobody awaits or returns keeps running with nobody watching: its rejection is not caught by the surrounding `try`, the function returns before the work is done, and depending on the runtime the failure is either an unhandled rejection that kills the process or a warning nobody reads. Every promise should be awaited, returned, collected for a later `Promise.all`, or discarded on purpose with `void` and a reason.
13
+
14
+ ## Message
15
+
16
+ promise is neither awaited nor returned; its rejection is lost
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ async function createUser(input: CreateUserInput): Promise<User> {
22
+ const user = await repository.insert(input);
23
+
24
+ // BAD: a failed email send rejects into the void
25
+ sendWelcomeEmail(user);
26
+
27
+ return user;
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```ts
34
+ async function createUser(input: CreateUserInput): Promise<User> {
35
+ const user = await repository.insert(input);
36
+
37
+ await sendWelcomeEmail(user);
38
+
39
+ return user;
40
+ }
41
+ ```
42
+
43
+ ```ts
44
+ async function createUser(input: CreateUserInput): Promise<User> {
45
+ const user = await repository.insert(input);
46
+
47
+ // the email is best-effort and failures are logged inside the mailer
48
+ void sendWelcomeEmail(user);
49
+
50
+ return user;
51
+ }
52
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - pattern: process.env.$NAME
8
+ - pattern: process.env[$NAME]
9
+ - pattern: import.meta.env.$NAME
10
+ ignore:
11
+ - '**/config/**'
12
+ - '**/config.ts'
13
+ - '**/env.ts'
14
+ - '**/env/**'
15
+ - '**/*.config.ts'
16
+ - '**/*.config.mts'
17
+ - '**/*.test.ts'
18
+ - '**/*.spec.ts'
19
+ - '**/scripts/**'
20
+ ---
21
+
22
+ ## Why
23
+
24
+ Reading `process.env` in the middle of a module scatters the list of variables the program needs across the codebase, so nobody can say what a deployment requires without grepping. Each read is an untyped, possibly undefined string that gets parsed and defaulted differently in every place. Read and validate the environment once in a config module and pass typed values from there.
25
+
26
+ ## Message
27
+
28
+ environment read outside the config module; validate env once and pass typed config
29
+
30
+ ## Bad
31
+
32
+ ```ts
33
+ export async function sendEmail(message: Message): Promise<void> {
34
+ // BAD: an untyped, unvalidated read buried in logic
35
+ const apiKey = process.env.SENDGRID_API_KEY;
36
+
37
+ await client.send(apiKey ?? '', message);
38
+ }
39
+ ```
40
+
41
+ ```ts
42
+ // BAD: the same variable is parsed differently in every module that reads it
43
+ const timeoutMs = Number(process.env['HTTP_TIMEOUT_MS'] ?? 5000);
44
+ ```
45
+
46
+ ## Good
47
+
48
+ ```ts
49
+ export async function sendEmail(config: EmailConfig, message: Message): Promise<void> {
50
+ await client.send(config.apiKey, message);
51
+ }
52
+ ```
53
+
54
+ ```ts
55
+ const timeoutMs = config.http.timeoutMs;
56
+ ```
@@ -0,0 +1,67 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - kind: catch_clause
8
+ has:
9
+ field: type
10
+ regex: 'any'
11
+ - pattern: $E as Error
12
+ inside:
13
+ kind: catch_clause
14
+ stopBy: end
15
+ ---
16
+
17
+ ## Why
18
+
19
+ Anything can be thrown in JavaScript, so a catch parameter is `unknown` and the only honest way to read `.message` is to check `instanceof Error` first. Typing the parameter as `any` or casting it with `as Error` skips that check, and the first time a string or a rejected fetch body is thrown the handler itself crashes on an undefined property. Narrow with `instanceof`, or normalize with `error instanceof Error ? error : new Error(String(error))`.
20
+
21
+ ## Message
22
+
23
+ catch parameter treated as `Error` without a check; keep it `unknown` and narrow with `instanceof`
24
+
25
+ ## Bad
26
+
27
+ ```ts
28
+ try {
29
+ await save(user);
30
+ // BAD: any disables checking on whatever was thrown
31
+ } catch (error: any) {
32
+ logger.error(error.message);
33
+ }
34
+ ```
35
+
36
+ ```ts
37
+ try {
38
+ await save(user);
39
+ } catch (error) {
40
+ // BAD: the cast crashes the handler if a non-Error was thrown
41
+ logger.error((error as Error).message);
42
+ }
43
+ ```
44
+
45
+ ## Good
46
+
47
+ ```ts
48
+ try {
49
+ await save(user);
50
+ } catch (error: unknown) {
51
+ if (error instanceof Error) {
52
+ logger.error(error.message);
53
+ }
54
+
55
+ throw error;
56
+ }
57
+ ```
58
+
59
+ ```ts
60
+ try {
61
+ await save(user);
62
+ } catch (error: unknown) {
63
+ const cause = error instanceof Error ? error : new Error(String(error));
64
+
65
+ throw new SaveError('saving user failed', { cause });
66
+ }
67
+ ```
@@ -0,0 +1,66 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ignore:
5
+ - '**/*.test.ts'
6
+ - '**/*.spec.ts'
7
+ ast:
8
+ rule:
9
+ any:
10
+ - pattern: $E.message === $S
11
+ - pattern: $E.message == $S
12
+ - pattern: $E.message.includes($$$ARGS)
13
+ - pattern: $E.message.startsWith($$$ARGS)
14
+ - pattern: $E.message.match($$$ARGS)
15
+ ---
16
+
17
+ ## Why
18
+
19
+ Branching on the text of an error message couples the caller to a string that was written for humans and will be reworded without anyone checking the callers. A custom error class gives the failure a name, an `instanceof` check that survives rewording, and a place to hang structured fields such as a status code or the offending id. Expected failures get their own class; message matching is left for logs.
20
+
21
+ ## Message
22
+
23
+ branching on error message text; throw a custom error class and check `instanceof`
24
+
25
+ ## Bad
26
+
27
+ ```ts
28
+ try {
29
+ await getUser(id);
30
+ } catch (error: unknown) {
31
+ // BAD: a reworded message silently turns this into a 500
32
+ if (error instanceof Error && error.message === 'user not found') {
33
+ res.status(404).end();
34
+ }
35
+ }
36
+ ```
37
+
38
+ ```ts
39
+ try {
40
+ await connect();
41
+ } catch (error: unknown) {
42
+ // BAD: substring matching on prose
43
+ if (error instanceof Error && error.message.includes('timeout')) {
44
+ return retry();
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Good
50
+
51
+ ```ts
52
+ class NotFoundError extends Error {
53
+ constructor(resource: string) {
54
+ super(`${resource} not found`);
55
+ this.name = 'NotFoundError';
56
+ }
57
+ }
58
+
59
+ try {
60
+ await getUser(id);
61
+ } catch (error: unknown) {
62
+ if (error instanceof NotFoundError) {
63
+ res.status(404).end();
64
+ }
65
+ }
66
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ severity: critical
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: catch_clause
7
+ has:
8
+ kind: statement_block
9
+ regex: '^\{\s*\}$'
10
+ ---
11
+
12
+ ## Why
13
+
14
+ An empty catch swallows the failure and lets the program continue in a state nobody designed for. The bug surfaces later, somewhere else, with no stack trace pointing home. Handle the error, rethrow it with context, or let it propagate. If ignoring it really is correct, the block needs a comment saying why, which makes it non-empty.
15
+
16
+ ## Message
17
+
18
+ empty catch swallows the error; handle it, rethrow with context, or let it propagate
19
+
20
+ ## Bad
21
+
22
+ ```ts
23
+ try {
24
+ await save(record);
25
+ // BAD: the failure vanishes
26
+ } catch {}
27
+ ```
28
+
29
+ ```ts
30
+ try {
31
+ await save(record);
32
+ // BAD: binding the error and dropping it is still swallowing
33
+ } catch (error) {}
34
+ ```
35
+
36
+ ## Good
37
+
38
+ ```ts
39
+ try {
40
+ await save(record);
41
+ } catch (error) {
42
+ throw new PersistenceError(`saving record ${record.id}`, { cause: error });
43
+ }
44
+ ```
45
+
46
+ ```ts
47
+ try {
48
+ await removeTempFile(path);
49
+ } catch {
50
+ // the file is already gone, which is the state we wanted
51
+ }
52
+ ```
@@ -0,0 +1,65 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: catch_clause
7
+ has:
8
+ kind: statement_block
9
+ regex: '^\{\s*(?:(?:console|log|logger|this\.logger|this\.log)\.\w+\([^;]*\);?\s*)+\}$'
10
+ ---
11
+
12
+ ## Why
13
+
14
+ Logging an error and carrying on is an empty catch with a paper trail: the caller gets a normal return and proceeds as if the operation succeeded, so the failure surfaces later as bad data or a confusing second error. The log line is rarely read until then. Rethrow with context, return an explicit failure the caller must handle, or, if continuing really is correct, say why in a comment so the choice is visible.
15
+
16
+ ## Message
17
+
18
+ catch only logs and continues; rethrow with context or return an explicit failure
19
+
20
+ ## Bad
21
+
22
+ ```ts
23
+ async function saveDraft(draft: Draft): Promise<void> {
24
+ try {
25
+ await repo.save(draft);
26
+ // BAD: the caller is told nothing and proceeds as if the save worked
27
+ } catch (error) {
28
+ console.error('failed to save draft', error);
29
+ }
30
+ }
31
+ ```
32
+
33
+ ```ts
34
+ async function saveDraft(draft: Draft): Promise<void> {
35
+ try {
36
+ await repo.save(draft);
37
+ // BAD: a structured logger does not change what the caller sees
38
+ } catch (error) {
39
+ logger.error({ error, draftId: draft.id }, 'failed to save draft');
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Good
45
+
46
+ ```ts
47
+ async function saveDraft(draft: Draft): Promise<void> {
48
+ try {
49
+ await repo.save(draft);
50
+ } catch (error) {
51
+ throw new PersistenceError(`saving draft ${draft.id}`, { cause: error });
52
+ }
53
+ }
54
+ ```
55
+
56
+ ```ts
57
+ async function warmCache(): Promise<void> {
58
+ try {
59
+ await cache.preload();
60
+ } catch (error) {
61
+ // a cold cache only costs latency, and the request path fills it on demand
62
+ logger.warn({ error }, 'cache preload failed, continuing cold');
63
+ }
64
+ }
65
+ ```
@@ -0,0 +1,64 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ignore:
5
+ - '**/*.test.ts'
6
+ - '**/*.spec.ts'
7
+ ast:
8
+ rule:
9
+ pattern: throw new Error($MSG)
10
+ constraints:
11
+ MSG:
12
+ kind: string
13
+ ---
14
+
15
+ ## Why
16
+
17
+ A bare `new Error('...')` with a fixed string gives the catcher nothing to branch on except the message text, and it carries no context about which record or input failed. A typed error class can be checked with `instanceof` and mapped to a status code at the boundary, and a message built from the inputs tells the on-call engineer what actually went wrong. Reserve plain `Error` for true invariants such as the `never` branch of an exhaustive switch, and even then include the value.
18
+
19
+ ## Message
20
+
21
+ bare `Error` with a fixed message; throw a typed error that carries context
22
+
23
+ ## Bad
24
+
25
+ ```ts
26
+ async function getUser(id: string): Promise<User> {
27
+ const user = await repository.find(id);
28
+
29
+ if (!user) {
30
+ // BAD: nothing to branch on and no hint of which user was missing
31
+ throw new Error('user not found');
32
+ }
33
+
34
+ return user;
35
+ }
36
+ ```
37
+
38
+ ## Good
39
+
40
+ ```ts
41
+ async function getUser(id: string): Promise<User> {
42
+ const user = await repository.find(id);
43
+
44
+ if (!user) {
45
+ throw new NotFoundError(`user ${id}`);
46
+ }
47
+
48
+ return user;
49
+ }
50
+ ```
51
+
52
+ ```ts
53
+ function label(status: Status): string {
54
+ switch (status) {
55
+ case Status.ACTIVE:
56
+ return 'running';
57
+ default: {
58
+ const exhaustive: never = status;
59
+
60
+ throw new Error(`unhandled status: ${String(exhaustive)}`);
61
+ }
62
+ }
63
+ }
64
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: binary_expression
7
+ all:
8
+ - has:
9
+ field: operator
10
+ regex: '^\|\|$'
11
+ - has:
12
+ field: right
13
+ any:
14
+ - kind: string
15
+ - kind: number
16
+ - kind: object
17
+ - kind: array
18
+ - kind: template_string
19
+ ---
20
+
21
+ ## Why
22
+
23
+ `value || fallback` replaces every falsy value, so a legitimate `0`, empty string, or `false` is silently swapped for the default and the bug only shows up when someone sets a timeout to zero or clears a name. `??` falls back only on `null` and `undefined`, which is what a default is for. Use `||` only when a falsy value really should be treated as missing, and say so.
24
+
25
+ ## Message
26
+
27
+ `||` with a default replaces 0, empty string, and false; use `??`
28
+
29
+ ## Bad
30
+
31
+ ```ts
32
+ // BAD: a timeout of 0 becomes 5000
33
+ const timeout = options.timeout || 5000;
34
+ ```
35
+
36
+ ```ts
37
+ // BAD: an empty name becomes 'anonymous' even when it was set on purpose
38
+ const name = user.name || 'anonymous';
39
+ ```
40
+
41
+ ## Good
42
+
43
+ ```ts
44
+ const timeout = options.timeout ?? 5000;
45
+ const name = user.name ?? 'anonymous';
46
+ ```
47
+
48
+ ```ts
49
+ const canEdit = isOwner || isAdmin;
50
+ ```
@@ -0,0 +1,60 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: export_statement
7
+ has:
8
+ any:
9
+ - kind: lexical_declaration
10
+ regex: '^let\b'
11
+ - kind: variable_declaration
12
+ ignore:
13
+ - '**/*.test.ts'
14
+ - '**/*.spec.ts'
15
+ ---
16
+
17
+ ## Why
18
+
19
+ An exported `let` is global mutable state: any importer can reassign it, and no reader of the module can know its value at a given moment without tracing every import. Tests that touch it leak into each other, and reloading or running two instances in one process breaks. Keep the state inside a function, class, or factory and expose operations on it, or export a `const`.
20
+
21
+ ## Message
22
+
23
+ exported let is global mutable state; expose functions over the state or export a const
24
+
25
+ ## Bad
26
+
27
+ ```ts
28
+ // BAD: any importer can reassign this
29
+ export let currentUser: User | undefined;
30
+
31
+ export function login(user: User): void {
32
+ currentUser = user;
33
+ }
34
+ ```
35
+
36
+ ```ts
37
+ // BAD: var is hoisted and reassignable from anywhere
38
+ export var requestCount = 0;
39
+ ```
40
+
41
+ ## Good
42
+
43
+ ```ts
44
+ export function createSession(): Session {
45
+ let currentUser: User | undefined;
46
+
47
+ return {
48
+ login(user: User): void {
49
+ currentUser = user;
50
+ },
51
+ current(): User | undefined {
52
+ return currentUser;
53
+ },
54
+ };
55
+ }
56
+ ```
57
+
58
+ ```ts
59
+ export const DEFAULT_PAGE_SIZE = 50;
60
+ ```
@@ -0,0 +1,34 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ pattern: $X as unknown as $T
7
+ ---
8
+
9
+ ## Why
10
+
11
+ `x as unknown as T` exists to defeat the one check a single `as` still performs, that the two types overlap. It converts any value into any type with no runtime check and no compiler objection, which is `any` with extra steps. If the value really is a `T`, a type guard or a schema can prove it; if it is not, the cast just moves the crash somewhere harder to debug.
12
+
13
+ ## Message
14
+
15
+ double assertion through `unknown` bypasses all checking; validate or narrow instead
16
+
17
+ ## Bad
18
+
19
+ ```ts
20
+ // BAD: the compiler has been told to accept anything here
21
+ const user = row as unknown as User;
22
+ ```
23
+
24
+ ## Good
25
+
26
+ ```ts
27
+ const user = userSchema.parse(row);
28
+ ```
29
+
30
+ ```ts
31
+ if (isUser(row)) {
32
+ return row;
33
+ }
34
+ ```
@@ -0,0 +1,48 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: predefined_type
7
+ regex: ^any$
8
+ ---
9
+
10
+ ## Why
11
+
12
+ `any` switches the type checker off for everything it touches, and the hole spreads through every call site. Prefer a real type, then a generic, then `Record<string, T>`, then `unknown` narrowed at the point of use. `any` is the last resort, and it needs a comment saying why.
13
+
14
+ ## Message
15
+
16
+ `any` disables type checking; use a real type, a generic, or `unknown`
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ // BAD: any hides the shape of the payload
22
+ function handle(payload: any) {
23
+ return payload.user.id;
24
+ }
25
+ ```
26
+
27
+ ```ts
28
+ // BAD: Record<string, any> is any with extra steps
29
+ const cache: Record<string, any> = {};
30
+ ```
31
+
32
+ ## Good
33
+
34
+ ```ts
35
+ // GOOD: unknown forces narrowing before use
36
+ function handle(payload: unknown) {
37
+ const parsed = payloadSchema.parse(payload);
38
+
39
+ return parsed.user.id;
40
+ }
41
+ ```
42
+
43
+ ```ts
44
+ // GOOD: a generic keeps the caller's type
45
+ function first<T>(items: readonly T[]): T | undefined {
46
+ return items[0];
47
+ }
48
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ignore:
5
+ - '**/*.test.ts'
6
+ - '**/*.test.tsx'
7
+ ast:
8
+ rule:
9
+ kind: non_null_expression
10
+ ---
11
+
12
+ ## Why
13
+
14
+ The `!` operator tells the compiler to trust you and turns a compile-time question into a runtime crash. Narrow with a guard, use optional chaining with a default, or throw an error that says what was missing. Tests are excluded because a failing assertion there is the intended outcome.
15
+
16
+ ## Message
17
+
18
+ non-null assertion trades a compile-time check for a runtime crash; narrow or throw
19
+
20
+ ## Bad
21
+
22
+ ```ts
23
+ // BAD: crashes with a bare TypeError when the user is missing
24
+ const name = users.find(user => user.id === id)!.name;
25
+ ```
26
+
27
+ ## Good
28
+
29
+ ```ts
30
+ // GOOD: the missing case has a name and a message
31
+ const user = users.find(candidate => candidate.id === id);
32
+
33
+ if (user === undefined) {
34
+ throw new NotFoundError(`user ${id}`);
35
+ }
36
+
37
+ const name = user.name;
38
+ ```
39
+
40
+ ```ts
41
+ // GOOD: optional chaining with a default when absence is fine
42
+ const name = users.find(user => user.id === id)?.name ?? 'anonymous';
43
+ ```