@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,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: package_identifier
|
|
7
|
+
regex: '[A-Z_]'
|
|
8
|
+
not:
|
|
9
|
+
regex: '^[a-z0-9]+_test$'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
A package name is typed at every call site, so it is short, lowercase, and a single word: `rotate`, `auth`, `client`. Mixed case or underscores in a package name break that rhythm, and the Go toolchain and most linters assume the lowercase form. Only the `_test` suffix on an external test package is conventional.
|
|
15
|
+
|
|
16
|
+
## Message
|
|
17
|
+
|
|
18
|
+
package names are one short lowercase word
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```go
|
|
23
|
+
// BAD: mixed case package name
|
|
24
|
+
package httpHelpers
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```go
|
|
28
|
+
// BAD: underscore in package name
|
|
29
|
+
package user_service
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Good
|
|
33
|
+
|
|
34
|
+
```go
|
|
35
|
+
package client
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```go
|
|
39
|
+
package client_test
|
|
40
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- generated code
|
|
6
|
+
- a method that must have a value receiver to satisfy an interface on the value type, with a comment saying so
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
When some methods on a type use a pointer receiver and others a value receiver, the method set differs between `T` and `*T`, so whether a value satisfies an interface depends on how it was declared, and a reader cannot tell from one method whether calls see shared state. If any method needs a pointer receiver, give every method on that type a pointer receiver.
|
|
12
|
+
|
|
13
|
+
## Message
|
|
14
|
+
|
|
15
|
+
type mixes pointer and value receivers; use one kind for every method
|
|
16
|
+
|
|
17
|
+
## Bad
|
|
18
|
+
|
|
19
|
+
```go
|
|
20
|
+
func (s *Service) Process(ctx context.Context, id string) error {
|
|
21
|
+
return nil
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// BAD: value receiver on a type that already uses pointer receivers
|
|
25
|
+
func (s Service) Name() string {
|
|
26
|
+
return s.name
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Good
|
|
31
|
+
|
|
32
|
+
```go
|
|
33
|
+
func (s *Service) Process(ctx context.Context, id string) error {
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func (s *Service) Name() string {
|
|
38
|
+
return s.name
|
|
39
|
+
}
|
|
40
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: parameter_declaration
|
|
7
|
+
regex: '^(this|self)\b'
|
|
8
|
+
inside:
|
|
9
|
+
kind: parameter_list
|
|
10
|
+
inside:
|
|
11
|
+
kind: method_declaration
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Go receivers are ordinary parameters and the convention is a one or two letter abbreviation of the type, used consistently across all methods on that type. `this` and `self` import another language's model and stand out as a tell that the author was not writing Go.
|
|
17
|
+
|
|
18
|
+
## Message
|
|
19
|
+
|
|
20
|
+
receiver named this/self; use a short abbreviation of the type
|
|
21
|
+
|
|
22
|
+
## Bad
|
|
23
|
+
|
|
24
|
+
```go
|
|
25
|
+
// BAD: receiver named like an object-oriented keyword
|
|
26
|
+
func (this *Server) Start() error {
|
|
27
|
+
return this.listener.Listen()
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
func (s *Server) Start() error {
|
|
35
|
+
return s.listener.Listen()
|
|
36
|
+
}
|
|
37
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- small immutable value types such as enums and thin wrappers on value receivers
|
|
6
|
+
- a type that is deliberately copied on every call and documented as such
|
|
7
|
+
- methods on named map, slice, or channel types
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A value receiver works on a copy, so a method that assigns to a field on a value receiver silently changes nothing, and a large struct is copied on every call. Use a pointer receiver when the method mutates state or the struct is large; use a value receiver for small immutable types such as a string enum with a `String` method.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
receiver kind does not fit the method: mutation or a large struct needs a pointer receiver
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
type Counter struct {
|
|
22
|
+
n int
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// BAD: value receiver mutates a copy, so the increment is lost
|
|
26
|
+
func (c Counter) Inc() {
|
|
27
|
+
c.n++
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
type Counter struct {
|
|
35
|
+
n int
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func (c *Counter) Inc() {
|
|
39
|
+
c.n++
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```go
|
|
44
|
+
type Status string
|
|
45
|
+
|
|
46
|
+
func (s Status) String() string {
|
|
47
|
+
return string(s)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: parameter_declaration
|
|
7
|
+
all:
|
|
8
|
+
- inside:
|
|
9
|
+
kind: parameter_list
|
|
10
|
+
inside:
|
|
11
|
+
kind: method_declaration
|
|
12
|
+
field: receiver
|
|
13
|
+
- has:
|
|
14
|
+
field: name
|
|
15
|
+
regex: '^[a-zA-Z_]\w{2,}$'
|
|
16
|
+
not:
|
|
17
|
+
regex: '^(this|self)$'
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why
|
|
21
|
+
|
|
22
|
+
A receiver is named with one or two letters abbreviating the type, `s` for `Service`, `c` for `Client`, and the same letters on every method of that type. A long receiver name such as `service` reads like a parameter and pushes the method body to the right on every line that touches it.
|
|
23
|
+
|
|
24
|
+
## Message
|
|
25
|
+
|
|
26
|
+
receiver name longer than two letters; use a short abbreviation of the type
|
|
27
|
+
|
|
28
|
+
## Bad
|
|
29
|
+
|
|
30
|
+
```go
|
|
31
|
+
// BAD: receiver named like a parameter
|
|
32
|
+
func (service *Service) Get(ctx context.Context, id string) (*Item, error) {
|
|
33
|
+
return service.repo.Get(ctx, id)
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Good
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
func (s *Service) Get(ctx context.Context, id string) (*Item, error) {
|
|
41
|
+
return s.repo.Get(ctx, id)
|
|
42
|
+
}
|
|
43
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: info
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- the variable is read again after its address is taken
|
|
6
|
+
- the module targets a Go version older than 1.26
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Since Go 1.26 `new` accepts an expression, so an optional pointer field can be set inline with `new(yearsSince(born))`. A throwaway local declared only so `&` has something to point at adds a name and a line that carry no meaning.
|
|
12
|
+
|
|
13
|
+
## Message
|
|
14
|
+
|
|
15
|
+
temporary declared only to take its address; use new(expr)
|
|
16
|
+
|
|
17
|
+
## Bad
|
|
18
|
+
|
|
19
|
+
```go
|
|
20
|
+
func person(name string, born time.Time) Person {
|
|
21
|
+
// BAD: age exists only so its address can be taken
|
|
22
|
+
age := yearsSince(born)
|
|
23
|
+
|
|
24
|
+
return Person{
|
|
25
|
+
Name: name,
|
|
26
|
+
Age: &age,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
func person(name string, born time.Time) Person {
|
|
35
|
+
return Person{
|
|
36
|
+
Name: name,
|
|
37
|
+
Age: new(yearsSince(born)),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a main of a few lines with no resources to release
|
|
6
|
+
- example or generated programs
|
|
7
|
+
- a main that only parses flags and calls one function
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
`os.Exit` and `log.Fatal` skip deferred calls, so a `main` that opens resources and exits on error leaks every one of them on the failure path. Move the body into `run() error`, let defers run on every exit, and keep `main` to calling `run`, logging the error, and exiting.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
main does the wiring itself; move it into run() error so defers run on every exit
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func main() {
|
|
22
|
+
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
|
23
|
+
if err != nil {
|
|
24
|
+
log.Fatal(err)
|
|
25
|
+
}
|
|
26
|
+
defer db.Close()
|
|
27
|
+
|
|
28
|
+
if err := serve(db); err != nil {
|
|
29
|
+
// BAD: exits from the middle of the wiring, skipping the deferred close
|
|
30
|
+
log.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Good
|
|
36
|
+
|
|
37
|
+
```go
|
|
38
|
+
func main() {
|
|
39
|
+
if err := run(); err != nil {
|
|
40
|
+
slog.Error("fatal error", "error", err)
|
|
41
|
+
os.Exit(1)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func run() error {
|
|
46
|
+
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
|
47
|
+
if err != nil {
|
|
48
|
+
return fmt.Errorf("open database: %w", err)
|
|
49
|
+
}
|
|
50
|
+
defer func() { _ = db.Close() }()
|
|
51
|
+
|
|
52
|
+
return serve(db)
|
|
53
|
+
}
|
|
54
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- an abstract class that enforces a method contract across a family of implementations and holds real shared behavior such as a template method
|
|
6
|
+
- framework-required base classes, such as ORM entities, component classes, or error hierarchies extending `Error`
|
|
7
|
+
- a hierarchy that is one level deep and whose subclasses differ only in the abstract method they implement
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
An abstract base class introduced to share a helper or two couples every subclass to a hierarchy, so the next feature that does not fit the base shape either bends the base or forks it. A shared function, or a dependency passed into a constructor, gives the same reuse without the inheritance tax and can be swapped or tested on its own. Reach for an abstract class only when there is a real contract to enforce across a family of implementations.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
abstract base class used for code sharing; prefer a shared function or injected dependency
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: the base exists only to share one helper with its subclasses
|
|
22
|
+
abstract class BaseService {
|
|
23
|
+
protected logDuration(label: string, startedAt: number): void {
|
|
24
|
+
logger.info(`${label} took ${Date.now() - startedAt}ms`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class UserService extends BaseService {
|
|
29
|
+
async getUser(id: string): Promise<User> {
|
|
30
|
+
const startedAt = Date.now();
|
|
31
|
+
const user = await repository.find(id);
|
|
32
|
+
|
|
33
|
+
this.logDuration('getUser', startedAt);
|
|
34
|
+
|
|
35
|
+
return user;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Good
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
function logDuration(label: string, startedAt: number): void {
|
|
44
|
+
logger.info(`${label} took ${Date.now() - startedAt}ms`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class UserService {
|
|
48
|
+
constructor(private readonly repository: UserRepository) {}
|
|
49
|
+
|
|
50
|
+
async getUser(id: string): Promise<User> {
|
|
51
|
+
const startedAt = Date.now();
|
|
52
|
+
const user = await this.repository.find(id);
|
|
53
|
+
|
|
54
|
+
logDuration('getUser', startedAt);
|
|
55
|
+
|
|
56
|
+
return user;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
abstract class BaseProcessor<TInput, TOutput> {
|
|
63
|
+
async process(input: TInput): Promise<TOutput> {
|
|
64
|
+
this.validate(input);
|
|
65
|
+
|
|
66
|
+
return this.execute(input);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected abstract validate(input: TInput): void;
|
|
70
|
+
|
|
71
|
+
protected abstract execute(input: TInput): Promise<TOutput>;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: function_declaration
|
|
7
|
+
inside:
|
|
8
|
+
kind: export_statement
|
|
9
|
+
not:
|
|
10
|
+
has:
|
|
11
|
+
field: return_type
|
|
12
|
+
regex: '.'
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
An exported function is a contract, and an inferred return type lets that contract drift silently: a change deep in the body widens or narrows the type and the error surfaces at some distant call site instead of at the definition. Writing the return type pins the contract, produces errors where the change was made, and documents the function without a comment. Internal helpers and callbacks can keep inference where the type is obvious.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
exported function has no return type; declare it so the contract cannot drift
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// BAD: the return type is whatever the body happens to produce today
|
|
27
|
+
export async function getUser(id: string) {
|
|
28
|
+
return repository.find(id);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Good
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export async function getUser(id: string): Promise<User | undefined> {
|
|
36
|
+
return repository.find(id);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
function buildWhere(filters: Filters) {
|
|
42
|
+
return {
|
|
43
|
+
...(filters.status && { status: filters.status }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function listUsers(filters: Filters): Promise<User[]> {
|
|
48
|
+
return repository.findMany(buildWhere(filters));
|
|
49
|
+
}
|
|
50
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: info
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a schema shared by several modules and placed in a module named for the domain concept it describes, such as `user.schema.ts` next to `user.service.ts`
|
|
6
|
+
- schemas that are the public contract of a package and are grouped deliberately for export
|
|
7
|
+
- a schema moved into its own file because it is large, when it stays in the same feature directory
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A Zod schema is the definition of a shape the code around it consumes, so when it lives in a distant `schemas/` or `types.ts` dump the reader has to jump files to learn what a function accepts, and changes to the consumer and the schema land in different places. Keep the schema next to the code that parses with it and derive the type from it there. A shared schema belongs with the domain module it describes, not in a catch-all.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
schema lives away from the code that uses it; colocate it with its consumer
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: the request schema lives in a distant catch-all module
|
|
22
|
+
import { createUserSchema } from '@/schemas';
|
|
23
|
+
|
|
24
|
+
export async function handleCreateUser(req: Request, res: Response): Promise<void> {
|
|
25
|
+
const input = createUserSchema.parse(req.body);
|
|
26
|
+
|
|
27
|
+
res.json(await createUser(input));
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const createUserSchema = z.object({
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
email: z.string().email(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
type CreateUserInput = z.infer<typeof createUserSchema>;
|
|
40
|
+
|
|
41
|
+
export async function handleCreateUser(req: Request, res: Response): Promise<void> {
|
|
42
|
+
const input = createUserSchema.parse(req.body);
|
|
43
|
+
|
|
44
|
+
res.json(await createUser(input));
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- switches over open types such as `string` or `number`, where a default branch is the only way to finish
|
|
6
|
+
- a switch whose default already assigns the value to `never` or calls an `assertNever` helper
|
|
7
|
+
- a switch that intentionally handles a subset and falls through to shared behavior for everything else
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A switch over a union or a const-object type with a plain `default` keeps compiling when a new variant is added, so the new case silently takes the default path at runtime. Assigning the switched value to `never` in the default branch makes the compiler reject any switch that forgot a case, turning a production bug into a build error at the moment the variant is introduced. The same applies to `if` chains that end in an unconditional fallback.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
switch over a union without a `never` check; new variants will fall through silently
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
function label(status: Status): string {
|
|
22
|
+
switch (status) {
|
|
23
|
+
case Status.PENDING:
|
|
24
|
+
return 'waiting';
|
|
25
|
+
case Status.ACTIVE:
|
|
26
|
+
return 'running';
|
|
27
|
+
// BAD: adding Status.CANCELLED compiles and returns 'done'
|
|
28
|
+
default:
|
|
29
|
+
return 'done';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Good
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
function label(status: Status): string {
|
|
38
|
+
switch (status) {
|
|
39
|
+
case Status.PENDING:
|
|
40
|
+
return 'waiting';
|
|
41
|
+
case Status.ACTIVE:
|
|
42
|
+
return 'running';
|
|
43
|
+
case Status.COMPLETED:
|
|
44
|
+
return 'done';
|
|
45
|
+
default: {
|
|
46
|
+
const exhaustive: never = status;
|
|
47
|
+
|
|
48
|
+
throw new Error(`unhandled status: ${String(exhaustive)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: info
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- parameters or properties that the function or class genuinely mutates
|
|
6
|
+
- builder or accumulator objects whose whole purpose is to be filled in
|
|
7
|
+
- types generated from a schema or an ORM where the modifier is not under the author's control
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A parameter typed as `T[]` or a property typed without `readonly` invites the next author to push into it or reassign it, and the compiler will let them even when the caller never expected its data to change. `readonly` on arrays and on identity fields such as `id` and `createdAt` documents the intent, rejects the mutation at compile time, and costs nothing at runtime. Data that is meant to be shared is safer when it cannot be edited in place.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
data that is never mutated is typed as mutable; mark it `readonly`
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: nothing stops a caller from reassigning id or pushing into tags
|
|
22
|
+
type User = {
|
|
23
|
+
id: string;
|
|
24
|
+
createdAt: Date;
|
|
25
|
+
tags: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function tagNames(tags: string[]): string[] {
|
|
29
|
+
return tags.map(tag => tag.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Good
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
type User = {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly createdAt: Date;
|
|
39
|
+
readonly tags: readonly string[];
|
|
40
|
+
name: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function tagNames(tags: readonly string[]): string[] {
|
|
44
|
+
return tags.map(tag => tag.toLowerCase());
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
pattern: z.enum([$$$VALUES])
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
An inline string array in `z.enum(['pending', 'active'])` is a second copy of values that the code also needs as constants, so adding a variant means finding every literal list and hoping none is missed. Deriving the tuple from a const object keeps one definition that feeds the schema, the type, and every `Status.PENDING` reference. The schema then cannot drift from the constants it validates.
|
|
12
|
+
|
|
13
|
+
## Message
|
|
14
|
+
|
|
15
|
+
`z.enum` with inline string literals; derive the values from a const object
|
|
16
|
+
|
|
17
|
+
## Bad
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// BAD: the values live only here and cannot be referenced as constants
|
|
21
|
+
const statusSchema = z.enum(['pending', 'active', 'completed']);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Good
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const Status = {
|
|
28
|
+
PENDING: 'pending',
|
|
29
|
+
ACTIVE: 'active',
|
|
30
|
+
COMPLETED: 'completed',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
type Status = (typeof Status)[keyof typeof Status];
|
|
34
|
+
|
|
35
|
+
const statusValues = Object.values(Status) as [Status, ...Status[]];
|
|
36
|
+
const statusSchema = z.enum(statusValues);
|
|
37
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
pattern: z.nativeEnum($X)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
`z.nativeEnum` exists to wrap a TypeScript `enum`, and it is deprecated in Zod v4, so it ties the schema to a construct the codebase avoids and to an API that will be removed. A const object with `as const` plus `z.enum` over its values gives the same runtime check with a plain object that is grep-friendly, tree-shakeable, and stays valid across Zod upgrades. The type comes from the same object, so there is still one source of truth.
|
|
12
|
+
|
|
13
|
+
## Message
|
|
14
|
+
|
|
15
|
+
`z.nativeEnum` is deprecated; use `z.enum` over the values of a const object
|
|
16
|
+
|
|
17
|
+
## Bad
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// BAD: deprecated in Zod v4 and tied to a TypeScript enum
|
|
21
|
+
const prioritySchema = z.nativeEnum(Priority);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Good
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const Priority = {
|
|
28
|
+
LOW: 'low',
|
|
29
|
+
HIGH: 'high',
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
type Priority = (typeof Priority)[keyof typeof Priority];
|
|
33
|
+
|
|
34
|
+
const priorityValues = Object.values(Priority) as [Priority, ...Priority[]];
|
|
35
|
+
const prioritySchema = z.enum(priorityValues);
|
|
36
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- types that describe internal state, function options, or return shapes that are never validated at runtime
|
|
6
|
+
- a type that is deliberately narrower or wider than the schema, such as a branded id, with a comment saying why
|
|
7
|
+
- types generated by another tool, such as an ORM or an API client, that the schema merely mirrors
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Writing a `type` by hand and then a Zod schema that describes the same shape creates two definitions that agree today and drift the first time one of them is edited, with the compiler unable to notice. Define the schema once and derive the type with `z.infer`, so the runtime check and the static type cannot disagree. Input variants come from the schema too, through `omit`, `pick`, and `partial`.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
hand-written type duplicates a schema; derive it with `z.infer`
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: two definitions of the same shape that nothing keeps in sync
|
|
22
|
+
type Item = {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const itemSchema = z.object({
|
|
28
|
+
id: z.string(),
|
|
29
|
+
name: z.string(),
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Good
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const itemSchema = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
name: z.string(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
type Item = z.infer<typeof itemSchema>;
|
|
42
|
+
|
|
43
|
+
const createItemSchema = itemSchema.omit({ id: true });
|
|
44
|
+
|
|
45
|
+
type CreateItemInput = z.infer<typeof createItemSchema>;
|
|
46
|
+
```
|