@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,117 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ ignore:
5
+ - '**/*.test.ts'
6
+ - '**/*.spec.ts'
7
+ - '**/*_test.go'
8
+ falsePositives:
9
+ - table-driven tests and test fixtures, where repetition is the point
10
+ - generated code
11
+ - two or three short lines that happen to look alike, such as consecutive field assignments or switch arms
12
+ - blocks that look alike today but belong to different domains and are expected to diverge, when the code says so
13
+ ---
14
+
15
+ ## Why
16
+
17
+ Two blocks that differ only in a name or a literal are one function with a parameter that has not been extracted yet. The next bug fix lands in one copy and not the other, and the reader has to diff them by eye to learn they are the same. Extract the block with the varying part as an argument, or loop over the values.
18
+
19
+ ## Message
20
+
21
+ copy-pasted block differs in one identifier; extract a function or loop over the values
22
+
23
+ ## Bad
24
+
25
+ ```ts
26
+ export async function exportReports(db: Db): Promise<void> {
27
+ // BAD: the second block is the first with 'sales' replaced by 'refunds'
28
+ const salesRows = await db.query('select * from sales where exported = false');
29
+ const salesCsv = toCsv(salesRows);
30
+ await storage.put(`exports/sales-${today()}.csv`, salesCsv);
31
+ await db.execute('update sales set exported = true where exported = false');
32
+ logger.info({ count: salesRows.length }, 'exported sales');
33
+
34
+ const refundRows = await db.query('select * from refunds where exported = false');
35
+ const refundCsv = toCsv(refundRows);
36
+ await storage.put(`exports/refunds-${today()}.csv`, refundCsv);
37
+ await db.execute('update refunds set exported = true where exported = false');
38
+ logger.info({ count: refundRows.length }, 'exported refunds');
39
+ }
40
+ ```
41
+
42
+ ```go
43
+ func ExportReports(ctx context.Context, db *sql.DB) error {
44
+ // BAD: the second block is the first with "sales" replaced by "refunds"
45
+ salesRows, err := query(ctx, db, "select * from sales where exported = false")
46
+ if err != nil {
47
+ return fmt.Errorf("querying sales: %w", err)
48
+ }
49
+ if err := storage.Put(ctx, "exports/sales-"+today()+".csv", toCSV(salesRows)); err != nil {
50
+ return fmt.Errorf("uploading sales: %w", err)
51
+ }
52
+ if _, err := db.ExecContext(ctx, "update sales set exported = true where exported = false"); err != nil {
53
+ return fmt.Errorf("marking sales: %w", err)
54
+ }
55
+
56
+ refundRows, err := query(ctx, db, "select * from refunds where exported = false")
57
+ if err != nil {
58
+ return fmt.Errorf("querying refunds: %w", err)
59
+ }
60
+ if err := storage.Put(ctx, "exports/refunds-"+today()+".csv", toCSV(refundRows)); err != nil {
61
+ return fmt.Errorf("uploading refunds: %w", err)
62
+ }
63
+ if _, err := db.ExecContext(ctx, "update refunds set exported = true where exported = false"); err != nil {
64
+ return fmt.Errorf("marking refunds: %w", err)
65
+ }
66
+
67
+ return nil
68
+ }
69
+ ```
70
+
71
+ ## Good
72
+
73
+ ```ts
74
+ const EXPORTED_TABLES = ['sales', 'refunds'] as const;
75
+
76
+ export async function exportReports(db: Db): Promise<void> {
77
+ for (const table of EXPORTED_TABLES) {
78
+ await exportTable(db, table);
79
+ }
80
+ }
81
+
82
+ async function exportTable(db: Db, table: string): Promise<void> {
83
+ const rows = await db.query(`select * from ${table} where exported = false`);
84
+ await storage.put(`exports/${table}-${today()}.csv`, toCsv(rows));
85
+ await db.execute(`update ${table} set exported = true where exported = false`);
86
+ logger.info({ table, count: rows.length }, 'exported table');
87
+ }
88
+ ```
89
+
90
+ ```go
91
+ var exportedTables = []string{"sales", "refunds"}
92
+
93
+ func ExportReports(ctx context.Context, db *sql.DB) error {
94
+ for _, table := range exportedTables {
95
+ if err := exportTable(ctx, db, table); err != nil {
96
+ return fmt.Errorf("exporting %s: %w", table, err)
97
+ }
98
+ }
99
+
100
+ return nil
101
+ }
102
+
103
+ func exportTable(ctx context.Context, db *sql.DB, table string) error {
104
+ rows, err := query(ctx, db, "select * from "+table+" where exported = false")
105
+ if err != nil {
106
+ return fmt.Errorf("querying: %w", err)
107
+ }
108
+ if err := storage.Put(ctx, "exports/"+table+"-"+today()+".csv", toCSV(rows)); err != nil {
109
+ return fmt.Errorf("uploading: %w", err)
110
+ }
111
+ if _, err := db.ExecContext(ctx, "update "+table+" set exported = true where exported = false"); err != nil {
112
+ return fmt.Errorf("marking: %w", err)
113
+ }
114
+
115
+ return nil
116
+ }
117
+ ```
@@ -0,0 +1,86 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a string literal compared in exactly one place
6
+ - a value from an external protocol or wire format compared once at the boundary where it is parsed into a typed value
7
+ - discriminant literals of a union or sum type that already declares the allowed values, such as `if (event.kind === 'created')` where `kind` is typed as a union of literals
8
+ - map keys, header names, and env variable names, which are identifiers rather than a closed set of states
9
+ ---
10
+
11
+ ## Why
12
+
13
+ Comparing the same handful of string literals in several places means the set of valid values lives nowhere: a typo in one comparison is a silent false branch, and adding a value means finding every switch by hand. Declare the set once as a const object with a derived union type in TypeScript, or a named string type with typed constants in Go, and let the compiler enforce exhaustiveness and spelling.
14
+
15
+ ## Message
16
+
17
+ string literals used as an enum; declare the set once as typed constants
18
+
19
+ ## Bad
20
+
21
+ ```ts
22
+ export function canShip(order: Order): boolean {
23
+ // BAD: the valid statuses exist only as scattered literals, and a typo compiles
24
+ return order.status === 'paid' || order.status === 'packed';
25
+ }
26
+
27
+ export function label(order: Order): string {
28
+ // BAD: same set, spelled again, with 'shiped' waiting to happen
29
+ if (order.status === 'shipped') {
30
+ return 'On its way';
31
+ }
32
+
33
+ return order.status === 'paid' ? 'Preparing' : 'Pending';
34
+ }
35
+ ```
36
+
37
+ ```go
38
+ func CanShip(o Order) bool {
39
+ // BAD: the valid statuses exist only as scattered literals
40
+ return o.Status == "paid" || o.Status == "packed"
41
+ }
42
+
43
+ func Label(o Order) string {
44
+ // BAD: same set, spelled again by hand
45
+ switch o.Status {
46
+ case "shipped":
47
+ return "On its way"
48
+ case "paid":
49
+ return "Preparing"
50
+ default:
51
+ return "Pending"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Good
57
+
58
+ ```ts
59
+ export const OrderStatus = {
60
+ PENDING: 'pending',
61
+ PAID: 'paid',
62
+ PACKED: 'packed',
63
+ SHIPPED: 'shipped',
64
+ } as const;
65
+
66
+ export type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
67
+
68
+ export function canShip(order: Order): boolean {
69
+ return order.status === OrderStatus.PAID || order.status === OrderStatus.PACKED;
70
+ }
71
+ ```
72
+
73
+ ```go
74
+ type Status string
75
+
76
+ const (
77
+ StatusPending Status = "pending"
78
+ StatusPaid Status = "paid"
79
+ StatusPacked Status = "packed"
80
+ StatusShipped Status = "shipped"
81
+ )
82
+
83
+ func CanShip(o Order) bool {
84
+ return o.Status == StatusPaid || o.Status == StatusPacked
85
+ }
86
+ ```
@@ -0,0 +1,47 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ ts:
6
+ rule:
7
+ kind: comment
8
+ regex: '^//\s*[-=*#]{4,}'
9
+ go:
10
+ rule:
11
+ kind: comment
12
+ regex: '^//\s*[-=*#]{4,}'
13
+ ---
14
+
15
+ ## Why
16
+
17
+ Banner comments and decorative separators are a sign the file has grown past one responsibility. They do not help navigation, editors fold on declarations rather than dashes, and they drift out of place as code moves. Split the file, or let the declarations speak for themselves.
18
+
19
+ ## Message
20
+
21
+ banner comments signal a file doing too much; split it instead of decorating it
22
+
23
+ ## Bad
24
+
25
+ ```ts
26
+ // BAD: decorative separator
27
+ // ----------------------------------------
28
+ export function createUser(): void {}
29
+ ```
30
+
31
+ ```go
32
+ // BAD: decorative section header
33
+ // ==== Handlers ====
34
+ func handle() {}
35
+ ```
36
+
37
+ ## Good
38
+
39
+ ```ts
40
+ // creates the user and returns its persisted form
41
+ export function createUser(): void {}
42
+ ```
43
+
44
+ ```go
45
+ // handle serves the health endpoint.
46
+ func handle() {}
47
+ ```
@@ -0,0 +1,47 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - an enum that never leaves the process (not stored, logged, or sent on the wire)
6
+ - protobuf or database enums with a fixed, documented numbering
7
+ - a hot path where the integer is a deliberate optimization and a String method covers logs
8
+ ---
9
+
10
+ ## Why
11
+
12
+ An `iota` enum that is stored, logged, or sent over the wire serializes as a bare number, so inserting a constant renumbers everything that was already saved and a log line shows `status=2` to whoever is debugging it. Back an enum that crosses a boundary with a string; the value stays stable when the list changes and is readable everywhere it appears.
13
+
14
+ ## Message
15
+
16
+ iota enum is serialized as a bare integer; back it with a string
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ type Status int
22
+
23
+ const (
24
+ StatusPending Status = iota
25
+ StatusCompleted
26
+ )
27
+
28
+ type Item struct {
29
+ // BAD: inserting a constant above StatusCompleted changes every stored record
30
+ Status Status `json:"status"`
31
+ }
32
+ ```
33
+
34
+ ## Good
35
+
36
+ ```go
37
+ type Status string
38
+
39
+ const (
40
+ StatusPending Status = "pending"
41
+ StatusCompleted Status = "completed"
42
+ )
43
+
44
+ type Item struct {
45
+ Status Status `json:"status"`
46
+ }
47
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - unrelated constants grouped for convenience, such as defaults or configuration keys
6
+ - a single constant
7
+ - values only ever used as map keys or labels, never as a parameter or field type
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A set of untyped string or int constants that stand for the states of one thing is an enum without a type, so any string is accepted where a status is expected and a typo compiles. Declare a named type and make the constants that type; the compiler then rejects a raw value and a `String` or `MarshalText` method has a type to hang on.
13
+
14
+ ## Message
15
+
16
+ related constants form an enum but have no named type; declare one
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ // BAD: untyped strings, so any string passes where a status is expected
22
+ const (
23
+ StatusPending = "pending"
24
+ StatusCompleted = "completed"
25
+ )
26
+
27
+ func Transition(status string) error {
28
+ return nil
29
+ }
30
+ ```
31
+
32
+ ## Good
33
+
34
+ ```go
35
+ type Status string
36
+
37
+ const (
38
+ StatusPending Status = "pending"
39
+ StatusCompleted Status = "completed"
40
+ )
41
+
42
+ func Transition(status Status) error {
43
+ return nil
44
+ }
45
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ severity: info
3
+ detect: judge
4
+ falsePositives:
5
+ - the type is passed to a function taking the interface in the same package, which already checks it
6
+ - an unexported helper type with a single local use
7
+ - the interface is not known to the implementing package
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A type that is meant to satisfy an interface but never checked against it breaks only at the distant call site that first assigns it, and the compiler error points there instead of at the missing method. `var _ Repository = (*PgRepo)(nil)` next to the type turns that into an error on the type itself the moment the interface changes.
13
+
14
+ ## Message
15
+
16
+ type meant to satisfy an interface is not checked; add var _ Iface = (*T)(nil)
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ type PgRepo struct {
22
+ db *sql.DB
23
+ }
24
+
25
+ // BAD: nothing checks PgRepo against Repository until a distant caller fails to compile
26
+ func (r *PgRepo) Get(ctx context.Context, id string) (*Item, error) {
27
+ return nil, nil
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```go
34
+ type PgRepo struct {
35
+ db *sql.DB
36
+ }
37
+
38
+ var _ Repository = (*PgRepo)(nil)
39
+
40
+ func (r *PgRepo) Get(ctx context.Context, id string) (*Item, error) {
41
+ return nil, nil
42
+ }
43
+ ```
@@ -0,0 +1,47 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - an interface a package exports for plugin authors to implement, where the package is the consumer
6
+ - a widely shared interface in the style of io.Reader
7
+ - a package that both defines and consumes the interface
8
+ ---
9
+
10
+ ## Why
11
+
12
+ An interface declared next to its implementation is shaped by what the implementation offers, not by what a caller needs, so it grows with the implementation and every consumer depends on the implementing package to use it. Declare the interface in the package that calls it, with only the methods that package uses; the implementing package exports a concrete type and satisfies the interface implicitly.
13
+
14
+ ## Message
15
+
16
+ interface declared beside its implementation; define it where it is consumed
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ package postgres
22
+
23
+ // BAD: the implementation dictates the interface, so every consumer imports postgres
24
+ type Repository interface {
25
+ Get(ctx context.Context, id string) (*Item, error)
26
+ Save(ctx context.Context, item *Item) error
27
+ }
28
+
29
+ type Repo struct {
30
+ db *sql.DB
31
+ }
32
+ ```
33
+
34
+ ## Good
35
+
36
+ ```go
37
+ package service
38
+
39
+ type Repository interface {
40
+ Get(ctx context.Context, id string) (*Item, error)
41
+ Save(ctx context.Context, item *Item) error
42
+ }
43
+
44
+ type Service struct {
45
+ repo Repository
46
+ }
47
+ ```
@@ -0,0 +1,53 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: interface_type
7
+ has:
8
+ kind: method_elem
9
+ nthChild:
10
+ position: 5
11
+ ofRule:
12
+ kind: method_elem
13
+ ---
14
+
15
+ ## Why
16
+
17
+ Every method on an interface is a method every implementation and every test double must provide, so a five-method interface makes the next fake and the next adapter five times the work of a one-method one. Small interfaces compose: declare `Reader` and `Writer` and embed them into `ReadWriter` where a consumer needs both.
18
+
19
+ ## Message
20
+
21
+ interface with five or more methods; split it and compose smaller interfaces
22
+
23
+ ## Bad
24
+
25
+ ```go
26
+ // BAD: every fake and adapter must implement all of these
27
+ type Storage interface {
28
+ Get(ctx context.Context, id string) ([]byte, error)
29
+ Put(ctx context.Context, id string, data []byte) error
30
+ Delete(ctx context.Context, id string) error
31
+ List(ctx context.Context, prefix string) ([]string, error)
32
+ Stat(ctx context.Context, id string) (Info, error)
33
+ }
34
+ ```
35
+
36
+ ## Good
37
+
38
+ ```go
39
+ type Reader interface {
40
+ Get(ctx context.Context, id string) ([]byte, error)
41
+ List(ctx context.Context, prefix string) ([]string, error)
42
+ }
43
+
44
+ type Writer interface {
45
+ Put(ctx context.Context, id string, data []byte) error
46
+ Delete(ctx context.Context, id string) error
47
+ }
48
+
49
+ type ReadWriter interface {
50
+ Reader
51
+ Writer
52
+ }
53
+ ```
@@ -0,0 +1,34 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: package_identifier
7
+ regex: '^(utils?|helpers?|common|misc|shared)$'
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A package called `utils` or `helpers` has no domain, so every unrelated function ends up there and the package grows into a dependency of everything. The name gives the reader no idea what `utils.Process` does or where the next function belongs. Name packages after the concept they own, such as `rotate` or `auth`, and split a grab-bag by what its functions act on.
13
+
14
+ ## Message
15
+
16
+ package named as a grab-bag; name it after the concept it owns
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ // BAD: no domain, will collect everything
22
+ package utils
23
+ ```
24
+
25
+ ```go
26
+ // BAD: no domain, will collect everything
27
+ package common
28
+ ```
29
+
30
+ ## Good
31
+
32
+ ```go
33
+ package rotate
34
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a type that wraps an external resource (connection, file) and is always built by a constructor
6
+ - a type whose constructor validates invariants that a zero value cannot satisfy
7
+ - a type that is unexported and only constructed in one place
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A struct whose zero value works can be declared as a field or a `var` and used immediately, the way `sync.Mutex` and `bytes.Buffer` are. When the zero value panics on first use, every caller has to remember the constructor, and forgetting it is a nil map write that only shows up at runtime. Initialize lazily inside the methods or make the zero state meaningful.
13
+
14
+ ## Message
15
+
16
+ zero value panics on first use; make the type usable without a constructor
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ type Cache struct {
22
+ mu sync.Mutex
23
+ items map[string]string
24
+ }
25
+
26
+ // BAD: the zero Cache panics here because items is nil
27
+ func (c *Cache) Set(key, value string) {
28
+ c.mu.Lock()
29
+ defer c.mu.Unlock()
30
+ c.items[key] = value
31
+ }
32
+ ```
33
+
34
+ ## Good
35
+
36
+ ```go
37
+ type Cache struct {
38
+ mu sync.Mutex
39
+ items map[string]string
40
+ }
41
+
42
+ func (c *Cache) Set(key, value string) {
43
+ c.mu.Lock()
44
+ defer c.mu.Unlock()
45
+ if c.items == nil {
46
+ c.items = make(map[string]string)
47
+ }
48
+ c.items[key] = value
49
+ }
50
+ ```
@@ -0,0 +1,55 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a collection that genuinely mixes unrelated types
6
+ - an interface used only for behavior, with no assertion back to a concrete type
7
+ - the caller never needs the concrete type again
8
+ ---
9
+
10
+ ## Why
11
+
12
+ Returning an interface or `any` from a lookup and letting callers type-assert back to what they put in moves a compile-time fact to a runtime check at every call site. A type parameter keeps the caller's concrete type through the function, so the assertion and its failure branch disappear and adding a new element type needs no new code.
13
+
14
+ ## Message
15
+
16
+ interface plus assertions where a type parameter would keep the caller's type
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ type HasID interface {
22
+ GetID() string
23
+ }
24
+
25
+ // BAD: callers get an interface back and must assert to recover their own type
26
+ func FindItem(items []HasID, id string) HasID {
27
+ for _, item := range items {
28
+ if item.GetID() == id {
29
+ return item
30
+ }
31
+ }
32
+
33
+ return nil
34
+ }
35
+ ```
36
+
37
+ ## Good
38
+
39
+ ```go
40
+ type HasID interface {
41
+ GetID() string
42
+ }
43
+
44
+ func FindItem[T HasID](items []T, id string) (T, bool) {
45
+ for _, item := range items {
46
+ if item.GetID() == id {
47
+ return item, true
48
+ }
49
+ }
50
+
51
+ var zero T
52
+
53
+ return zero, false
54
+ }
55
+ ```
@@ -0,0 +1,44 @@
1
+ ---
2
+ severity: major
3
+ detect: judge
4
+ falsePositives:
5
+ - a long-lived background loop started from main or a Start method that has a matching Stop
6
+ - a goroutine that reports through a channel the caller reads
7
+ - fire-and-forget by design with the error handled and logged inside the goroutine
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A bare `go f()` in a request path has no one waiting for it, so its errors vanish, its panics take the process down, and the function returns before the work is done. Bound the goroutines with an `errgroup` or a `sync.WaitGroup`, propagate the first error, and let the context cancel the rest.
13
+
14
+ ## Message
15
+
16
+ goroutine started with nothing waiting for it or collecting its error
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ func (s *Service) ProcessAll(ctx context.Context, items []Item) error {
22
+ for _, item := range items {
23
+ // BAD: nothing waits for these goroutines or sees their errors
24
+ go s.process(ctx, item)
25
+ }
26
+
27
+ return nil
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```go
34
+ func (s *Service) ProcessAll(ctx context.Context, items []Item) error {
35
+ g, ctx := errgroup.WithContext(ctx)
36
+ for _, item := range items {
37
+ g.Go(func() error {
38
+ return s.process(ctx, item)
39
+ })
40
+ }
41
+
42
+ return g.Wait()
43
+ }
44
+ ```