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.
@@ -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; // Has meaningful callback, skip
261
+ return;
350
262
  }
351
263
  }
352
264
  else {
353
- return; // Not an arrow function callback, assume it's handled
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, // Cannot auto-fix without context
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; // +1 for the newline
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; // length of `*/`
88
+ const absoluteEnd = absoluteStart + 2;
117
89
  context.report({
118
90
  loc: {
119
91
  start: sourceCode.getLocFromIndex(absoluteStart),