@rdjperron/lint 0.1.0 → 0.2.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/README.md +146 -57
- package/dist/config.d.mts +113 -0
- package/dist/config.mjs +218 -0
- package/dist/config.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/{anti-slop.mjs → index.mjs} +253 -94
- package/dist/index.mjs.map +1 -0
- package/dist/verify.d.mts +92 -0
- package/dist/verify.mjs +400 -0
- package/dist/verify.mjs.map +1 -0
- package/docs/rules/README.md +43 -0
- package/docs/rules/no-array-filter-map.md +60 -0
- package/docs/rules/no-chained-type-assertions.md +52 -0
- package/docs/rules/no-conditional-empty-object-spread.md +50 -0
- package/docs/rules/no-hardcoded-jsx-text.md +52 -0
- package/docs/rules/no-known-value-widening.md +62 -0
- package/docs/rules/no-module-mocking.md +53 -0
- package/docs/rules/no-object-parameters.md +55 -0
- package/docs/rules/no-reduce-accumulator-copy.md +57 -0
- package/docs/rules/no-reflect-apply.md +56 -0
- package/docs/rules/no-reflect-get.md +51 -0
- package/docs/rules/no-runtime-typeof.md +61 -0
- package/docs/rules/no-shape-in-symbol-names.md +53 -0
- package/docs/rules/no-unknown-parameters.md +55 -0
- package/docs/rules/no-unknown-returns.md +61 -0
- package/docs/rules/no-unknown-type-aliases.md +52 -0
- package/docs/rules/no-unsafe-dictionary-type.md +56 -0
- package/docs/rules/no-use-effect.md +55 -0
- package/docs/rules/no-widen-then-assert.md +53 -0
- package/docs/rules/require-safety-comment-for-type-assertion.md +59 -0
- package/docs/rules/static-public-env.md +55 -0
- package/fixtures/README.md +22 -23
- package/fixtures/{anti-slop/no-array-filter-map.fixture.ts → no-array-filter-map.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-chained-type-assertions.fixture.ts → no-chained-type-assertions.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-conditional-empty-object-spread.fixture.ts → no-conditional-empty-object-spread.fixture.ts} +1 -1
- package/fixtures/{i18n/no-hardcoded-jsx-text.fixture.tsx → no-hardcoded-jsx-text.fixture.tsx} +1 -1
- package/fixtures/{anti-slop/no-known-value-widening.fixture.ts → no-known-value-widening.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-module-mocking.fixture.ts → no-module-mocking.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-object-parameters.fixture.ts → no-object-parameters.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-reduce-accumulator-copy.fixture.ts → no-reduce-accumulator-copy.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-reflect-apply.fixture.ts → no-reflect-apply.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-reflect-get.fixture.ts → no-reflect-get.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-runtime-typeof.fixture.ts → no-runtime-typeof.fixture.ts} +1 -1
- package/fixtures/no-shape-in-symbol-names.fixture.ts +2 -0
- package/fixtures/{anti-slop/no-unknown-parameters.fixture.ts → no-unknown-parameters.fixture.ts} +1 -1
- package/fixtures/{anti-slop/no-unknown-returns.fixture.ts → no-unknown-returns.fixture.ts} +1 -1
- package/fixtures/no-unknown-type-aliases.fixture.ts +2 -0
- package/fixtures/no-unsafe-dictionary-type.fixture.ts +2 -0
- package/fixtures/{react/no-use-effect.fixture.tsx → no-use-effect.fixture.tsx} +1 -1
- package/fixtures/{anti-slop/no-widen-then-assert.fixture.ts → no-widen-then-assert.fixture.ts} +1 -1
- package/fixtures/{anti-slop/require-safety-comment-for-type-assertion.fixture.ts → require-safety-comment-for-type-assertion.fixture.ts} +1 -1
- package/fixtures/{expo/static-public-env.fixture.ts → static-public-env.fixture.ts} +1 -1
- package/package.json +27 -26
- package/dist/anti-slop.d.mts +0 -6
- package/dist/anti-slop.mjs.map +0 -1
- package/dist/expo.d.mts +0 -5
- package/dist/expo.mjs +0 -51
- package/dist/expo.mjs.map +0 -1
- package/dist/i18n.d.mts +0 -5
- package/dist/i18n.mjs +0 -50
- package/dist/i18n.mjs.map +0 -1
- package/dist/react.d.mts +0 -5
- package/dist/react.mjs +0 -73
- package/dist/react.mjs.map +0 -1
- package/dist/rule-options-DrAXz8BD.mjs +0 -18
- package/dist/rule-options-DrAXz8BD.mjs.map +0 -1
- package/fixtures/anti-slop/no-shape-in-symbol-names.fixture.ts +0 -2
- package/fixtures/anti-slop/no-unknown-type-aliases.fixture.ts +0 -2
- package/fixtures/anti-slop/no-unsafe-dictionary-type.fixture.ts +0 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# rdj/no-conditional-empty-object-spread
|
|
2
|
+
|
|
3
|
+
Rejects a spread inside an object literal whose argument is a conditional with `{}` on either
|
|
4
|
+
branch.
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
`...(value !== undefined ? { value } : {})` is how a field becomes optional without anyone
|
|
9
|
+
saying so. The object's keys can only be read by evaluating the conditional, so a reviewer
|
|
10
|
+
counting the keys of the literal gets the wrong answer. The rule ships no autofix on purpose,
|
|
11
|
+
and a test asserts that: any rewrite changes which keys the object ends up with.
|
|
12
|
+
|
|
13
|
+
## Rejected
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const result = { ...(value !== undefined ? { value } : {}) };
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> This conditional spread hides property omission behind an empty object. Build the object in
|
|
20
|
+
> separate statements and add the property only when present.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const result = { ...(condition ? {} : { value }) };
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> This conditional spread hides property omission behind an empty object. Build the object in
|
|
27
|
+
> separate statements and add the property only when present.
|
|
28
|
+
|
|
29
|
+
## Accepted
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const result = { value };
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const result = condition ? { value } : {};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
None.
|
|
42
|
+
|
|
43
|
+
## When it is right to turn it off
|
|
44
|
+
|
|
45
|
+
Nothing needs it off. Building the object in statements and adding the property inside an `if`
|
|
46
|
+
produces the same object and states the omission where a reader looks for it.
|
|
47
|
+
|
|
48
|
+
## Origin
|
|
49
|
+
|
|
50
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# rdj/no-hardcoded-jsx-text
|
|
2
|
+
|
|
3
|
+
Rejects a word written straight into JSX children, whether as text or as a string literal child.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
A string typed into a component is a string no translator will ever see, and it is found only
|
|
8
|
+
when someone reads that file again. Sending every visible word through the dictionary makes the
|
|
9
|
+
set of strings on screen countable. The rule looks at JSX text and at a string literal placed
|
|
10
|
+
among an element's children. It never looks at attribute values, so `alt="Company logo"` is out
|
|
11
|
+
of scope and needs a different check.
|
|
12
|
+
|
|
13
|
+
## Rejected
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
const a = <button>Save changes</button>;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> Do not write words straight into JSX. Every string on screen comes from the translation
|
|
20
|
+
> dictionary.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
const a = <p>{"Hello world"}</p>;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> Do not write words straight into JSX. Every string on screen comes from the translation
|
|
27
|
+
> dictionary.
|
|
28
|
+
|
|
29
|
+
## Accepted
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
const a = <p>{t("save")}</p>;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
const a = <p>2026</p>;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
`pattern`, a regex source compiled with the `u` flag, default `\p{L}{3,}`. Three letters in a
|
|
42
|
+
row is a word, so digits, separators and bullets are not. A project that only ships Latin
|
|
43
|
+
script can narrow it, for example `[A-Za-zÀ-ÿ]{3,}`.
|
|
44
|
+
|
|
45
|
+
## When it is right to turn it off
|
|
46
|
+
|
|
47
|
+
A surface no user reads in their own language: a debug screen, a developer tools panel, a
|
|
48
|
+
component gallery. Scope it to those files and keep it on everywhere a user can arrive.
|
|
49
|
+
|
|
50
|
+
## Origin
|
|
51
|
+
|
|
52
|
+
Written in this package.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# rdj/no-known-value-widening
|
|
2
|
+
|
|
3
|
+
Rejects a value the file already describes flowing into an explicitly broad target type.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
When a literal whose keys are right there is annotated `Record<string, Command>`, the compiler
|
|
8
|
+
forgets which keys exist and `commands.strat` type checks. The annotation looks like extra
|
|
9
|
+
rigour and is the opposite. The rule names what the target discarded: `unknown`, `object`,
|
|
10
|
+
`open dictionary` (a broad-keyed `Record`, an index signature, a mapped type), `anonymous
|
|
11
|
+
object` and `generic container`. It also fires on a call that hands a known value to a type
|
|
12
|
+
guard whose parameter is `unknown`, because the guard then has nothing left to decide.
|
|
13
|
+
|
|
14
|
+
## Rejected
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
type Command = () => void;
|
|
18
|
+
const startCommand = () => {};
|
|
19
|
+
const commands: Record<string, Command> = { start: startCommand };
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> The explicit open dictionary type on binding `commands` discards known type evidence. Keep
|
|
23
|
+
> inference, validate with `satisfies`, or use a named owner contract.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
function isString(value: unknown): value is string {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
isString("known");
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> The explicit unknown type on argument for parameter `value` of `isString` discards known type
|
|
33
|
+
> evidence. Keep inference, validate with `satisfies`, or use a named owner contract.
|
|
34
|
+
|
|
35
|
+
## Accepted
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const commands = { start: startCommand } satisfies Record<string, Command>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
interface Commands {
|
|
43
|
+
readonly start: Command;
|
|
44
|
+
}
|
|
45
|
+
const commands: Commands = { start: startCommand };
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
An empty literal keeps its broad annotation: `const commands: Record<string, Command> = {}` is
|
|
49
|
+
accepted, because an accumulator that starts empty has no evidence to discard.
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
None.
|
|
54
|
+
|
|
55
|
+
## When it is right to turn it off
|
|
56
|
+
|
|
57
|
+
Nothing measured needed it off. `satisfies` and a named interface cover the annotate-for-safety
|
|
58
|
+
habit, and the empty accumulator is already exempt.
|
|
59
|
+
|
|
60
|
+
## Origin
|
|
61
|
+
|
|
62
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# rdj/no-module-mocking
|
|
2
|
+
|
|
3
|
+
Rejects `vi.mock`, `vi.doMock`, `jest.mock` and `jest.unstable_mockModule`.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
A module mock replaces a dependency by file path, so the test exercises the mock and proves
|
|
8
|
+
nothing about the seam the code actually uses. When the real module's API changes the test
|
|
9
|
+
keeps passing. It also makes a test unreadable in isolation, since the behaviour under test is
|
|
10
|
+
defined in a factory somewhere above the assertions. The rule resolves the object rather than
|
|
11
|
+
trusting the name: a global `vi` or `jest` counts, so does one imported from `vitest` or
|
|
12
|
+
`@jest/globals` under any alias, and a local object of your own does not.
|
|
13
|
+
|
|
14
|
+
## Rejected
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { vi } from "vitest";
|
|
18
|
+
vi.mock("./user-store");
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> Replace module mocking with dependency injection through a real interface, service layer, or
|
|
22
|
+
> faithful test implementation.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
jest.unstable_mockModule("./user-store");
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> Replace module mocking with dependency injection through a real interface, service layer, or
|
|
29
|
+
> faithful test implementation.
|
|
30
|
+
|
|
31
|
+
## Accepted
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const store = new InMemoryUserStore();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
vi.spyOn(store, "save");
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Options
|
|
42
|
+
|
|
43
|
+
None.
|
|
44
|
+
|
|
45
|
+
## When it is right to turn it off
|
|
46
|
+
|
|
47
|
+
A third-party module with no injectable seam, where the only way to reach a branch is to
|
|
48
|
+
replace the module itself. Scope the override to that test file and say which dependency has
|
|
49
|
+
no seam, so the exception disappears when the seam is added.
|
|
50
|
+
|
|
51
|
+
## Origin
|
|
52
|
+
|
|
53
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# rdj/no-object-parameters
|
|
2
|
+
|
|
3
|
+
Rejects a function parameter annotated `object`, including through a local alias that resolves
|
|
4
|
+
to `object`.
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
`object` tells a caller only that the value is not a primitive. Every field read inside the
|
|
9
|
+
body then needs a cast, an index signature or `Reflect`, and the call site gets no help at all.
|
|
10
|
+
The rule resolves aliases, so `type Bag = object` does not hide it, and it follows generic
|
|
11
|
+
aliases such as `Identity<object>`. A type parameter constrained by `object` is a different
|
|
12
|
+
claim and stays legal.
|
|
13
|
+
|
|
14
|
+
## Rejected
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
type Bag = object;
|
|
18
|
+
function consume(value: Bag): void {}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> Parameter `value` uses the broad `object` type. Accept a named owner type; parse external
|
|
22
|
+
> input at its boundary before calling this function.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
function consume({ value }: object = {}): void {}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> Parameter `{ value }` uses the broad `object` type. Accept a named owner type; parse external
|
|
29
|
+
> input at its boundary before calling this function.
|
|
30
|
+
|
|
31
|
+
## Accepted
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
interface Owner {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
}
|
|
37
|
+
function consume(value: Owner): void {}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
function consume<Value extends object>(value: Value): void {}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
None.
|
|
47
|
+
|
|
48
|
+
## When it is right to turn it off
|
|
49
|
+
|
|
50
|
+
A declaration file that mirrors a third-party signature you do not own, where narrowing the
|
|
51
|
+
parameter would misdescribe what the library accepts. Scope it to the declaration file.
|
|
52
|
+
|
|
53
|
+
## Origin
|
|
54
|
+
|
|
55
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# rdj/no-reduce-accumulator-copy
|
|
2
|
+
|
|
3
|
+
Rejects a reducer callback that copies its own accumulator on every step.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`Object.assign({}, acc, item)` inside a `reduce` allocates a new object per element and copies
|
|
8
|
+
everything collected so far, so the work grows with the square of the input. The pattern reads
|
|
9
|
+
as careful immutability and is a performance bug at any real size. The rule covers
|
|
10
|
+
`Object.assign` into a fresh literal, `Array.from(acc)`, and `concat`, `slice`, `toSpliced`,
|
|
11
|
+
`toSorted`, `toReversed` and `with` on an accumulator whose initial value is a known array. It
|
|
12
|
+
follows a `const` alias of the accumulator. The spread form, `[...acc, item]`, belongs to
|
|
13
|
+
`oxc/no-accumulating-spread` and is deliberately left alone here.
|
|
14
|
+
|
|
15
|
+
## Rejected
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
items.reduce((acc, item) => Object.assign({}, acc, { [item.id]: item }), {});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic
|
|
22
|
+
> work. Mutate a fresh, locally owned accumulator and return it, or use an iterator
|
|
23
|
+
> pipeline/flatMap.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
items.reduce((acc, item) => acc.concat([item]), []);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic
|
|
30
|
+
> work. Mutate a fresh, locally owned accumulator and return it, or use an iterator
|
|
31
|
+
> pipeline/flatMap.
|
|
32
|
+
|
|
33
|
+
## Accepted
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
items.reduce((acc, item) => {
|
|
37
|
+
acc.push(item);
|
|
38
|
+
return acc;
|
|
39
|
+
}, []);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
items.reduce((acc, item) => Object.assign(acc, item), {});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
None.
|
|
49
|
+
|
|
50
|
+
## When it is right to turn it off
|
|
51
|
+
|
|
52
|
+
Nothing needs it off. The accumulator passed to `reduce` is owned by the reduction, so mutating
|
|
53
|
+
it and returning it is safe and is what the message asks for.
|
|
54
|
+
|
|
55
|
+
## Origin
|
|
56
|
+
|
|
57
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# rdj/no-reflect-apply
|
|
2
|
+
|
|
3
|
+
Rejects `Reflect.apply` on the global `Reflect`.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`Reflect.apply(operation, owner, args)` takes the arguments as an array, so the checker sees
|
|
8
|
+
nothing about arity or argument types and every mistake surfaces at runtime. The call also
|
|
9
|
+
hides who is being called, which is the one thing a reader wants from a dispatch site. The rule
|
|
10
|
+
resolves the receiver, so a local object or parameter named `Reflect` is not reported. The
|
|
11
|
+
computed spelling `Reflect["apply"]` is.
|
|
12
|
+
|
|
13
|
+
## Rejected
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const value = Reflect.apply(operation, owner, args);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named
|
|
20
|
+
> interface.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const value = Reflect["apply"](operation, owner, args);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named
|
|
27
|
+
> interface.
|
|
28
|
+
|
|
29
|
+
## Accepted
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const value = operation.apply(owner, args);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const Reflect = {
|
|
37
|
+
apply() {
|
|
38
|
+
return 1;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
Reflect.apply();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
None.
|
|
47
|
+
|
|
48
|
+
## When it is right to turn it off
|
|
49
|
+
|
|
50
|
+
A `Proxy` `apply` trap, where forwarding with `Reflect.apply(target, thisArg, args)` is the
|
|
51
|
+
documented way to keep the original behaviour. Scope the override to the file that defines the
|
|
52
|
+
proxy.
|
|
53
|
+
|
|
54
|
+
## Origin
|
|
55
|
+
|
|
56
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# rdj/no-reflect-get
|
|
2
|
+
|
|
3
|
+
Rejects `Reflect.get` on the global `Reflect`.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`Reflect.get(owner, key)` returns `any` and asks the checker no questions, so a misspelled key
|
|
8
|
+
and a renamed field both survive to runtime. It is usually reached for when the value was never
|
|
9
|
+
parsed into a type that has the field. Plain property access says the same thing and gets
|
|
10
|
+
checked. The rule resolves the receiver, so a local object or parameter named `Reflect` is not
|
|
11
|
+
reported, and it catches the computed spelling `Reflect["get"]`.
|
|
12
|
+
|
|
13
|
+
## Rejected
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const value = Reflect.get(owner, key);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type
|
|
20
|
+
> before reading it.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const value = Reflect["get"](owner, key);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type
|
|
27
|
+
> before reading it.
|
|
28
|
+
|
|
29
|
+
## Accepted
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const value = owner.property;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const value = owner[key];
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
None.
|
|
42
|
+
|
|
43
|
+
## When it is right to turn it off
|
|
44
|
+
|
|
45
|
+
A `Proxy` `get` trap, where `Reflect.get(target, key, receiver)` forwards with the right
|
|
46
|
+
receiver and writing `target[key]` would break getters on the prototype chain. Scope the
|
|
47
|
+
override to the file that defines the proxy.
|
|
48
|
+
|
|
49
|
+
## Origin
|
|
50
|
+
|
|
51
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# rdj/no-runtime-typeof
|
|
2
|
+
|
|
3
|
+
Rejects every `typeof` operator except one compared against the string `"undefined"`.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`typeof value === "string"` tells you how the value is represented, not what it means or where
|
|
8
|
+
it came from. Narrowing this way scatters half a parser across the branches that consume the
|
|
9
|
+
value, and each branch has to guess again. The fix is to decode the value once, at the boundary
|
|
10
|
+
where it arrives, and branch on the result. Probing whether a binding exists at all,
|
|
11
|
+
`typeof document === "undefined"`, is a different question and stays allowed.
|
|
12
|
+
|
|
13
|
+
## Rejected
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
if (typeof input === "string") use(input);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> A `typeof` check narrows a representation without establishing its contract. Parse input at
|
|
20
|
+
> its I/O boundary, then branch on the domain value.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
function parse(value: unknown): string {
|
|
24
|
+
if (typeof value !== "string") throw new Error();
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> A `typeof` check narrows a representation without establishing its contract. Parse input at
|
|
30
|
+
> its I/O boundary, then branch on the domain value.
|
|
31
|
+
|
|
32
|
+
## Accepted
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const isServer = typeof document === "undefined";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// with { allowInTypeGuards: true }
|
|
40
|
+
function isString(value: unknown): value is string {
|
|
41
|
+
return typeof value === "string";
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Options
|
|
46
|
+
|
|
47
|
+
`allowInTypeGuards`, boolean, default `false`. When true, a `typeof` is allowed if the nearest
|
|
48
|
+
enclosing function returns a type predicate, `value is T` or `asserts value is T`. Nearest is
|
|
49
|
+
literal: a callback declared inside the guard is still reported, since its own return type is
|
|
50
|
+
not a predicate.
|
|
51
|
+
|
|
52
|
+
## When it is right to turn it off
|
|
53
|
+
|
|
54
|
+
Decoder modules, the one place per kind of outside value where a route param, a stored blob or
|
|
55
|
+
a token payload becomes a named type. There `typeof` is the parse the rule asks for, not a
|
|
56
|
+
substitute for one. Tests are the other case: `expect(typeof api.run).toBe("function")` asserts
|
|
57
|
+
the shape of your own public API, which the rule reads as unparsed input.
|
|
58
|
+
|
|
59
|
+
## Origin
|
|
60
|
+
|
|
61
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# rdj/no-shape-in-symbol-names
|
|
2
|
+
|
|
3
|
+
Rejects the case-insensitive substring `shape` in any name you declare or reference, except a
|
|
4
|
+
static member name owned by another value.
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
`PayloadShape` and `userShape` name the structure of a value instead of its role, which is the
|
|
9
|
+
tell of a type that was written to satisfy the compiler rather than to describe the domain. A
|
|
10
|
+
reader learns nothing from the suffix, and the name survives every refactor that changes what
|
|
11
|
+
the value is for. The match is a plain substring on the lowered name, so `reshape` and
|
|
12
|
+
`shapeOf` are reported too. Reading someone else's member, `schema.shape.id`, is not your name
|
|
13
|
+
to choose and is allowed.
|
|
14
|
+
|
|
15
|
+
## Rejected
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
type PayloadShape = { id: string };
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> Rename symbol "PayloadShape" for its domain role; "shape" describes structure rather than
|
|
22
|
+
> ownership.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
type Payload = { shape: string };
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> Rename symbol "shape" for its domain role; "shape" describes structure rather than ownership.
|
|
29
|
+
|
|
30
|
+
## Accepted
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
declare const schema: ExternalSchema;
|
|
34
|
+
const field = schema.shape.id;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
type Payload = { id: string };
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Options
|
|
42
|
+
|
|
43
|
+
None. The term is fixed in the rule source.
|
|
44
|
+
|
|
45
|
+
## When it is right to turn it off
|
|
46
|
+
|
|
47
|
+
A domain where a shape is the thing itself, drawing, geometry, a layout engine. There `shape`
|
|
48
|
+
is the domain role, which is exactly what the rule asks names to state. Scope the override to
|
|
49
|
+
that module.
|
|
50
|
+
|
|
51
|
+
## Origin
|
|
52
|
+
|
|
53
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# rdj/no-unknown-parameters
|
|
2
|
+
|
|
3
|
+
Rejects a parameter whose annotation is or contains `unknown`, except a parameter named `cause`
|
|
4
|
+
and the subject of a type predicate.
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
An `unknown` parameter moves the parsing work from the caller, who knows what the value is, to
|
|
9
|
+
the body, which does not. Every call site then passes whatever it has and the function decides
|
|
10
|
+
at runtime. The two escapes are narrow: `cause` is the error-enrichment convention, and a type
|
|
11
|
+
guard's subject is `unknown` by construction. Note that the first escape is matched by the
|
|
12
|
+
parameter's name alone, not by anything about its type.
|
|
13
|
+
|
|
14
|
+
## Rejected
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
function parse(value: unknown): void {}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> Parameter `value` leaves input unparsed. Accept a named domain type; run the expected schema
|
|
21
|
+
> or parser at the I/O boundary before calling this function.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
export function parse({ value }: unknown = {}): void {}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> Parameter `{ value }` leaves input unparsed. Accept a named domain type; run the expected
|
|
28
|
+
> schema or parser at the I/O boundary before calling this function.
|
|
29
|
+
|
|
30
|
+
## Accepted
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
function enrich(cause: unknown): void {}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
function isString(value: unknown): value is string {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
None.
|
|
45
|
+
|
|
46
|
+
## When it is right to turn it off
|
|
47
|
+
|
|
48
|
+
The error decoder that turns a rejection into a domain failure. It is handed whatever was
|
|
49
|
+
thrown, and TypeScript itself types a `catch` binding as `unknown`, so the contract is honest
|
|
50
|
+
and the parameter is rarely called `cause`. Scope it to that one module and let every caller
|
|
51
|
+
above it take the named failure type.
|
|
52
|
+
|
|
53
|
+
## Origin
|
|
54
|
+
|
|
55
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# rdj/no-unknown-returns
|
|
2
|
+
|
|
3
|
+
Rejects an explicit return annotation that resolves to `unknown`, including `Promise<unknown>`
|
|
4
|
+
and a union containing `unknown`.
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
A function that returns `unknown` hands its caller a value and no way to use it, so the caller
|
|
9
|
+
either asserts or writes the parse the function should have done. Multiply that by the call
|
|
10
|
+
sites and one missing decode becomes several. The rule resolves local aliases and generic
|
|
11
|
+
instantiations, so `type UnknownValue = unknown` and `Identity<unknown>` are caught as well. An
|
|
12
|
+
alias that is not visible in the file cannot be resolved and passes.
|
|
13
|
+
|
|
14
|
+
## Rejected
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
function load(): unknown {
|
|
18
|
+
return input;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> This function exposes `unknown` to its caller. Parse the value at its boundary and return a
|
|
23
|
+
> named domain type.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
type Loader = () => Promise<unknown>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> This function exposes `unknown` to its caller. Parse the value at its boundary and return a
|
|
30
|
+
> named domain type.
|
|
31
|
+
|
|
32
|
+
## Accepted
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
function load(): Promise<User> {
|
|
36
|
+
return promise;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
function cause(): { cause: unknown } {
|
|
42
|
+
return { cause: input };
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
An unannotated function is not a contract and is never reported: `function infer() { return
|
|
47
|
+
input; }` passes.
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
None.
|
|
52
|
+
|
|
53
|
+
## When it is right to turn it off
|
|
54
|
+
|
|
55
|
+
A declaration file that mirrors an external API whose real return type is `unknown`, `JSON.parse`
|
|
56
|
+
being the obvious one. In your own code the boundary is the place to parse, which is what the
|
|
57
|
+
message asks for.
|
|
58
|
+
|
|
59
|
+
## Origin
|
|
60
|
+
|
|
61
|
+
Vendored from dmmulroy/anti-slop, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`.
|