@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,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
confirm: implCount
|
|
5
|
+
falsePositives:
|
|
6
|
+
- a port at an architecture boundary whose second implementation is a fake or mock in tests
|
|
7
|
+
- a small consumer-side interface in Go, one or two methods declared next to the code that uses them, which narrows a dependency rather than mirroring it
|
|
8
|
+
- an interface that is part of a published library's contract, where implementations live outside this repository
|
|
9
|
+
- an interface with several implementations, or one whose second implementation arrives in the same change
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
An interface with one implementation and no test double is a copy of a type's method list that has to be edited every time the type changes, and it forces readers through an extra jump to find the code that runs. It does not decouple anything, because nothing else plugs in. Use the concrete type until a second implementation or a test fake exists; extracting an interface then is a mechanical change.
|
|
15
|
+
|
|
16
|
+
## Message
|
|
17
|
+
|
|
18
|
+
interface has a single implementation and no test double; use the concrete type until a second one exists
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// BAD: the interface mirrors the only class that implements it
|
|
24
|
+
export interface UserRepository {
|
|
25
|
+
findById(id: string): Promise<User | undefined>;
|
|
26
|
+
insert(input: CreateUserInput): Promise<User>;
|
|
27
|
+
delete(id: string): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class PostgresUserRepository implements UserRepository {
|
|
31
|
+
async findById(id: string): Promise<User | undefined> {
|
|
32
|
+
return this.db.selectOne(usersTable, { id });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async insert(input: CreateUserInput): Promise<User> {
|
|
36
|
+
return this.db.insert(usersTable, input);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async delete(id: string): Promise<void> {
|
|
40
|
+
await this.db.delete(usersTable, { id });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```go
|
|
46
|
+
// BAD: the interface mirrors the whole method set of the only type that satisfies it
|
|
47
|
+
type UserStore interface {
|
|
48
|
+
FindByID(ctx context.Context, id string) (User, error)
|
|
49
|
+
Insert(ctx context.Context, in CreateUserInput) (User, error)
|
|
50
|
+
Delete(ctx context.Context, id string) error
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var _ UserStore = (*PostgresUserStore)(nil)
|
|
54
|
+
|
|
55
|
+
type PostgresUserStore struct {
|
|
56
|
+
db *sql.DB
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Good
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
export class PostgresUserRepository {
|
|
64
|
+
async findById(id: string): Promise<User | undefined> {
|
|
65
|
+
return this.db.selectOne(usersTable, { id });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async insert(input: CreateUserInput): Promise<User> {
|
|
69
|
+
return this.db.insert(usersTable, input);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```go
|
|
75
|
+
// UserFinder is the one method the notifier needs; any store that can find users satisfies it.
|
|
76
|
+
type UserFinder interface {
|
|
77
|
+
FindByID(ctx context.Context, id string) (User, error)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Notifier struct {
|
|
81
|
+
users UserFinder
|
|
82
|
+
}
|
|
83
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
confirm: refCount
|
|
5
|
+
ignore:
|
|
6
|
+
- '**/index.ts'
|
|
7
|
+
- '**/*.test.ts'
|
|
8
|
+
- '**/*.spec.ts'
|
|
9
|
+
- '**/*_test.go'
|
|
10
|
+
falsePositives:
|
|
11
|
+
- the public API of a library package or a package entry point, where callers live outside this repository
|
|
12
|
+
- CLI binaries, `main`, `init`, and framework entry points such as route or plugin registrations
|
|
13
|
+
- symbols reached by reflection, templates, serialization tags, or generated code
|
|
14
|
+
- methods that exist to satisfy an interface, such as `String()` or `MarshalJSON()`
|
|
15
|
+
- types and constants that describe a wire format or database schema, which are referenced by name only in data
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
An exported symbol that nothing references is code that must still compile, be reviewed, and be kept consistent with every refactor, while offering nothing in return. Exporting it also signals to readers that something depends on it, so they leave it alone. Delete it; version control remembers it if it is ever needed.
|
|
21
|
+
|
|
22
|
+
## Message
|
|
23
|
+
|
|
24
|
+
exported symbol is referenced nowhere; delete it or make it private
|
|
25
|
+
|
|
26
|
+
## Bad
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// BAD: exported but no caller in the repository
|
|
30
|
+
export function formatLegacyDate(date: Date): string {
|
|
31
|
+
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatDate(date: Date): string {
|
|
35
|
+
return date.toISOString().slice(0, 10);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
// BAD: exported but no caller in the repository
|
|
41
|
+
func FormatLegacyDate(t time.Time) string {
|
|
42
|
+
return t.Format("2/1/2006")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func FormatDate(t time.Time) string {
|
|
46
|
+
return t.Format(time.DateOnly)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Good
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
export function formatDate(date: Date): string {
|
|
54
|
+
return date.toISOString().slice(0, 10);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```go
|
|
59
|
+
func FormatDate(t time.Time) string {
|
|
60
|
+
return t.Format(time.DateOnly)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: judge
|
|
4
|
+
ignore:
|
|
5
|
+
- '**/handlers/**'
|
|
6
|
+
- '**/handler/**'
|
|
7
|
+
- '**/api/**'
|
|
8
|
+
- '**/routes/**'
|
|
9
|
+
- '**/router/**'
|
|
10
|
+
- '**/controllers/**'
|
|
11
|
+
- '**/middleware/**'
|
|
12
|
+
- '**/transport/**'
|
|
13
|
+
- '**/http/**'
|
|
14
|
+
- '**/*.handler.ts'
|
|
15
|
+
- '**/*.controller.ts'
|
|
16
|
+
- '**/*.route.ts'
|
|
17
|
+
- '**/main.go'
|
|
18
|
+
- '**/cmd/**'
|
|
19
|
+
- '**/*.test.ts'
|
|
20
|
+
- '**/*.spec.ts'
|
|
21
|
+
- '**/*_test.go'
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
A service function that takes the framework's request or response type can only be called by that framework: not from a job, a CLI, another transport, or a unit test without a fake request. It also pulls every HTTP concern, from headers to status codes, into the layer that should only know the domain. Give the service plain typed inputs and return plain typed results; the handler translates at the edge.
|
|
27
|
+
|
|
28
|
+
## Message
|
|
29
|
+
|
|
30
|
+
http framework type in a domain signature; take plain input and return a plain result
|
|
31
|
+
|
|
32
|
+
## Bad
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export class OrderService {
|
|
36
|
+
// BAD: the service can only be driven by express
|
|
37
|
+
async create(req: Request, res: Response): Promise<void> {
|
|
38
|
+
const input = createOrderSchema.parse(req.body);
|
|
39
|
+
const order = await this.repo.insert(input);
|
|
40
|
+
res.status(201).json(order);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```go
|
|
46
|
+
// BAD: the service can only be driven by net/http
|
|
47
|
+
func (s *OrderService) Create(w http.ResponseWriter, r *http.Request) {
|
|
48
|
+
var in CreateOrderInput
|
|
49
|
+
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
50
|
+
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
order, err := s.repo.Insert(r.Context(), in)
|
|
55
|
+
if err != nil {
|
|
56
|
+
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
writeJSON(w, http.StatusCreated, order)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Good
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export class OrderService {
|
|
68
|
+
async create(input: CreateOrderInput): Promise<Order> {
|
|
69
|
+
return this.repo.insert(input);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```go
|
|
75
|
+
func (s *OrderService) Create(ctx context.Context, in CreateOrderInput) (Order, error) {
|
|
76
|
+
order, err := s.repo.Insert(ctx, in)
|
|
77
|
+
if err != nil {
|
|
78
|
+
return Order{}, fmt.Errorf("inserting order: %w", err)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return order, nil
|
|
82
|
+
}
|
|
83
|
+
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- parsing and validating the request, and mapping a service result or error to a status code, which is the handler's job
|
|
6
|
+
- a single guard clause such as checking authentication before delegating
|
|
7
|
+
- a small script, example, or prototype with no service layer at all
|
|
8
|
+
- middleware, which operates on the request itself rather than on domain state
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Business rules written inside an HTTP handler can only be reached through HTTP, so they cannot be reused by a CLI, a queue consumer, or a scheduled job, and they can only be tested by spinning up requests. The handler grows with every rule until nobody can see the request handling for the logic. Keep the handler to parsing input, calling one service function, and mapping the result to a response; the rules live in the service where they can be called from anywhere.
|
|
14
|
+
|
|
15
|
+
## Message
|
|
16
|
+
|
|
17
|
+
business logic inside an http handler; move it to a service the handler calls
|
|
18
|
+
|
|
19
|
+
## Bad
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
router.post('/checkout', async (req, res) => {
|
|
23
|
+
const cart = await carts.find(req.session.cartId);
|
|
24
|
+
// BAD: pricing, stock, and persistence rules live in the route
|
|
25
|
+
let total = 0;
|
|
26
|
+
for (const line of cart.lines) {
|
|
27
|
+
const product = await products.find(line.productId);
|
|
28
|
+
if (product.stock < line.quantity) {
|
|
29
|
+
return res.status(409).json({ error: 'out of stock' });
|
|
30
|
+
}
|
|
31
|
+
total += product.price * line.quantity;
|
|
32
|
+
}
|
|
33
|
+
if (cart.coupon) {
|
|
34
|
+
total = total * (1 - cart.coupon.percent / 100);
|
|
35
|
+
}
|
|
36
|
+
const order = await orders.insert({ cartId: cart.id, total });
|
|
37
|
+
res.status(201).json(order);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```go
|
|
42
|
+
func (h *Handler) Checkout(w http.ResponseWriter, r *http.Request) {
|
|
43
|
+
cart, err := h.carts.Find(r.Context(), sessionCartID(r))
|
|
44
|
+
if err != nil {
|
|
45
|
+
http.Error(w, "cart not found", http.StatusNotFound)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
// BAD: pricing, stock, and persistence rules live in the handler
|
|
49
|
+
var total int
|
|
50
|
+
for _, line := range cart.Lines {
|
|
51
|
+
product, err := h.products.Find(r.Context(), line.ProductID)
|
|
52
|
+
if err != nil || product.Stock < line.Quantity {
|
|
53
|
+
http.Error(w, "out of stock", http.StatusConflict)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
total += product.Price * line.Quantity
|
|
57
|
+
}
|
|
58
|
+
if cart.Coupon != nil {
|
|
59
|
+
total = total * (100 - cart.Coupon.Percent) / 100
|
|
60
|
+
}
|
|
61
|
+
order, err := h.orders.Insert(r.Context(), Order{CartID: cart.ID, Total: total})
|
|
62
|
+
if err != nil {
|
|
63
|
+
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
writeJSON(w, http.StatusCreated, order)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Good
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
router.post('/checkout', async (req, res) => {
|
|
74
|
+
const result = await checkout.run({ cartId: req.session.cartId });
|
|
75
|
+
|
|
76
|
+
if (!result.ok) {
|
|
77
|
+
return res.status(statusFor(result.error)).json({ error: result.error.message });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
res.status(201).json(result.order);
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```go
|
|
85
|
+
func (h *Handler) Checkout(w http.ResponseWriter, r *http.Request) {
|
|
86
|
+
order, err := h.checkout.Run(r.Context(), checkout.Input{CartID: sessionCartID(r)})
|
|
87
|
+
if err != nil {
|
|
88
|
+
writeError(w, err)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
writeJSON(w, http.StatusCreated, order)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
ts:
|
|
6
|
+
utils:
|
|
7
|
+
param:
|
|
8
|
+
any:
|
|
9
|
+
- kind: required_parameter
|
|
10
|
+
- kind: optional_parameter
|
|
11
|
+
rule:
|
|
12
|
+
matches: param
|
|
13
|
+
any:
|
|
14
|
+
- has:
|
|
15
|
+
kind: type_annotation
|
|
16
|
+
has:
|
|
17
|
+
kind: predefined_type
|
|
18
|
+
regex: '^boolean$'
|
|
19
|
+
- all:
|
|
20
|
+
- not:
|
|
21
|
+
has:
|
|
22
|
+
kind: type_annotation
|
|
23
|
+
- has:
|
|
24
|
+
field: value
|
|
25
|
+
any:
|
|
26
|
+
- kind: 'true'
|
|
27
|
+
- kind: 'false'
|
|
28
|
+
all:
|
|
29
|
+
- any:
|
|
30
|
+
- follows:
|
|
31
|
+
matches: param
|
|
32
|
+
stopBy: end
|
|
33
|
+
- precedes:
|
|
34
|
+
matches: param
|
|
35
|
+
stopBy: end
|
|
36
|
+
go:
|
|
37
|
+
rule:
|
|
38
|
+
kind: parameter_declaration
|
|
39
|
+
has:
|
|
40
|
+
field: type
|
|
41
|
+
regex: '^bool$'
|
|
42
|
+
inside:
|
|
43
|
+
kind: parameter_list
|
|
44
|
+
not:
|
|
45
|
+
follows:
|
|
46
|
+
kind: parameter_list
|
|
47
|
+
any:
|
|
48
|
+
- follows:
|
|
49
|
+
kind: parameter_declaration
|
|
50
|
+
stopBy: end
|
|
51
|
+
- precedes:
|
|
52
|
+
kind: parameter_declaration
|
|
53
|
+
stopBy: end
|
|
54
|
+
ignore:
|
|
55
|
+
- '**/*.test.ts'
|
|
56
|
+
- '**/*.spec.ts'
|
|
57
|
+
- '**/*_test.go'
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Why
|
|
61
|
+
|
|
62
|
+
A boolean argument reads as `render(user, true)` at the call site, so every reader has to open the definition to learn what `true` means, and the second flag turns the function into four functions in a trench coat. Callers cannot add a third mode without touching every existing call. Take an options object or struct with named fields, or split the function into two named behaviors.
|
|
63
|
+
|
|
64
|
+
## Message
|
|
65
|
+
|
|
66
|
+
boolean positional parameter; use an options object or two functions
|
|
67
|
+
|
|
68
|
+
## Bad
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// BAD: render(user, true) says nothing at the call site
|
|
72
|
+
export function render(user: User, compact: boolean): string {
|
|
73
|
+
return compact ? user.name : `${user.name} <${user.email}>`;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```go
|
|
78
|
+
// BAD: Render(user, true) says nothing at the call site
|
|
79
|
+
func Render(user User, compact bool) string {
|
|
80
|
+
if compact {
|
|
81
|
+
return user.Name
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return user.Name + " <" + user.Email + ">"
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Good
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
type RenderOptions = {
|
|
92
|
+
compact?: boolean;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function render(user: User, options: RenderOptions = {}): string {
|
|
96
|
+
return options.compact ? user.name : `${user.name} <${user.email}>`;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
export function renderCompact(user: User): string {
|
|
102
|
+
return user.name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function renderFull(user: User): string {
|
|
106
|
+
return `${user.name} <${user.email}>`;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```go
|
|
111
|
+
type RenderOptions struct {
|
|
112
|
+
Compact bool
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func Render(user User, opts RenderOptions) string {
|
|
116
|
+
if opts.Compact {
|
|
117
|
+
return user.Name
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return user.Name + " <" + user.Email + ">"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```go
|
|
125
|
+
func IsAdmin(user User) bool {
|
|
126
|
+
return user.Role == RoleAdmin
|
|
127
|
+
}
|
|
128
|
+
```
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
ts:
|
|
6
|
+
utils:
|
|
7
|
+
param:
|
|
8
|
+
any:
|
|
9
|
+
- kind: required_parameter
|
|
10
|
+
- kind: optional_parameter
|
|
11
|
+
rule:
|
|
12
|
+
kind: formal_parameters
|
|
13
|
+
inside:
|
|
14
|
+
kind: method_definition
|
|
15
|
+
has:
|
|
16
|
+
field: name
|
|
17
|
+
regex: '^constructor$'
|
|
18
|
+
has:
|
|
19
|
+
matches: param
|
|
20
|
+
follows:
|
|
21
|
+
matches: param
|
|
22
|
+
stopBy: end
|
|
23
|
+
follows:
|
|
24
|
+
matches: param
|
|
25
|
+
stopBy: end
|
|
26
|
+
follows:
|
|
27
|
+
matches: param
|
|
28
|
+
stopBy: end
|
|
29
|
+
go:
|
|
30
|
+
utils:
|
|
31
|
+
param:
|
|
32
|
+
kind: parameter_declaration
|
|
33
|
+
rule:
|
|
34
|
+
kind: parameter_list
|
|
35
|
+
not:
|
|
36
|
+
follows:
|
|
37
|
+
kind: parameter_list
|
|
38
|
+
inside:
|
|
39
|
+
kind: function_declaration
|
|
40
|
+
has:
|
|
41
|
+
field: name
|
|
42
|
+
regex: '^(New|Must)[A-Z]?'
|
|
43
|
+
has:
|
|
44
|
+
matches: param
|
|
45
|
+
follows:
|
|
46
|
+
matches: param
|
|
47
|
+
stopBy: end
|
|
48
|
+
follows:
|
|
49
|
+
matches: param
|
|
50
|
+
stopBy: end
|
|
51
|
+
follows:
|
|
52
|
+
matches: param
|
|
53
|
+
stopBy: end
|
|
54
|
+
ignore:
|
|
55
|
+
- '**/*.test.ts'
|
|
56
|
+
- '**/*.spec.ts'
|
|
57
|
+
- '**/*_test.go'
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Why
|
|
61
|
+
|
|
62
|
+
A constructor with four positional arguments is called as `new Server('0.0.0.0', 8080, true, 30)`, which no reader can decode and no caller can extend without breaking every other caller. Constructors grow more often than any other signature, because every new dependency and setting lands there. Take one options object or config struct with named, defaultable fields, or use functional options in Go.
|
|
63
|
+
|
|
64
|
+
## Message
|
|
65
|
+
|
|
66
|
+
constructor takes 4+ positional arguments; take an options object or config struct
|
|
67
|
+
|
|
68
|
+
## Bad
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
export class Server {
|
|
72
|
+
// BAD: new Server('0.0.0.0', 8080, true, 30) is unreadable and cannot grow
|
|
73
|
+
constructor(host: string, port: number, tls: boolean, timeoutSeconds: number) {
|
|
74
|
+
this.address = `${host}:${port}`;
|
|
75
|
+
this.tls = tls;
|
|
76
|
+
this.timeoutSeconds = timeoutSeconds;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```go
|
|
82
|
+
// BAD: NewServer("0.0.0.0", 8080, true, 30*time.Second) is unreadable and cannot grow
|
|
83
|
+
func NewServer(host string, port int, tls bool, timeout time.Duration) *Server {
|
|
84
|
+
return &Server{addr: fmt.Sprintf("%s:%d", host, port), tls: tls, timeout: timeout}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Good
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
type ServerOptions = {
|
|
92
|
+
host: string;
|
|
93
|
+
port: number;
|
|
94
|
+
tls?: boolean;
|
|
95
|
+
timeoutSeconds?: number;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export class Server {
|
|
99
|
+
constructor(options: ServerOptions) {
|
|
100
|
+
this.address = `${options.host}:${options.port}`;
|
|
101
|
+
this.tls = options.tls ?? false;
|
|
102
|
+
this.timeoutSeconds = options.timeoutSeconds ?? 30;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```go
|
|
108
|
+
type Config struct {
|
|
109
|
+
Host string
|
|
110
|
+
Port int
|
|
111
|
+
TLS bool
|
|
112
|
+
Timeout time.Duration
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func NewServer(cfg Config) *Server {
|
|
116
|
+
return &Server{addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), tls: cfg.TLS, timeout: cfg.Timeout}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```go
|
|
121
|
+
type Option func(*Server)
|
|
122
|
+
|
|
123
|
+
func WithTimeout(d time.Duration) Option {
|
|
124
|
+
return func(s *Server) { s.timeout = d }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func NewServer(addr string, opts ...Option) *Server {
|
|
128
|
+
s := &Server{addr: addr, timeout: 30 * time.Second}
|
|
129
|
+
for _, opt := range opts {
|
|
130
|
+
opt(s)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return s
|
|
134
|
+
}
|
|
135
|
+
```
|