@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,138 @@
|
|
|
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
|
+
any:
|
|
13
|
+
- all:
|
|
14
|
+
- any:
|
|
15
|
+
- kind: function_declaration
|
|
16
|
+
- kind: method_definition
|
|
17
|
+
- kind: function_expression
|
|
18
|
+
- kind: arrow_function
|
|
19
|
+
- regex: '^(?:[^\n]*\n){60}'
|
|
20
|
+
- kind: formal_parameters
|
|
21
|
+
has:
|
|
22
|
+
matches: param
|
|
23
|
+
follows:
|
|
24
|
+
matches: param
|
|
25
|
+
stopBy: end
|
|
26
|
+
follows:
|
|
27
|
+
matches: param
|
|
28
|
+
stopBy: end
|
|
29
|
+
follows:
|
|
30
|
+
matches: param
|
|
31
|
+
stopBy: end
|
|
32
|
+
follows:
|
|
33
|
+
matches: param
|
|
34
|
+
stopBy: end
|
|
35
|
+
follows:
|
|
36
|
+
matches: param
|
|
37
|
+
stopBy: end
|
|
38
|
+
go:
|
|
39
|
+
utils:
|
|
40
|
+
param:
|
|
41
|
+
kind: parameter_declaration
|
|
42
|
+
rule:
|
|
43
|
+
any:
|
|
44
|
+
- all:
|
|
45
|
+
- any:
|
|
46
|
+
- kind: function_declaration
|
|
47
|
+
- kind: method_declaration
|
|
48
|
+
- kind: func_literal
|
|
49
|
+
- regex: '^(?:[^\n]*\n){60}'
|
|
50
|
+
- kind: parameter_list
|
|
51
|
+
not:
|
|
52
|
+
follows:
|
|
53
|
+
kind: parameter_list
|
|
54
|
+
has:
|
|
55
|
+
matches: param
|
|
56
|
+
follows:
|
|
57
|
+
matches: param
|
|
58
|
+
stopBy: end
|
|
59
|
+
follows:
|
|
60
|
+
matches: param
|
|
61
|
+
stopBy: end
|
|
62
|
+
follows:
|
|
63
|
+
matches: param
|
|
64
|
+
stopBy: end
|
|
65
|
+
follows:
|
|
66
|
+
matches: param
|
|
67
|
+
stopBy: end
|
|
68
|
+
follows:
|
|
69
|
+
matches: param
|
|
70
|
+
stopBy: end
|
|
71
|
+
ignore:
|
|
72
|
+
- '**/*.test.ts'
|
|
73
|
+
- '**/*.spec.ts'
|
|
74
|
+
- '**/*_test.go'
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Why
|
|
78
|
+
|
|
79
|
+
A function with six or more parameters or more than sixty lines is doing several jobs, and each new requirement makes it longer and its call sites more fragile, since positional arguments are easy to swap and impossible to read. Group related parameters into a typed options object or struct, and split the body along the steps it already performs. The pieces get names, tests, and a chance of being reused.
|
|
80
|
+
|
|
81
|
+
## Message
|
|
82
|
+
|
|
83
|
+
function is too wide: more than 5 parameters or over 60 lines; group parameters into a struct and split the body
|
|
84
|
+
|
|
85
|
+
## Bad
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// BAD: six positional arguments nobody can order from memory
|
|
89
|
+
export function createInvoice(
|
|
90
|
+
customerId: string,
|
|
91
|
+
items: LineItem[],
|
|
92
|
+
currency: string,
|
|
93
|
+
dueDate: Date,
|
|
94
|
+
notes: string,
|
|
95
|
+
sendEmail: boolean
|
|
96
|
+
): Invoice {
|
|
97
|
+
return build(customerId, items, currency, dueDate, notes, sendEmail);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```go
|
|
102
|
+
// BAD: six positional arguments nobody can order from memory
|
|
103
|
+
func CreateInvoice(customerID string, items []LineItem, currency string, dueDate time.Time, notes string, sendEmail bool) Invoice {
|
|
104
|
+
return build(customerID, items, currency, dueDate, notes, sendEmail)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Good
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
type CreateInvoiceInput = {
|
|
112
|
+
customerId: string;
|
|
113
|
+
items: LineItem[];
|
|
114
|
+
currency: string;
|
|
115
|
+
dueDate: Date;
|
|
116
|
+
notes?: string;
|
|
117
|
+
sendEmail?: boolean;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export function createInvoice(input: CreateInvoiceInput): Invoice {
|
|
121
|
+
return build(input);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```go
|
|
126
|
+
type CreateInvoiceInput struct {
|
|
127
|
+
CustomerID string
|
|
128
|
+
Items []LineItem
|
|
129
|
+
Currency string
|
|
130
|
+
DueDate time.Time
|
|
131
|
+
Notes string
|
|
132
|
+
SendEmail bool
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func CreateInvoice(in CreateInvoiceInput) Invoice {
|
|
136
|
+
return build(in)
|
|
137
|
+
}
|
|
138
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- asserting that a collaborator at a boundary received the right call, such as the mailer being sent the right message, when that call is the observable outcome
|
|
6
|
+
- calling an unexported or private pure function directly and asserting on its return value
|
|
7
|
+
- reaching into unexported fields to set up state before exercising the public behavior
|
|
8
|
+
- asserting call counts for behavior the contract promises, such as a cache hitting the loader exactly once
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
A test that spies on private methods or inspects unexported fields passes only while the code is written one particular way, so a refactor that keeps every behavior still turns the suite red and teaches people to delete tests. It also documents nothing a caller can rely on. Assert on what the caller sees: return values, thrown errors, persisted state, and calls to external systems.
|
|
14
|
+
|
|
15
|
+
## Message
|
|
16
|
+
|
|
17
|
+
test asserts on internals; assert on observable behavior instead
|
|
18
|
+
|
|
19
|
+
## Bad
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
it('creates a user', async () => {
|
|
23
|
+
const service = new UserService(repo);
|
|
24
|
+
// BAD: the test is coupled to a private helper's name and arguments
|
|
25
|
+
const spy = vi.spyOn(service as never, 'normalizeEmail');
|
|
26
|
+
|
|
27
|
+
await service.create({ email: 'Ada@Example.com' });
|
|
28
|
+
|
|
29
|
+
expect(spy).toHaveBeenCalledWith('Ada@Example.com');
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
func TestPutStoresEntry(t *testing.T) {
|
|
35
|
+
c := cache.New()
|
|
36
|
+
|
|
37
|
+
c.Put("k", []byte("v"))
|
|
38
|
+
|
|
39
|
+
// BAD: the test knows the map is called entries and breaks when storage changes
|
|
40
|
+
if len(c.entries) != 1 {
|
|
41
|
+
t.Fatalf("expected 1 entry, got %d", len(c.entries))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Good
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
it('creates a user with a normalized email', async () => {
|
|
50
|
+
const service = new UserService(repo);
|
|
51
|
+
|
|
52
|
+
const user = await service.create({ email: 'Ada@Example.com' });
|
|
53
|
+
|
|
54
|
+
expect(user.email).toBe('ada@example.com');
|
|
55
|
+
expect(await repo.findById(user.id)).toEqual(user);
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```go
|
|
60
|
+
func TestPutThenGet(t *testing.T) {
|
|
61
|
+
c := cache.New()
|
|
62
|
+
|
|
63
|
+
c.Put("k", []byte("v"))
|
|
64
|
+
|
|
65
|
+
got, ok := c.Get("k")
|
|
66
|
+
if !ok || string(got) != "v" {
|
|
67
|
+
t.Fatalf("Get(k) = %q, %v; want v, true", got, ok)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- doc comments on exported symbols that state the contract, even when short
|
|
6
|
+
- comments explaining why, a constraint, or a non-obvious consequence
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
A comment that says what the next line already says is noise the reader has to check against the code, and it goes stale the first time the code changes. Comments earn their place by saying why, naming a constraint, or warning about a consequence the code cannot show.
|
|
12
|
+
|
|
13
|
+
## Message
|
|
14
|
+
|
|
15
|
+
comment restates the code; say why or delete it
|
|
16
|
+
|
|
17
|
+
## Bad
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// BAD: the comment is the line, in English
|
|
21
|
+
// increment the counter
|
|
22
|
+
counter += 1;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
func total(orders []Order) int {
|
|
27
|
+
sum := 0
|
|
28
|
+
// BAD: the comment is the loop, in English
|
|
29
|
+
// loop over the orders and add up the amounts
|
|
30
|
+
for _, order := range orders {
|
|
31
|
+
sum += order.Amount
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return sum
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Good
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// the API rejects batches over 100 items, so chunk before sending
|
|
42
|
+
const batches = chunk(items, 100);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```go
|
|
46
|
+
// Find returns ErrNotFound rather than a nil user so callers cannot forget the missing case.
|
|
47
|
+
func Find(id string) (*User, error) {
|
|
48
|
+
return repo.Find(id)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
ts:
|
|
6
|
+
rule:
|
|
7
|
+
kind: comment
|
|
8
|
+
regex: '^(//|/\*+)[\s*]*(TODO|FIXME|HACK|XXX)\b'
|
|
9
|
+
go:
|
|
10
|
+
rule:
|
|
11
|
+
kind: comment
|
|
12
|
+
regex: '^(//|/\*+)[\s*]*(TODO|FIXME|HACK|XXX)\b'
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
A TODO in merged code is a known defect with no owner, no deadline, and no ticket, and it stays there until someone rediscovers the problem the hard way. Either do the work before merging, or open an issue and reference it so the gap is tracked where people actually look. A comment that explains a deliberate limitation and points at the issue is fine; a bare marker is not.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
todo marker shipped in code; do the work or link the tracking issue
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
export function parsePrice(raw: string): number {
|
|
27
|
+
// BAD: known gap with no owner
|
|
28
|
+
// TODO: handle currencies other than USD
|
|
29
|
+
return Number(raw.replace('$', ''));
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
func ParsePrice(raw string) (int, error) {
|
|
35
|
+
// BAD: known shortcut with no owner
|
|
36
|
+
// FIXME: this breaks on prices over 1000
|
|
37
|
+
return strconv.Atoi(strings.TrimPrefix(raw, "$"))
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Good
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// only USD is supported; other currencies are rejected upstream by the validator (see #482)
|
|
45
|
+
export function parsePrice(raw: string): number {
|
|
46
|
+
return Number(raw.replace('$', ''));
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```go
|
|
51
|
+
// ParsePrice accepts USD only; other currencies are rejected by the validator (see #482).
|
|
52
|
+
func ParsePrice(raw string) (int, error) {
|
|
53
|
+
return strconv.Atoi(strings.TrimPrefix(raw, "$"))
|
|
54
|
+
}
|
|
55
|
+
```
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
ts:
|
|
6
|
+
rule:
|
|
7
|
+
kind: new_expression
|
|
8
|
+
pattern: new Promise($$$ARGS)
|
|
9
|
+
has:
|
|
10
|
+
pattern: setTimeout($$$TIMER)
|
|
11
|
+
stopBy: end
|
|
12
|
+
go:
|
|
13
|
+
rule:
|
|
14
|
+
pattern:
|
|
15
|
+
context: 'func f() { time.Sleep($D) }'
|
|
16
|
+
selector: call_expression
|
|
17
|
+
ignore:
|
|
18
|
+
- '**/*.test.ts'
|
|
19
|
+
- '**/*.spec.ts'
|
|
20
|
+
- '**/*_test.go'
|
|
21
|
+
- '**/main.go'
|
|
22
|
+
- '**/cmd/**'
|
|
23
|
+
- '**/e2e/**'
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Why
|
|
27
|
+
|
|
28
|
+
A fixed delay is a guess about how long something else takes, and the guess is wrong on a slow CI runner, a loaded server, or a faster machine that now waits for nothing. The code passes locally and fails intermittently elsewhere, which is the most expensive kind of bug to chase. Wait on the actual signal: a promise, a channel, a readiness check, or a context-aware timer for a real backoff.
|
|
29
|
+
|
|
30
|
+
## Message
|
|
31
|
+
|
|
32
|
+
fixed sleep used as synchronization; wait on a promise, channel, or readiness signal instead
|
|
33
|
+
|
|
34
|
+
## Bad
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
async function startWorker(worker: Worker): Promise<void> {
|
|
38
|
+
worker.start();
|
|
39
|
+
// BAD: hoping startup is done after two seconds
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
41
|
+
worker.send(job);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```go
|
|
46
|
+
func startWorker(w *Worker) {
|
|
47
|
+
go w.Run()
|
|
48
|
+
// BAD: hoping the goroutine is ready after a fixed delay
|
|
49
|
+
time.Sleep(500 * time.Millisecond)
|
|
50
|
+
w.Send(job)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Good
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
async function startWorker(worker: Worker): Promise<void> {
|
|
58
|
+
await worker.start();
|
|
59
|
+
worker.send(job);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { once } from 'node:events';
|
|
65
|
+
|
|
66
|
+
async function startWorker(worker: Worker): Promise<void> {
|
|
67
|
+
worker.start();
|
|
68
|
+
await once(worker, 'ready');
|
|
69
|
+
worker.send(job);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```go
|
|
74
|
+
func startWorker(w *Worker) {
|
|
75
|
+
go w.Run()
|
|
76
|
+
<-w.Ready()
|
|
77
|
+
w.Send(job)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```go
|
|
82
|
+
func retry(ctx context.Context, delay time.Duration, attempt func() error) error {
|
|
83
|
+
for {
|
|
84
|
+
if err := attempt(); err == nil {
|
|
85
|
+
return nil
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
select {
|
|
89
|
+
case <-ctx.Done():
|
|
90
|
+
return ctx.Err()
|
|
91
|
+
case <-time.After(delay):
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
ts:
|
|
6
|
+
rule:
|
|
7
|
+
any:
|
|
8
|
+
- kind: string
|
|
9
|
+
regex: '^["'']https?://'
|
|
10
|
+
- kind: template_string
|
|
11
|
+
regex: '^`https?://'
|
|
12
|
+
not:
|
|
13
|
+
regex: 'https?://(www\.)?w3\.org|https?://schemas\.|https?://json-schema\.org'
|
|
14
|
+
go:
|
|
15
|
+
rule:
|
|
16
|
+
any:
|
|
17
|
+
- kind: interpreted_string_literal
|
|
18
|
+
regex: '^"https?://'
|
|
19
|
+
- kind: raw_string_literal
|
|
20
|
+
regex: '^`https?://'
|
|
21
|
+
not:
|
|
22
|
+
regex: 'https?://(www\.)?w3\.org|https?://schemas\.|https?://json-schema\.org'
|
|
23
|
+
ignore:
|
|
24
|
+
- '**/config/**'
|
|
25
|
+
- '**/config.ts'
|
|
26
|
+
- '**/config.go'
|
|
27
|
+
- '**/constants.ts'
|
|
28
|
+
- '**/constants.go'
|
|
29
|
+
- '**/*.config.ts'
|
|
30
|
+
- '**/*.test.ts'
|
|
31
|
+
- '**/*.spec.ts'
|
|
32
|
+
- '**/*_test.go'
|
|
33
|
+
- '**/testdata/**'
|
|
34
|
+
- '**/fixtures/**'
|
|
35
|
+
- '**/__mocks__/**'
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Why
|
|
39
|
+
|
|
40
|
+
A URL written inline is an environment decision buried in logic: it points at production from a test run, at staging after a deploy, and at nothing once the host moves. Every environment change becomes a code change and a search across the repo. Read the base URL from configuration and build paths on top of it.
|
|
41
|
+
|
|
42
|
+
## Message
|
|
43
|
+
|
|
44
|
+
hardcoded url in logic; read the base url from config and build the path on it
|
|
45
|
+
|
|
46
|
+
## Bad
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
50
|
+
// BAD: the host is fixed in code
|
|
51
|
+
const response = await fetch(`https://api.example.com/users/${id}`);
|
|
52
|
+
|
|
53
|
+
return userSchema.parse(await response.json());
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```go
|
|
58
|
+
func FetchUser(ctx context.Context, id string) (User, error) {
|
|
59
|
+
// BAD: the host is fixed in code
|
|
60
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/users/"+id, nil)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return User{}, fmt.Errorf("building request: %w", err)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return doUser(req)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Good
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export async function fetchUser(config: ApiConfig, id: string): Promise<User> {
|
|
73
|
+
const response = await fetch(new URL(`/users/${id}`, config.baseUrl));
|
|
74
|
+
|
|
75
|
+
return userSchema.parse(await response.json());
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```go
|
|
80
|
+
func (c *Client) FetchUser(ctx context.Context, id string) (User, error) {
|
|
81
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL.JoinPath("users", id).String(), nil)
|
|
82
|
+
if err != nil {
|
|
83
|
+
return User{}, fmt.Errorf("building request: %w", err)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return c.doUser(req)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
ignore:
|
|
5
|
+
- '**/*.test.ts'
|
|
6
|
+
- '**/*.spec.ts'
|
|
7
|
+
- '**/*_test.go'
|
|
8
|
+
- '**/constants.ts'
|
|
9
|
+
- '**/constants.go'
|
|
10
|
+
- '**/testdata/**'
|
|
11
|
+
- '**/fixtures/**'
|
|
12
|
+
falsePositives:
|
|
13
|
+
- the literals 0, 1, -1, 2, and 100, and small loop bounds or step sizes
|
|
14
|
+
- array, slice, or tuple indexes and offsets
|
|
15
|
+
- the right-hand side of a named constant declaration, which is where the number belongs
|
|
16
|
+
- a unit conversion factor next to a named constant, such as `const TIMEOUT_MS = 30 * 1000`
|
|
17
|
+
- well-known protocol values where the call name makes the meaning obvious, such as `res.status(404)` or `os.Exit(1)`
|
|
18
|
+
- numbers in a plain arithmetic formula whose surrounding function name explains them, such as a percentage or area calculation
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
A bare `30000` in a call or a comparison forces every reader to guess what it means and whether the `30000` forty lines down is the same thing. When the value has to change, the search finds every unrelated occurrence too. Give the number a name that says what it is, and reuse that name so the two places cannot drift.
|
|
24
|
+
|
|
25
|
+
## Message
|
|
26
|
+
|
|
27
|
+
magic number in logic; name it as a constant that says what it means
|
|
28
|
+
|
|
29
|
+
## Bad
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
async function pollJob(id: string): Promise<Job> {
|
|
33
|
+
let attempts = 0;
|
|
34
|
+
|
|
35
|
+
while (true) {
|
|
36
|
+
const job = await fetchJob(id);
|
|
37
|
+
// BAD: nobody knows why five, or whether the five below is the same five
|
|
38
|
+
if (job.done || attempts > 5) {
|
|
39
|
+
return job;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
attempts += 1;
|
|
43
|
+
// BAD: a duration with no name
|
|
44
|
+
await delay(1500);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```go
|
|
50
|
+
func (s *Service) ListRecent(ctx context.Context) ([]Item, error) {
|
|
51
|
+
// BAD: nobody knows whether 250 is a page size, an API cap, or a guess
|
|
52
|
+
items, err := s.repo.List(ctx, 250)
|
|
53
|
+
if err != nil {
|
|
54
|
+
return nil, fmt.Errorf("listing items: %w", err)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return items, nil
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Good
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const MAX_POLL_ATTEMPTS = 5;
|
|
65
|
+
const POLL_INTERVAL_MS = 1500;
|
|
66
|
+
|
|
67
|
+
async function pollJob(id: string): Promise<Job> {
|
|
68
|
+
let attempts = 0;
|
|
69
|
+
|
|
70
|
+
while (true) {
|
|
71
|
+
const job = await fetchJob(id);
|
|
72
|
+
if (job.done || attempts > MAX_POLL_ATTEMPTS) {
|
|
73
|
+
return job;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
attempts += 1;
|
|
77
|
+
await delay(POLL_INTERVAL_MS);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```go
|
|
83
|
+
// recentPageSize matches the upstream API's maximum page size.
|
|
84
|
+
const recentPageSize = 250
|
|
85
|
+
|
|
86
|
+
func (s *Service) ListRecent(ctx context.Context) ([]Item, error) {
|
|
87
|
+
items, err := s.repo.List(ctx, recentPageSize)
|
|
88
|
+
if err != nil {
|
|
89
|
+
return nil, fmt.Errorf("listing items: %w", err)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return items, nil
|
|
93
|
+
}
|
|
94
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
ts:
|
|
6
|
+
rule:
|
|
7
|
+
any:
|
|
8
|
+
- pattern: console.log($$$ARGS)
|
|
9
|
+
- pattern: console.debug($$$ARGS)
|
|
10
|
+
- pattern: console.dir($$$ARGS)
|
|
11
|
+
- pattern: console.table($$$ARGS)
|
|
12
|
+
go:
|
|
13
|
+
rule:
|
|
14
|
+
kind: call_expression
|
|
15
|
+
has:
|
|
16
|
+
field: function
|
|
17
|
+
regex: '^(fmt\.Print(ln|f)?|println|print)$'
|
|
18
|
+
ignore:
|
|
19
|
+
- '**/*.test.ts'
|
|
20
|
+
- '**/*.spec.ts'
|
|
21
|
+
- '**/cli.ts'
|
|
22
|
+
- '**/scripts/**'
|
|
23
|
+
- '**/bin/**'
|
|
24
|
+
- '**/main.go'
|
|
25
|
+
- '**/cmd/**'
|
|
26
|
+
- '**/*_test.go'
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Why
|
|
30
|
+
|
|
31
|
+
A print statement left in library code writes unstructured text to stdout with no level, no context, and no way to turn it off, and it pollutes the output of any program that embeds this code. Use the project's logger so the line carries a level and structured fields and can be filtered or shipped. If the line was for debugging, delete it.
|
|
32
|
+
|
|
33
|
+
## Message
|
|
34
|
+
|
|
35
|
+
debug print in library code; use the structured logger or remove it
|
|
36
|
+
|
|
37
|
+
## Bad
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
export async function createUser(input: CreateUserInput): Promise<User> {
|
|
41
|
+
// BAD: unstructured stdout with no level
|
|
42
|
+
console.log('creating user', input.email);
|
|
43
|
+
|
|
44
|
+
return repo.insert(input);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```go
|
|
49
|
+
func CreateUser(ctx context.Context, in CreateUserInput) (User, error) {
|
|
50
|
+
// BAD: unstructured stdout with no level
|
|
51
|
+
fmt.Println("creating user", in.Email)
|
|
52
|
+
|
|
53
|
+
return repo.Insert(ctx, in)
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Good
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
export async function createUser(input: CreateUserInput): Promise<User> {
|
|
61
|
+
logger.info({ email: input.email }, 'creating user');
|
|
62
|
+
|
|
63
|
+
return repo.insert(input);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```go
|
|
68
|
+
func CreateUser(ctx context.Context, in CreateUserInput) (User, error) {
|
|
69
|
+
slog.InfoContext(ctx, "creating user", "email", in.Email)
|
|
70
|
+
|
|
71
|
+
return repo.Insert(ctx, in)
|
|
72
|
+
}
|
|
73
|
+
```
|