@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,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ignore:
|
|
5
|
+
- '**/*.config.ts'
|
|
6
|
+
- '**/app/**'
|
|
7
|
+
- '**/pages/**'
|
|
8
|
+
ast:
|
|
9
|
+
rule:
|
|
10
|
+
pattern: export default $X
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
A default export has no name of its own, so every importer invents one and the same function ends up with three names across the codebase. Named exports keep one name, refactor safely, and autocomplete. Framework files that require a default export, such as config files and route modules, are excluded.
|
|
16
|
+
|
|
17
|
+
## Message
|
|
18
|
+
|
|
19
|
+
default export has no name of its own; use a named export
|
|
20
|
+
|
|
21
|
+
## Bad
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// BAD: importers will call this anything they like
|
|
25
|
+
export default function createUser(input: CreateUserInput): Promise<User> {
|
|
26
|
+
return repository.insert(input);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Good
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// GOOD: one name everywhere
|
|
34
|
+
export function createUser(input: CreateUserInput): Promise<User> {
|
|
35
|
+
return repository.insert(input);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: info
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: object
|
|
7
|
+
has:
|
|
8
|
+
nthChild: 2
|
|
9
|
+
regex: '^[^\n]*$'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
An object literal with two or more properties squeezed onto one line hides the key/value pairs in a wall of punctuation and turns every added property into a diff on the same line. One property per line lets the reader scan keys down the left edge and makes each change a one-line diff. Single-property objects such as `{ id }` are fine inline.
|
|
15
|
+
|
|
16
|
+
## Message
|
|
17
|
+
|
|
18
|
+
object with several properties on one line; put one property per line
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// BAD: two properties squeezed onto one line
|
|
24
|
+
const options = { timeout: 5000, retries: 3 };
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// BAD: inline argument object with several properties
|
|
29
|
+
await db.user.update({ where: { id }, data: input });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Good
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const options = {
|
|
36
|
+
timeout: 5000,
|
|
37
|
+
retries: 3,
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
await db.user.update({
|
|
43
|
+
where: { id },
|
|
44
|
+
data: input,
|
|
45
|
+
});
|
|
46
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: function_expression
|
|
7
|
+
inside:
|
|
8
|
+
kind: arguments
|
|
9
|
+
not:
|
|
10
|
+
has:
|
|
11
|
+
kind: this
|
|
12
|
+
stopBy: end
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
A `function` expression passed as a callback carries its own `this`, which is almost never what the surrounding code wants, and the keyword adds noise to what is usually a one-line transform. An arrow keeps the outer `this`, reads as an expression, and is the form every reader expects inside `map`, `filter`, event handlers, and route definitions. Callbacks that deliberately use their own `this`, such as some test framework hooks, are left alone.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
`function` expression as a callback; use an arrow function
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// BAD: function expression where an arrow is expected
|
|
27
|
+
const activeUsers = users.filter(function (user) {
|
|
28
|
+
return user.isActive;
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Good
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const activeUsers = users.filter(user => user.isActive);
|
|
36
|
+
|
|
37
|
+
app.get('/health', (req, res) => res.json({ status: 'ok' }));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
describe('parser', function () {
|
|
42
|
+
this.timeout(10_000);
|
|
43
|
+
});
|
|
44
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: lexical_declaration
|
|
7
|
+
has:
|
|
8
|
+
kind: variable_declarator
|
|
9
|
+
has:
|
|
10
|
+
kind: arrow_function
|
|
11
|
+
field: value
|
|
12
|
+
inside:
|
|
13
|
+
any:
|
|
14
|
+
- kind: program
|
|
15
|
+
- kind: export_statement
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
A top-level function written as `const f = () => {}` is not hoisted, so callers above it in the file break, and it shows up in stack traces and debuggers as an anonymous arrow bound to a variable. A `function` declaration is hoisted, carries its name, and stands out visually as a unit of the module. Arrows are for callbacks and inline expressions, where lexical `this` and brevity actually help.
|
|
21
|
+
|
|
22
|
+
## Message
|
|
23
|
+
|
|
24
|
+
top-level arrow function; use a `function` declaration
|
|
25
|
+
|
|
26
|
+
## Bad
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// BAD: exported arrow is not hoisted and is anonymous in stack traces
|
|
30
|
+
export const createUser = async (input: CreateUserInput): Promise<User> => {
|
|
31
|
+
return repository.insert(input);
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// BAD: module-level helper written as an arrow
|
|
37
|
+
const toSlug = (title: string) => title.toLowerCase().replaceAll(' ', '-');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Good
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
export async function createUser(input: CreateUserInput): Promise<User> {
|
|
44
|
+
return repository.insert(input);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
function toSlug(title: string): string {
|
|
50
|
+
return title.toLowerCase().replaceAll(' ', '-');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function slugs(titles: readonly string[]): string[] {
|
|
54
|
+
return titles.map(title => toSlug(title));
|
|
55
|
+
}
|
|
56
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- re-exports of values such as functions, classes, const objects, or schemas
|
|
6
|
+
- a `type` or `interface` declared inline with `export type` or `export interface`
|
|
7
|
+
- re-exports that mix types and values and already mark the types with an inline `type` modifier
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Re-exporting a type through a plain `export { X } from` keeps a runtime edge to the module even though nothing from it survives compilation. `export type { X } from` is erased, keeps `isolatedModules` builds happy, and tells the reader that the module contributes only types. It mirrors `import type` so the two read the same way.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
re-export is a type; use `export type`
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: User is a type, but the re-export keeps a runtime edge to ./user
|
|
22
|
+
export { User } from './user';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Good
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export type { User } from './user';
|
|
29
|
+
export { createUser } from './user';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export { type User, createUser } from './user';
|
|
34
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: import_statement
|
|
7
|
+
any:
|
|
8
|
+
- has:
|
|
9
|
+
field: source
|
|
10
|
+
regex: '^.\.'
|
|
11
|
+
precedes:
|
|
12
|
+
kind: import_statement
|
|
13
|
+
stopBy: end
|
|
14
|
+
has:
|
|
15
|
+
field: source
|
|
16
|
+
regex: '^.[^.]'
|
|
17
|
+
- has:
|
|
18
|
+
field: source
|
|
19
|
+
regex: '^.@/'
|
|
20
|
+
precedes:
|
|
21
|
+
kind: import_statement
|
|
22
|
+
stopBy: end
|
|
23
|
+
has:
|
|
24
|
+
field: source
|
|
25
|
+
regex: '^.[^.]'
|
|
26
|
+
not:
|
|
27
|
+
has:
|
|
28
|
+
field: source
|
|
29
|
+
regex: '^.@/'
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why
|
|
33
|
+
|
|
34
|
+
Imports come in three groups, external packages, then internal `@/` modules, then relative paths, with a blank line between groups. A reader scans the top of a file to learn what it depends on, and a fixed order makes external dependencies and local coupling visible at a glance. Mixed groups force a line-by-line read and produce noisy diffs when imports are added.
|
|
35
|
+
|
|
36
|
+
## Message
|
|
37
|
+
|
|
38
|
+
imports out of order; external packages, then `@/` modules, then relative paths
|
|
39
|
+
|
|
40
|
+
## Bad
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// BAD: relative import placed before an external package
|
|
44
|
+
import { formatDate } from './format';
|
|
45
|
+
import { z } from 'zod';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { z } from 'zod';
|
|
50
|
+
// BAD: internal module placed before an external package
|
|
51
|
+
import { config } from '@/config';
|
|
52
|
+
import { PrismaClient } from '@prisma/client';
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Good
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { PrismaClient } from '@prisma/client';
|
|
59
|
+
import { z } from 'zod';
|
|
60
|
+
|
|
61
|
+
import { config } from '@/config';
|
|
62
|
+
import { logger } from '@/lib/logger';
|
|
63
|
+
|
|
64
|
+
import { formatDate } from './format';
|
|
65
|
+
import type { User } from './user';
|
|
66
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- imports whose binding is used as a runtime value anywhere in the file, including in `instanceof`, `typeof`, decorators, or `satisfies`
|
|
6
|
+
- imports of a const object that doubles as a type through `typeof`
|
|
7
|
+
- files where every import in the statement is already marked with an inline `type` modifier
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A binding that is only ever used in type positions should be imported with `import type`, which is erased at compile time. A plain import of a type keeps a runtime dependency on the module, can drag a circular import into existence, and hides from the reader that nothing from that module runs. The distinction is cheap to write and tells the next person exactly what the file needs at runtime.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
import is only used as a type; use `import type`
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: User is only used in a type annotation
|
|
22
|
+
import { User } from './user';
|
|
23
|
+
|
|
24
|
+
export function displayName(user: User): string {
|
|
25
|
+
return user.name;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Good
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import type { User } from './user';
|
|
33
|
+
|
|
34
|
+
export function displayName(user: User): string {
|
|
35
|
+
return user.name;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { UserService } from './user.service';
|
|
41
|
+
import type { User } from './user';
|
|
42
|
+
|
|
43
|
+
export function build(): UserService {
|
|
44
|
+
return new UserService();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function displayName(user: User): string {
|
|
48
|
+
return user.name;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
kind: import_statement
|
|
7
|
+
not:
|
|
8
|
+
has:
|
|
9
|
+
kind: import_clause
|
|
10
|
+
precedes:
|
|
11
|
+
kind: import_statement
|
|
12
|
+
stopBy: end
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
A side-effect import such as `import './polyfills'` runs code for its effect alone, so it stands apart from the imports that bind names. Placing it last, after a blank line, makes the effect visible instead of burying it among ordinary imports where a reader assumes nothing happens. A comment saying what the effect is helps the next person decide whether it can be removed.
|
|
18
|
+
|
|
19
|
+
## Message
|
|
20
|
+
|
|
21
|
+
side-effect import belongs after all named imports
|
|
22
|
+
|
|
23
|
+
## Bad
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// BAD: side-effect import hidden at the top of the list
|
|
27
|
+
import './polyfills';
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
|
|
30
|
+
import { config } from '@/config';
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Good
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
|
|
38
|
+
import { config } from '@/config';
|
|
39
|
+
|
|
40
|
+
// registers the fetch polyfill for node 18
|
|
41
|
+
import './polyfills';
|
|
42
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
any:
|
|
7
|
+
- pattern: $LOGGER.$METHOD($MSG)
|
|
8
|
+
- pattern: $LOGGER.$METHOD($MSG, $$$REST)
|
|
9
|
+
constraints:
|
|
10
|
+
METHOD:
|
|
11
|
+
regex: '^(log|info|warn|error|debug|trace|fatal)$'
|
|
12
|
+
MSG:
|
|
13
|
+
regex: '^.[A-Z][a-z]'
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
Log lines are grepped and read in bulk, and a mix of `Starting server` and `connected to database` makes the stream look like it came from two systems. Messages are lowercase fragments, with proper nouns and acronyms kept as they are, and variable data goes in the structured fields rather than the sentence. A consistent shape makes the output scannable and the fields queryable.
|
|
19
|
+
|
|
20
|
+
## Message
|
|
21
|
+
|
|
22
|
+
log message starts with a capital letter; write it as a lowercase fragment
|
|
23
|
+
|
|
24
|
+
## Bad
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// BAD: capitalized sentence in a log line
|
|
28
|
+
logger.info('Starting server on port 3000');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// BAD: console output follows the same convention
|
|
33
|
+
console.error('Failed to connect', error);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Good
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
logger.info('starting server', { port: 3000 });
|
|
40
|
+
logger.error('failed to connect', { error });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
logger.info('connecting to PostgreSQL');
|
|
45
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- names that already read as a yes/no question with a prefix like `is`, `has`, `should`, `can`, `was`, `needs`, or `allows`
|
|
6
|
+
- boolean properties whose name is dictated by an external schema, framework prop, or API response
|
|
7
|
+
- loop or callback parameters of one or two letters in a very short scope
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A boolean whose name is a bare noun or verb, such as `valid`, `children`, or `retry`, forces the reader to look up its type before an `if` on it makes sense. Names that read as a yes/no question, such as `isValid`, `hasChildren`, or `shouldRetry`, carry the type in the name and make conditions read as prose. This applies to variables, properties, parameters, and functions that return a boolean.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
boolean name does not read as a question; prefix with is, has, should, or can
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: valid could be a noun, a verb, or an adjective
|
|
22
|
+
const valid = schema.safeParse(data).success;
|
|
23
|
+
|
|
24
|
+
// BAD: children sounds like a collection, not a flag
|
|
25
|
+
const children = node.children.length > 0;
|
|
26
|
+
|
|
27
|
+
// BAD: retry reads as an action, but it returns a yes/no answer
|
|
28
|
+
function retry(attempt: number, error: Error): boolean {
|
|
29
|
+
return attempt < 3 && error instanceof TimeoutError;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Good
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const isValid = schema.safeParse(data).success;
|
|
37
|
+
const hasChildren = node.children.length > 0;
|
|
38
|
+
|
|
39
|
+
function shouldRetry(attempt: number, error: Error): boolean {
|
|
40
|
+
return attempt < 3 && error instanceof TimeoutError;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- conventional short names in tiny scopes, such as `i` in a counting loop, `e` in a catch, `x` in a one-line arrow, or `T` as a type parameter
|
|
6
|
+
- domain abbreviations that are standard in the codebase, such as `db`, `req`, `res`, `ctx`, `id`, or `url`
|
|
7
|
+
- names that match an external API or schema field the code has to mirror
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
An identifier is read far more often than it is written, and a name like `data`, `tmp`, `res2`, or `handleStuff` tells the reader nothing they did not already know from the type. Good names say what the value is or what the function does, so the surrounding code needs fewer comments and a wrong assumption is caught on sight. Length is not the goal; precision is, and a short name in a short scope is fine.
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
identifier does not describe what it holds or does; use a descriptive name
|
|
17
|
+
|
|
18
|
+
## Bad
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// BAD: data, res, and tmp say nothing about what they hold
|
|
22
|
+
async function process(data: string): Promise<string[]> {
|
|
23
|
+
const res = await fetchRows(data);
|
|
24
|
+
const tmp = res.filter(r => r.active);
|
|
25
|
+
|
|
26
|
+
return tmp.map(t => t.id);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// BAD: a numbered copy of a name is a sign the first name was wrong
|
|
32
|
+
const user2 = await repository.find(managerId);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Good
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
async function activeRowIds(tableName: string): Promise<string[]> {
|
|
39
|
+
const rows = await fetchRows(tableName);
|
|
40
|
+
const activeRows = rows.filter(row => row.active);
|
|
41
|
+
|
|
42
|
+
return activeRows.map(row => row.id);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const manager = await repository.find(managerId);
|
|
48
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: minor
|
|
3
|
+
detect: ast
|
|
4
|
+
ast:
|
|
5
|
+
rule:
|
|
6
|
+
pattern: const $NAME = $VALUE
|
|
7
|
+
inside:
|
|
8
|
+
any:
|
|
9
|
+
- kind: program
|
|
10
|
+
- kind: export_statement
|
|
11
|
+
constraints:
|
|
12
|
+
NAME:
|
|
13
|
+
regex: '^[a-z][a-zA-Z0-9]*$'
|
|
14
|
+
VALUE:
|
|
15
|
+
any:
|
|
16
|
+
- kind: number
|
|
17
|
+
- kind: string
|
|
18
|
+
- kind: 'true'
|
|
19
|
+
- kind: 'false'
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Why
|
|
23
|
+
|
|
24
|
+
A module-level constant holding a literal value is a configuration knob, and SCREAMING_SNAKE_CASE marks it as one at every use site. A camelCase name such as `maxRetries` looks like a local variable, so a reader inside a function cannot tell whether it is a fixed limit or something computed nearby. Local constants stay camelCase; only the module-level literals get the loud name.
|
|
25
|
+
|
|
26
|
+
## Message
|
|
27
|
+
|
|
28
|
+
module-level literal constant is camelCase; use SCREAMING_SNAKE_CASE
|
|
29
|
+
|
|
30
|
+
## Bad
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// BAD: a fixed module-level limit dressed as a local variable
|
|
34
|
+
const maxRetries = 3;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// BAD: exported literal config in camelCase
|
|
39
|
+
export const defaultTimeout = 10_000;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Good
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const MAX_RETRIES = 3;
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_TIMEOUT = 10_000;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
export const userSchema = z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function retry(): void {
|
|
56
|
+
const attempts = 3;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
kind: string
|
|
13
|
+
not:
|
|
14
|
+
inside:
|
|
15
|
+
kind: augmented_assignment_expression
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
Building a string with `+` scatters the literal parts and the values across quotes and operators, so the reader has to reassemble the final shape in their head and a missing space or quote is easy to miss. A template literal shows the string as it will appear with the values slotted in place. Incremental building in a loop with `+=` is a different pattern and is left alone.
|
|
21
|
+
|
|
22
|
+
## Message
|
|
23
|
+
|
|
24
|
+
string built with `+`; use a template literal
|
|
25
|
+
|
|
26
|
+
## Bad
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// BAD: the final shape is spread across three fragments and two operators
|
|
30
|
+
const greeting = 'hello, ' + name + '!';
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// BAD: path assembled by concatenation
|
|
35
|
+
const url = baseUrl + '/users/' + userId + '/posts';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Good
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const greeting = `hello, ${name}!`;
|
|
42
|
+
const url = `${baseUrl}/users/${userId}/posts`;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
let csv = '';
|
|
47
|
+
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
csv += row.join(',') + '\n';
|
|
50
|
+
}
|
|
51
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
severity: info
|
|
3
|
+
detect: judge
|
|
4
|
+
falsePositives:
|
|
5
|
+
- intersections in generic constraints such as `T extends Identifiable & Timestamped`
|
|
6
|
+
- 'branded types of the form `T & { readonly __brand: B }`'
|
|
7
|
+
- intersections with a union or a mapped type, which `extends` cannot express
|
|
8
|
+
- an intersection of two object literals that is used once and never extended again
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Extending a named object shape with `type Admin = User & { role: 'admin' }` is checked lazily on every use, and when two members conflict the intersection silently becomes `never` for that property instead of an error at the declaration. `interface Admin extends User` is checked once, rejects conflicting members where they are declared, and gives shorter error messages that name the interface rather than expanding the whole intersection. Intersections are for unions, brands, and generic constraints.
|
|
14
|
+
|
|
15
|
+
## Message
|
|
16
|
+
|
|
17
|
+
object shape extended with an intersection; use `interface extends`
|
|
18
|
+
|
|
19
|
+
## Bad
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// BAD: extending a named object shape through an intersection
|
|
23
|
+
type Admin = User & {
|
|
24
|
+
role: 'admin';
|
|
25
|
+
permissions: readonly string[];
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Good
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface Admin extends User {
|
|
33
|
+
role: 'admin';
|
|
34
|
+
permissions: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
type UserId = string & { readonly __brand: 'UserId' };
|
|
40
|
+
|
|
41
|
+
function sortByCreation<T extends Identifiable & Timestamped>(items: readonly T[]): T[] {
|
|
42
|
+
return [...items].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
43
|
+
}
|
|
44
|
+
```
|