@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,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
ignore:
|
|
5
|
+
- '**/*.test.ts'
|
|
6
|
+
- '**/*.spec.ts'
|
|
7
|
+
- '**/*_test.go'
|
|
8
|
+
falsePositives:
|
|
9
|
+
- table-driven tests and test fixtures, where repetition is the point
|
|
10
|
+
- generated code
|
|
11
|
+
- two or three short lines that happen to look alike, such as consecutive field assignments or switch arms
|
|
12
|
+
- blocks that look alike today but belong to different domains and are expected to diverge, when the code says so
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
Two blocks that differ only in a name or a literal are one function with a parameter that has not been extracted yet. The next bug fix lands in one copy and not the other, and the reader has to diff them by eye to learn they are the same. Extract the block with the varying part as an argument, or loop over the values.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
copy-pasted block differs in one identifier; extract a function or loop over the values
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
export async function exportReports(db: Db): Promise<void> {
|
|
27
|
+
// BAD: the second block is the first with 'sales' replaced by 'refunds'
|
|
28
|
+
const salesRows = await db.query('select * from sales where exported = false');
|
|
29
|
+
const salesCsv = toCsv(salesRows);
|
|
30
|
+
await storage.put(`exports/sales-${today()}.csv`, salesCsv);
|
|
31
|
+
await db.execute('update sales set exported = true where exported = false');
|
|
32
|
+
logger.info({ count: salesRows.length }, 'exported sales');
|
|
33
|
+
|
|
34
|
+
const refundRows = await db.query('select * from refunds where exported = false');
|
|
35
|
+
const refundCsv = toCsv(refundRows);
|
|
36
|
+
await storage.put(`exports/refunds-${today()}.csv`, refundCsv);
|
|
37
|
+
await db.execute('update refunds set exported = true where exported = false');
|
|
38
|
+
logger.info({ count: refundRows.length }, 'exported refunds');
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```go
|
|
43
|
+
func ExportReports(ctx context.Context, db *sql.DB) error {
|
|
44
|
+
// BAD: the second block is the first with "sales" replaced by "refunds"
|
|
45
|
+
salesRows, err := query(ctx, db, "select * from sales where exported = false")
|
|
46
|
+
if err != nil {
|
|
47
|
+
return fmt.Errorf("querying sales: %w", err)
|
|
48
|
+
}
|
|
49
|
+
if err := storage.Put(ctx, "exports/sales-"+today()+".csv", toCSV(salesRows)); err != nil {
|
|
50
|
+
return fmt.Errorf("uploading sales: %w", err)
|
|
51
|
+
}
|
|
52
|
+
if _, err := db.ExecContext(ctx, "update sales set exported = true where exported = false"); err != nil {
|
|
53
|
+
return fmt.Errorf("marking sales: %w", err)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
refundRows, err := query(ctx, db, "select * from refunds where exported = false")
|
|
57
|
+
if err != nil {
|
|
58
|
+
return fmt.Errorf("querying refunds: %w", err)
|
|
59
|
+
}
|
|
60
|
+
if err := storage.Put(ctx, "exports/refunds-"+today()+".csv", toCSV(refundRows)); err != nil {
|
|
61
|
+
return fmt.Errorf("uploading refunds: %w", err)
|
|
62
|
+
}
|
|
63
|
+
if _, err := db.ExecContext(ctx, "update refunds set exported = true where exported = false"); err != nil {
|
|
64
|
+
return fmt.Errorf("marking refunds: %w", err)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return nil
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Good
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const EXPORTED_TABLES = ['sales', 'refunds'] as const;
|
|
75
|
+
|
|
76
|
+
export async function exportReports(db: Db): Promise<void> {
|
|
77
|
+
for (const table of EXPORTED_TABLES) {
|
|
78
|
+
await exportTable(db, table);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function exportTable(db: Db, table: string): Promise<void> {
|
|
83
|
+
const rows = await db.query(`select * from ${table} where exported = false`);
|
|
84
|
+
await storage.put(`exports/${table}-${today()}.csv`, toCsv(rows));
|
|
85
|
+
await db.execute(`update ${table} set exported = true where exported = false`);
|
|
86
|
+
logger.info({ table, count: rows.length }, 'exported table');
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```go
|
|
91
|
+
var exportedTables = []string{"sales", "refunds"}
|
|
92
|
+
|
|
93
|
+
func ExportReports(ctx context.Context, db *sql.DB) error {
|
|
94
|
+
for _, table := range exportedTables {
|
|
95
|
+
if err := exportTable(ctx, db, table); err != nil {
|
|
96
|
+
return fmt.Errorf("exporting %s: %w", table, err)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return nil
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func exportTable(ctx context.Context, db *sql.DB, table string) error {
|
|
104
|
+
rows, err := query(ctx, db, "select * from "+table+" where exported = false")
|
|
105
|
+
if err != nil {
|
|
106
|
+
return fmt.Errorf("querying: %w", err)
|
|
107
|
+
}
|
|
108
|
+
if err := storage.Put(ctx, "exports/"+table+"-"+today()+".csv", toCSV(rows)); err != nil {
|
|
109
|
+
return fmt.Errorf("uploading: %w", err)
|
|
110
|
+
}
|
|
111
|
+
if _, err := db.ExecContext(ctx, "update "+table+" set exported = true where exported = false"); err != nil {
|
|
112
|
+
return fmt.Errorf("marking: %w", err)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return nil
|
|
116
|
+
}
|
|
117
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a string literal compared in exactly one place
|
|
6
|
+
- a value from an external protocol or wire format compared once at the boundary where it is parsed into a typed value
|
|
7
|
+
- discriminant literals of a union or sum type that already declares the allowed values, such as `if (event.kind === 'created')` where `kind` is typed as a union of literals
|
|
8
|
+
- map keys, header names, and env variable names, which are identifiers rather than a closed set of states
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Comparing the same handful of string literals in several places means the set of valid values lives nowhere: a typo in one comparison is a silent false branch, and adding a value means finding every switch by hand. Declare the set once as a const object with a derived union type in TypeScript, or a named string type with typed constants in Go, and let the compiler enforce exhaustiveness and spelling.
|
|
14
|
+
|
|
15
|
+
## Message
|
|
16
|
+
|
|
17
|
+
string literals used as an enum; declare the set once as typed constants
|
|
18
|
+
|
|
19
|
+
## Bad
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
export function canShip(order: Order): boolean {
|
|
23
|
+
// BAD: the valid statuses exist only as scattered literals, and a typo compiles
|
|
24
|
+
return order.status === 'paid' || order.status === 'packed';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function label(order: Order): string {
|
|
28
|
+
// BAD: same set, spelled again, with 'shiped' waiting to happen
|
|
29
|
+
if (order.status === 'shipped') {
|
|
30
|
+
return 'On its way';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return order.status === 'paid' ? 'Preparing' : 'Pending';
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```go
|
|
38
|
+
func CanShip(o Order) bool {
|
|
39
|
+
// BAD: the valid statuses exist only as scattered literals
|
|
40
|
+
return o.Status == "paid" || o.Status == "packed"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func Label(o Order) string {
|
|
44
|
+
// BAD: same set, spelled again by hand
|
|
45
|
+
switch o.Status {
|
|
46
|
+
case "shipped":
|
|
47
|
+
return "On its way"
|
|
48
|
+
case "paid":
|
|
49
|
+
return "Preparing"
|
|
50
|
+
default:
|
|
51
|
+
return "Pending"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Good
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
export const OrderStatus = {
|
|
60
|
+
PENDING: 'pending',
|
|
61
|
+
PAID: 'paid',
|
|
62
|
+
PACKED: 'packed',
|
|
63
|
+
SHIPPED: 'shipped',
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
export type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
|
|
67
|
+
|
|
68
|
+
export function canShip(order: Order): boolean {
|
|
69
|
+
return order.status === OrderStatus.PAID || order.status === OrderStatus.PACKED;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```go
|
|
74
|
+
type Status string
|
|
75
|
+
|
|
76
|
+
const (
|
|
77
|
+
StatusPending Status = "pending"
|
|
78
|
+
StatusPaid Status = "paid"
|
|
79
|
+
StatusPacked Status = "packed"
|
|
80
|
+
StatusShipped Status = "shipped"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
func CanShip(o Order) bool {
|
|
84
|
+
return o.Status == StatusPaid || o.Status == StatusPacked
|
|
85
|
+
}
|
|
86
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
ts:
|
|
6
|
+
rule:
|
|
7
|
+
kind: comment
|
|
8
|
+
regex: '^//\s*[-=*#]{4,}'
|
|
9
|
+
go:
|
|
10
|
+
rule:
|
|
11
|
+
kind: comment
|
|
12
|
+
regex: '^//\s*[-=*#]{4,}'
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
Banner comments and decorative separators are a sign the file has grown past one responsibility. They do not help navigation, editors fold on declarations rather than dashes, and they drift out of place as code moves. Split the file, or let the declarations speak for themselves.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
banner comments signal a file doing too much; split it instead of decorating it
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// BAD: decorative separator
|
|
27
|
+
// ----------------------------------------
|
|
28
|
+
export function createUser(): void {}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```go
|
|
32
|
+
// BAD: decorative section header
|
|
33
|
+
// ==== Handlers ====
|
|
34
|
+
func handle() {}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Good
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// creates the user and returns its persisted form
|
|
41
|
+
export function createUser(): void {}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```go
|
|
45
|
+
// handle serves the health endpoint.
|
|
46
|
+
func handle() {}
|
|
47
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- an enum that never leaves the process (not stored, logged, or sent on the wire)
|
|
6
|
+
- protobuf or database enums with a fixed, documented numbering
|
|
7
|
+
- a hot path where the integer is a deliberate optimization and a String method covers logs
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
An `iota` enum that is stored, logged, or sent over the wire serializes as a bare number, so inserting a constant renumbers everything that was already saved and a log line shows `status=2` to whoever is debugging it. Back an enum that crosses a boundary with a string; the value stays stable when the list changes and is readable everywhere it appears.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
iota enum is serialized as a bare integer; back it with a string
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
type Status int
|
|
22
|
+
|
|
23
|
+
const (
|
|
24
|
+
StatusPending Status = iota
|
|
25
|
+
StatusCompleted
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
type Item struct {
|
|
29
|
+
// BAD: inserting a constant above StatusCompleted changes every stored record
|
|
30
|
+
Status Status `json:"status"`
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Good
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
type Status string
|
|
38
|
+
|
|
39
|
+
const (
|
|
40
|
+
StatusPending Status = "pending"
|
|
41
|
+
StatusCompleted Status = "completed"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
type Item struct {
|
|
45
|
+
Status Status `json:"status"`
|
|
46
|
+
}
|
|
47
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- unrelated constants grouped for convenience, such as defaults or configuration keys
|
|
6
|
+
- a single constant
|
|
7
|
+
- values only ever used as map keys or labels, never as a parameter or field type
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A set of untyped string or int constants that stand for the states of one thing is an enum without a type, so any string is accepted where a status is expected and a typo compiles. Declare a named type and make the constants that type; the compiler then rejects a raw value and a `String` or `MarshalText` method has a type to hang on.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
related constants form an enum but have no named type; declare one
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
// BAD: untyped strings, so any string passes where a status is expected
|
|
22
|
+
const (
|
|
23
|
+
StatusPending = "pending"
|
|
24
|
+
StatusCompleted = "completed"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
func Transition(status string) error {
|
|
28
|
+
return nil
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Good
|
|
33
|
+
|
|
34
|
+
```go
|
|
35
|
+
type Status string
|
|
36
|
+
|
|
37
|
+
const (
|
|
38
|
+
StatusPending Status = "pending"
|
|
39
|
+
StatusCompleted Status = "completed"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
func Transition(status Status) error {
|
|
43
|
+
return nil
|
|
44
|
+
}
|
|
45
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: info
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- the type is passed to a function taking the interface in the same package, which already checks it
|
|
6
|
+
- an unexported helper type with a single local use
|
|
7
|
+
- the interface is not known to the implementing package
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A type that is meant to satisfy an interface but never checked against it breaks only at the distant call site that first assigns it, and the compiler error points there instead of at the missing method. `var _ Repository = (*PgRepo)(nil)` next to the type turns that into an error on the type itself the moment the interface changes.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
type meant to satisfy an interface is not checked; add var _ Iface = (*T)(nil)
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
type PgRepo struct {
|
|
22
|
+
db *sql.DB
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// BAD: nothing checks PgRepo against Repository until a distant caller fails to compile
|
|
26
|
+
func (r *PgRepo) Get(ctx context.Context, id string) (*Item, error) {
|
|
27
|
+
return nil, nil
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
type PgRepo struct {
|
|
35
|
+
db *sql.DB
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var _ Repository = (*PgRepo)(nil)
|
|
39
|
+
|
|
40
|
+
func (r *PgRepo) Get(ctx context.Context, id string) (*Item, error) {
|
|
41
|
+
return nil, nil
|
|
42
|
+
}
|
|
43
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- an interface a package exports for plugin authors to implement, where the package is the consumer
|
|
6
|
+
- a widely shared interface in the style of io.Reader
|
|
7
|
+
- a package that both defines and consumes the interface
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
An interface declared next to its implementation is shaped by what the implementation offers, not by what a caller needs, so it grows with the implementation and every consumer depends on the implementing package to use it. Declare the interface in the package that calls it, with only the methods that package uses; the implementing package exports a concrete type and satisfies the interface implicitly.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
interface declared beside its implementation; define it where it is consumed
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
package postgres
|
|
22
|
+
|
|
23
|
+
// BAD: the implementation dictates the interface, so every consumer imports postgres
|
|
24
|
+
type Repository interface {
|
|
25
|
+
Get(ctx context.Context, id string) (*Item, error)
|
|
26
|
+
Save(ctx context.Context, item *Item) error
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Repo struct {
|
|
30
|
+
db *sql.DB
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Good
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
package service
|
|
38
|
+
|
|
39
|
+
type Repository interface {
|
|
40
|
+
Get(ctx context.Context, id string) (*Item, error)
|
|
41
|
+
Save(ctx context.Context, item *Item) error
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Service struct {
|
|
45
|
+
repo Repository
|
|
46
|
+
}
|
|
47
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: interface_type
|
|
7
|
+
has:
|
|
8
|
+
kind: method_elem
|
|
9
|
+
nthChild:
|
|
10
|
+
position: 5
|
|
11
|
+
ofRule:
|
|
12
|
+
kind: method_elem
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
Every method on an interface is a method every implementation and every test double must provide, so a five-method interface makes the next fake and the next adapter five times the work of a one-method one. Small interfaces compose: declare `Reader` and `Writer` and embed them into `ReadWriter` where a consumer needs both.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
interface with five or more methods; split it and compose smaller interfaces
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
// BAD: every fake and adapter must implement all of these
|
|
27
|
+
type Storage interface {
|
|
28
|
+
Get(ctx context.Context, id string) ([]byte, error)
|
|
29
|
+
Put(ctx context.Context, id string, data []byte) error
|
|
30
|
+
Delete(ctx context.Context, id string) error
|
|
31
|
+
List(ctx context.Context, prefix string) ([]string, error)
|
|
32
|
+
Stat(ctx context.Context, id string) (Info, error)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Good
|
|
37
|
+
|
|
38
|
+
```go
|
|
39
|
+
type Reader interface {
|
|
40
|
+
Get(ctx context.Context, id string) ([]byte, error)
|
|
41
|
+
List(ctx context.Context, prefix string) ([]string, error)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Writer interface {
|
|
45
|
+
Put(ctx context.Context, id string, data []byte) error
|
|
46
|
+
Delete(ctx context.Context, id string) error
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type ReadWriter interface {
|
|
50
|
+
Reader
|
|
51
|
+
Writer
|
|
52
|
+
}
|
|
53
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: package_identifier
|
|
7
|
+
regex: '^(utils?|helpers?|common|misc|shared)$'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A package called `utils` or `helpers` has no domain, so every unrelated function ends up there and the package grows into a dependency of everything. The name gives the reader no idea what `utils.Process` does or where the next function belongs. Name packages after the concept they own, such as `rotate` or `auth`, and split a grab-bag by what its functions act on.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
package named as a grab-bag; name it after the concept it owns
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
// BAD: no domain, will collect everything
|
|
22
|
+
package utils
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
// BAD: no domain, will collect everything
|
|
27
|
+
package common
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Good
|
|
31
|
+
|
|
32
|
+
```go
|
|
33
|
+
package rotate
|
|
34
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a type that wraps an external resource (connection, file) and is always built by a constructor
|
|
6
|
+
- a type whose constructor validates invariants that a zero value cannot satisfy
|
|
7
|
+
- a type that is unexported and only constructed in one place
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A struct whose zero value works can be declared as a field or a `var` and used immediately, the way `sync.Mutex` and `bytes.Buffer` are. When the zero value panics on first use, every caller has to remember the constructor, and forgetting it is a nil map write that only shows up at runtime. Initialize lazily inside the methods or make the zero state meaningful.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
zero value panics on first use; make the type usable without a constructor
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
type Cache struct {
|
|
22
|
+
mu sync.Mutex
|
|
23
|
+
items map[string]string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// BAD: the zero Cache panics here because items is nil
|
|
27
|
+
func (c *Cache) Set(key, value string) {
|
|
28
|
+
c.mu.Lock()
|
|
29
|
+
defer c.mu.Unlock()
|
|
30
|
+
c.items[key] = value
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Good
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
type Cache struct {
|
|
38
|
+
mu sync.Mutex
|
|
39
|
+
items map[string]string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func (c *Cache) Set(key, value string) {
|
|
43
|
+
c.mu.Lock()
|
|
44
|
+
defer c.mu.Unlock()
|
|
45
|
+
if c.items == nil {
|
|
46
|
+
c.items = make(map[string]string)
|
|
47
|
+
}
|
|
48
|
+
c.items[key] = value
|
|
49
|
+
}
|
|
50
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a collection that genuinely mixes unrelated types
|
|
6
|
+
- an interface used only for behavior, with no assertion back to a concrete type
|
|
7
|
+
- the caller never needs the concrete type again
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Returning an interface or `any` from a lookup and letting callers type-assert back to what they put in moves a compile-time fact to a runtime check at every call site. A type parameter keeps the caller's concrete type through the function, so the assertion and its failure branch disappear and adding a new element type needs no new code.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
interface plus assertions where a type parameter would keep the caller's type
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
type HasID interface {
|
|
22
|
+
GetID() string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// BAD: callers get an interface back and must assert to recover their own type
|
|
26
|
+
func FindItem(items []HasID, id string) HasID {
|
|
27
|
+
for _, item := range items {
|
|
28
|
+
if item.GetID() == id {
|
|
29
|
+
return item
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Good
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
type HasID interface {
|
|
41
|
+
GetID() string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func FindItem[T HasID](items []T, id string) (T, bool) {
|
|
45
|
+
for _, item := range items {
|
|
46
|
+
if item.GetID() == id {
|
|
47
|
+
return item, true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var zero T
|
|
52
|
+
|
|
53
|
+
return zero, false
|
|
54
|
+
}
|
|
55
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a long-lived background loop started from main or a Start method that has a matching Stop
|
|
6
|
+
- a goroutine that reports through a channel the caller reads
|
|
7
|
+
- fire-and-forget by design with the error handled and logged inside the goroutine
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A bare `go f()` in a request path has no one waiting for it, so its errors vanish, its panics take the process down, and the function returns before the work is done. Bound the goroutines with an `errgroup` or a `sync.WaitGroup`, propagate the first error, and let the context cancel the rest.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
goroutine started with nothing waiting for it or collecting its error
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (s *Service) ProcessAll(ctx context.Context, items []Item) error {
|
|
22
|
+
for _, item := range items {
|
|
23
|
+
// BAD: nothing waits for these goroutines or sees their errors
|
|
24
|
+
go s.process(ctx, item)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
func (s *Service) ProcessAll(ctx context.Context, items []Item) error {
|
|
35
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
36
|
+
for _, item := range items {
|
|
37
|
+
g.Go(func() error {
|
|
38
|
+
return s.process(ctx, item)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return g.Wait()
|
|
43
|
+
}
|
|
44
|
+
```
|