@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,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
|
+
```
|