@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,64 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: call_expression
7
+ all:
8
+ - has:
9
+ field: function
10
+ regex: '(^context\.With\w+|Context)$'
11
+ - has:
12
+ field: arguments
13
+ has:
14
+ kind: nil
15
+ nthChild: 1
16
+ ---
17
+
18
+ ## Why
19
+
20
+ A nil context compiles and then panics inside the first function that calls a method on it, often far from where the nil was passed. When there is no context to forward yet, use `context.Background()` at the top of the program or `context.TODO()` to mark a spot that still needs to be wired.
21
+
22
+ ## Message
23
+
24
+ nil passed as a context; use context.Background() or context.TODO()
25
+
26
+ ## Bad
27
+
28
+ ```go
29
+ func fetch(url string) (*http.Response, error) {
30
+ // BAD: nil context panics inside the request
31
+ req, err := http.NewRequestWithContext(nil, http.MethodGet, url, nil)
32
+ if err != nil {
33
+ return nil, err
34
+ }
35
+
36
+ return http.DefaultClient.Do(req)
37
+ }
38
+ ```
39
+
40
+ ```go
41
+ func timeout() (context.Context, context.CancelFunc) {
42
+ // BAD: nil parent context
43
+ return context.WithTimeout(nil, time.Second)
44
+ }
45
+ ```
46
+
47
+ ## Good
48
+
49
+ ```go
50
+ func fetch(ctx context.Context, url string) (*http.Response, error) {
51
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
52
+ if err != nil {
53
+ return nil, err
54
+ }
55
+
56
+ return http.DefaultClient.Do(req)
57
+ }
58
+ ```
59
+
60
+ ```go
61
+ func timeout() (context.Context, context.CancelFunc) {
62
+ return context.WithTimeout(context.Background(), time.Second)
63
+ }
64
+ ```
@@ -0,0 +1,44 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: field_declaration
7
+ has:
8
+ field: type
9
+ regex: '^context\.Context$'
10
+ ---
11
+
12
+ ## Why
13
+
14
+ A context stored in a struct is captured once at construction and then reused by every method call, so cancellation and deadlines belong to the wrong request and the value never expires. Contexts are per call: pass `ctx` as the first parameter of each method that needs it.
15
+
16
+ ## Message
17
+
18
+ context stored in a struct outlives the request it belongs to; pass ctx per call
19
+
20
+ ## Bad
21
+
22
+ ```go
23
+ type Service struct {
24
+ // BAD: captured once, reused by every call
25
+ ctx context.Context
26
+ repo Repository
27
+ }
28
+
29
+ func (s *Service) Get(id string) (*Item, error) {
30
+ return s.repo.Get(s.ctx, id)
31
+ }
32
+ ```
33
+
34
+ ## Good
35
+
36
+ ```go
37
+ type Service struct {
38
+ repo Repository
39
+ }
40
+
41
+ func (s *Service) Get(ctx context.Context, id string) (*Item, error) {
42
+ return s.repo.Get(ctx, id)
43
+ }
44
+ ```
@@ -0,0 +1,53 @@
1
+ ---
2
+ severity: critical
3
+ detect: judge
4
+ falsePositives:
5
+ - read-only resources such as response bodies, files opened with os.Open, and listeners
6
+ - a Close preceded by an explicit Sync or Flush whose error is checked
7
+ - a deferred Rollback that is expected to fail after a successful Commit
8
+ ---
9
+
10
+ ## Why
11
+
12
+ On a written file, a buffered writer, or a transaction, Close is where the last bytes are flushed and the commit happens. Discarding that error turns a full disk or a broken connection into a function that reports success while the data was lost. Check the close error on anything that was written to and fold it into the returned error.
13
+
14
+ ## Message
15
+
16
+ close error on a written resource is discarded; a failed close can mean lost data
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ func write(path string, data []byte) error {
22
+ f, err := os.Create(path)
23
+ if err != nil {
24
+ return fmt.Errorf("creating %s: %w", path, err)
25
+ }
26
+ // BAD: a failed close on a written file means the data may never have been flushed
27
+ defer func() { _ = f.Close() }()
28
+
29
+ _, err = f.Write(data)
30
+
31
+ return err
32
+ }
33
+ ```
34
+
35
+ ## Good
36
+
37
+ ```go
38
+ func write(path string, data []byte) (err error) {
39
+ f, err := os.Create(path)
40
+ if err != nil {
41
+ return fmt.Errorf("creating %s: %w", path, err)
42
+ }
43
+ defer func() {
44
+ if closeErr := f.Close(); closeErr != nil && err == nil {
45
+ err = fmt.Errorf("closing %s: %w", path, closeErr)
46
+ }
47
+ }()
48
+
49
+ _, err = f.Write(data)
50
+
51
+ return err
52
+ }
53
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: type_assertion_expression
7
+ has:
8
+ field: operand
9
+ regex: '^err$'
10
+ ---
11
+
12
+ ## Why
13
+
14
+ A type assertion on an error only sees the outermost value, so it stops matching as soon as the error is wrapped with `%w` anywhere below. `errors.AsType` (or `errors.As`) walks the chain and returns the typed error wherever it sits. A type switch on `err.(type)` has the same weakness.
15
+
16
+ ## Message
17
+
18
+ type assertion on err misses wrapped errors; use errors.AsType or errors.As
19
+
20
+ ## Bad
21
+
22
+ ```go
23
+ func field(err error) string {
24
+ // BAD: assertion only sees the outermost error
25
+ if ve, ok := err.(*ValidationError); ok {
26
+ return ve.Field
27
+ }
28
+
29
+ return ""
30
+ }
31
+ ```
32
+
33
+ ## Good
34
+
35
+ ```go
36
+ func field(err error) string {
37
+ if ve, ok := errors.AsType[*ValidationError](err); ok {
38
+ return ve.Field
39
+ }
40
+
41
+ return ""
42
+ }
43
+ ```
44
+
45
+ ```go
46
+ func field(err error) string {
47
+ var ve *ValidationError
48
+ if errors.As(err, &ve) {
49
+ return ve.Field
50
+ }
51
+
52
+ return ""
53
+ }
54
+ ```
@@ -0,0 +1,84 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ignore:
5
+ - "**/*_test.go"
6
+ ast:
7
+ rule:
8
+ any:
9
+ - kind: short_var_declaration
10
+ - kind: assignment_statement
11
+ has:
12
+ field: left
13
+ regex: '(^|,\s*)_$'
14
+ all:
15
+ - has:
16
+ field: right
17
+ has:
18
+ kind: call_expression
19
+ not:
20
+ has:
21
+ field: function
22
+ regex: '(\.Close|\.Load|\.LoadOrStore)$|^fmt\.Fprint|^os\.LookupEnv$'
23
+ ---
24
+
25
+ ## Why
26
+
27
+ Assigning a returned error to the blank identifier makes the failure disappear at compile time and at runtime: the file is not removed, the number is zero, and the program keeps going with the wrong state. This is the Go form of an empty catch. Check the error and handle or return it; if it truly cannot matter, say why in a comment on the call so the next reader does not have to guess.
28
+
29
+ ## Message
30
+
31
+ returned error discarded with the blank identifier; check it or comment why it cannot matter
32
+
33
+ ## Bad
34
+
35
+ ```go
36
+ func cleanup(path string) {
37
+ // BAD: a failed removal leaves the file behind and nobody knows
38
+ _ = os.Remove(path)
39
+ }
40
+ ```
41
+
42
+ ```go
43
+ func parseLimit(raw string) int {
44
+ // BAD: bad input silently becomes zero
45
+ limit, _ := strconv.Atoi(raw)
46
+
47
+ return limit
48
+ }
49
+ ```
50
+
51
+ ## Good
52
+
53
+ ```go
54
+ func cleanup(path string) error {
55
+ if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
56
+ return fmt.Errorf("removing %s: %w", path, err)
57
+ }
58
+
59
+ return nil
60
+ }
61
+ ```
62
+
63
+ ```go
64
+ func parseLimit(raw string) (int, error) {
65
+ limit, err := strconv.Atoi(raw)
66
+ if err != nil {
67
+ return 0, fmt.Errorf("parsing limit %q: %w", raw, err)
68
+ }
69
+
70
+ return limit, nil
71
+ }
72
+ ```
73
+
74
+ ```go
75
+ func readAll(path string) ([]byte, error) {
76
+ f, err := os.Open(path)
77
+ if err != nil {
78
+ return nil, fmt.Errorf("opening %s: %w", path, err)
79
+ }
80
+ defer func() { _ = f.Close() }()
81
+
82
+ return io.ReadAll(f)
83
+ }
84
+ ```
@@ -0,0 +1,77 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: call_expression
7
+ has:
8
+ field: function
9
+ regex: '^panic$'
10
+ not:
11
+ any:
12
+ - inside:
13
+ kind: function_declaration
14
+ stopBy: end
15
+ has:
16
+ field: name
17
+ regex: '^(main|init|Must\w*)$'
18
+ - inside:
19
+ kind: default_case
20
+ stopBy: end
21
+ ignore:
22
+ - '**/*_test.go'
23
+ ---
24
+
25
+ ## Why
26
+
27
+ A panic in library code turns an operational failure such as a missing row or a bad input into a process crash that the caller cannot handle. Panics belong to genuinely unrecoverable situations: `init` setup that cannot proceed, `Must` helpers that document the contract in their name, and impossible states after exhaustive handling. Everything else returns an error.
28
+
29
+ ## Message
30
+
31
+ panic in library code; return an error and let the caller decide
32
+
33
+ ## Bad
34
+
35
+ ```go
36
+ func (s *Store) Get(id string) *Item {
37
+ item, ok := s.items[id]
38
+ if !ok {
39
+ // BAD: a missing row is an operational error, not a violated invariant
40
+ panic("item not found")
41
+ }
42
+
43
+ return item
44
+ }
45
+ ```
46
+
47
+ ## Good
48
+
49
+ ```go
50
+ func (s *Store) Get(id string) (*Item, error) {
51
+ item, ok := s.items[id]
52
+ if !ok {
53
+ return nil, fmt.Errorf("item %s: %w", id, ErrNotFound)
54
+ }
55
+
56
+ return item, nil
57
+ }
58
+ ```
59
+
60
+ ```go
61
+ func init() {
62
+ if err := setupTransport(); err != nil {
63
+ panic(fmt.Sprintf("transport initialization failed: %v", err))
64
+ }
65
+ }
66
+ ```
67
+
68
+ ```go
69
+ func label(status Status) string {
70
+ switch status {
71
+ case StatusActive, StatusPending:
72
+ return string(status)
73
+ default:
74
+ panic(fmt.Sprintf("unhandled status: %v", status))
75
+ }
76
+ }
77
+ ```
@@ -0,0 +1,63 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: binary_expression
7
+ regex: '(==|!=)'
8
+ has:
9
+ any:
10
+ - kind: identifier
11
+ - kind: selector_expression
12
+ regex: '(^|\.)Err[A-Z]\w*$'
13
+ ---
14
+
15
+ ## Why
16
+
17
+ Comparing an error with `==` only matches the exact value, so it works until someone wraps the error with `%w` one layer down, and then the branch silently stops firing. `errors.Is` walks the chain and keeps working as the code around it grows.
18
+
19
+ ## Message
20
+
21
+ error compared with == misses wrapped errors; use errors.Is
22
+
23
+ ## Bad
24
+
25
+ ```go
26
+ func status(err error) int {
27
+ // BAD: equality misses the sentinel once it has been wrapped
28
+ if err == ErrNotFound {
29
+ return http.StatusNotFound
30
+ }
31
+
32
+ return http.StatusInternalServerError
33
+ }
34
+ ```
35
+
36
+ ```go
37
+ func retry(err error) bool {
38
+ // BAD: equality misses the sentinel once it has been wrapped
39
+ return err != store.ErrConflict
40
+ }
41
+ ```
42
+
43
+ ## Good
44
+
45
+ ```go
46
+ func status(err error) int {
47
+ if errors.Is(err, ErrNotFound) {
48
+ return http.StatusNotFound
49
+ }
50
+
51
+ return http.StatusInternalServerError
52
+ }
53
+ ```
54
+
55
+ ```go
56
+ func load() error {
57
+ if err := read(); err != nil {
58
+ return err
59
+ }
60
+
61
+ return nil
62
+ }
63
+ ```
@@ -0,0 +1,71 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: call_expression
7
+ all:
8
+ - has:
9
+ field: function
10
+ regex: '^fmt\.Errorf$'
11
+ - has:
12
+ field: arguments
13
+ all:
14
+ - has:
15
+ kind: interpreted_string_literal
16
+ regex: '%[vs]'
17
+ not:
18
+ regex: '%w'
19
+ - has:
20
+ regex: '^err(\.Error\(\))?$'
21
+ ---
22
+
23
+ ## Why
24
+
25
+ Formatting an error with `%v` or `%s` flattens it to text, so `errors.Is` and `errors.As` upstream can no longer see the sentinel or type underneath. The wrap looks identical at a glance and the breakage only appears when a caller starts matching on the cause. Use `%w` for the error argument so the chain survives.
26
+
27
+ ## Message
28
+
29
+ fmt.Errorf with %v drops the error chain; wrap with %w
30
+
31
+ ## Bad
32
+
33
+ ```go
34
+ func load(path string) error {
35
+ if err := read(path); err != nil {
36
+ // BAD: %v flattens err so errors.Is stops working upstream
37
+ return fmt.Errorf("reading %s: %v", path, err)
38
+ }
39
+
40
+ return nil
41
+ }
42
+ ```
43
+
44
+ ```go
45
+ func load(path string) error {
46
+ if err := read(path); err != nil {
47
+ // BAD: err.Error() flattens the chain to a string
48
+ return fmt.Errorf("reading %s: %s", path, err.Error())
49
+ }
50
+
51
+ return nil
52
+ }
53
+ ```
54
+
55
+ ## Good
56
+
57
+ ```go
58
+ func load(path string) error {
59
+ if err := read(path); err != nil {
60
+ return fmt.Errorf("reading %s: %w", path, err)
61
+ }
62
+
63
+ return nil
64
+ }
65
+ ```
66
+
67
+ ```go
68
+ func check(code int) error {
69
+ return fmt.Errorf("unexpected status %d", code)
70
+ }
71
+ ```
@@ -0,0 +1,95 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: var_spec
7
+ inside:
8
+ kind: var_declaration
9
+ stopBy: end
10
+ not:
11
+ inside:
12
+ any:
13
+ - kind: function_declaration
14
+ - kind: method_declaration
15
+ - kind: func_literal
16
+ stopBy: end
17
+ any:
18
+ - has:
19
+ kind: map_type
20
+ - has:
21
+ kind: slice_type
22
+ - has:
23
+ kind: expression_list
24
+ has:
25
+ kind: composite_literal
26
+ has:
27
+ any:
28
+ - kind: map_type
29
+ - kind: slice_type
30
+ - has:
31
+ kind: expression_list
32
+ has:
33
+ kind: call_expression
34
+ regex: '^make\('
35
+ ignore:
36
+ - '**/*_test.go'
37
+ ---
38
+
39
+ ## Why
40
+
41
+ A package-level map or slice is shared by every goroutine and every test in the process, with no owner to guard it. Concurrent writes race, tests leak state into each other, and nothing in a function signature says the function depends on it. Put the collection in a struct with its own mutex, construct it explicitly, and pass it to what needs it.
42
+
43
+ ## Message
44
+
45
+ package-level mutable collection is shared global state; own it in a struct and pass it in
46
+
47
+ ## Bad
48
+
49
+ ```go
50
+ package cache
51
+
52
+ // BAD: every goroutine writes to the same unguarded map
53
+ var entries = map[string][]byte{}
54
+
55
+ func Put(key string, value []byte) {
56
+ entries[key] = value
57
+ }
58
+ ```
59
+
60
+ ```go
61
+ package registry
62
+
63
+ // BAD: appended to from init functions across the package, in no defined order
64
+ var handlers []Handler
65
+ ```
66
+
67
+ ## Good
68
+
69
+ ```go
70
+ package cache
71
+
72
+ type Cache struct {
73
+ mu sync.RWMutex
74
+ entries map[string][]byte
75
+ }
76
+
77
+ func New() *Cache {
78
+ return &Cache{entries: make(map[string][]byte)}
79
+ }
80
+
81
+ func (c *Cache) Put(key string, value []byte) {
82
+ c.mu.Lock()
83
+ defer c.mu.Unlock()
84
+
85
+ c.entries[key] = value
86
+ }
87
+ ```
88
+
89
+ ```go
90
+ package registry
91
+
92
+ var ErrDuplicate = errors.New("handler already registered")
93
+
94
+ const defaultCapacity = 16
95
+ ```
@@ -0,0 +1,61 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: call_expression
7
+ has:
8
+ field: function
9
+ regex: '^(os\.Exit|log\.Fatal(f|ln)?)$'
10
+ not:
11
+ inside:
12
+ kind: function_declaration
13
+ stopBy: end
14
+ has:
15
+ field: name
16
+ regex: '^main$'
17
+ ignore:
18
+ - '**/*_test.go'
19
+ ---
20
+
21
+ ## Why
22
+
23
+ `os.Exit` and the `log.Fatal` family end the process on the spot, skipping every deferred close and flush up the stack and making the function impossible to reuse or test. Return an error instead and let `main` be the one place that decides to exit.
24
+
25
+ ## Message
26
+
27
+ os.Exit outside main skips every defer; return an error and exit from main
28
+
29
+ ## Bad
30
+
31
+ ```go
32
+ func run() error {
33
+ cfg, err := loadConfig()
34
+ if err != nil {
35
+ // BAD: exits from a library-style function, skipping every defer up the stack
36
+ log.Fatalf("loading config: %v", err)
37
+ }
38
+
39
+ return serve(cfg)
40
+ }
41
+ ```
42
+
43
+ ## Good
44
+
45
+ ```go
46
+ func run() error {
47
+ cfg, err := loadConfig()
48
+ if err != nil {
49
+ return fmt.Errorf("loading config: %w", err)
50
+ }
51
+
52
+ return serve(cfg)
53
+ }
54
+
55
+ func main() {
56
+ if err := run(); err != nil {
57
+ slog.Error("fatal error", "error", err)
58
+ os.Exit(1)
59
+ }
60
+ }
61
+ ```