@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,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a promise explicitly discarded with `void` and a comment saying why, such as fire-and-forget telemetry
|
|
6
|
+
- a promise stored in a variable or array and awaited later, including through `Promise.all`
|
|
7
|
+
- a call that returns something other than a promise, when the return type is visible in the diff or obvious from the name
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A promise nobody awaits or returns keeps running with nobody watching: its rejection is not caught by the surrounding `try`, the function returns before the work is done, and depending on the runtime the failure is either an unhandled rejection that kills the process or a warning nobody reads. Every promise should be awaited, returned, collected for a later `Promise.all`, or discarded on purpose with `void` and a reason.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
promise is neither awaited nor returned; its rejection is lost
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
async function createUser(input: CreateUserInput): Promise<User> {
|
|
22
|
+
const user = await repository.insert(input);
|
|
23
|
+
|
|
24
|
+
// BAD: a failed email send rejects into the void
|
|
25
|
+
sendWelcomeEmail(user);
|
|
26
|
+
|
|
27
|
+
return user;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
async function createUser(input: CreateUserInput): Promise<User> {
|
|
35
|
+
const user = await repository.insert(input);
|
|
36
|
+
|
|
37
|
+
await sendWelcomeEmail(user);
|
|
38
|
+
|
|
39
|
+
return user;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
async function createUser(input: CreateUserInput): Promise<User> {
|
|
45
|
+
const user = await repository.insert(input);
|
|
46
|
+
|
|
47
|
+
// the email is best-effort and failures are logged inside the mailer
|
|
48
|
+
void sendWelcomeEmail(user);
|
|
49
|
+
|
|
50
|
+
return user;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
any:
|
|
7
|
+
- pattern: process.env.$NAME
|
|
8
|
+
- pattern: process.env[$NAME]
|
|
9
|
+
- pattern: import.meta.env.$NAME
|
|
10
|
+
ignore:
|
|
11
|
+
- '**/config/**'
|
|
12
|
+
- '**/config.ts'
|
|
13
|
+
- '**/env.ts'
|
|
14
|
+
- '**/env/**'
|
|
15
|
+
- '**/*.config.ts'
|
|
16
|
+
- '**/*.config.mts'
|
|
17
|
+
- '**/*.test.ts'
|
|
18
|
+
- '**/*.spec.ts'
|
|
19
|
+
- '**/scripts/**'
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Why
|
|
23
|
+
|
|
24
|
+
Reading `process.env` in the middle of a module scatters the list of variables the program needs across the codebase, so nobody can say what a deployment requires without grepping. Each read is an untyped, possibly undefined string that gets parsed and defaulted differently in every place. Read and validate the environment once in a config module and pass typed values from there.
|
|
25
|
+
|
|
26
|
+
## Message
|
|
27
|
+
|
|
28
|
+
environment read outside the config module; validate env once and pass typed config
|
|
29
|
+
|
|
30
|
+
## Bad
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export async function sendEmail(message: Message): Promise<void> {
|
|
34
|
+
// BAD: an untyped, unvalidated read buried in logic
|
|
35
|
+
const apiKey = process.env.SENDGRID_API_KEY;
|
|
36
|
+
|
|
37
|
+
await client.send(apiKey ?? '', message);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// BAD: the same variable is parsed differently in every module that reads it
|
|
43
|
+
const timeoutMs = Number(process.env['HTTP_TIMEOUT_MS'] ?? 5000);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Good
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export async function sendEmail(config: EmailConfig, message: Message): Promise<void> {
|
|
50
|
+
await client.send(config.apiKey, message);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const timeoutMs = config.http.timeoutMs;
|
|
56
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
any:
|
|
7
|
+
- kind: catch_clause
|
|
8
|
+
has:
|
|
9
|
+
field: type
|
|
10
|
+
regex: 'any'
|
|
11
|
+
- pattern: $E as Error
|
|
12
|
+
inside:
|
|
13
|
+
kind: catch_clause
|
|
14
|
+
stopBy: end
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
Anything can be thrown in JavaScript, so a catch parameter is `unknown` and the only honest way to read `.message` is to check `instanceof Error` first. Typing the parameter as `any` or casting it with `as Error` skips that check, and the first time a string or a rejected fetch body is thrown the handler itself crashes on an undefined property. Narrow with `instanceof`, or normalize with `error instanceof Error ? error : new Error(String(error))`.
|
|
20
|
+
|
|
21
|
+
## Message
|
|
22
|
+
|
|
23
|
+
catch parameter treated as `Error` without a check; keep it `unknown` and narrow with `instanceof`
|
|
24
|
+
|
|
25
|
+
## Bad
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
try {
|
|
29
|
+
await save(user);
|
|
30
|
+
// BAD: any disables checking on whatever was thrown
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
logger.error(error.message);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
try {
|
|
38
|
+
await save(user);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// BAD: the cast crashes the handler if a non-Error was thrown
|
|
41
|
+
logger.error((error as Error).message);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Good
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
try {
|
|
49
|
+
await save(user);
|
|
50
|
+
} catch (error: unknown) {
|
|
51
|
+
if (error instanceof Error) {
|
|
52
|
+
logger.error(error.message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
try {
|
|
61
|
+
await save(user);
|
|
62
|
+
} catch (error: unknown) {
|
|
63
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
64
|
+
|
|
65
|
+
throw new SaveError('saving user failed', { cause });
|
|
66
|
+
}
|
|
67
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ignore:
|
|
5
|
+
- '**/*.test.ts'
|
|
6
|
+
- '**/*.spec.ts'
|
|
7
|
+
ast:
|
|
8
|
+
rule:
|
|
9
|
+
any:
|
|
10
|
+
- pattern: $E.message === $S
|
|
11
|
+
- pattern: $E.message == $S
|
|
12
|
+
- pattern: $E.message.includes($$$ARGS)
|
|
13
|
+
- pattern: $E.message.startsWith($$$ARGS)
|
|
14
|
+
- pattern: $E.message.match($$$ARGS)
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
Branching on the text of an error message couples the caller to a string that was written for humans and will be reworded without anyone checking the callers. A custom error class gives the failure a name, an `instanceof` check that survives rewording, and a place to hang structured fields such as a status code or the offending id. Expected failures get their own class; message matching is left for logs.
|
|
20
|
+
|
|
21
|
+
## Message
|
|
22
|
+
|
|
23
|
+
branching on error message text; throw a custom error class and check `instanceof`
|
|
24
|
+
|
|
25
|
+
## Bad
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
try {
|
|
29
|
+
await getUser(id);
|
|
30
|
+
} catch (error: unknown) {
|
|
31
|
+
// BAD: a reworded message silently turns this into a 500
|
|
32
|
+
if (error instanceof Error && error.message === 'user not found') {
|
|
33
|
+
res.status(404).end();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
try {
|
|
40
|
+
await connect();
|
|
41
|
+
} catch (error: unknown) {
|
|
42
|
+
// BAD: substring matching on prose
|
|
43
|
+
if (error instanceof Error && error.message.includes('timeout')) {
|
|
44
|
+
return retry();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Good
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
class NotFoundError extends Error {
|
|
53
|
+
constructor(resource: string) {
|
|
54
|
+
super(`${resource} not found`);
|
|
55
|
+
this.name = 'NotFoundError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await getUser(id);
|
|
61
|
+
} catch (error: unknown) {
|
|
62
|
+
if (error instanceof NotFoundError) {
|
|
63
|
+
res.status(404).end();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: critical
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: catch_clause
|
|
7
|
+
has:
|
|
8
|
+
kind: statement_block
|
|
9
|
+
regex: '^\{\s*\}$'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
An empty catch swallows the failure and lets the program continue in a state nobody designed for. The bug surfaces later, somewhere else, with no stack trace pointing home. Handle the error, rethrow it with context, or let it propagate. If ignoring it really is correct, the block needs a comment saying why, which makes it non-empty.
|
|
15
|
+
|
|
16
|
+
## Message
|
|
17
|
+
|
|
18
|
+
empty catch swallows the error; handle it, rethrow with context, or let it propagate
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
try {
|
|
24
|
+
await save(record);
|
|
25
|
+
// BAD: the failure vanishes
|
|
26
|
+
} catch {}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
try {
|
|
31
|
+
await save(record);
|
|
32
|
+
// BAD: binding the error and dropping it is still swallowing
|
|
33
|
+
} catch (error) {}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Good
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
try {
|
|
40
|
+
await save(record);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new PersistenceError(`saving record ${record.id}`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
try {
|
|
48
|
+
await removeTempFile(path);
|
|
49
|
+
} catch {
|
|
50
|
+
// the file is already gone, which is the state we wanted
|
|
51
|
+
}
|
|
52
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: catch_clause
|
|
7
|
+
has:
|
|
8
|
+
kind: statement_block
|
|
9
|
+
regex: '^\{\s*(?:(?:console|log|logger|this\.logger|this\.log)\.\w+\([^;]*\);?\s*)+\}$'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Logging an error and carrying on is an empty catch with a paper trail: the caller gets a normal return and proceeds as if the operation succeeded, so the failure surfaces later as bad data or a confusing second error. The log line is rarely read until then. Rethrow with context, return an explicit failure the caller must handle, or, if continuing really is correct, say why in a comment so the choice is visible.
|
|
15
|
+
|
|
16
|
+
## Message
|
|
17
|
+
|
|
18
|
+
catch only logs and continues; rethrow with context or return an explicit failure
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
async function saveDraft(draft: Draft): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
await repo.save(draft);
|
|
26
|
+
// BAD: the caller is told nothing and proceeds as if the save worked
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('failed to save draft', error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
async function saveDraft(draft: Draft): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
await repo.save(draft);
|
|
37
|
+
// BAD: a structured logger does not change what the caller sees
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.error({ error, draftId: draft.id }, 'failed to save draft');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Good
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
async function saveDraft(draft: Draft): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
await repo.save(draft);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new PersistenceError(`saving draft ${draft.id}`, { cause: error });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
async function warmCache(): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
await cache.preload();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// a cold cache only costs latency, and the request path fills it on demand
|
|
62
|
+
logger.warn({ error }, 'cache preload failed, continuing cold');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ignore:
|
|
5
|
+
- '**/*.test.ts'
|
|
6
|
+
- '**/*.spec.ts'
|
|
7
|
+
ast:
|
|
8
|
+
rule:
|
|
9
|
+
pattern: throw new Error($MSG)
|
|
10
|
+
constraints:
|
|
11
|
+
MSG:
|
|
12
|
+
kind: string
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
A bare `new Error('...')` with a fixed string gives the catcher nothing to branch on except the message text, and it carries no context about which record or input failed. A typed error class can be checked with `instanceof` and mapped to a status code at the boundary, and a message built from the inputs tells the on-call engineer what actually went wrong. Reserve plain `Error` for true invariants such as the `never` branch of an exhaustive switch, and even then include the value.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
bare `Error` with a fixed message; throw a typed error that carries context
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
async function getUser(id: string): Promise<User> {
|
|
27
|
+
const user = await repository.find(id);
|
|
28
|
+
|
|
29
|
+
if (!user) {
|
|
30
|
+
// BAD: nothing to branch on and no hint of which user was missing
|
|
31
|
+
throw new Error('user not found');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return user;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Good
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
async function getUser(id: string): Promise<User> {
|
|
42
|
+
const user = await repository.find(id);
|
|
43
|
+
|
|
44
|
+
if (!user) {
|
|
45
|
+
throw new NotFoundError(`user ${id}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return user;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
function label(status: Status): string {
|
|
54
|
+
switch (status) {
|
|
55
|
+
case Status.ACTIVE:
|
|
56
|
+
return 'running';
|
|
57
|
+
default: {
|
|
58
|
+
const exhaustive: never = status;
|
|
59
|
+
|
|
60
|
+
throw new Error(`unhandled status: ${String(exhaustive)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: binary_expression
|
|
7
|
+
all:
|
|
8
|
+
- has:
|
|
9
|
+
field: operator
|
|
10
|
+
regex: '^\|\|$'
|
|
11
|
+
- has:
|
|
12
|
+
field: right
|
|
13
|
+
any:
|
|
14
|
+
- kind: string
|
|
15
|
+
- kind: number
|
|
16
|
+
- kind: object
|
|
17
|
+
- kind: array
|
|
18
|
+
- kind: template_string
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
`value || fallback` replaces every falsy value, so a legitimate `0`, empty string, or `false` is silently swapped for the default and the bug only shows up when someone sets a timeout to zero or clears a name. `??` falls back only on `null` and `undefined`, which is what a default is for. Use `||` only when a falsy value really should be treated as missing, and say so.
|
|
24
|
+
|
|
25
|
+
## Message
|
|
26
|
+
|
|
27
|
+
`||` with a default replaces 0, empty string, and false; use `??`
|
|
28
|
+
|
|
29
|
+
## Bad
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// BAD: a timeout of 0 becomes 5000
|
|
33
|
+
const timeout = options.timeout || 5000;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// BAD: an empty name becomes 'anonymous' even when it was set on purpose
|
|
38
|
+
const name = user.name || 'anonymous';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Good
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const timeout = options.timeout ?? 5000;
|
|
45
|
+
const name = user.name ?? 'anonymous';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const canEdit = isOwner || isAdmin;
|
|
50
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: export_statement
|
|
7
|
+
has:
|
|
8
|
+
any:
|
|
9
|
+
- kind: lexical_declaration
|
|
10
|
+
regex: '^let\b'
|
|
11
|
+
- kind: variable_declaration
|
|
12
|
+
ignore:
|
|
13
|
+
- '**/*.test.ts'
|
|
14
|
+
- '**/*.spec.ts'
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
An exported `let` is global mutable state: any importer can reassign it, and no reader of the module can know its value at a given moment without tracing every import. Tests that touch it leak into each other, and reloading or running two instances in one process breaks. Keep the state inside a function, class, or factory and expose operations on it, or export a `const`.
|
|
20
|
+
|
|
21
|
+
## Message
|
|
22
|
+
|
|
23
|
+
exported let is global mutable state; expose functions over the state or export a const
|
|
24
|
+
|
|
25
|
+
## Bad
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// BAD: any importer can reassign this
|
|
29
|
+
export let currentUser: User | undefined;
|
|
30
|
+
|
|
31
|
+
export function login(user: User): void {
|
|
32
|
+
currentUser = user;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// BAD: var is hoisted and reassignable from anywhere
|
|
38
|
+
export var requestCount = 0;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Good
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export function createSession(): Session {
|
|
45
|
+
let currentUser: User | undefined;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
login(user: User): void {
|
|
49
|
+
currentUser = user;
|
|
50
|
+
},
|
|
51
|
+
current(): User | undefined {
|
|
52
|
+
return currentUser;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
export const DEFAULT_PAGE_SIZE = 50;
|
|
60
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
pattern: $X as unknown as $T
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
`x as unknown as T` exists to defeat the one check a single `as` still performs, that the two types overlap. It converts any value into any type with no runtime check and no compiler objection, which is `any` with extra steps. If the value really is a `T`, a type guard or a schema can prove it; if it is not, the cast just moves the crash somewhere harder to debug.
|
|
12
|
+
|
|
13
|
+
## Message
|
|
14
|
+
|
|
15
|
+
double assertion through `unknown` bypasses all checking; validate or narrow instead
|
|
16
|
+
|
|
17
|
+
## Bad
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// BAD: the compiler has been told to accept anything here
|
|
21
|
+
const user = row as unknown as User;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Good
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const user = userSchema.parse(row);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
if (isUser(row)) {
|
|
32
|
+
return row;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: predefined_type
|
|
7
|
+
regex: ^any$
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
`any` switches the type checker off for everything it touches, and the hole spreads through every call site. Prefer a real type, then a generic, then `Record<string, T>`, then `unknown` narrowed at the point of use. `any` is the last resort, and it needs a comment saying why.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
`any` disables type checking; use a real type, a generic, or `unknown`
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: any hides the shape of the payload
|
|
22
|
+
function handle(payload: any) {
|
|
23
|
+
return payload.user.id;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// BAD: Record<string, any> is any with extra steps
|
|
29
|
+
const cache: Record<string, any> = {};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Good
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// GOOD: unknown forces narrowing before use
|
|
36
|
+
function handle(payload: unknown) {
|
|
37
|
+
const parsed = payloadSchema.parse(payload);
|
|
38
|
+
|
|
39
|
+
return parsed.user.id;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// GOOD: a generic keeps the caller's type
|
|
45
|
+
function first<T>(items: readonly T[]): T | undefined {
|
|
46
|
+
return items[0];
|
|
47
|
+
}
|
|
48
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: ast
|
|
4
|
+
ignore:
|
|
5
|
+
- '**/*.test.ts'
|
|
6
|
+
- '**/*.test.tsx'
|
|
7
|
+
ast:
|
|
8
|
+
rule:
|
|
9
|
+
kind: non_null_expression
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
The `!` operator tells the compiler to trust you and turns a compile-time question into a runtime crash. Narrow with a guard, use optional chaining with a default, or throw an error that says what was missing. Tests are excluded because a failing assertion there is the intended outcome.
|
|
15
|
+
|
|
16
|
+
## Message
|
|
17
|
+
|
|
18
|
+
non-null assertion trades a compile-time check for a runtime crash; narrow or throw
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// BAD: crashes with a bare TypeError when the user is missing
|
|
24
|
+
const name = users.find(user => user.id === id)!.name;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Good
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// GOOD: the missing case has a name and a message
|
|
31
|
+
const user = users.find(candidate => candidate.id === id);
|
|
32
|
+
|
|
33
|
+
if (user === undefined) {
|
|
34
|
+
throw new NotFoundError(`user ${id}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const name = user.name;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// GOOD: optional chaining with a default when absence is fine
|
|
42
|
+
const name = users.find(user => user.id === id)?.name ?? 'anonymous';
|
|
43
|
+
```
|