@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,53 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: parameter_declaration
7
+ all:
8
+ - has:
9
+ field: type
10
+ regex: '^error$'
11
+ - inside:
12
+ kind: parameter_list
13
+ inside:
14
+ any:
15
+ - kind: function_declaration
16
+ - kind: method_declaration
17
+ - kind: func_literal
18
+ field: result
19
+ - precedes:
20
+ kind: parameter_declaration
21
+ stopBy: end
22
+ ---
23
+
24
+ ## Why
25
+
26
+ Every Go API returns the error last, so `value, err := f()` is muscle memory for the reader. An error in any other position forces callers to look up the signature, and the mismatch shows up as swapped variables at the call site.
27
+
28
+ ## Message
29
+
30
+ error is the last return value
31
+
32
+ ## Bad
33
+
34
+ ```go
35
+ // BAD: error before the value
36
+ func load(path string) (error, []byte) {
37
+ return nil, nil
38
+ }
39
+ ```
40
+
41
+ ## Good
42
+
43
+ ```go
44
+ func load(path string) ([]byte, error) {
45
+ return nil, nil
46
+ }
47
+ ```
48
+
49
+ ```go
50
+ func load(path string) (data []byte, err error) {
51
+ return nil, nil
52
+ }
53
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a handler that logs and then writes a sanitized response instead of returning the error
6
+ - a log line that adds details the caller cannot reconstruct (a debug dump of the request)
7
+ - the error is returned from a top-level run function where nothing above will log it
8
+ ---
9
+
10
+ ## Why
11
+
12
+ An error is either handled or propagated, never both. Logging it and then returning it means every layer up the stack logs the same failure again, and the operator reads three stack-less copies of one event. Wrap it with context and return it; the layer that finally handles it logs once.
13
+
14
+ ## Message
15
+
16
+ error is logged and returned; handle it or propagate it, not both
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ func (s *Service) Get(ctx context.Context, id string) (*Item, error) {
22
+ item, err := s.repo.Get(ctx, id)
23
+ if err != nil {
24
+ // BAD: the caller will log this again
25
+ s.logger.Error("failed to get item", "error", err)
26
+
27
+ return nil, fmt.Errorf("get item %s: %w", id, err)
28
+ }
29
+
30
+ return item, nil
31
+ }
32
+ ```
33
+
34
+ ## Good
35
+
36
+ ```go
37
+ func (s *Service) Get(ctx context.Context, id string) (*Item, error) {
38
+ item, err := s.repo.Get(ctx, id)
39
+ if err != nil {
40
+ return nil, fmt.Errorf("get item %s: %w", id, err)
41
+ }
42
+
43
+ return item, nil
44
+ }
45
+ ```
@@ -0,0 +1,48 @@
1
+ ---
2
+ severity: info
3
+ detect: judge
4
+ falsePositives:
5
+ - a block whose only statement is the return
6
+ - a return that directly follows the opening brace of an if, else, or case body
7
+ - a one-line function body
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A blank line before `return` separates the work of a function from its result, so the eye lands on the exit without reading through the last loop or condition. When the return is glued to the statement above it, the two read as one unit and the function's shape is harder to scan.
13
+
14
+ ## Message
15
+
16
+ return has a blank line before it unless it is the only statement in the block
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ func total(items []Item) int {
22
+ sum := 0
23
+ for _, item := range items {
24
+ sum += item.Value
25
+ }
26
+ // BAD: return glued to the loop above it
27
+ return sum
28
+ }
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```go
34
+ func total(items []Item) int {
35
+ sum := 0
36
+ for _, item := range items {
37
+ sum += item.Value
38
+ }
39
+
40
+ return sum
41
+ }
42
+ ```
43
+
44
+ ```go
45
+ func name(item Item) string {
46
+ return item.Name
47
+ }
48
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ severity: info
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: composite_literal
7
+ regex: '^[^\n]*$'
8
+ all:
9
+ - has:
10
+ field: type
11
+ any:
12
+ - kind: type_identifier
13
+ - kind: qualified_type
14
+ - kind: generic_type
15
+ - has:
16
+ field: body
17
+ has:
18
+ kind: keyed_element
19
+ precedes:
20
+ kind: keyed_element
21
+ stopBy: end
22
+ ---
23
+
24
+ ## Why
25
+
26
+ A struct literal with two or more fields goes one field per line, so gofmt aligns the values and a later field is a one-line diff instead of a rewrite of the whole literal. A single field can stay inline.
27
+
28
+ ## Message
29
+
30
+ struct literal with several fields goes one field per line
31
+
32
+ ## Bad
33
+
34
+ ```go
35
+ func build() Item {
36
+ // BAD: several fields crammed on one line
37
+ return Item{ID: "abc", Name: "example"}
38
+ }
39
+ ```
40
+
41
+ ## Good
42
+
43
+ ```go
44
+ func build() Item {
45
+ return Item{
46
+ ID: "abc",
47
+ Name: "example",
48
+ }
49
+ }
50
+ ```
51
+
52
+ ```go
53
+ func build() Item {
54
+ return Item{ID: "abc"}
55
+ }
56
+ ```
@@ -0,0 +1,42 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a file with a single import or a single group
6
+ - generated code
7
+ - an import block the diff did not touch
8
+ ---
9
+
10
+ ## Why
11
+
12
+ Imports read as four groups separated by blank lines: standard library, external modules, this module's own packages, and side-effect imports. gofmt only sorts within a group, so an import dropped into the wrong group stays there and the reader has to scan every line to learn what the file depends on. Keep the groups in that order with one blank line between each.
13
+
14
+ ## Message
15
+
16
+ imports are grouped stdlib, external, internal, side-effect with a blank line between groups
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ import (
22
+ "context"
23
+ // BAD: external module mixed into the stdlib group
24
+ "github.com/google/uuid"
25
+ "fmt"
26
+
27
+ "project/pkg/client"
28
+ )
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```go
34
+ import (
35
+ "context"
36
+ "fmt"
37
+
38
+ "github.com/google/uuid"
39
+
40
+ "project/pkg/client"
41
+ )
42
+ ```
@@ -0,0 +1,51 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: import_declaration
7
+ regex: '\n[ \t]*([^_\s][^\n]*)?"[^"\n]*"[ \t]*(//[^\n]*)?\n[ \t]*_[ \t]+"'
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A side-effect import (`_ "project/pkg/log"`) registers something at init time and is easy to miss when it sits inside the internal group, where gofmt sorts it by path. Give side-effect imports their own group after the internal one so the reader sees at a glance which imports exist only for their init.
13
+
14
+ ## Message
15
+
16
+ side-effect imports go in their own group after the internal imports
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ import (
22
+ "context"
23
+ "fmt"
24
+
25
+ "project/pkg/client"
26
+ // BAD: side-effect import glued to the internal group
27
+ _ "project/pkg/log"
28
+ )
29
+ ```
30
+
31
+ ## Good
32
+
33
+ ```go
34
+ import (
35
+ "context"
36
+ "fmt"
37
+
38
+ "project/pkg/client"
39
+
40
+ _ "project/pkg/log"
41
+ )
42
+ ```
43
+
44
+ ```go
45
+ import (
46
+ "embed"
47
+
48
+ _ "project/pkg/log"
49
+ _ "project/pkg/metrics"
50
+ )
51
+ ```
@@ -0,0 +1,39 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a factory that picks among several implementations at runtime
6
+ - returning error or a standard interface such as io.Reader where the concrete type is an implementation detail
7
+ - a parameter that needs unexported fields of the concrete type
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A function that accepts an interface can be called with any implementation and any test double, while a function that returns its concrete type lets the caller use every method and field and decide for itself which interface to view it through. Returning an interface from a constructor hides the type for no gain and forces an assertion on anyone who needs more than the interface offers.
13
+
14
+ ## Message
15
+
16
+ accept interfaces and return concrete types
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ // BAD: constructor hides the concrete type behind an interface
22
+ func New(db *sql.DB) Repository {
23
+ return &PgRepo{db: db}
24
+ }
25
+ ```
26
+
27
+ ## Good
28
+
29
+ ```go
30
+ func New(db *sql.DB) *PgRepo {
31
+ return &PgRepo{db: db}
32
+ }
33
+ ```
34
+
35
+ ```go
36
+ func NewService(repo Repository) *Service {
37
+ return &Service{repo: repo}
38
+ }
39
+ ```
@@ -0,0 +1,47 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a channel that moves work between goroutines rather than iterating a collection
6
+ - an API that must stay compatible with Go older than 1.23
7
+ - a callback that needs to return an error per element (iter.Seq2 with an error value)
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A generator built on a channel and a goroutine leaks the goroutine when the caller stops early, costs a context switch per element, and forces the caller into a manual `for range ch` with no way to break cleanly. `iter.Seq[T]` expresses the same sequence as a plain function, works with `for range`, and stops when the loop body does.
13
+
14
+ ## Message
15
+
16
+ channel or callback generator; return an iter.Seq so callers can range and break
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ // BAD: leaks the goroutine when the caller stops early
22
+ func (s *Store) Items() <-chan Item {
23
+ ch := make(chan Item)
24
+ go func() {
25
+ defer close(ch)
26
+ for _, item := range s.items {
27
+ ch <- item
28
+ }
29
+ }()
30
+
31
+ return ch
32
+ }
33
+ ```
34
+
35
+ ## Good
36
+
37
+ ```go
38
+ func (s *Store) Items() iter.Seq[Item] {
39
+ return func(yield func(Item) bool) {
40
+ for _, item := range s.items {
41
+ if !yield(item) {
42
+ return
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
@@ -0,0 +1,80 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: call_expression
7
+ any:
8
+ - all:
9
+ - has:
10
+ field: function
11
+ regex: '\.(Debug|Info|Warn|Error)$'
12
+ - has:
13
+ field: arguments
14
+ has:
15
+ kind: interpreted_string_literal
16
+ nthChild: 1
17
+ regex: '^"[A-Z][a-z]'
18
+ - all:
19
+ - has:
20
+ field: function
21
+ regex: '\.(Debug|Info|Warn|Error)Context$'
22
+ - has:
23
+ field: arguments
24
+ has:
25
+ kind: interpreted_string_literal
26
+ nthChild: 2
27
+ regex: '^"[A-Z][a-z]'
28
+ ignore:
29
+ - '**/*_test.go'
30
+ ---
31
+
32
+ ## Why
33
+
34
+ Log messages are lowercase fragments, like error strings, so they read uniformly in a stream and can be grepped without guessing the case. A message that starts with a proper noun such as PostgreSQL will be flagged too; keep the noun and accept the finding, or lead with a lowercase word.
35
+
36
+ ## Message
37
+
38
+ log messages are lowercase fragments
39
+
40
+ ## Bad
41
+
42
+ ```go
43
+ func (s *Server) Start(port int) {
44
+ // BAD: capitalized log message
45
+ slog.Info("Starting server", "port", port)
46
+ }
47
+ ```
48
+
49
+ ```go
50
+ func (s *Service) Save(ctx context.Context, item Item) error {
51
+ if err := s.repo.Save(ctx, item); err != nil {
52
+ // BAD: capitalized log message
53
+ s.logger.ErrorContext(ctx, "Failed to save item", "error", err)
54
+
55
+ return err
56
+ }
57
+
58
+ return nil
59
+ }
60
+ ```
61
+
62
+ ## Good
63
+
64
+ ```go
65
+ func (s *Server) Start(port int) {
66
+ slog.Info("starting server", "port", port)
67
+ }
68
+ ```
69
+
70
+ ```go
71
+ func (s *Service) Save(ctx context.Context, item Item) error {
72
+ if err := s.repo.Save(ctx, item); err != nil {
73
+ s.logger.ErrorContext(ctx, "saving item", "error", err)
74
+
75
+ return err
76
+ }
77
+
78
+ return nil
79
+ }
80
+ ```
@@ -0,0 +1,46 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ kind: call_expression
7
+ has:
8
+ field: function
9
+ regex: '^(log|fmt)\.Print(f|ln)?$'
10
+ ignore:
11
+ - '**/main.go'
12
+ - '**/cmd/**'
13
+ - '**/*_test.go'
14
+ ---
15
+
16
+ ## Why
17
+
18
+ `log.Printf` and `fmt.Println` produce unstructured text with no level and no fields, so nothing downstream can filter by severity or query by key. `log/slog` gives a level, a message, and typed attributes for the same line of code. Command entry points that print output for a user are exempt.
19
+
20
+ ## Message
21
+
22
+ unstructured log call; use log/slog with a level and key-value attributes
23
+
24
+ ## Bad
25
+
26
+ ```go
27
+ func (s *Service) Process(id string) {
28
+ // BAD: no level, no fields
29
+ log.Printf("processing item %s", id)
30
+ }
31
+ ```
32
+
33
+ ```go
34
+ func (s *Service) Process(id string) {
35
+ // BAD: output that is really a log line
36
+ fmt.Println("processing", id)
37
+ }
38
+ ```
39
+
40
+ ## Good
41
+
42
+ ```go
43
+ func (s *Service) Process(id string) {
44
+ slog.Info("processing item", "id", id)
45
+ }
46
+ ```
@@ -0,0 +1,97 @@
1
+ ---
2
+ severity: minor
3
+ detect: ast
4
+ ast:
5
+ rule:
6
+ any:
7
+ - kind: identifier
8
+ any:
9
+ - inside:
10
+ kind: var_spec
11
+ - inside:
12
+ kind: const_spec
13
+ - inside:
14
+ kind: parameter_declaration
15
+ - inside:
16
+ kind: function_declaration
17
+ field: name
18
+ - inside:
19
+ kind: expression_list
20
+ inside:
21
+ kind: short_var_declaration
22
+ field: left
23
+ - kind: field_identifier
24
+ any:
25
+ - inside:
26
+ kind: field_declaration
27
+ - inside:
28
+ kind: method_declaration
29
+ field: name
30
+ - inside:
31
+ kind: method_elem
32
+ - kind: type_identifier
33
+ inside:
34
+ kind: type_spec
35
+ field: name
36
+ regex: '(Id|Http|Https|Url|Uri|Json|Api|Sql|Uuid|Xml|Html|Grpc|Tls|Tcp|Udp|Ip|Db|Cpu|Ttl)([A-Z]|$)'
37
+ ---
38
+
39
+ ## Why
40
+
41
+ Go keeps acronyms in one case: `userID`, `HTTPClient`, `parseURL`. Mixed-case forms like `userId` and `HttpClient` are the naming of other languages and stand out immediately in a Go codebase, and a mix of both spellings means the reader has to guess which one a given identifier uses. A leading acronym on an unexported name is lowercase as a whole (`httpClient`), which this rule does not flag.
42
+
43
+ ## Message
44
+
45
+ acronyms keep one case in identifiers: userID, HTTPClient, parseURL
46
+
47
+ ## Bad
48
+
49
+ ```go
50
+ type Client struct {
51
+ // BAD: acronym in mixed case
52
+ userId string
53
+ }
54
+ ```
55
+
56
+ ```go
57
+ // BAD: acronym in mixed case
58
+ func parseUrl(raw string) (*url.URL, error) {
59
+ return url.Parse(raw)
60
+ }
61
+ ```
62
+
63
+ ```go
64
+ func (c *Client) fetch() error {
65
+ // BAD: acronym in mixed case
66
+ responseJson, err := c.read()
67
+ if err != nil {
68
+ return err
69
+ }
70
+
71
+ return c.store(responseJson)
72
+ }
73
+ ```
74
+
75
+ ## Good
76
+
77
+ ```go
78
+ type Client struct {
79
+ userID string
80
+ httpClient *http.Client
81
+ }
82
+
83
+ func parseURL(raw string) (*url.URL, error) {
84
+ return url.Parse(raw)
85
+ }
86
+ ```
87
+
88
+ ```go
89
+ func (c *Client) fetch() error {
90
+ responseJSON, err := c.read()
91
+ if err != nil {
92
+ return err
93
+ }
94
+
95
+ return c.store(responseJSON)
96
+ }
97
+ ```
@@ -0,0 +1,33 @@
1
+ ---
2
+ severity: info
3
+ detect: judge
4
+ falsePositives:
5
+ - multi-method interfaces, which are named for the role they play (Repository, Storage)
6
+ - a method name that does not form a natural er noun
7
+ - a name that matches an existing standard library interface
8
+ ---
9
+
10
+ ## Why
11
+
12
+ A single-method interface is named for the action with an `er` suffix: `Reader`, `Processor`, `Saver`. The name then states the capability, reads naturally at the call site, and matches how the standard library names the same shapes. Names like `IStorage` or `StorageInterface` import other languages' conventions and say nothing about what the value can do.
13
+
14
+ ## Message
15
+
16
+ single-method interface is named for its action with an er suffix
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ // BAD: single-method interface named like a class instead of a capability
22
+ type IStorage interface {
23
+ Save(ctx context.Context, item Item) error
24
+ }
25
+ ```
26
+
27
+ ## Good
28
+
29
+ ```go
30
+ type Saver interface {
31
+ Save(ctx context.Context, item Item) error
32
+ }
33
+ ```
@@ -0,0 +1,37 @@
1
+ ---
2
+ severity: minor
3
+ detect: judge
4
+ falsePositives:
5
+ - a name that only coincidentally shares a prefix with the package
6
+ - exported names in package main
7
+ - a name that would become a keyword or ambiguous when the prefix is dropped
8
+ ---
9
+
10
+ ## Why
11
+
12
+ The package name is read together with every export, so `rotate.FileRotator` and `auth.AuthToken` say the same word twice at every call site. Let the package carry the context and name the export for what remains: `rotate.File`, `auth.Token`. This is also why a package's primary constructor is plain `New`.
13
+
14
+ ## Message
15
+
16
+ exported name repeats the package name; let the package carry the context
17
+
18
+ ## Bad
19
+
20
+ ```go
21
+ package rotate
22
+
23
+ // BAD: rotate.FileRotator repeats the package name
24
+ type FileRotator struct {
25
+ path string
26
+ }
27
+ ```
28
+
29
+ ## Good
30
+
31
+ ```go
32
+ package rotate
33
+
34
+ type File struct {
35
+ path string
36
+ }
37
+ ```