@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,40 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: package_identifier
7
+ regex: '[A-Z_]'
8
+ not:
9
+ regex: '^[a-z0-9]+_test$'
10
+ ---
11
+
12
+ ## Why
13
+
14
+ A package name is typed at every call site, so it is short, lowercase, and a single word: `rotate`, `auth`, `client`. Mixed case or underscores in a package name break that rhythm, and the Go toolchain and most linters assume the lowercase form. Only the `_test` suffix on an external test package is conventional.
15
+
16
+ ## Message
17
+
18
+ package names are one short lowercase word
19
+
20
+ ## Bad
21
+
22
+ ```go
23
+ // BAD: mixed case package name
24
+ package httpHelpers
25
+ ```
26
+
27
+ ```go
28
+ // BAD: underscore in package name
29
+ package user_service
30
+ ```
31
+
32
+ ## Good
33
+
34
+ ```go
35
+ package client
36
+ ```
37
+
38
+ ```go
39
+ package client_test
40
+ ```
@@ -0,0 +1,40 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - generated code
6
+ - a method that must have a value receiver to satisfy an interface on the value type, with a comment saying so
7
+ ---
8
+
9
+ ## Why
10
+
11
+ When some methods on a type use a pointer receiver and others a value receiver, the method set differs between `T` and `*T`, so whether a value satisfies an interface depends on how it was declared, and a reader cannot tell from one method whether calls see shared state. If any method needs a pointer receiver, give every method on that type a pointer receiver.
12
+
13
+ ## Message
14
+
15
+ type mixes pointer and value receivers; use one kind for every method
16
+
17
+ ## Bad
18
+
19
+ ```go
20
+ func (s *Service) Process(ctx context.Context, id string) error {
21
+ return nil
22
+ }
23
+
24
+ // BAD: value receiver on a type that already uses pointer receivers
25
+ func (s Service) Name() string {
26
+ return s.name
27
+ }
28
+ ```
29
+
30
+ ## Good
31
+
32
+ ```go
33
+ func (s *Service) Process(ctx context.Context, id string) error {
34
+ return nil
35
+ }
36
+
37
+ func (s *Service) Name() string {
38
+ return s.name
39
+ }
40
+ ```
@@ -0,0 +1,37 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: parameter_declaration
7
+ regex: '^(this|self)\b'
8
+ inside:
9
+ kind: parameter_list
10
+ inside:
11
+ kind: method_declaration
12
+ ---
13
+
14
+ ## Why
15
+
16
+ Go receivers are ordinary parameters and the convention is a one or two letter abbreviation of the type, used consistently across all methods on that type. `this` and `self` import another language's model and stand out as a tell that the author was not writing Go.
17
+
18
+ ## Message
19
+
20
+ receiver named this/self; use a short abbreviation of the type
21
+
22
+ ## Bad
23
+
24
+ ```go
25
+ // BAD: receiver named like an object-oriented keyword
26
+ func (this *Server) Start() error {
27
+ return this.listener.Listen()
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```go
34
+ func (s *Server) Start() error {
35
+ return s.listener.Listen()
36
+ }
37
+ ```
@@ -0,0 +1,49 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - small immutable value types such as enums and thin wrappers on value receivers
6
+ - a type that is deliberately copied on every call and documented as such
7
+ - methods on named map, slice, or channel types
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A value receiver works on a copy, so a method that assigns to a field on a value receiver silently changes nothing, and a large struct is copied on every call. Use a pointer receiver when the method mutates state or the struct is large; use a value receiver for small immutable types such as a string enum with a `String` method.
13
+
14
+ ## Message
15
+
16
+ receiver kind does not fit the method: mutation or a large struct needs a pointer receiver
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ type Counter struct {
22
+ n int
23
+ }
24
+
25
+ // BAD: value receiver mutates a copy, so the increment is lost
26
+ func (c Counter) Inc() {
27
+ c.n++
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```go
34
+ type Counter struct {
35
+ n int
36
+ }
37
+
38
+ func (c *Counter) Inc() {
39
+ c.n++
40
+ }
41
+ ```
42
+
43
+ ```go
44
+ type Status string
45
+
46
+ func (s Status) String() string {
47
+ return string(s)
48
+ }
49
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: parameter_declaration
7
+ all:
8
+ - inside:
9
+ kind: parameter_list
10
+ inside:
11
+ kind: method_declaration
12
+ field: receiver
13
+ - has:
14
+ field: name
15
+ regex: '^[a-zA-Z_]\w{2,}$'
16
+ not:
17
+ regex: '^(this|self)$'
18
+ ---
19
+
20
+ ## Why
21
+
22
+ A receiver is named with one or two letters abbreviating the type, `s` for `Service`, `c` for `Client`, and the same letters on every method of that type. A long receiver name such as `service` reads like a parameter and pushes the method body to the right on every line that touches it.
23
+
24
+ ## Message
25
+
26
+ receiver name longer than two letters; use a short abbreviation of the type
27
+
28
+ ## Bad
29
+
30
+ ```go
31
+ // BAD: receiver named like a parameter
32
+ func (service *Service) Get(ctx context.Context, id string) (*Item, error) {
33
+ return service.repo.Get(ctx, id)
34
+ }
35
+ ```
36
+
37
+ ## Good
38
+
39
+ ```go
40
+ func (s *Service) Get(ctx context.Context, id string) (*Item, error) {
41
+ return s.repo.Get(ctx, id)
42
+ }
43
+ ```
@@ -0,0 +1,40 @@
1
+ ---
2
+ severity: info
3
+ detect: judge
4
+ falsePositives:
5
+ - the variable is read again after its address is taken
6
+ - the module targets a Go version older than 1.26
7
+ ---
8
+
9
+ ## Why
10
+
11
+ Since Go 1.26 `new` accepts an expression, so an optional pointer field can be set inline with `new(yearsSince(born))`. A throwaway local declared only so `&` has something to point at adds a name and a line that carry no meaning.
12
+
13
+ ## Message
14
+
15
+ temporary declared only to take its address; use new(expr)
16
+
17
+ ## Bad
18
+
19
+ ```go
20
+ func person(name string, born time.Time) Person {
21
+ // BAD: age exists only so its address can be taken
22
+ age := yearsSince(born)
23
+
24
+ return Person{
25
+ Name: name,
26
+ Age: &age,
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```go
34
+ func person(name string, born time.Time) Person {
35
+ return Person{
36
+ Name: name,
37
+ Age: new(yearsSince(born)),
38
+ }
39
+ }
40
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a main of a few lines with no resources to release
6
+ - example or generated programs
7
+ - a main that only parses flags and calls one function
8
+ ---
9
+
10
+ ## Why
11
+
12
+ `os.Exit` and `log.Fatal` skip deferred calls, so a `main` that opens resources and exits on error leaks every one of them on the failure path. Move the body into `run() error`, let defers run on every exit, and keep `main` to calling `run`, logging the error, and exiting.
13
+
14
+ ## Message
15
+
16
+ main does the wiring itself; move it into run() error so defers run on every exit
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ func main() {
22
+ db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
23
+ if err != nil {
24
+ log.Fatal(err)
25
+ }
26
+ defer db.Close()
27
+
28
+ if err := serve(db); err != nil {
29
+ // BAD: exits from the middle of the wiring, skipping the deferred close
30
+ log.Fatal(err)
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Good
36
+
37
+ ```go
38
+ func main() {
39
+ if err := run(); err != nil {
40
+ slog.Error("fatal error", "error", err)
41
+ os.Exit(1)
42
+ }
43
+ }
44
+
45
+ func run() error {
46
+ db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
47
+ if err != nil {
48
+ return fmt.Errorf("open database: %w", err)
49
+ }
50
+ defer func() { _ = db.Close() }()
51
+
52
+ return serve(db)
53
+ }
54
+ ```
@@ -0,0 +1,73 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - an abstract class that enforces a method contract across a family of implementations and holds real shared behavior such as a template method
6
+ - framework-required base classes, such as ORM entities, component classes, or error hierarchies extending `Error`
7
+ - a hierarchy that is one level deep and whose subclasses differ only in the abstract method they implement
8
+ ---
9
+
10
+ ## Why
11
+
12
+ An abstract base class introduced to share a helper or two couples every subclass to a hierarchy, so the next feature that does not fit the base shape either bends the base or forks it. A shared function, or a dependency passed into a constructor, gives the same reuse without the inheritance tax and can be swapped or tested on its own. Reach for an abstract class only when there is a real contract to enforce across a family of implementations.
13
+
14
+ ## Message
15
+
16
+ abstract base class used for code sharing; prefer a shared function or injected dependency
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ // BAD: the base exists only to share one helper with its subclasses
22
+ abstract class BaseService {
23
+ protected logDuration(label: string, startedAt: number): void {
24
+ logger.info(`${label} took ${Date.now() - startedAt}ms`);
25
+ }
26
+ }
27
+
28
+ class UserService extends BaseService {
29
+ async getUser(id: string): Promise<User> {
30
+ const startedAt = Date.now();
31
+ const user = await repository.find(id);
32
+
33
+ this.logDuration('getUser', startedAt);
34
+
35
+ return user;
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## Good
41
+
42
+ ```ts
43
+ function logDuration(label: string, startedAt: number): void {
44
+ logger.info(`${label} took ${Date.now() - startedAt}ms`);
45
+ }
46
+
47
+ class UserService {
48
+ constructor(private readonly repository: UserRepository) {}
49
+
50
+ async getUser(id: string): Promise<User> {
51
+ const startedAt = Date.now();
52
+ const user = await this.repository.find(id);
53
+
54
+ logDuration('getUser', startedAt);
55
+
56
+ return user;
57
+ }
58
+ }
59
+ ```
60
+
61
+ ```ts
62
+ abstract class BaseProcessor<TInput, TOutput> {
63
+ async process(input: TInput): Promise<TOutput> {
64
+ this.validate(input);
65
+
66
+ return this.execute(input);
67
+ }
68
+
69
+ protected abstract validate(input: TInput): void;
70
+
71
+ protected abstract execute(input: TInput): Promise<TOutput>;
72
+ }
73
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: function_declaration
7
+ inside:
8
+ kind: export_statement
9
+ not:
10
+ has:
11
+ field: return_type
12
+ regex: '.'
13
+ ---
14
+
15
+ ## Why
16
+
17
+ An exported function is a contract, and an inferred return type lets that contract drift silently: a change deep in the body widens or narrows the type and the error surfaces at some distant call site instead of at the definition. Writing the return type pins the contract, produces errors where the change was made, and documents the function without a comment. Internal helpers and callbacks can keep inference where the type is obvious.
18
+
19
+ ## Message
20
+
21
+ exported function has no return type; declare it so the contract cannot drift
22
+
23
+ ## Bad
24
+
25
+ ```ts
26
+ // BAD: the return type is whatever the body happens to produce today
27
+ export async function getUser(id: string) {
28
+ return repository.find(id);
29
+ }
30
+ ```
31
+
32
+ ## Good
33
+
34
+ ```ts
35
+ export async function getUser(id: string): Promise<User | undefined> {
36
+ return repository.find(id);
37
+ }
38
+ ```
39
+
40
+ ```ts
41
+ function buildWhere(filters: Filters) {
42
+ return {
43
+ ...(filters.status && { status: filters.status }),
44
+ };
45
+ }
46
+
47
+ export function listUsers(filters: Filters): Promise<User[]> {
48
+ return repository.findMany(buildWhere(filters));
49
+ }
50
+ ```
@@ -0,0 +1,46 @@
1
+ ---
2
+ severity: info
3
+ detect: judge
4
+ falsePositives:
5
+ - a schema shared by several modules and placed in a module named for the domain concept it describes, such as `user.schema.ts` next to `user.service.ts`
6
+ - schemas that are the public contract of a package and are grouped deliberately for export
7
+ - a schema moved into its own file because it is large, when it stays in the same feature directory
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A Zod schema is the definition of a shape the code around it consumes, so when it lives in a distant `schemas/` or `types.ts` dump the reader has to jump files to learn what a function accepts, and changes to the consumer and the schema land in different places. Keep the schema next to the code that parses with it and derive the type from it there. A shared schema belongs with the domain module it describes, not in a catch-all.
13
+
14
+ ## Message
15
+
16
+ schema lives away from the code that uses it; colocate it with its consumer
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ // BAD: the request schema lives in a distant catch-all module
22
+ import { createUserSchema } from '@/schemas';
23
+
24
+ export async function handleCreateUser(req: Request, res: Response): Promise<void> {
25
+ const input = createUserSchema.parse(req.body);
26
+
27
+ res.json(await createUser(input));
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```ts
34
+ const createUserSchema = z.object({
35
+ name: z.string().min(1),
36
+ email: z.string().email(),
37
+ });
38
+
39
+ type CreateUserInput = z.infer<typeof createUserSchema>;
40
+
41
+ export async function handleCreateUser(req: Request, res: Response): Promise<void> {
42
+ const input = createUserSchema.parse(req.body);
43
+
44
+ res.json(await createUser(input));
45
+ }
46
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - switches over open types such as `string` or `number`, where a default branch is the only way to finish
6
+ - a switch whose default already assigns the value to `never` or calls an `assertNever` helper
7
+ - a switch that intentionally handles a subset and falls through to shared behavior for everything else
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A switch over a union or a const-object type with a plain `default` keeps compiling when a new variant is added, so the new case silently takes the default path at runtime. Assigning the switched value to `never` in the default branch makes the compiler reject any switch that forgot a case, turning a production bug into a build error at the moment the variant is introduced. The same applies to `if` chains that end in an unconditional fallback.
13
+
14
+ ## Message
15
+
16
+ switch over a union without a `never` check; new variants will fall through silently
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ function label(status: Status): string {
22
+ switch (status) {
23
+ case Status.PENDING:
24
+ return 'waiting';
25
+ case Status.ACTIVE:
26
+ return 'running';
27
+ // BAD: adding Status.CANCELLED compiles and returns 'done'
28
+ default:
29
+ return 'done';
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Good
35
+
36
+ ```ts
37
+ function label(status: Status): string {
38
+ switch (status) {
39
+ case Status.PENDING:
40
+ return 'waiting';
41
+ case Status.ACTIVE:
42
+ return 'running';
43
+ case Status.COMPLETED:
44
+ return 'done';
45
+ default: {
46
+ const exhaustive: never = status;
47
+
48
+ throw new Error(`unhandled status: ${String(exhaustive)}`);
49
+ }
50
+ }
51
+ }
52
+ ```
@@ -0,0 +1,46 @@
1
+ ---
2
+ severity: info
3
+ detect: judge
4
+ falsePositives:
5
+ - parameters or properties that the function or class genuinely mutates
6
+ - builder or accumulator objects whose whole purpose is to be filled in
7
+ - types generated from a schema or an ORM where the modifier is not under the author's control
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A parameter typed as `T[]` or a property typed without `readonly` invites the next author to push into it or reassign it, and the compiler will let them even when the caller never expected its data to change. `readonly` on arrays and on identity fields such as `id` and `createdAt` documents the intent, rejects the mutation at compile time, and costs nothing at runtime. Data that is meant to be shared is safer when it cannot be edited in place.
13
+
14
+ ## Message
15
+
16
+ data that is never mutated is typed as mutable; mark it `readonly`
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ // BAD: nothing stops a caller from reassigning id or pushing into tags
22
+ type User = {
23
+ id: string;
24
+ createdAt: Date;
25
+ tags: string[];
26
+ };
27
+
28
+ function tagNames(tags: string[]): string[] {
29
+ return tags.map(tag => tag.toLowerCase());
30
+ }
31
+ ```
32
+
33
+ ## Good
34
+
35
+ ```ts
36
+ type User = {
37
+ readonly id: string;
38
+ readonly createdAt: Date;
39
+ readonly tags: readonly string[];
40
+ name: string;
41
+ };
42
+
43
+ function tagNames(tags: readonly string[]): string[] {
44
+ return tags.map(tag => tag.toLowerCase());
45
+ }
46
+ ```
@@ -0,0 +1,37 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ pattern: z.enum([$$$VALUES])
7
+ ---
8
+
9
+ ## Why
10
+
11
+ An inline string array in `z.enum(['pending', 'active'])` is a second copy of values that the code also needs as constants, so adding a variant means finding every literal list and hoping none is missed. Deriving the tuple from a const object keeps one definition that feeds the schema, the type, and every `Status.PENDING` reference. The schema then cannot drift from the constants it validates.
12
+
13
+ ## Message
14
+
15
+ `z.enum` with inline string literals; derive the values from a const object
16
+
17
+ ## Bad
18
+
19
+ ```ts
20
+ // BAD: the values live only here and cannot be referenced as constants
21
+ const statusSchema = z.enum(['pending', 'active', 'completed']);
22
+ ```
23
+
24
+ ## Good
25
+
26
+ ```ts
27
+ const Status = {
28
+ PENDING: 'pending',
29
+ ACTIVE: 'active',
30
+ COMPLETED: 'completed',
31
+ } as const;
32
+
33
+ type Status = (typeof Status)[keyof typeof Status];
34
+
35
+ const statusValues = Object.values(Status) as [Status, ...Status[]];
36
+ const statusSchema = z.enum(statusValues);
37
+ ```
@@ -0,0 +1,36 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ pattern: z.nativeEnum($X)
7
+ ---
8
+
9
+ ## Why
10
+
11
+ `z.nativeEnum` exists to wrap a TypeScript `enum`, and it is deprecated in Zod v4, so it ties the schema to a construct the codebase avoids and to an API that will be removed. A const object with `as const` plus `z.enum` over its values gives the same runtime check with a plain object that is grep-friendly, tree-shakeable, and stays valid across Zod upgrades. The type comes from the same object, so there is still one source of truth.
12
+
13
+ ## Message
14
+
15
+ `z.nativeEnum` is deprecated; use `z.enum` over the values of a const object
16
+
17
+ ## Bad
18
+
19
+ ```ts
20
+ // BAD: deprecated in Zod v4 and tied to a TypeScript enum
21
+ const prioritySchema = z.nativeEnum(Priority);
22
+ ```
23
+
24
+ ## Good
25
+
26
+ ```ts
27
+ const Priority = {
28
+ LOW: 'low',
29
+ HIGH: 'high',
30
+ } as const;
31
+
32
+ type Priority = (typeof Priority)[keyof typeof Priority];
33
+
34
+ const priorityValues = Object.values(Priority) as [Priority, ...Priority[]];
35
+ const prioritySchema = z.enum(priorityValues);
36
+ ```
@@ -0,0 +1,46 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - types that describe internal state, function options, or return shapes that are never validated at runtime
6
+ - a type that is deliberately narrower or wider than the schema, such as a branded id, with a comment saying why
7
+ - types generated by another tool, such as an ORM or an API client, that the schema merely mirrors
8
+ ---
9
+
10
+ ## Why
11
+
12
+ Writing a `type` by hand and then a Zod schema that describes the same shape creates two definitions that agree today and drift the first time one of them is edited, with the compiler unable to notice. Define the schema once and derive the type with `z.infer`, so the runtime check and the static type cannot disagree. Input variants come from the schema too, through `omit`, `pick`, and `partial`.
13
+
14
+ ## Message
15
+
16
+ hand-written type duplicates a schema; derive it with `z.infer`
17
+
18
+ ## Bad
19
+
20
+ ```ts
21
+ // BAD: two definitions of the same shape that nothing keeps in sync
22
+ type Item = {
23
+ id: string;
24
+ name: string;
25
+ };
26
+
27
+ const itemSchema = z.object({
28
+ id: z.string(),
29
+ name: z.string(),
30
+ });
31
+ ```
32
+
33
+ ## Good
34
+
35
+ ```ts
36
+ const itemSchema = z.object({
37
+ id: z.string(),
38
+ name: z.string(),
39
+ });
40
+
41
+ type Item = z.infer<typeof itemSchema>;
42
+
43
+ const createItemSchema = itemSchema.omit({ id: true });
44
+
45
+ type CreateItemInput = z.infer<typeof createItemSchema>;
46
+ ```