@sarj/eslint-plugin 4.0.0 → 4.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 +69 -6
- package/dist/index.cjs +849 -251
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +854 -253
- package/dist/index.js.map +1 -1
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -12,10 +12,47 @@ import sarj from "@sarj/eslint-plugin";
|
|
|
12
12
|
export default [...sarj.configs.recommended];
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
51 rules. Each rule's source under `src/rules/` carries its own `@fileoverview` rationale plus `meta.docs.description` + `meta.messages` — read the file for the full reasoning, including the false positives it deliberately does not fire on.
|
|
16
16
|
|
|
17
17
|
Presets: `recommended` (warn-first), `strict` (every rule at error), `style-guide` (formatting/naming subset).
|
|
18
18
|
|
|
19
|
+
## New in 4.1.0 — `no-hand-rolled-sleep`
|
|
20
|
+
|
|
21
|
+
`new Promise((resolve) => setTimeout(resolve, ms))` is `node:timers/promises`'s
|
|
22
|
+
`setTimeout` rewritten by hand, minus the `AbortSignal` — so it is a capability
|
|
23
|
+
loss, not verbosity. The hand-rolled sleep holds a live timer that nothing can
|
|
24
|
+
clear, and its mirror image, the `Promise.race([work, rejectAfter(ms)])` timeout
|
|
25
|
+
arm, leaks the timer the other way: when `work` wins, nothing clears it and it
|
|
26
|
+
keeps the event loop alive until it fires.
|
|
27
|
+
|
|
28
|
+
Shipped only after confirming nothing already enabled reports this position. The
|
|
29
|
+
enabled set was resolved with `ESLint#calculateConfigForFile` against the shipped
|
|
30
|
+
`eslint.strict.mjs` (204 rules before this one) and a file containing every
|
|
31
|
+
shape was linted through it: no report. `eslint-plugin-unicorn` 72 has no
|
|
32
|
+
promisified-timer rule among its 341; `unicorn/prefer-abort-signal-timeout` covers the
|
|
33
|
+
`AbortController` + `setTimeout` idiom and not the race arm; core
|
|
34
|
+
`no-promise-executor-return` fires on the concise-arrow spelling only and its
|
|
35
|
+
remedy ("add braces") entrenches the hand-rolled sleep. The polling-loop variant
|
|
36
|
+
(`while (!done) await sleep(ms)`) is deliberately absent — core `no-await-in-loop`
|
|
37
|
+
is already enabled and reports that exact position.
|
|
38
|
+
|
|
39
|
+
Measured over 1,471 files containing `setTimeout` across 15 OSS repos (hono,
|
|
40
|
+
tRPC, drizzle-orm, undici, vitest, got, cal.com, documenso, dub, formbricks,
|
|
41
|
+
midday, openstatus, papermark, unkey, zod) and seven internal ones: 85 + 11
|
|
42
|
+
sleeps and 2 + 1 leaky race arms at the default settings, **0 false positives**.
|
|
43
|
+
|
|
44
|
+
`checkClientModules` is the option that matters. A browser or React Native
|
|
45
|
+
bundle cannot import `node:timers/promises` and the web platform ships no
|
|
46
|
+
equivalent, so client modules are skipped by default — 79% of the internal
|
|
47
|
+
corpus's occurrences live in `.tsx` components where the fix cannot be applied.
|
|
48
|
+
Turn it on only in a tree where every file resolves `node:` builtins. The race
|
|
49
|
+
message is reported everywhere regardless: `AbortSignal.timeout` is on the web
|
|
50
|
+
platform too.
|
|
51
|
+
|
|
52
|
+
| Rule | What it catches | Preset |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `no-hand-rolled-sleep` | `new Promise((r) => setTimeout(r, ms))` in any spelling, and an uncleared `Promise.race`/`Promise.any` timeout arm. Options: `checkClientModules` (default `false`), `allowIn`. | warn / error |
|
|
55
|
+
|
|
19
56
|
## New in 2.14.0 — `no-tautological-expect`
|
|
20
57
|
|
|
21
58
|
The TS half of SARJ057. An `expect(...)` whose operands are all literals has
|
|
@@ -68,6 +105,7 @@ Both distilled from two years of PR-review comments across ~1,065 PRs.
|
|
|
68
105
|
|---|---|---|
|
|
69
106
|
| `no-zod-native-enum` | `z.nativeEnum(...)` and `z.enum(SomeTsEnum)` — the schema-layer back door around `no-enum`. Autofixes an inline string-literal object to `z.enum([...])`. | warn / error |
|
|
70
107
|
| `prefer-zod-enum` | `z.union([z.literal("a"), z.literal("b")])` — autofixes closed string choices to the shorter, equivalent `z.enum(["a", "b"])`. | warn / error |
|
|
108
|
+
| `prefer-zod-infer` | An `interface`/`type` that restates a Zod schema declared in the same module instead of deriving it with `z.infer`. Options: `ignoreTypeNames`, `requireIdenticalShape` (default `true`). | warn / error |
|
|
71
109
|
| `prefer-module-level-constant` | A literal-only `const` collection (array, object, `Set`, `Map`, `Object.freeze`) or non-global regex declared inside a function body, never mutated and never escaping — hoist it to module scope. Options: `minElements` (default 3), `checkRegex`, `ignoreTestFiles`. | warn / error |
|
|
72
110
|
| `prefer-non-nullable-collection` | An array type explicitly combined with `null`/`undefined`, creating two equivalent empty states. | warn / error |
|
|
73
111
|
|
|
@@ -109,6 +147,26 @@ already recognises. Set `"prefix"` or `"suffix"` to pin one:
|
|
|
109
147
|
"@sarj/zod-naming-convention": ["error", { convention: "suffix" }]
|
|
110
148
|
```
|
|
111
149
|
|
|
150
|
+
### `prefer-zod-infer`: `requireIdenticalShape` / `ignoreTypeNames`
|
|
151
|
+
|
|
152
|
+
By default the rule only reports a type whose members match its schema's keys
|
|
153
|
+
one for one — same names, same optionality, same nullability, no `.transform()`
|
|
154
|
+
anywhere in the pair. On a 30,759-file, 17-repo sweep that is 5 reports and 5
|
|
155
|
+
true positives. Drop the shape comparison to report on name correlation alone
|
|
156
|
+
(8 reports on the same corpus, 1 of them noise, and it catches twins that have
|
|
157
|
+
already drifted):
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
"@sarj/prefer-zod-infer": ["error", { requireIdenticalShape: false }]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`ignoreTypeNames` takes regex sources matched against the declared type name,
|
|
164
|
+
for the pair that is genuinely meant to be maintained by hand:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
"@sarj/prefer-zod-infer": ["error", { ignoreTypeNames: ["^LegacyUser$"] }]
|
|
168
|
+
```
|
|
169
|
+
|
|
112
170
|
### `prefer-string-literal-union`: `ignoreFields`
|
|
113
171
|
|
|
114
172
|
Field names whose value set is owned by a vendor and genuinely open (a Slack
|
|
@@ -125,7 +183,7 @@ Glob patterns whose files opt out (generated code already opts out by default).
|
|
|
125
183
|
|
|
126
184
|
## Configurable rules
|
|
127
185
|
|
|
128
|
-
Most rules take no options. These
|
|
186
|
+
Most rules take no options. These do, because they encode a codebase's
|
|
129
187
|
architecture rather than a language fact — the defaults describe one convention
|
|
130
188
|
and every repo gets to name its own.
|
|
131
189
|
|
|
@@ -135,10 +193,15 @@ and every repo gets to name its own.
|
|
|
135
193
|
| `no-dynamic-sql` | `methods` | `["prepare", "exec", "query"]` | Statement-taking methods to inspect |
|
|
136
194
|
| `no-storage-in-stateless-modules` | `modules` | `[]` (rule off) | Directories declared stateless |
|
|
137
195
|
| `no-storage-in-stateless-modules` | `methods` | `["prepare", "put", "getWithMetadata"]` | Storage methods to flag |
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
196
|
+
| `no-hand-rolled-sleep` | `checkClientModules` | `false` | Also report the sleep form in browser/React Native modules |
|
|
197
|
+
| `no-hand-rolled-sleep` | `allowIn` | `[]` | Glob patterns for a sanctioned sleep wrapper module |
|
|
198
|
+
|
|
199
|
+
The path options on the first three rules are **regular-expression sources
|
|
200
|
+
matched against the absolute filename**, not globs — so they can express both
|
|
201
|
+
path separators. `allowIn` is the exception, on `no-hand-rolled-sleep` as on
|
|
202
|
+
`require-fetch-timeout`: it takes minimatch-ish **globs**, also matched against
|
|
203
|
+
the absolute path, so anchor them with a `**/` prefix. Supplying an option
|
|
204
|
+
**replaces** the default rather than extending it.
|
|
142
205
|
|
|
143
206
|
`no-storage-in-stateless-modules` is a **no-op until `modules` is set**. The
|
|
144
207
|
method names alone (`put`, `prepare`) carry no type information, so the rule is
|