eslint-plugin-reliability 3.1.7 → 3.1.8
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/package.json +5 -17
- package/src/index.d.ts +15 -196
- package/src/index.js +1 -9
- package/src/oxlint.d.ts +17 -20
- package/src/oxlint.js +0 -18
- package/src/rules/error-handling/error-message.js +0 -17
- package/src/rules/error-handling/no-missing-error-context.js +1 -34
- package/src/rules/error-handling/no-silent-errors.js +1 -21
- package/src/rules/error-handling/no-unhandled-promise.js +3 -147
- package/src/rules/reliability/no-await-in-loop.js +0 -18
- package/src/rules/reliability/no-jsdoc-terminator-in-example.js +2 -30
- package/src/rules/reliability/no-missing-null-checks.js +4 -137
- package/src/rules/reliability/no-unsafe-type-narrowing.js +3 -25
- package/src/rules/reliability/require-network-timeout.js +0 -14
- package/CHANGELOG.md +0 -164
- package/src/lib/eslint-plugin-reliability.d.ts +0 -1
- package/src/rules/error-handling/error-message.d.ts +0 -20
- package/src/rules/error-handling/no-missing-error-context.d.ts +0 -26
- package/src/rules/error-handling/no-silent-errors.d.ts +0 -24
- package/src/rules/error-handling/no-unhandled-promise.d.ts +0 -49
- package/src/rules/reliability/no-await-in-loop.d.ts +0 -24
- package/src/rules/reliability/no-jsdoc-terminator-in-example.d.ts +0 -35
- package/src/rules/reliability/no-missing-null-checks.d.ts +0 -35
- package/src/rules/reliability/no-unsafe-type-narrowing.d.ts +0 -24
- package/src/rules/reliability/require-network-timeout.d.ts +0 -12
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
-
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
-
* MIT license that can be found in the LICENSE file.
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.noUnhandledPromise = void 0;
|
|
9
4
|
exports.isLikelyPromiseExpression = isLikelyPromiseExpression;
|
|
10
5
|
exports.isPromiseHandled = isPromiseHandled;
|
|
11
6
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
12
7
|
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
13
|
-
/**
|
|
14
|
-
* Built-in / library calls that are KNOWN to NOT return a promise. Firing
|
|
15
|
-
* on these produces FPs (e.g., `setTimeout(...)`, `console.log(...)`,
|
|
16
|
-
* `Math.floor(...)` are not unhandled promises).
|
|
17
|
-
*/
|
|
18
8
|
const NEVER_RETURNS_PROMISE_FUNCTIONS = new Set([
|
|
19
9
|
'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
|
|
20
10
|
'setImmediate', 'clearImmediate',
|
|
@@ -25,79 +15,45 @@ const NEVER_RETURNS_PROMISE_FUNCTIONS = new Set([
|
|
|
25
15
|
'Array', 'Object',
|
|
26
16
|
]);
|
|
27
17
|
const NEVER_RETURNS_PROMISE_METHODS = new Set([
|
|
28
|
-
// console / logger
|
|
29
18
|
'log', 'error', 'warn', 'info', 'debug', 'trace', 'group', 'groupEnd',
|
|
30
19
|
'time', 'timeEnd', 'assert',
|
|
31
|
-
// Math
|
|
32
20
|
'floor', 'ceil', 'round', 'abs', 'min', 'max', 'pow', 'sqrt', 'random',
|
|
33
21
|
'sin', 'cos', 'tan', 'log2', 'log10',
|
|
34
|
-
// String / Array helpers
|
|
35
22
|
'slice', 'split', 'join', 'concat', 'includes', 'indexOf', 'lastIndexOf',
|
|
36
23
|
'startsWith', 'endsWith', 'replace', 'replaceAll', 'trim', 'toLowerCase', 'toUpperCase',
|
|
37
24
|
'repeat', 'padStart', 'padEnd', 'charAt', 'charCodeAt', 'codePointAt',
|
|
38
25
|
'push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort',
|
|
39
26
|
'map', 'filter', 'reduce', 'reduceRight', 'forEach', 'every', 'some', 'find', 'findIndex',
|
|
40
27
|
'flat', 'flatMap', 'fill', 'copyWithin', 'entries', 'keys', 'values',
|
|
41
|
-
// JSON
|
|
42
28
|
'parse', 'stringify',
|
|
43
|
-
// AbortController/AbortSignal
|
|
44
29
|
'abort', 'addEventListener', 'removeEventListener', 'dispatchEvent',
|
|
45
|
-
// Date / Buffer / Number / Array static helpers (sync)
|
|
46
30
|
'now', 'parse', 'UTC', 'from', 'of', 'isArray', 'isBuffer',
|
|
47
31
|
'isInteger', 'isFinite', 'isNaN', 'isSafeInteger',
|
|
48
32
|
'fromCharCode', 'fromCodePoint', 'raw',
|
|
49
|
-
// Object helpers (sync)
|
|
50
33
|
'assign', 'freeze', 'isFrozen', 'create', 'defineProperty', 'defineProperties',
|
|
51
34
|
'getOwnPropertyDescriptor', 'getOwnPropertyNames', 'getPrototypeOf', 'setPrototypeOf',
|
|
52
35
|
'preventExtensions', 'isExtensible', 'seal', 'isSealed', 'fromEntries',
|
|
53
|
-
// Promise constructors that are themselves a promise but the callee is OK
|
|
54
36
|
]);
|
|
55
|
-
/**
|
|
56
|
-
* Globals whose methods are conventionally synchronous (no method on these
|
|
57
|
-
* namespaces returns a Promise in the standard library). Used in addition
|
|
58
|
-
* to `NEVER_RETURNS_PROMISE_METHODS` because matching by method name alone
|
|
59
|
-
* is too coarse: `from` is sync on `Array`/`Buffer`/`Date` but could be
|
|
60
|
-
* async on a user-defined object.
|
|
61
|
-
*/
|
|
62
37
|
const SYNC_NAMESPACE_OBJECTS = new Set([
|
|
63
38
|
'Math', 'JSON', 'Date', 'Buffer', 'Array', 'Object', 'Number', 'String',
|
|
64
39
|
'Boolean', 'Symbol', 'BigInt', 'Reflect', 'console', 'process',
|
|
65
40
|
]);
|
|
66
|
-
/**
|
|
67
|
-
* Returns true if the call MIGHT return a Promise (default — we want to
|
|
68
|
-
* preserve detection for unknown calls). Returns false only when the
|
|
69
|
-
* callee is a known synchronous built-in (`setTimeout`, `console.log`,
|
|
70
|
-
* `Math.floor`, etc.) — those structurally never return promises and
|
|
71
|
-
* firing on them produces FPs. Keeping the default as "could be a
|
|
72
|
-
* promise" preserves recall on user-defined async functions.
|
|
73
|
-
*
|
|
74
|
-
* Exported for direct Layer-2 unit testing: the non-CallExpression early
|
|
75
|
-
* return is only reachable when called with a non-call node (kept for the
|
|
76
|
-
* future `checkIdentifier` listener), which the current CallExpression-only
|
|
77
|
-
* listener never produces.
|
|
78
|
-
*/
|
|
79
41
|
function isLikelyPromiseExpression(node) {
|
|
80
42
|
if (node.type !== 'CallExpression')
|
|
81
43
|
return false;
|
|
82
44
|
const callee = node.callee;
|
|
83
|
-
// Direct calls — skip known synchronous built-ins
|
|
84
45
|
if (callee.type === 'Identifier') {
|
|
85
46
|
const name = callee.name;
|
|
86
47
|
if (NEVER_RETURNS_PROMISE_FUNCTIONS.has(name))
|
|
87
48
|
return false;
|
|
88
49
|
return true;
|
|
89
50
|
}
|
|
90
|
-
// Method calls — skip known synchronous methods (Math.*, Array.*,
|
|
91
|
-
// String.*, console.*, JSON.*)
|
|
92
51
|
if (callee.type === 'MemberExpression') {
|
|
93
52
|
const prop = callee.property;
|
|
94
53
|
if (prop.type === 'Identifier') {
|
|
95
54
|
if (NEVER_RETURNS_PROMISE_METHODS.has(prop.name))
|
|
96
55
|
return false;
|
|
97
56
|
}
|
|
98
|
-
// Static helpers on known sync namespaces — `Buffer.from`, `Date.now`,
|
|
99
|
-
// `Array.isArray`, `Object.keys`, etc. The standard library never
|
|
100
|
-
// returns a Promise from any method on these globals.
|
|
101
57
|
const obj = callee.object;
|
|
102
58
|
if (obj.type === 'Identifier' && SYNC_NAMESPACE_OBJECTS.has(obj.name)) {
|
|
103
59
|
return false;
|
|
@@ -106,15 +62,6 @@ function isLikelyPromiseExpression(node) {
|
|
|
106
62
|
}
|
|
107
63
|
return true;
|
|
108
64
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Returns true when the call's parent indicates the promise is delegated
|
|
111
|
-
* to a caller and therefore not "unhandled" at this site. This covers:
|
|
112
|
-
* - `return fn()` — the enclosing function returns the promise; its
|
|
113
|
-
* caller takes responsibility.
|
|
114
|
-
* - `() => fn()` — concise-body arrow returns the promise.
|
|
115
|
-
* - `.then(() => fn())` — already inside a promise chain (handled by
|
|
116
|
-
* `isInsidePromiseCallback`, but the arrow-body case is the same).
|
|
117
|
-
*/
|
|
118
65
|
function isPromiseDelegatedToCaller(node) {
|
|
119
66
|
const parent = node.parent;
|
|
120
67
|
if (!parent)
|
|
@@ -125,9 +72,6 @@ function isPromiseDelegatedToCaller(node) {
|
|
|
125
72
|
return true;
|
|
126
73
|
return false;
|
|
127
74
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Check if a CallExpression is inside a promise chain callback
|
|
130
|
-
*/
|
|
131
75
|
function isInsidePromiseCallback(node) {
|
|
132
76
|
let current = node;
|
|
133
77
|
let depth = 0;
|
|
@@ -137,10 +81,8 @@ function isInsidePromiseCallback(node) {
|
|
|
137
81
|
.parent;
|
|
138
82
|
if (!parent)
|
|
139
83
|
break;
|
|
140
|
-
// Check if we're inside an arrow function or function expression
|
|
141
84
|
if (parent.type === 'ArrowFunctionExpression' ||
|
|
142
85
|
parent.type === 'FunctionExpression') {
|
|
143
|
-
// Check if this function is an argument to a promise method (.then, .catch, .finally)
|
|
144
86
|
const funcParent = parent
|
|
145
87
|
.parent;
|
|
146
88
|
if (funcParent &&
|
|
@@ -153,7 +95,6 @@ function isInsidePromiseCallback(node) {
|
|
|
153
95
|
if (methodName === 'then' ||
|
|
154
96
|
methodName === 'catch' ||
|
|
155
97
|
methodName === 'finally') {
|
|
156
|
-
// We're inside a promise chain callback
|
|
157
98
|
return true;
|
|
158
99
|
}
|
|
159
100
|
}
|
|
@@ -164,16 +105,7 @@ function isInsidePromiseCallback(node) {
|
|
|
164
105
|
}
|
|
165
106
|
return false;
|
|
166
107
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Check if promise is handled (has .catch, .then, or is in try/catch)
|
|
169
|
-
*
|
|
170
|
-
* Exported for direct Layer-2 unit testing: the Identifier branch is only
|
|
171
|
-
* reachable when called with an Identifier node (kept for the future
|
|
172
|
-
* `checkIdentifier` listener), which the current CallExpression-only
|
|
173
|
-
* listener never produces.
|
|
174
|
-
*/
|
|
175
108
|
function isPromiseHandled(node) {
|
|
176
|
-
// For identifiers, check if they're used in a promise chain
|
|
177
109
|
if (node.type === 'Identifier') {
|
|
178
110
|
const parent = node.parent;
|
|
179
111
|
if (parent &&
|
|
@@ -184,19 +116,16 @@ function isPromiseHandled(node) {
|
|
|
184
116
|
if (methodName === 'catch' ||
|
|
185
117
|
methodName === 'then' ||
|
|
186
118
|
methodName === 'finally') {
|
|
187
|
-
// Check if this MemberExpression is used as a callee (called)
|
|
188
119
|
const memberParent = parent.parent;
|
|
189
120
|
if (memberParent &&
|
|
190
121
|
memberParent.type === 'CallExpression' &&
|
|
191
122
|
memberParent.callee === parent) {
|
|
192
|
-
// Promise is handled by .then(), .catch(), or .finally()
|
|
193
123
|
return true;
|
|
194
124
|
}
|
|
195
125
|
}
|
|
196
126
|
}
|
|
197
127
|
}
|
|
198
128
|
}
|
|
199
|
-
// For CallExpressions, traverse up the AST to find if this promise is part of a handled chain
|
|
200
129
|
let current = node;
|
|
201
130
|
let depth = 0;
|
|
202
131
|
const maxDepth = 10;
|
|
@@ -205,29 +134,24 @@ function isPromiseHandled(node) {
|
|
|
205
134
|
.parent;
|
|
206
135
|
if (!parent)
|
|
207
136
|
break;
|
|
208
|
-
// Check if parent is a MemberExpression with .catch/.then/.finally
|
|
209
137
|
if (parent.type === 'MemberExpression' && parent.object === current) {
|
|
210
138
|
if (parent.property.type === 'Identifier') {
|
|
211
139
|
const methodName = parent.property.name;
|
|
212
140
|
if (methodName === 'catch' ||
|
|
213
141
|
methodName === 'then' ||
|
|
214
142
|
methodName === 'finally') {
|
|
215
|
-
// Check if this MemberExpression is used as a callee (called)
|
|
216
143
|
const memberParent = parent.parent;
|
|
217
144
|
if (memberParent &&
|
|
218
145
|
memberParent.type === 'CallExpression' &&
|
|
219
146
|
memberParent.callee === parent) {
|
|
220
|
-
// Promise is handled by .then(), .catch(), or .finally()
|
|
221
147
|
return true;
|
|
222
148
|
}
|
|
223
149
|
}
|
|
224
150
|
}
|
|
225
151
|
}
|
|
226
|
-
// Check if in try/catch block
|
|
227
152
|
if (parent.type === 'TryStatement') {
|
|
228
153
|
return true;
|
|
229
154
|
}
|
|
230
|
-
// Check if in await expression
|
|
231
155
|
if (parent.type === 'AwaitExpression') {
|
|
232
156
|
return true;
|
|
233
157
|
}
|
|
@@ -314,55 +238,40 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
314
238
|
if (isTestFile) {
|
|
315
239
|
return {};
|
|
316
240
|
}
|
|
317
|
-
// const sourceCode = context.sourceCode; // Not used
|
|
318
|
-
/**
|
|
319
|
-
* Check call expressions for unhandled promises
|
|
320
|
-
*/
|
|
321
241
|
function checkCallExpression(node) {
|
|
322
|
-
// Skip CallExpressions that are inside promise chain callbacks
|
|
323
242
|
if (isInsidePromiseCallback(node)) {
|
|
324
243
|
return;
|
|
325
244
|
}
|
|
326
|
-
// `return fn()` / `() => fn()` — the promise is delegated to the
|
|
327
|
-
// caller. Flagging here would force `await` everywhere a promise
|
|
328
|
-
// is forwarded, which is wrong: forwarding IS handling.
|
|
329
245
|
if (isPromiseDelegatedToCaller(node)) {
|
|
330
246
|
return;
|
|
331
247
|
}
|
|
332
|
-
// Skip calls to promise methods (.then, .catch, .finally) as they are handled by definition
|
|
333
|
-
// But only if they have meaningful callbacks
|
|
334
248
|
if (node.callee.type === 'MemberExpression' &&
|
|
335
249
|
node.callee.property.type === 'Identifier') {
|
|
336
250
|
const methodName = node.callee.property.name;
|
|
337
251
|
if (methodName === 'then' ||
|
|
338
252
|
methodName === 'catch' ||
|
|
339
253
|
methodName === 'finally') {
|
|
340
|
-
// Check if the callback is empty or meaningless
|
|
341
254
|
if (node.arguments.length > 0 &&
|
|
342
255
|
node.arguments[0].type === 'ArrowFunctionExpression') {
|
|
343
256
|
const callback = node.arguments[0];
|
|
344
257
|
if (callback.body.type === 'BlockStatement' &&
|
|
345
258
|
callback.body.body.length === 0) {
|
|
346
|
-
// Empty callback - don't skip, this should be flagged
|
|
347
259
|
}
|
|
348
260
|
else {
|
|
349
|
-
return;
|
|
261
|
+
return;
|
|
350
262
|
}
|
|
351
263
|
}
|
|
352
264
|
else {
|
|
353
|
-
return;
|
|
265
|
+
return;
|
|
354
266
|
}
|
|
355
267
|
}
|
|
356
268
|
}
|
|
357
|
-
// Check if it's a promise-returning function
|
|
358
269
|
if (!isLikelyPromiseExpression(node)) {
|
|
359
270
|
return;
|
|
360
271
|
}
|
|
361
|
-
// Check if it's already handled
|
|
362
272
|
if (isPromiseHandled(node)) {
|
|
363
273
|
return;
|
|
364
274
|
}
|
|
365
|
-
// Check if it's in a void expression
|
|
366
275
|
if (ignoreVoidExpressions) {
|
|
367
276
|
const parent = node
|
|
368
277
|
.parent;
|
|
@@ -372,13 +281,9 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
372
281
|
return;
|
|
373
282
|
}
|
|
374
283
|
}
|
|
375
|
-
// Skip if this CallExpression is an argument to another CallExpression
|
|
376
|
-
// (e.g., console.log(fetch(url)) - we don't want to flag fetch(url) here)
|
|
377
284
|
const parent = node
|
|
378
285
|
.parent;
|
|
379
286
|
if (parent && parent.type === 'CallExpression') {
|
|
380
|
-
// Only skip if it's not part of a promise chain
|
|
381
|
-
// If it's the object of a MemberExpression with .then/.catch/.finally, it's a promise
|
|
382
287
|
const grandParent = parent.parent;
|
|
383
288
|
if (!(grandParent &&
|
|
384
289
|
grandParent.type === 'MemberExpression' &&
|
|
@@ -396,7 +301,7 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
396
301
|
suggest: [
|
|
397
302
|
{
|
|
398
303
|
messageId: 'addCatch',
|
|
399
|
-
fix: () => null,
|
|
304
|
+
fix: () => null,
|
|
400
305
|
},
|
|
401
306
|
{
|
|
402
307
|
messageId: 'useTryCatch',
|
|
@@ -409,55 +314,6 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
409
314
|
],
|
|
410
315
|
});
|
|
411
316
|
}
|
|
412
|
-
/**
|
|
413
|
-
* Check identifier expressions for unhandled promises
|
|
414
|
-
* Note: Currently unused, keeping for future implementation
|
|
415
|
-
*/
|
|
416
|
-
/*
|
|
417
|
-
function checkIdentifier(node: TSESTree.Identifier) {
|
|
418
|
-
// Skip identifiers that are inside promise chain callbacks
|
|
419
|
-
if (isInsidePromiseCallback({ type: 'CallExpression', callee: node, arguments: [], optional: false } as TSESTree.CallExpression)) {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Check if it's a promise-like identifier
|
|
424
|
-
if (!isLikelyPromiseExpression(node)) {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Check if it's already handled
|
|
429
|
-
if (isPromiseHandled(node)) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Check if it's in a void expression
|
|
434
|
-
if (ignoreVoidExpressions) {
|
|
435
|
-
const parent = (node as TSESTree.Node & { parent?: TSESTree.Node }).parent;
|
|
436
|
-
if (parent && parent.type === 'UnaryExpression' && parent.operator === 'void') {
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
context.report({
|
|
442
|
-
node,
|
|
443
|
-
messageId: 'unhandledPromise',
|
|
444
|
-
suggest: [
|
|
445
|
-
{
|
|
446
|
-
messageId: 'addCatch',
|
|
447
|
-
fix: () => null,
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
messageId: 'useTryCatch',
|
|
451
|
-
fix: () => null,
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
messageId: 'useAwait',
|
|
455
|
-
fix: () => null,
|
|
456
|
-
},
|
|
457
|
-
],
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
*/
|
|
461
317
|
return {
|
|
462
318
|
CallExpression: checkCallExpression,
|
|
463
319
|
};
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
-
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
-
* MIT license that can be found in the LICENSE file.
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.noAwaitInLoop = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -85,20 +80,16 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
85
80
|
const [options] = context.options;
|
|
86
81
|
const { allowForOf = false, allowWhile = false } = options || {};
|
|
87
82
|
function analyzeLoop(node, loopType) {
|
|
88
|
-
// Skip allowed loop types
|
|
89
83
|
if ((loopType === 'ForOfStatement' && allowForOf) ||
|
|
90
84
|
(loopType === 'WhileStatement' && allowWhile)) {
|
|
91
85
|
return;
|
|
92
86
|
}
|
|
93
|
-
// Find await expressions within the loop
|
|
94
87
|
const awaitExpressions = [];
|
|
95
|
-
// Properties to skip to avoid circular references
|
|
96
88
|
const skipProperties = new Set(['parent', 'tokens', 'comments', 'loc', 'range']);
|
|
97
89
|
function findAwaits(currentNode) {
|
|
98
90
|
if (currentNode.type === 'AwaitExpression') {
|
|
99
91
|
awaitExpressions.push(currentNode);
|
|
100
92
|
}
|
|
101
|
-
// Don't traverse into nested functions (different scope)
|
|
102
93
|
if (currentNode.type !== 'FunctionDeclaration' &&
|
|
103
94
|
currentNode.type !== 'FunctionExpression' &&
|
|
104
95
|
currentNode.type !== 'ArrowFunctionExpression') {
|
|
@@ -121,7 +112,6 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
121
112
|
}
|
|
122
113
|
findAwaits(node);
|
|
123
114
|
if (awaitExpressions.length > 0) {
|
|
124
|
-
// Analyze the loop context to determine the best suggestion
|
|
125
115
|
const loopContext = analyzeLoopContext(node, loopType);
|
|
126
116
|
for (const awaitExpr of awaitExpressions) {
|
|
127
117
|
context.report({
|
|
@@ -148,9 +138,7 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
148
138
|
operations: [],
|
|
149
139
|
node,
|
|
150
140
|
};
|
|
151
|
-
// Properties to skip to avoid circular references
|
|
152
141
|
const skipProps = new Set(['parent', 'tokens', 'comments', 'loc', 'range']);
|
|
153
|
-
// Analyze what operations are being performed in the loop
|
|
154
142
|
function analyzeOperations(currentNode) {
|
|
155
143
|
if (currentNode.type === 'CallExpression') {
|
|
156
144
|
if (currentNode.callee.type === 'Identifier') {
|
|
@@ -161,23 +149,19 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
161
149
|
loopCtx.operations.push(currentNode.callee.property.name);
|
|
162
150
|
}
|
|
163
151
|
}
|
|
164
|
-
// Check for dependencies between iterations
|
|
165
152
|
if (currentNode.type === 'AssignmentExpression' ||
|
|
166
153
|
currentNode.type === 'UpdateExpression') {
|
|
167
154
|
loopCtx.hasDependencies = true;
|
|
168
155
|
}
|
|
169
|
-
// Check for side effects that might require sequential execution
|
|
170
156
|
if (currentNode.type === 'CallExpression' &&
|
|
171
157
|
(loopCtx.operations.includes('push') ||
|
|
172
158
|
loopCtx.operations.includes('splice') ||
|
|
173
159
|
loopCtx.operations.includes('delete'))) {
|
|
174
160
|
loopCtx.hasSideEffects = true;
|
|
175
161
|
}
|
|
176
|
-
// Count operations
|
|
177
162
|
if (currentNode.type === 'AwaitExpression') {
|
|
178
163
|
loopCtx.operationCount++;
|
|
179
164
|
}
|
|
180
|
-
// Recursively analyze
|
|
181
165
|
for (const key in currentNode) {
|
|
182
166
|
if (skipProps.has(key))
|
|
183
167
|
continue;
|
|
@@ -195,7 +179,6 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
195
179
|
}
|
|
196
180
|
}
|
|
197
181
|
analyzeOperations(node);
|
|
198
|
-
// Determine if operations can be concurrent
|
|
199
182
|
if (loopCtx.hasDependencies || loopCtx.hasSideEffects) {
|
|
200
183
|
loopCtx.isSequential = true;
|
|
201
184
|
loopCtx.estimatedPerformance = `sequential (${loopCtx.operationCount} operations)`;
|
|
@@ -206,7 +189,6 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
206
189
|
}
|
|
207
190
|
return loopCtx;
|
|
208
191
|
}
|
|
209
|
-
// oxlint-disable-next-line consistent-function-scoping
|
|
210
192
|
function getSuggestion(loopCtx) {
|
|
211
193
|
if (loopCtx.isSequential) {
|
|
212
194
|
return 'operations may need to be sequential - consider if concurrency is safe';
|
|
@@ -1,55 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
-
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
-
* MIT license that can be found in the LICENSE file.
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.noJsdocTerminatorInExample = void 0;
|
|
9
4
|
exports.findTerminatorsInExamples = findTerminatorsInExamples;
|
|
10
5
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
11
|
-
/**
|
|
12
|
-
* Check whether a comment line is inside an @example block.
|
|
13
|
-
* We track this by scanning line-by-line for @example / next-tag boundaries.
|
|
14
|
-
*
|
|
15
|
-
* Exported for direct Layer-2 unit testing: a real parser terminates the
|
|
16
|
-
* enclosing block comment at the first star-slash sequence, so a comment
|
|
17
|
-
* value containing one can never reach this function through a
|
|
18
|
-
* normally-parsed file.
|
|
19
|
-
*/
|
|
20
6
|
function findTerminatorsInExamples(commentText) {
|
|
21
7
|
const lines = commentText.split('\n');
|
|
22
8
|
let inExample = false;
|
|
23
9
|
const offsets = [];
|
|
24
10
|
let currentOffset = 0;
|
|
25
11
|
for (const line of lines) {
|
|
26
|
-
// Strip leading whitespace and optional `*` prefix (block comment lines)
|
|
27
12
|
const stripped = line.replace(/^\s*\*?\s?/, '');
|
|
28
|
-
// Detect @example start
|
|
29
13
|
if (/^\s*@example\b/i.test(stripped)) {
|
|
30
14
|
inExample = true;
|
|
31
|
-
currentOffset += line.length + 1;
|
|
15
|
+
currentOffset += line.length + 1;
|
|
32
16
|
continue;
|
|
33
17
|
}
|
|
34
|
-
// Detect any other JSDoc tag → exits @example scope
|
|
35
18
|
if (/^\s*@\w+/.test(stripped) && !stripped.startsWith('@example')) {
|
|
36
19
|
inExample = false;
|
|
37
20
|
currentOffset += line.length + 1;
|
|
38
21
|
continue;
|
|
39
22
|
}
|
|
40
|
-
// If inside an example block, look for `*/` that isn't the closing
|
|
41
|
-
// terminator of the block comment itself (which is always at the very end)
|
|
42
23
|
if (inExample) {
|
|
43
|
-
// Find `*/` occurrences within the original line content
|
|
44
24
|
let searchStart = 0;
|
|
45
25
|
while (searchStart < line.length) {
|
|
46
26
|
const idx = line.indexOf('*/', searchStart);
|
|
47
27
|
if (idx === -1)
|
|
48
28
|
break;
|
|
49
|
-
// The absolute offset within the full comment text
|
|
50
29
|
const absoluteOffset = currentOffset + idx;
|
|
51
|
-
// Don't flag the final `*/` that closes the entire comment —
|
|
52
|
-
// it always sits at the very end of the block comment.
|
|
53
30
|
const remaining = commentText.substring(absoluteOffset + 2).trim();
|
|
54
31
|
if (remaining.length > 0) {
|
|
55
32
|
offsets.push(absoluteOffset);
|
|
@@ -97,23 +74,18 @@ exports.noJsdocTerminatorInExample = (0, eslint_devkit_1.createRule)({
|
|
|
97
74
|
Program() {
|
|
98
75
|
const comments = sourceCode.getAllComments();
|
|
99
76
|
for (const comment of comments) {
|
|
100
|
-
// Only inspect block comments (JSDoc is always /* … */)
|
|
101
77
|
if (comment.type !== 'Block') {
|
|
102
78
|
continue;
|
|
103
79
|
}
|
|
104
80
|
const commentText = comment.value;
|
|
105
|
-
// Quick bail-out: no @example or no `*/` inside → nothing to check
|
|
106
81
|
if (!/@example\b/i.test(commentText) ||
|
|
107
82
|
!commentText.includes('*/')) {
|
|
108
83
|
continue;
|
|
109
84
|
}
|
|
110
85
|
const offsets = findTerminatorsInExamples(commentText);
|
|
111
86
|
for (const offset of offsets) {
|
|
112
|
-
// Calculate the absolute position in the source file.
|
|
113
|
-
// comment.range[0] points to the opening `/*`, so the content
|
|
114
|
-
// starts at range[0] + 2.
|
|
115
87
|
const absoluteStart = comment.range[0] + 2 + offset;
|
|
116
|
-
const absoluteEnd = absoluteStart + 2;
|
|
88
|
+
const absoluteEnd = absoluteStart + 2;
|
|
117
89
|
context.report({
|
|
118
90
|
loc: {
|
|
119
91
|
start: sourceCode.getLocFromIndex(absoluteStart),
|