@noctcore/eslint-plugin-async-safety 0.3.0 → 0.3.2
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/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/docs/rules/forward-abort-signal.md +5 -3
- package/docs/rules/no-concurrent-shared-mutation.md +5 -3
- package/docs/rules/no-shared-mutable-module-state.md +5 -5
- package/docs/rules/prefer-parallel-awaits.md +17 -6
- package/docs/rules/require-client-timeout.md +5 -4
- package/docs/rules/require-fetch-timeout.md +5 -3
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -20,13 +20,15 @@ A function (declaration, expression, or arrow) that:
|
|
|
20
20
|
"Forwarding" is deliberately generous: passing the signal as an argument, into an options object, assigning it,
|
|
21
21
|
or returning it all count — so the rule errs toward silence rather than false positives.
|
|
22
22
|
|
|
23
|
-
```ts
|
|
24
|
-
//
|
|
23
|
+
```ts bad
|
|
24
|
+
// signal accepted, fetch left uncancellable
|
|
25
25
|
async function load(url: string, signal: AbortSignal) {
|
|
26
26
|
return await fetch(url);
|
|
27
27
|
}
|
|
28
|
+
```
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
```ts good
|
|
31
|
+
// signal forwarded
|
|
30
32
|
async function load(url: string, signal: AbortSignal) {
|
|
31
33
|
return await fetch(url, { signal });
|
|
32
34
|
}
|
|
@@ -18,14 +18,16 @@ Inside an `async` callback passed to `.map`/`.flatMap`/`.forEach` whose result i
|
|
|
18
18
|
- a self-referential assignment (`x = x + …`);
|
|
19
19
|
- an update expression on an outer binding (`x++`, `--x`).
|
|
20
20
|
|
|
21
|
-
```ts
|
|
22
|
-
//
|
|
21
|
+
```ts bad
|
|
22
|
+
// lost updates
|
|
23
23
|
let total = 0;
|
|
24
24
|
await Promise.all(items.map(async (item) => {
|
|
25
25
|
total += await priceOf(item);
|
|
26
26
|
}));
|
|
27
|
+
```
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
```ts good
|
|
30
|
+
// collect, then reduce
|
|
29
31
|
const prices = await Promise.all(items.map((item) => priceOf(item)));
|
|
30
32
|
const total = prices.reduce((a, b) => a + b, 0);
|
|
31
33
|
```
|
|
@@ -21,16 +21,16 @@ module-scoped mutable binding performed inside an **exported async function** or
|
|
|
21
21
|
Exported functions qualify when they are `async` or named like a handler (`GET`/`POST`/…, `loader`, `action`,
|
|
22
22
|
`handler`, `*Handler`, `middleware`).
|
|
23
23
|
|
|
24
|
-
```ts
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
// ✗ shared across requests
|
|
24
|
+
```ts bad filename=src/server/metrics.ts options={"include":["**/server/**"]}
|
|
25
|
+
// shared across requests
|
|
28
26
|
let requestCount = 0;
|
|
29
27
|
export async function GET() {
|
|
30
28
|
requestCount += 1;
|
|
31
29
|
}
|
|
30
|
+
```
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
```ts good filename=src/server/metrics.ts options={"include":["**/server/**"]}
|
|
33
|
+
// per-request
|
|
34
34
|
export async function GET() {
|
|
35
35
|
const requestCount = 1;
|
|
36
36
|
}
|
|
@@ -18,15 +18,26 @@ A run of two or more **consecutive** statements of the exact shape `const <id> =
|
|
|
18
18
|
writes is usually intentional;
|
|
19
19
|
- no later statement references an earlier statement's binding (no data dependency).
|
|
20
20
|
|
|
21
|
-
```ts
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const
|
|
21
|
+
```ts bad
|
|
22
|
+
// sequential, but independent
|
|
23
|
+
async function loadHome() {
|
|
24
|
+
const user = await getUser();
|
|
25
|
+
const feed = await getFeed();
|
|
26
|
+
return { user, feed };
|
|
27
|
+
}
|
|
28
|
+
```
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
```ts good
|
|
31
|
+
// concurrent
|
|
32
|
+
async function loadHome() {
|
|
33
|
+
const [user, feed] = await Promise.all([getUser(), getFeed()]);
|
|
34
|
+
return { user, feed };
|
|
35
|
+
}
|
|
28
36
|
```
|
|
29
37
|
|
|
38
|
+
Only runs inside a block (a function body, or any `{ ... }`) are checked. Top-level `await`s at
|
|
39
|
+
module scope are not.
|
|
40
|
+
|
|
30
41
|
The rule is intentionally conservative — a data dependency, a mutation-looking callee, a `let`, a nested call
|
|
31
42
|
argument, or any non-await statement between them all suppress it.
|
|
32
43
|
|
|
@@ -60,14 +60,15 @@ A NestJS API that talks to S3 and sends mail over SMTP:
|
|
|
60
60
|
],
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
```ts
|
|
64
|
-
//
|
|
63
|
+
```ts bad reports=2 options={"clients":[{"callee":"S3Client","construct":true,"requireAnyOf":["requestHandler"]},{"callee":"nodemailer.createTransport","requireAnyOf":["connectionTimeout","socketTimeout"]}]}
|
|
64
|
+
// no request handler, so no connection or request timeout
|
|
65
65
|
this.s3 = new S3Client({ region, credentials });
|
|
66
66
|
|
|
67
|
-
//
|
|
67
|
+
// SMTP transport with default (unbounded in practice) timeouts
|
|
68
68
|
this.transporter = nodemailer.createTransport({ host, port, secure: true });
|
|
69
|
+
```
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
```ts good options={"clients":[{"callee":"S3Client","construct":true,"requireAnyOf":["requestHandler"]},{"callee":"nodemailer.createTransport","requireAnyOf":["connectionTimeout","socketTimeout"]}]}
|
|
71
72
|
this.s3 = new S3Client({
|
|
72
73
|
region,
|
|
73
74
|
credentials,
|
|
@@ -23,12 +23,14 @@ The rule is purely syntactic and stays silent whenever it cannot see the argumen
|
|
|
23
23
|
It reports only when the arguments are plainly signal-free: a bare string/template URL, or a visible options
|
|
24
24
|
object literal with neither key.
|
|
25
25
|
|
|
26
|
-
```ts
|
|
27
|
-
//
|
|
26
|
+
```ts bad reports=2
|
|
27
|
+
// no timeout: can hang forever
|
|
28
28
|
await fetch('https://api.example.com/data');
|
|
29
29
|
await fetch(url, { method: 'POST' });
|
|
30
|
+
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
```ts good
|
|
33
|
+
// bounded
|
|
32
34
|
await fetch('https://api.example.com/data', { signal: AbortSignal.timeout(10000) });
|
|
33
35
|
await fetch(url, { method: 'POST', signal: controller.signal });
|
|
34
36
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noctcore/eslint-plugin-async-safety",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "ESLint rules for async correctness: fetch timeouts, AbortSignal forwarding, and shared-state / concurrency races.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"test": "vitest run"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@noctcore/eslint-utils": "^0.1.
|
|
51
|
+
"@noctcore/eslint-utils": "^0.1.1",
|
|
52
52
|
"@typescript-eslint/utils": "^8.61.1"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|