@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,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- '`as const` on a literal, which narrows rather than widens'
|
|
6
|
+
- a cast inside a branded-type constructor such as `return id as UserId`, where the function is the single point that mints the brand
|
|
7
|
+
- a cast that follows a runtime check in the same block, such as after `typeof`, `in`, or `Array.isArray`
|
|
8
|
+
- a cast to `unknown` on its own, which discards type information rather than inventing it
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
`value as T` does not check anything at runtime; it tells the compiler to stop looking. Used on data that came from outside the program, such as a request body, a parsed file, a database row, or an environment variable, it turns a type error into a crash or silent corruption somewhere downstream. Validate external data with a schema or a type guard and let the type come from the check.
|
|
14
|
+
|
|
15
|
+
## Message
|
|
16
|
+
|
|
17
|
+
type assertion on unvalidated data; validate with a schema or a type guard instead
|
|
18
|
+
|
|
19
|
+
## Bad
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
app.post('/users', async (req, res) => {
|
|
23
|
+
// BAD: whatever the client sent is now a CreateUserInput as far as the compiler knows
|
|
24
|
+
const input = req.body as CreateUserInput;
|
|
25
|
+
|
|
26
|
+
res.json(await createUser(input));
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// BAD: env vars may be undefined; the cast promises otherwise
|
|
32
|
+
const apiKey = process.env.API_KEY as string;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Good
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
app.post('/users', async (req, res) => {
|
|
39
|
+
const input = createUserSchema.parse(req.body);
|
|
40
|
+
|
|
41
|
+
res.json(await createUser(input));
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const env = envSchema.parse(process.env);
|
|
47
|
+
const apiKey = env.API_KEY;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
function UserId(id: string): UserId {
|
|
52
|
+
return id as UserId;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: major
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
any:
|
|
7
|
+
- pattern: JSON.parse($$$ARGS) as $T
|
|
8
|
+
- pattern: 'const $X: $T = JSON.parse($$$ARGS)'
|
|
9
|
+
constraints:
|
|
10
|
+
T:
|
|
11
|
+
not:
|
|
12
|
+
regex: '^unknown$'
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
`JSON.parse` returns whatever the text contained, and a cast or annotation on the result is a promise the program cannot keep. The first malformed file or changed API payload then fails deep inside code that trusted the shape, with an error that says nothing about the real cause. Parse to `unknown` and run the result through a schema so the failure is caught at the boundary with a message that names the bad field.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
`JSON.parse` result cast to a type without validation; parse it through a schema
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// BAD: the shape of raw is asserted, not checked
|
|
27
|
+
const config = JSON.parse(raw) as Config;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// BAD: annotation is a cast in disguise
|
|
32
|
+
const config: Config = JSON.parse(raw);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Good
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const config = configSchema.parse(JSON.parse(raw));
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const data: unknown = JSON.parse(raw);
|
|
43
|
+
const result = configSchema.safeParse(data);
|
|
44
|
+
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
throw new ConfigError('invalid config', { cause: result.error });
|
|
47
|
+
}
|
|
48
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
any:
|
|
7
|
+
- pattern: $P.then($$$ARGS)
|
|
8
|
+
- pattern: $P.catch($$$ARGS)
|
|
9
|
+
- pattern: $P.finally($$$ARGS)
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
`async`/`await` reads top to bottom and keeps error handling in an ordinary `try`. Promise chains split the same logic across callbacks, lose stack context, and make the return value of the surrounding function harder to see.
|
|
15
|
+
|
|
16
|
+
## Message
|
|
17
|
+
|
|
18
|
+
promise chain; use async/await
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
function loadUser(id: string) {
|
|
24
|
+
// BAD: callback chain where a straight line would do
|
|
25
|
+
return fetchUser(id)
|
|
26
|
+
.then(user => enrich(user))
|
|
27
|
+
.catch(error => report(error));
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
async function loadUser(id: string): Promise<User> {
|
|
35
|
+
try {
|
|
36
|
+
const user = await fetchUser(id);
|
|
37
|
+
|
|
38
|
+
return await enrich(user);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
report(error);
|
|
41
|
+
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- awaits where a later call uses the result of an earlier one, or where order matters for side effects such as writes
|
|
6
|
+
- operations against a resource that must not see concurrent calls, such as a single database transaction or a rate-limited API
|
|
7
|
+
- a sequence deliberately kept serial with a comment explaining why
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Awaiting independent calls one after another makes the total latency the sum of the parts when it could be the slowest one. `Promise.all` runs them together and still gives typed results in order, and `Promise.allSettled` does the same when each result should be handled on its own. Serial awaits are right when one call feeds the next or when the resource cannot take concurrent calls; otherwise they are a slow habit.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
independent awaits run one after another; run them together with `Promise.all`
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: three round trips in sequence that share no data
|
|
22
|
+
async function loadProfile(userId: string): Promise<Profile> {
|
|
23
|
+
const user = await getUser(userId);
|
|
24
|
+
const posts = await getPosts(userId);
|
|
25
|
+
const followers = await getFollowers(userId);
|
|
26
|
+
|
|
27
|
+
return { user, posts, followers };
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Good
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
async function loadProfile(userId: string): Promise<Profile> {
|
|
35
|
+
const [user, posts, followers] = await Promise.all([getUser(userId), getPosts(userId), getFollowers(userId)]);
|
|
36
|
+
|
|
37
|
+
return { user, posts, followers };
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
async function moveFunds(from: string, to: string, amount: number): Promise<void> {
|
|
43
|
+
await debit(from, amount);
|
|
44
|
+
await credit(to, amount);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: info
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a short trailing comment on a single value inside an object or array literal, such as a unit or a magic number's meaning
|
|
6
|
+
- 'directive comments such as `eslint-disable` or `@ts-expect-error`, which must sit where the tool expects them'
|
|
7
|
+
- a comment on a line of its own inside a function body that explains the following statement
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A comment that documents a declaration belongs on the line directly above it, as a `//` or `/** */` block, so editors show it on hover and a reader finds it before the signature rather than after. A trailing comment at the end of a declaration line is easy to miss, wraps badly, and is not picked up as documentation. Explanations inside the body of a function describe the body, not the contract.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
declaration documented in a trailing comment; put the comment on the line above
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: the doc is a trailing comment the tooling cannot see
|
|
22
|
+
export function parseDuration(input: string): number { // parses "5m", "2h" into milliseconds
|
|
23
|
+
return toMilliseconds(input);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Good
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// parses "5m" or "2h" into milliseconds
|
|
31
|
+
export function parseDuration(input: string): number {
|
|
32
|
+
return toMilliseconds(input);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
/**
|
|
38
|
+
* retries the operation up to three times with exponential backoff, then rethrows
|
|
39
|
+
*/
|
|
40
|
+
export function retry<T>(operation: () => Promise<T>): Promise<T> {
|
|
41
|
+
return withBackoff(operation, 3);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
pattern: const $NAME = $OBJ
|
|
7
|
+
inside:
|
|
8
|
+
any:
|
|
9
|
+
- kind: program
|
|
10
|
+
- kind: export_statement
|
|
11
|
+
constraints:
|
|
12
|
+
NAME:
|
|
13
|
+
regex: '^[A-Z]'
|
|
14
|
+
OBJ:
|
|
15
|
+
kind: object
|
|
16
|
+
has:
|
|
17
|
+
kind: pair
|
|
18
|
+
not:
|
|
19
|
+
has:
|
|
20
|
+
kind: pair
|
|
21
|
+
has:
|
|
22
|
+
field: value
|
|
23
|
+
not:
|
|
24
|
+
any:
|
|
25
|
+
- kind: string
|
|
26
|
+
- kind: number
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Why
|
|
30
|
+
|
|
31
|
+
A PascalCase const object of literal values is meant to act as an enum, but without `as const` every value widens to `string` or `number`, so `typeof Status[keyof typeof Status]` is just `string` and the compiler cannot check a switch over it. Adding `as const` freezes the values to their literals, makes the derived union real, and marks the object as readonly at the type level. It is the difference between a named constant set and a bag of strings.
|
|
32
|
+
|
|
33
|
+
## Message
|
|
34
|
+
|
|
35
|
+
literal const object without `as const`; its values widen to `string`
|
|
36
|
+
|
|
37
|
+
## Bad
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// BAD: Status.PENDING is typed as string, not 'pending'
|
|
41
|
+
const Status = {
|
|
42
|
+
PENDING: 'pending',
|
|
43
|
+
ACTIVE: 'active',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type Status = (typeof Status)[keyof typeof Status];
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Good
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const Status = {
|
|
53
|
+
PENDING: 'pending',
|
|
54
|
+
ACTIVE: 'active',
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
type Status = (typeof Status)[keyof typeof Status];
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const defaults = {
|
|
62
|
+
timeout: 5000,
|
|
63
|
+
retries: 3,
|
|
64
|
+
};
|
|
65
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: enum_declaration
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
TypeScript enums are a runtime construct with their own semantics: numeric enums accept any number, `const enum` behaves differently under isolated modules, and they do not compose with string literal unions or Zod. A const object with `as const` plus a derived union type gives the same ergonomics with plain objects and plain strings.
|
|
12
|
+
|
|
13
|
+
## Message
|
|
14
|
+
|
|
15
|
+
enum is a runtime construct; use a const object with `as const` and a derived union
|
|
16
|
+
|
|
17
|
+
## Bad
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// BAD: a runtime enum with numeric holes
|
|
21
|
+
enum Status {
|
|
22
|
+
Active,
|
|
23
|
+
Inactive,
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Good
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// GOOD: a const object and its derived union
|
|
31
|
+
export const Status = {
|
|
32
|
+
ACTIVE: 'active',
|
|
33
|
+
INACTIVE: 'inactive',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type Status = (typeof Status)[keyof typeof Status];
|
|
37
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
any:
|
|
7
|
+
- kind: if_statement
|
|
8
|
+
not:
|
|
9
|
+
has:
|
|
10
|
+
field: consequence
|
|
11
|
+
kind: statement_block
|
|
12
|
+
- kind: else_clause
|
|
13
|
+
not:
|
|
14
|
+
has:
|
|
15
|
+
any:
|
|
16
|
+
- kind: statement_block
|
|
17
|
+
- kind: if_statement
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why
|
|
21
|
+
|
|
22
|
+
A braceless `if (x) return;` works until someone adds a second statement under it and only the first one stays conditional. Braces make the body a visible block, keep the diff small when a line is added, and remove a whole class of indentation bugs. The two extra characters cost nothing to read.
|
|
23
|
+
|
|
24
|
+
## Message
|
|
25
|
+
|
|
26
|
+
braceless `if` body; wrap it in a block
|
|
27
|
+
|
|
28
|
+
## Bad
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// BAD: a second statement added here would run unconditionally
|
|
32
|
+
if (!user) return undefined;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
if (user.isActive) {
|
|
37
|
+
activate(user);
|
|
38
|
+
// BAD: braceless else body
|
|
39
|
+
} else deactivate(user);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Good
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
if (!user) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (user.isActive) {
|
|
50
|
+
activate(user);
|
|
51
|
+
} else if (user.isPending) {
|
|
52
|
+
remind(user);
|
|
53
|
+
} else {
|
|
54
|
+
deactivate(user);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a single `if` with an `else` where both branches are one or two lines and neither is an error path
|
|
6
|
+
- branches that must run cleanup or logging before returning, where flattening would duplicate that code
|
|
7
|
+
- a `switch` or a chain of `if` returning a value per case, which is already flat
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Nesting the happy path inside a pyramid of `if` blocks pushes the code the function exists for to the deepest indentation and makes every reader hold the whole condition stack in their head. Checking preconditions first and returning or throwing early keeps the main logic at the top level, reads in the order the cases are decided, and makes each failure case a self-contained line. The result is shorter, flatter, and easier to change.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
nested conditionals wrap the happy path; check preconditions first and return early
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: the update is buried two levels deep under checks that could exit early
|
|
22
|
+
async function updateUser(id: string, input: UpdateUserInput): Promise<User> {
|
|
23
|
+
const user = await repository.find(id);
|
|
24
|
+
if (user) {
|
|
25
|
+
if (user.isActive) {
|
|
26
|
+
return repository.update(id, input);
|
|
27
|
+
} else {
|
|
28
|
+
throw new ForbiddenError(`user ${id} is deactivated`);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
throw new NotFoundError(`user ${id}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Good
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
async function updateUser(id: string, input: UpdateUserInput): Promise<User> {
|
|
40
|
+
const user = await repository.find(id);
|
|
41
|
+
|
|
42
|
+
if (!user) {
|
|
43
|
+
throw new NotFoundError(`user ${id}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!user.isActive) {
|
|
47
|
+
throw new ForbiddenError(`user ${id} is deactivated`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return repository.update(id, input);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: ternary_expression
|
|
7
|
+
inside:
|
|
8
|
+
kind: ternary_expression
|
|
9
|
+
stopBy:
|
|
10
|
+
not:
|
|
11
|
+
kind: parenthesized_expression
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
A ternary is readable when it has one condition and two outcomes. Nesting another inside turns it into a puzzle the reader has to unfold, and the precedence rules do not help. Use an if chain, a switch, or a lookup object.
|
|
17
|
+
|
|
18
|
+
## Message
|
|
19
|
+
|
|
20
|
+
nested ternary; use an if chain, a switch, or a lookup
|
|
21
|
+
|
|
22
|
+
## Bad
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// BAD: three outcomes folded into one expression
|
|
26
|
+
const label = count === 0 ? 'none' : count === 1 ? 'one' : 'many';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Good
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// GOOD: a ternary inside a callback is its own expression, not a nested branch
|
|
33
|
+
const handler = isEnabled ? () => (isDark ? darkTheme : lightTheme) : undefined;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// GOOD: one condition, two outcomes
|
|
38
|
+
const label = isEmpty ? 'none' : 'some';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// GOOD: a function names the branching
|
|
43
|
+
function describeCount(count: number): string {
|
|
44
|
+
if (count === 0) {
|
|
45
|
+
return 'none';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (count === 1) {
|
|
49
|
+
return 'one';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return 'many';
|
|
53
|
+
}
|
|
54
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
any:
|
|
7
|
+
- pattern: new $CLS($MSG)
|
|
8
|
+
- pattern: new $CLS($MSG, $$$REST)
|
|
9
|
+
constraints:
|
|
10
|
+
CLS:
|
|
11
|
+
regex: 'Error$'
|
|
12
|
+
MSG:
|
|
13
|
+
regex: '^.[A-Z][a-z]'
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
Error messages get wrapped and joined with other messages, as in `loading config: reading file: permission denied`, and a capitalized fragment in the middle of that chain reads like a new sentence. Messages are lowercase fragments without trailing punctuation so they compose cleanly and match the log lines around them. Acronyms and proper nouns at the start are fine.
|
|
19
|
+
|
|
20
|
+
## Message
|
|
21
|
+
|
|
22
|
+
error message starts with a capital letter; write it as a lowercase fragment
|
|
23
|
+
|
|
24
|
+
## Bad
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
if (!config) {
|
|
28
|
+
// BAD: capitalized message reads wrong once it is wrapped
|
|
29
|
+
throw new Error('Config file is missing');
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
if (!user) {
|
|
35
|
+
// BAD: capitalized message in a custom error
|
|
36
|
+
throw new NotFoundError(`User ${id} does not exist`, 404);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Good
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
if (!config) {
|
|
44
|
+
throw new ConfigError('config file is missing');
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
if (!user) {
|
|
50
|
+
throw new NotFoundError(`user ${id} does not exist`);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
throw new UpstreamError('HTTP 502 from payments service');
|
|
56
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- a catch that logs and then handles the error without rethrowing, such as falling back to a default or skipping one item in a batch
|
|
6
|
+
- the outermost handler, controller, job runner, or CLI entrypoint, which is the boundary and should log
|
|
7
|
+
- a catch that adds context by wrapping the error in a typed error before rethrowing, without logging
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
When every layer catches an error, logs it, and rethrows, one failure produces a stack of near-identical log lines and the boundary that finally handles it logs it again. Errors should propagate untouched, or wrapped with context, until they reach the boundary, such as the request handler or the job runner, where they are logged once with full context and turned into a response. Inner layers that only log and rethrow are noise generators.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
catch logs and rethrows in an inner layer; let it propagate and log once at the boundary
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
class UserService {
|
|
22
|
+
async getUser(id: string): Promise<User> {
|
|
23
|
+
try {
|
|
24
|
+
return await this.repository.find(id);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// BAD: the handler above will log this same error again
|
|
27
|
+
this.logger.error('get user failed', { error });
|
|
28
|
+
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Good
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
class UserService {
|
|
39
|
+
async getUser(id: string): Promise<User> {
|
|
40
|
+
const user = await this.repository.find(id);
|
|
41
|
+
|
|
42
|
+
if (!user) {
|
|
43
|
+
throw new NotFoundError(`user ${id}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return user;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
async function handleGetUser(req: Request, res: Response): Promise<void> {
|
|
53
|
+
try {
|
|
54
|
+
res.json(await userService.getUser(req.params.id));
|
|
55
|
+
} catch (error: unknown) {
|
|
56
|
+
if (error instanceof NotFoundError) {
|
|
57
|
+
res.status(404).json({ error: error.message });
|
|
58
|
+
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logger.error('get user failed', { error, userId: req.params.id });
|
|
63
|
+
res.status(500).json({ error: 'internal error' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: export_statement
|
|
7
|
+
has:
|
|
8
|
+
kind: export_clause
|
|
9
|
+
not:
|
|
10
|
+
has:
|
|
11
|
+
field: source
|
|
12
|
+
kind: string
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
An export list at the bottom of a file separates the decision to export from the thing being exported, so a reader looking at a declaration cannot tell whether it is public without scrolling. Exporting at the declaration site keeps that intent next to the code and means a rename or removal touches one place instead of two. Re-exports from another module are a different construct and are fine.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
export list detached from its declarations; export at the declaration site
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const MAX_RETRIES = 3;
|
|
27
|
+
|
|
28
|
+
function getUser(id: string): Promise<User> {
|
|
29
|
+
return repository.find(id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// BAD: the export decision lives far from the declarations
|
|
33
|
+
export { MAX_RETRIES, getUser };
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Good
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
export const MAX_RETRIES = 3;
|
|
40
|
+
|
|
41
|
+
export function getUser(id: string): Promise<User> {
|
|
42
|
+
return repository.find(id);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
export { createUser } from './user';
|
|
48
|
+
export type { User } from './user';
|
|
49
|
+
```
|