@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,49 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ignore:
5
+ - "**/*_test.go"
6
+ ast:
7
+ rule:
8
+ any:
9
+ - pattern: map[string]any
10
+ - pattern: map[string]interface{}
11
+ ---
12
+
13
+ ## Why
14
+
15
+ A `map[string]any` pushes every field access to runtime with a type assertion and a nil check, and the compiler can no longer tell you when a field is renamed. Decode JSON and config into a struct. Tests are excluded: decoding a payload into a map to inspect it is the honest way to assert on wire format. If the shape is genuinely open, a generic constrained by an interface still beats a bag of `any`.
16
+
17
+ ## Message
18
+
19
+ map[string]any defers every field to runtime; decode into a struct
20
+
21
+ ## Bad
22
+
23
+ ```go
24
+ func parse(raw []byte) error {
25
+ // BAD: every access is an assertion waiting to fail
26
+ var payload map[string]any
27
+
28
+ return json.Unmarshal(raw, &payload)
29
+ }
30
+ ```
31
+
32
+ ## Good
33
+
34
+ ```go
35
+ type Payload struct {
36
+ UserID string `json:"user_id"`
37
+ Amount int `json:"amount"`
38
+ }
39
+
40
+ func parse(raw []byte) (Payload, error) {
41
+ var payload Payload
42
+
43
+ if err := json.Unmarshal(raw, &payload); err != nil {
44
+ return Payload{}, fmt.Errorf("decoding payload: %w", err)
45
+ }
46
+
47
+ return payload, nil
48
+ }
49
+ ```
@@ -0,0 +1,48 @@
1
+ ---
2
+ severity: major
3
+ detect: judge
4
+ falsePositives:
5
+ - a genuinely dynamic shape such as user-defined metadata or plugin configuration
6
+ - map[string]json.RawMessage used for a two-phase decode on a discriminator field
7
+ - pass-through data that is never read by field name
8
+ ---
9
+
10
+ ## Why
11
+
12
+ Decoding JSON into a map and then reading `m["name"]` moves every field name and type into runtime lookups, so a renamed field or a wrong type compiles fine and fails on the first request. A struct with tags gives the decoder the shape up front, validates types on unmarshal, and lets the compiler catch a misspelled field.
13
+
14
+ ## Message
15
+
16
+ json decoded into a map; declare a struct with tags so fields are checked at compile time
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ func parse(raw []byte) (string, error) {
22
+ // BAD: fields live in string keys that nothing checks
23
+ var payload map[string]string
24
+ if err := json.Unmarshal(raw, &payload); err != nil {
25
+ return "", err
26
+ }
27
+
28
+ return payload["name"], nil
29
+ }
30
+ ```
31
+
32
+ ## Good
33
+
34
+ ```go
35
+ type Request struct {
36
+ Name string `json:"name"`
37
+ Email string `json:"email,omitempty"`
38
+ }
39
+
40
+ func parse(raw []byte) (Request, error) {
41
+ var req Request
42
+ if err := json.Unmarshal(raw, &req); err != nil {
43
+ return Request{}, fmt.Errorf("decoding request: %w", err)
44
+ }
45
+
46
+ return req, nil
47
+ }
48
+ ```
@@ -0,0 +1,71 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - kind: type_identifier
8
+ regex: '^any$'
9
+ - kind: interface_type
10
+ regex: '^interface\s*\{\s*\}$'
11
+ not:
12
+ any:
13
+ - inside:
14
+ kind: type_constraint
15
+ - inside:
16
+ kind: variadic_parameter_declaration
17
+ - inside:
18
+ kind: map_type
19
+ regex: '^map\[(string|any|interface\{\})\]'
20
+ ---
21
+
22
+ ## Why
23
+
24
+ A parameter, field, or return typed `any` pushes every use to a runtime assertion and hides the real shape from the compiler and the reader. Prefer a concrete type, then a type parameter with a constraint, then a small interface; `any` is the last resort for genuinely dynamic data. Type constraints (`[T any]`) and printf-style `...any` variadics are not flagged; maps of `any` are covered by their own rules.
25
+
26
+ ## Message
27
+
28
+ any erases the type; use a concrete type, a type parameter, or a small interface
29
+
30
+ ## Bad
31
+
32
+ ```go
33
+ // BAD: any erases the type and every caller pays with assertions
34
+ func Process(data any) any {
35
+ return data
36
+ }
37
+ ```
38
+
39
+ ```go
40
+ type Event struct {
41
+ // BAD: payload has no shape the compiler can check
42
+ Payload interface{}
43
+ }
44
+ ```
45
+
46
+ ## Good
47
+
48
+ ```go
49
+ type Sizer interface {
50
+ Size() int
51
+ }
52
+
53
+ func Largest[T Sizer](items []T) (T, bool) {
54
+ var zero T
55
+ if len(items) == 0 {
56
+ return zero, false
57
+ }
58
+
59
+ return items[0], true
60
+ }
61
+ ```
62
+
63
+ ```go
64
+ type Event struct {
65
+ Payload Payload
66
+ }
67
+
68
+ func Logf(format string, args ...any) {
69
+ slog.Info(fmt.Sprintf(format, args...))
70
+ }
71
+ ```
@@ -0,0 +1,35 @@
1
+ ---
2
+ severity: major
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: map_type
7
+ has:
8
+ field: key
9
+ regex: '^(any|interface\{\s*\})$'
10
+ ---
11
+
12
+ ## Why
13
+
14
+ A map keyed by `any` says nothing about what goes in or comes out, so both the lookup and the value need runtime assertions and a typo in a key type compiles fine. Give the map a concrete key type and, where the values vary, a small interface or a typed union such as a string enum.
15
+
16
+ ## Message
17
+
18
+ map[any]any is fully untyped; give the map concrete key and value types
19
+
20
+ ## Bad
21
+
22
+ ```go
23
+ type Registry struct {
24
+ // BAD: neither keys nor values have a checkable type
25
+ handlers map[any]any
26
+ }
27
+ ```
28
+
29
+ ## Good
30
+
31
+ ```go
32
+ type Registry struct {
33
+ handlers map[string]HandlerFunc
34
+ }
35
+ ```
@@ -0,0 +1,81 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - kind: function_declaration
8
+ has:
9
+ field: name
10
+ regex: '^[A-Z]'
11
+ - kind: method_declaration
12
+ has:
13
+ field: name
14
+ regex: '^[A-Z]'
15
+ - kind: type_declaration
16
+ inside:
17
+ kind: source_file
18
+ has:
19
+ kind: type_spec
20
+ has:
21
+ field: name
22
+ regex: '^[A-Z]'
23
+ not:
24
+ follows:
25
+ kind: comment
26
+ ignore:
27
+ - '**/*_test.go'
28
+ - '**/*.pb.go'
29
+ - '**/*_gen.go'
30
+ ---
31
+
32
+ ## Why
33
+
34
+ An exported name is the package's contract, and its doc comment is what `go doc` and every editor show at the call site. Without one the reader opens the source to learn what the function promises, and the omission tends to spread to the next export. One sentence starting with the name is enough.
35
+
36
+ ## Message
37
+
38
+ exported declaration has no doc comment
39
+
40
+ ## Bad
41
+
42
+ ```go
43
+ // BAD: exported function with no doc comment
44
+ func Process(ctx context.Context, item Item) error {
45
+ return nil
46
+ }
47
+ ```
48
+
49
+ ```go
50
+ // BAD: exported type with no doc comment
51
+ type Service struct {
52
+ repo Repository
53
+ }
54
+ ```
55
+
56
+ ```go
57
+ // BAD: exported method with no doc comment
58
+ func (s *Service) Process(ctx context.Context, item Item) error {
59
+ return nil
60
+ }
61
+ ```
62
+
63
+ ## Good
64
+
65
+ ```go
66
+ // Process validates and persists item.
67
+ func Process(ctx context.Context, item Item) error {
68
+ return nil
69
+ }
70
+ ```
71
+
72
+ ```go
73
+ // Service processes items according to business rules.
74
+ type Service struct {
75
+ repo Repository
76
+ }
77
+
78
+ func (s *Service) process(ctx context.Context, item Item) error {
79
+ return nil
80
+ }
81
+ ```
@@ -0,0 +1,36 @@
1
+ ---
2
+ severity: info
3
+ detect: judge
4
+ falsePositives:
5
+ - a package comment, which starts with "Package name"
6
+ - a deprecation notice starting with "Deprecated:"
7
+ - a comment on a declaration inside a grouped var or const block
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A doc comment begins with the name it documents and reads as a complete sentence: `// Process validates and persists the item.` That form is what `go doc` and editors index, and it keeps the comment attached to the thing it describes when the file is skimmed. A comment that opens with "This function" or a bare description does not identify its subject.
13
+
14
+ ## Message
15
+
16
+ doc comment does not start with the name it documents
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ // BAD: doc comment does not begin with the name it documents
22
+ // This function validates and persists the item.
23
+ func Process(ctx context.Context, item Item) error {
24
+ return nil
25
+ }
26
+ ```
27
+
28
+ ## Good
29
+
30
+ ```go
31
+ // Process validates and persists item. It returns an error if the item
32
+ // fails validation or cannot be saved.
33
+ func Process(ctx context.Context, item Item) error {
34
+ return nil
35
+ }
36
+ ```
@@ -0,0 +1,47 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a channel that hands off ownership or coordinates goroutines, such as a work queue or cancellation
6
+ - a semaphore channel that bounds concurrency rather than guarding a value
7
+ - an actor loop that owns the state by design and is documented as such
8
+ ---
9
+
10
+ ## Why
11
+
12
+ Guarding a plain value with a channel used as a lock, or with a goroutine that serializes closures, hides a mutex behind extra machinery that the reader has to decode before they can see it is just mutual exclusion. Channels are for moving data between goroutines; a `sync.Mutex` next to the field it protects says exactly what is guarded and is what the race detector and every Go reader expect.
13
+
14
+ ## Message
15
+
16
+ channel used as a lock around plain state; use a sync.Mutex next to the field
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ type Cache struct {
22
+ lock chan struct{}
23
+ items map[string]string
24
+ }
25
+
26
+ func (c *Cache) Set(key, value string) {
27
+ // BAD: a one-slot channel standing in for a mutex
28
+ c.lock <- struct{}{}
29
+ c.items[key] = value
30
+ <-c.lock
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
+ c.items[key] = value
46
+ }
47
+ ```
@@ -0,0 +1,37 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a package that constructs several types, where NewX disambiguates
6
+ - package main
7
+ - a constructor for a secondary type when New is already taken by the primary one
8
+ ---
9
+
10
+ ## Why
11
+
12
+ The package name already says what is being built, so `service.NewService(...)` says it twice while `service.New(...)` reads cleanly. Name the constructor of a package's primary type `New`; reserve `NewX` for secondary types that need to be told apart.
13
+
14
+ ## Message
15
+
16
+ constructor for the package's primary type is named New, not NewX
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ package service
22
+
23
+ // BAD: service.NewService repeats the package name
24
+ func NewService(repo Repository) *Service {
25
+ return &Service{repo: repo}
26
+ }
27
+ ```
28
+
29
+ ## Good
30
+
31
+ ```go
32
+ package service
33
+
34
+ func New(repo Repository) *Service {
35
+ return &Service{repo: repo}
36
+ }
37
+ ```
@@ -0,0 +1,47 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: parameter_declaration
7
+ all:
8
+ - has:
9
+ field: type
10
+ regex: '^context\.Context$'
11
+ - inside:
12
+ kind: parameter_list
13
+ inside:
14
+ any:
15
+ - kind: function_declaration
16
+ - kind: method_declaration
17
+ - kind: func_literal
18
+ field: parameters
19
+ - follows:
20
+ kind: parameter_declaration
21
+ stopBy: end
22
+ ---
23
+
24
+ ## Why
25
+
26
+ The context is the first parameter of every function that takes one, named `ctx`, across the standard library and the ecosystem. Putting it anywhere else makes call sites read differently from every other function and is the first thing a reviewer will ask to move.
27
+
28
+ ## Message
29
+
30
+ context.Context is the first parameter
31
+
32
+ ## Bad
33
+
34
+ ```go
35
+ // BAD: context after the id
36
+ func (s *Service) Get(id string, ctx context.Context) (*Item, error) {
37
+ return s.repo.Get(ctx, id)
38
+ }
39
+ ```
40
+
41
+ ## Good
42
+
43
+ ```go
44
+ func (s *Service) Get(ctx context.Context, id string) (*Item, error) {
45
+ return s.repo.Get(ctx, id)
46
+ }
47
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - pure computation with no network, disk, or database access
6
+ - a method satisfying an interface that has no context, such as http.Handler where r.Context() is used inside
7
+ - test helpers
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A function that talks to the network, a database, or another process without a `context.Context` cannot be cancelled or bounded by a deadline, so a hung dependency hangs every caller and the request that started it. Take `ctx` as the first parameter and pass it through to the call that does the I/O.
13
+
14
+ ## Message
15
+
16
+ function does I/O without a context; take ctx and pass it to the call
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ // BAD: a network call with no way to cancel or bound it
22
+ func (c *Client) Fetch(id string) (*Item, error) {
23
+ resp, err := c.http.Get(c.baseURL + "/items/" + id)
24
+ if err != nil {
25
+ return nil, err
26
+ }
27
+ defer func() { _ = resp.Body.Close() }()
28
+
29
+ return decode(resp.Body)
30
+ }
31
+ ```
32
+
33
+ ## Good
34
+
35
+ ```go
36
+ func (c *Client) Fetch(ctx context.Context, id string) (*Item, error) {
37
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/items/"+id, nil)
38
+ if err != nil {
39
+ return nil, err
40
+ }
41
+
42
+ resp, err := c.http.Do(req)
43
+ if err != nil {
44
+ return nil, err
45
+ }
46
+ defer func() { _ = resp.Body.Close() }()
47
+
48
+ return decode(resp.Body)
49
+ }
50
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - pattern:
8
+ context: 'func f() { errors.New($MSG) }'
9
+ selector: call_expression
10
+ - pattern:
11
+ context: 'func f() { fmt.Errorf($MSG, $$$ARGS) }'
12
+ selector: call_expression
13
+ constraints:
14
+ MSG:
15
+ regex: '^"[A-Z]'
16
+ ---
17
+
18
+ ## Why
19
+
20
+ Error messages get wrapped: `reading config: opening file: permission denied`. A capitalized fragment in the middle of that chain reads wrong, and trailing punctuation doubles up. Error strings are lowercase fragments without a final period.
21
+
22
+ ## Message
23
+
24
+ error strings are lowercase fragments so they read well when wrapped
25
+
26
+ ## Bad
27
+
28
+ ```go
29
+ func load(path string) error {
30
+ // BAD: capitalized message
31
+ return errors.New("Config file is missing")
32
+ }
33
+ ```
34
+
35
+ ```go
36
+ func load(path string) error {
37
+ // BAD: capitalized wrapped message
38
+ return fmt.Errorf("Reading %s: %w", path, err)
39
+ }
40
+ ```
41
+
42
+ ## Good
43
+
44
+ ```go
45
+ func load(path string) error {
46
+ return fmt.Errorf("reading %s: %w", path, err)
47
+ }
48
+ ```
49
+
50
+ ```go
51
+ var ErrMissingConfig = errors.New("config file is missing")
52
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - the inline form `if err := f(); err != nil`
6
+ - an error deliberately collected and checked after a loop or a Wait
7
+ - a second call that does not depend on the first result
8
+ ---
9
+
10
+ ## Why
11
+
12
+ An error is checked on the line after the call that produced it. Anything that runs in between operates on a value that may be the zero value, and the reader has to hold the pending error in their head while following unrelated work. The immediate `if err != nil` keeps the happy path and the failure path next to each other.
13
+
14
+ ## Message
15
+
16
+ check the error on the line after the call that returned it
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ func load(ctx context.Context, id string) (Item, error) {
22
+ item, err := repo.Get(ctx, id)
23
+ // BAD: works on item before err is checked
24
+ item.Normalize()
25
+ if err != nil {
26
+ return Item{}, fmt.Errorf("get item %s: %w", id, err)
27
+ }
28
+
29
+ return item, nil
30
+ }
31
+ ```
32
+
33
+ ## Good
34
+
35
+ ```go
36
+ func load(ctx context.Context, id string) (Item, error) {
37
+ item, err := repo.Get(ctx, id)
38
+ if err != nil {
39
+ return Item{}, fmt.Errorf("get item %s: %w", id, err)
40
+ }
41
+ item.Normalize()
42
+
43
+ return item, nil
44
+ }
45
+ ```
@@ -0,0 +1,65 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: defer_statement
7
+ has:
8
+ kind: call_expression
9
+ has:
10
+ field: function
11
+ regex: '\.Close$'
12
+ ---
13
+
14
+ ## Why
15
+
16
+ A bare `defer f.Close()` drops the returned error without saying so, which is also what `errcheck` flags. For read-only resources the error is not actionable, so discard it explicitly with `_ =` to record that decision; for anything that was written to, check the error, because a failed close can mean the data never reached disk.
17
+
18
+ ## Message
19
+
20
+ bare defer Close ignores its error silently; discard it with _ = or check it
21
+
22
+ ## Bad
23
+
24
+ ```go
25
+ func fetch(url string) error {
26
+ resp, err := http.Get(url)
27
+ if err != nil {
28
+ return err
29
+ }
30
+ // BAD: close error dropped without saying so
31
+ defer resp.Body.Close()
32
+
33
+ return nil
34
+ }
35
+ ```
36
+
37
+ ## Good
38
+
39
+ ```go
40
+ func fetch(url string) error {
41
+ resp, err := http.Get(url)
42
+ if err != nil {
43
+ return err
44
+ }
45
+ defer func() { _ = resp.Body.Close() }()
46
+
47
+ return nil
48
+ }
49
+ ```
50
+
51
+ ```go
52
+ func write(path string) error {
53
+ f, err := os.Create(path)
54
+ if err != nil {
55
+ return err
56
+ }
57
+ defer func() {
58
+ if err := f.Close(); err != nil {
59
+ slog.Error("closing output file", "error", err)
60
+ }
61
+ }()
62
+
63
+ return nil
64
+ }
65
+ ```