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