@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.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/SKILL.md +149 -0
- package/dist/analyzer.d.ts +5 -0
- package/dist/analyzer.js +13 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/analyzers/ast.d.ts +12 -0
- package/dist/analyzers/ast.js +73 -0
- package/dist/analyzers/ast.js.map +1 -0
- package/dist/analyzers/judge-chunks.d.ts +13 -0
- package/dist/analyzers/judge-chunks.js +87 -0
- package/dist/analyzers/judge-chunks.js.map +1 -0
- package/dist/analyzers/judge-validate.d.ts +19 -0
- package/dist/analyzers/judge-validate.js +81 -0
- package/dist/analyzers/judge-validate.js.map +1 -0
- package/dist/analyzers/judge.d.ts +44 -0
- package/dist/analyzers/judge.js +177 -0
- package/dist/analyzers/judge.js.map +1 -0
- package/dist/change.d.ts +19 -0
- package/dist/change.js +87 -0
- package/dist/change.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +125 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +91 -0
- package/dist/config.js +61 -0
- package/dist/config.js.map +1 -0
- package/dist/confirm.d.ts +11 -0
- package/dist/confirm.js +100 -0
- package/dist/confirm.js.map +1 -0
- package/dist/finding.d.ts +13 -0
- package/dist/finding.js +18 -0
- package/dist/finding.js.map +1 -0
- package/dist/lang.d.ts +16 -0
- package/dist/lang.js +37 -0
- package/dist/lang.js.map +1 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +34 -0
- package/dist/model.js.map +1 -0
- package/dist/report.d.ts +124 -0
- package/dist/report.js +112 -0
- package/dist/report.js.map +1 -0
- package/dist/rule.d.ts +66 -0
- package/dist/rule.js +259 -0
- package/dist/rule.js.map +1 -0
- package/dist/scan.d.ts +14 -0
- package/dist/scan.js +33 -0
- package/dist/scan.js.map +1 -0
- package/dist/score.d.ts +147 -0
- package/dist/score.js +142 -0
- package/dist/score.js.map +1 -0
- package/dist/shared/concurrency.d.ts +5 -0
- package/dist/shared/concurrency.js +17 -0
- package/dist/shared/concurrency.js.map +1 -0
- package/dist/shared/env.d.ts +27 -0
- package/dist/shared/env.js +29 -0
- package/dist/shared/env.js.map +1 -0
- package/dist/shared/errors.d.ts +18 -0
- package/dist/shared/errors.js +26 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/log.d.ts +2 -0
- package/dist/shared/log.js +18 -0
- package/dist/shared/log.js.map +1 -0
- package/dist/shared/paths.d.ts +3 -0
- package/dist/shared/paths.js +6 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/prompts.d.ts +3 -0
- package/dist/shared/prompts.js +34 -0
- package/dist/shared/prompts.js.map +1 -0
- package/package.json +79 -0
- package/prompts/judge.md +37 -0
- package/rules/any/futureproof/abstraction/pass-through-wrapper.md +70 -0
- package/rules/any/futureproof/abstraction/single-caller-helper.md +38 -0
- package/rules/any/futureproof/abstraction/single-impl-interface.md +83 -0
- package/rules/any/futureproof/exports/dead-export.md +62 -0
- package/rules/any/futureproof/layering/framework-type-in-domain.md +83 -0
- package/rules/any/futureproof/layering/logic-in-handler.md +94 -0
- package/rules/any/futureproof/params/boolean-positional-param.md +128 -0
- package/rules/any/futureproof/params/positional-config-args.md +135 -0
- package/rules/any/futureproof/structure/wide-function.md +138 -0
- package/rules/any/futureproof/testing/test-asserts-implementation.md +70 -0
- package/rules/any/hacky/comments/comment-restates-code.md +50 -0
- package/rules/any/hacky/comments/shipped-todo-comment.md +55 -0
- package/rules/any/hacky/concurrency/sleep-based-sync.md +95 -0
- package/rules/any/hacky/config/hardcoded-url.md +88 -0
- package/rules/any/hacky/constants/magic-number.md +94 -0
- package/rules/any/hacky/debugging/debug-print.md +73 -0
- package/rules/any/hacky/duplication/copy-paste-block.md +117 -0
- package/rules/any/hacky/types/stringly-typed-enum.md +86 -0
- package/rules/any/idiom/comments/no-section-banners.md +47 -0
- package/rules/go/futureproof/constants/string-enums-when-serialized.md +47 -0
- package/rules/go/futureproof/constants/typed-constants-for-enums.md +45 -0
- package/rules/go/futureproof/interfaces/compile-time-impl-assertion.md +43 -0
- package/rules/go/futureproof/interfaces/define-interfaces-at-consumer.md +47 -0
- package/rules/go/futureproof/interfaces/keep-interfaces-small.md +53 -0
- package/rules/go/futureproof/naming/no-utils-helpers-common-package.md +34 -0
- package/rules/go/futureproof/structs/zero-value-usable.md +50 -0
- package/rules/go/futureproof/types/generics-over-any.md +55 -0
- package/rules/go/hacky/concurrency/no-goroutine-without-wait.md +44 -0
- package/rules/go/hacky/context/never-pass-nil-context.md +64 -0
- package/rules/go/hacky/context/no-context-in-struct.md +44 -0
- package/rules/go/hacky/errors/check-close-error-on-writable.md +53 -0
- package/rules/go/hacky/errors/custom-error-type-errors-as.md +54 -0
- package/rules/go/hacky/errors/ignored-error-blank.md +84 -0
- package/rules/go/hacky/errors/panic-only-unrecoverable.md +77 -0
- package/rules/go/hacky/errors/sentinel-errors-with-errors-is.md +63 -0
- package/rules/go/hacky/errors/wrap-errors-with-w.md +71 -0
- package/rules/go/hacky/state/package-level-mutable-var.md +95 -0
- package/rules/go/hacky/structure/os-exit-only-in-main.md +61 -0
- package/rules/go/hacky/type-safety/no-map-string-any.md +49 -0
- package/rules/go/hacky/types/json-into-struct-not-map.md +48 -0
- package/rules/go/hacky/types/no-any.md +71 -0
- package/rules/go/hacky/types/no-map-any-any.md +35 -0
- package/rules/go/idiom/comments/doc-comment-on-exported.md +81 -0
- package/rules/go/idiom/comments/doc-comment-starts-with-name.md +36 -0
- package/rules/go/idiom/concurrency/mutex-over-channels-for-state.md +47 -0
- package/rules/go/idiom/constructors/constructor-named-new.md +37 -0
- package/rules/go/idiom/context/context-is-first-param.md +47 -0
- package/rules/go/idiom/context/io-takes-context.md +50 -0
- package/rules/go/idiom/errors/capitalized-error-message.md +52 -0
- package/rules/go/idiom/errors/check-error-immediately.md +45 -0
- package/rules/go/idiom/errors/defer-close-explicit-discard.md +65 -0
- package/rules/go/idiom/errors/error-is-last-return-value.md +53 -0
- package/rules/go/idiom/errors/no-log-and-return.md +45 -0
- package/rules/go/idiom/formatting/blank-line-before-return.md +48 -0
- package/rules/go/idiom/formatting/multiline-struct-literals.md +56 -0
- package/rules/go/idiom/imports/import-order-groups.md +42 -0
- package/rules/go/idiom/imports/side-effect-imports-own-group.md +51 -0
- package/rules/go/idiom/interfaces/accept-interfaces-return-concrete.md +39 -0
- package/rules/go/idiom/iterators/prefer-iter-seq.md +47 -0
- package/rules/go/idiom/logging/lowercase-log-messages.md +80 -0
- package/rules/go/idiom/logging/use-slog.md +46 -0
- package/rules/go/idiom/naming/acronyms-consistent-case.md +97 -0
- package/rules/go/idiom/naming/interface-er-suffix.md +33 -0
- package/rules/go/idiom/naming/no-package-name-stutter.md +37 -0
- package/rules/go/idiom/naming/package-name-single-lowercase-word.md +40 -0
- package/rules/go/idiom/receivers/consistent-receiver-kind-per-type.md +40 -0
- package/rules/go/idiom/receivers/no-this-receiver.md +37 -0
- package/rules/go/idiom/receivers/pointer-vs-value-receiver-choice.md +49 -0
- package/rules/go/idiom/receivers/short-receiver-names.md +43 -0
- package/rules/go/idiom/structs/new-expr-for-pointer-fields.md +40 -0
- package/rules/go/idiom/structure/main-delegates-to-run.md +54 -0
- package/rules/ts/futureproof/classes/composition-over-abstract-base.md +73 -0
- package/rules/ts/futureproof/functions/explicit-return-type-on-exports.md +50 -0
- package/rules/ts/futureproof/structure/colocate-zod-schemas.md +46 -0
- package/rules/ts/futureproof/types/exhaustive-switch-never-check.md +52 -0
- package/rules/ts/futureproof/types/readonly-for-immutable-data.md +46 -0
- package/rules/ts/futureproof/zod/enum-values-from-const-object.md +37 -0
- package/rules/ts/futureproof/zod/no-z-native-enum.md +36 -0
- package/rules/ts/futureproof/zod/schema-first-infer-type.md +46 -0
- package/rules/ts/hacky/async/no-floating-promises.md +52 -0
- package/rules/ts/hacky/config/env-read-outside-config.md +56 -0
- package/rules/ts/hacky/errors/catch-param-typed-unknown.md +67 -0
- package/rules/ts/hacky/errors/custom-error-class-instanceof.md +66 -0
- package/rules/ts/hacky/errors/empty-catch.md +52 -0
- package/rules/ts/hacky/errors/log-and-swallow.md +65 -0
- package/rules/ts/hacky/errors/throw-typed-error-with-context.md +64 -0
- package/rules/ts/hacky/operators/nullish-coalescing-over-or.md +50 -0
- package/rules/ts/hacky/state/exported-let.md +60 -0
- package/rules/ts/hacky/type-safety/no-double-assertion.md +34 -0
- package/rules/ts/hacky/type-safety/no-explicit-any.md +48 -0
- package/rules/ts/hacky/type-safety/no-non-null-assertion.md +43 -0
- package/rules/ts/hacky/type-safety/no-unchecked-type-assertion.md +54 -0
- package/rules/ts/hacky/type-safety/validate-parsed-json.md +48 -0
- package/rules/ts/idiom/async/no-promise-chains.md +45 -0
- package/rules/ts/idiom/async/parallelize-independent-awaits.md +46 -0
- package/rules/ts/idiom/comments/doc-comment-above-declaration.md +43 -0
- package/rules/ts/idiom/constants/as-const-for-literal-config.md +65 -0
- package/rules/ts/idiom/constants/no-enum.md +37 -0
- package/rules/ts/idiom/control-flow/always-brace-if.md +56 -0
- package/rules/ts/idiom/control-flow/guard-clauses-early-return.md +52 -0
- package/rules/ts/idiom/control-flow/no-nested-ternary.md +54 -0
- package/rules/ts/idiom/errors/error-message-lowercase.md +56 -0
- package/rules/ts/idiom/errors/log-at-boundary-not-every-layer.md +66 -0
- package/rules/ts/idiom/exports/inline-export-at-declaration.md +49 -0
- package/rules/ts/idiom/exports/no-default-export.md +37 -0
- package/rules/ts/idiom/formatting/multiline-object-literals.md +46 -0
- package/rules/ts/idiom/functions/arrow-for-callbacks.md +44 -0
- package/rules/ts/idiom/functions/function-declaration-for-top-level.md +56 -0
- package/rules/ts/idiom/imports/export-type-for-types.md +34 -0
- package/rules/ts/idiom/imports/import-order.md +66 -0
- package/rules/ts/idiom/imports/import-type-for-types.md +50 -0
- package/rules/ts/idiom/imports/side-effect-imports-last.md +42 -0
- package/rules/ts/idiom/logging/structured-logging-lowercase.md +45 -0
- package/rules/ts/idiom/naming/boolean-name-question-prefix.md +42 -0
- package/rules/ts/idiom/naming/descriptive-identifier-quality.md +48 -0
- package/rules/ts/idiom/naming/screaming-snake-module-constants.md +58 -0
- package/rules/ts/idiom/strings/template-literals-over-concat.md +51 -0
- package/rules/ts/idiom/types/interface-extends-over-intersection.md +44 -0
- package/rules/ts/idiom/types/null-vs-undefined-convention.md +43 -0
- package/rules/ts/idiom/types/prefer-type-over-interface.md +64 -0
- package/rules/ts/idiom/types/satisfies-over-annotation.md +43 -0
- package/rules/ts/idiom/types/type-predicates-for-narrowing.md +53 -0
- package/rules/ts/idiom/variables/const-by-default.md +41 -0
- 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
|
+
```
|