eslint-plugin-reliability 3.0.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 +159 -0
- package/package.json +55 -0
- package/src/index.d.ts +180 -0
- package/src/index.js +57 -0
- package/src/lib/eslint-plugin-reliability.d.ts +1 -0
- package/src/lib/eslint-plugin-reliability.js +6 -0
- package/src/rules/error-handling/error-message.d.ts +20 -0
- package/src/rules/error-handling/error-message.js +146 -0
- package/src/rules/error-handling/no-missing-error-context.d.ts +26 -0
- package/src/rules/error-handling/no-missing-error-context.js +183 -0
- package/src/rules/error-handling/no-silent-errors.d.ts +24 -0
- package/src/rules/error-handling/no-silent-errors.js +178 -0
- package/src/rules/error-handling/no-unhandled-promise.d.ts +26 -0
- package/src/rules/error-handling/no-unhandled-promise.js +351 -0
- package/src/rules/reliability/no-await-in-loop.d.ts +24 -0
- package/src/rules/reliability/no-await-in-loop.js +234 -0
- package/src/rules/reliability/no-missing-null-checks.d.ts +26 -0
- package/src/rules/reliability/no-missing-null-checks.js +351 -0
- package/src/rules/reliability/no-unsafe-type-narrowing.d.ts +24 -0
- package/src/rules/reliability/no-unsafe-type-narrowing.js +176 -0
- package/src/rules/reliability/require-network-timeout.d.ts +12 -0
- package/src/rules/reliability/require-network-timeout.js +57 -0
|
@@ -0,0 +1,351 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noUnhandledPromise = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
/**
|
|
12
|
+
* Check if a node is a Promise-like expression
|
|
13
|
+
* For now, we check all CallExpressions since we can't statically determine
|
|
14
|
+
* which functions return promises. The isPromiseHandled function will filter out
|
|
15
|
+
* non-promise calls that are inside handled promise chains.
|
|
16
|
+
*/
|
|
17
|
+
function isPromiseExpression(node) {
|
|
18
|
+
// Function calls that might return promises
|
|
19
|
+
if (node.type === 'CallExpression') {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
// Await expressions (already handled)
|
|
23
|
+
if (node.type === 'AwaitExpression') {
|
|
24
|
+
return false; // Already handled
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a CallExpression is inside a promise chain callback
|
|
30
|
+
*/
|
|
31
|
+
function isInsidePromiseCallback(node) {
|
|
32
|
+
let current = node;
|
|
33
|
+
let depth = 0;
|
|
34
|
+
const maxDepth = 10;
|
|
35
|
+
while (current && depth < maxDepth) {
|
|
36
|
+
const parent = current
|
|
37
|
+
.parent;
|
|
38
|
+
if (!parent)
|
|
39
|
+
break;
|
|
40
|
+
// Check if we're inside an arrow function or function expression
|
|
41
|
+
if (parent.type === 'ArrowFunctionExpression' ||
|
|
42
|
+
parent.type === 'FunctionExpression') {
|
|
43
|
+
// Check if this function is an argument to a promise method (.then, .catch, .finally)
|
|
44
|
+
const funcParent = parent
|
|
45
|
+
.parent;
|
|
46
|
+
if (funcParent &&
|
|
47
|
+
funcParent.type === 'CallExpression' &&
|
|
48
|
+
funcParent.callee &&
|
|
49
|
+
funcParent.callee.type === 'MemberExpression') {
|
|
50
|
+
const memberExpr = funcParent.callee;
|
|
51
|
+
if (memberExpr.property.type === 'Identifier') {
|
|
52
|
+
const methodName = memberExpr.property.name;
|
|
53
|
+
if (methodName === 'then' ||
|
|
54
|
+
methodName === 'catch' ||
|
|
55
|
+
methodName === 'finally') {
|
|
56
|
+
// We're inside a promise chain callback
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
current = parent;
|
|
63
|
+
depth++;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if promise is handled (has .catch, .then, or is in try/catch)
|
|
69
|
+
*/
|
|
70
|
+
function isPromiseHandled(node) {
|
|
71
|
+
// For identifiers, check if they're used in a promise chain
|
|
72
|
+
if (node.type === 'Identifier') {
|
|
73
|
+
const parent = node.parent;
|
|
74
|
+
if (parent &&
|
|
75
|
+
parent.type === 'MemberExpression' &&
|
|
76
|
+
parent.object === node) {
|
|
77
|
+
if (parent.property.type === 'Identifier') {
|
|
78
|
+
const methodName = parent.property.name;
|
|
79
|
+
if (methodName === 'catch' ||
|
|
80
|
+
methodName === 'then' ||
|
|
81
|
+
methodName === 'finally') {
|
|
82
|
+
// Check if this MemberExpression is used as a callee (called)
|
|
83
|
+
const memberParent = parent.parent;
|
|
84
|
+
if (memberParent &&
|
|
85
|
+
memberParent.type === 'CallExpression' &&
|
|
86
|
+
memberParent.callee === parent) {
|
|
87
|
+
// Promise is handled by .then(), .catch(), or .finally()
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// For CallExpressions, traverse up the AST to find if this promise is part of a handled chain
|
|
95
|
+
let current = node;
|
|
96
|
+
let depth = 0;
|
|
97
|
+
const maxDepth = 10;
|
|
98
|
+
while (current && depth < maxDepth) {
|
|
99
|
+
const parent = current
|
|
100
|
+
.parent;
|
|
101
|
+
if (!parent)
|
|
102
|
+
break;
|
|
103
|
+
// Check if parent is a MemberExpression with .catch/.then/.finally
|
|
104
|
+
if (parent.type === 'MemberExpression' && parent.object === current) {
|
|
105
|
+
if (parent.property.type === 'Identifier') {
|
|
106
|
+
const methodName = parent.property.name;
|
|
107
|
+
if (methodName === 'catch' ||
|
|
108
|
+
methodName === 'then' ||
|
|
109
|
+
methodName === 'finally') {
|
|
110
|
+
// Check if this MemberExpression is used as a callee (called)
|
|
111
|
+
const memberParent = parent.parent;
|
|
112
|
+
if (memberParent &&
|
|
113
|
+
memberParent.type === 'CallExpression' &&
|
|
114
|
+
memberParent.callee === parent) {
|
|
115
|
+
// Promise is handled by .then(), .catch(), or .finally()
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Check if in try/catch block
|
|
122
|
+
if (parent.type === 'TryStatement') {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
// Check if in await expression
|
|
126
|
+
if (parent.type === 'AwaitExpression') {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
current = parent;
|
|
130
|
+
depth++;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
135
|
+
name: 'no-unhandled-promise',
|
|
136
|
+
meta: {
|
|
137
|
+
type: 'problem',
|
|
138
|
+
docs: {
|
|
139
|
+
description: 'Detects unhandled Promise rejections',
|
|
140
|
+
},
|
|
141
|
+
hasSuggestions: true,
|
|
142
|
+
messages: {
|
|
143
|
+
unhandledPromise: (0, eslint_devkit_1.formatLLMMessage)({
|
|
144
|
+
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
145
|
+
issueName: 'Unhandled promise',
|
|
146
|
+
cwe: 'CWE-1024',
|
|
147
|
+
description: 'Unhandled Promise rejection detected',
|
|
148
|
+
severity: 'HIGH',
|
|
149
|
+
fix: 'Add .catch() handler or use try/catch with await',
|
|
150
|
+
documentationLink: 'https://rules.sonarsource.com/javascript/RSPEC-4635/',
|
|
151
|
+
}),
|
|
152
|
+
addCatch: (0, eslint_devkit_1.formatLLMMessage)({
|
|
153
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
154
|
+
issueName: 'Add catch handler',
|
|
155
|
+
description: 'Add .catch() handler to promise',
|
|
156
|
+
severity: 'LOW',
|
|
157
|
+
fix: 'promise.catch(error => console.error(error))',
|
|
158
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch',
|
|
159
|
+
}),
|
|
160
|
+
useTryCatch: (0, eslint_devkit_1.formatLLMMessage)({
|
|
161
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
162
|
+
issueName: 'Use try/catch',
|
|
163
|
+
description: 'Use try/catch with await',
|
|
164
|
+
severity: 'LOW',
|
|
165
|
+
fix: 'try { await promise; } catch (error) { handle(error); }',
|
|
166
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch',
|
|
167
|
+
}),
|
|
168
|
+
useAwait: (0, eslint_devkit_1.formatLLMMessage)({
|
|
169
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
170
|
+
issueName: 'Use await',
|
|
171
|
+
description: 'Use await to handle promise',
|
|
172
|
+
severity: 'LOW',
|
|
173
|
+
fix: 'await promise;',
|
|
174
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await',
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
schema: [
|
|
178
|
+
{
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
ignoreInTests: {
|
|
182
|
+
type: 'boolean',
|
|
183
|
+
default: true,
|
|
184
|
+
description: 'Ignore promises in test files',
|
|
185
|
+
},
|
|
186
|
+
ignoreVoidExpressions: {
|
|
187
|
+
type: 'boolean',
|
|
188
|
+
default: false,
|
|
189
|
+
description: 'Ignore promises in void expressions',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
defaultOptions: [
|
|
197
|
+
{
|
|
198
|
+
ignoreInTests: true,
|
|
199
|
+
ignoreVoidExpressions: false,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
create(context, [options = {}]) {
|
|
203
|
+
const { ignoreInTests = true, ignoreVoidExpressions = false } = options || {};
|
|
204
|
+
const filename = context.getFilename();
|
|
205
|
+
const isTestFile = ignoreInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
206
|
+
if (isTestFile) {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
// const sourceCode = context.sourceCode || context.sourceCode; // Not used
|
|
210
|
+
/**
|
|
211
|
+
* Check call expressions for unhandled promises
|
|
212
|
+
*/
|
|
213
|
+
function checkCallExpression(node) {
|
|
214
|
+
// Skip CallExpressions that are inside promise chain callbacks
|
|
215
|
+
if (isInsidePromiseCallback(node)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Skip calls to promise methods (.then, .catch, .finally) as they are handled by definition
|
|
219
|
+
// But only if they have meaningful callbacks
|
|
220
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
221
|
+
node.callee.property.type === 'Identifier') {
|
|
222
|
+
const methodName = node.callee.property.name;
|
|
223
|
+
if (methodName === 'then' ||
|
|
224
|
+
methodName === 'catch' ||
|
|
225
|
+
methodName === 'finally') {
|
|
226
|
+
// Check if the callback is empty or meaningless
|
|
227
|
+
if (node.arguments.length > 0 &&
|
|
228
|
+
node.arguments[0].type === 'ArrowFunctionExpression') {
|
|
229
|
+
const callback = node.arguments[0];
|
|
230
|
+
if (callback.body.type === 'BlockStatement' &&
|
|
231
|
+
callback.body.body.length === 0) {
|
|
232
|
+
// Empty callback - don't skip, this should be flagged
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
return; // Has meaningful callback, skip
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
return; // Not an arrow function callback, assume it's handled
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Check if it's a promise-returning function
|
|
244
|
+
if (!isPromiseExpression(node)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Check if it's already handled
|
|
248
|
+
if (isPromiseHandled(node)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Check if it's in a void expression
|
|
252
|
+
if (ignoreVoidExpressions) {
|
|
253
|
+
const parent = node
|
|
254
|
+
.parent;
|
|
255
|
+
if (parent &&
|
|
256
|
+
parent.type === 'UnaryExpression' &&
|
|
257
|
+
parent.operator === 'void') {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Skip if this CallExpression is an argument to another CallExpression
|
|
262
|
+
// (e.g., console.log(fetch(url)) - we don't want to flag fetch(url) here)
|
|
263
|
+
const parent = node
|
|
264
|
+
.parent;
|
|
265
|
+
if (parent && parent.type === 'CallExpression') {
|
|
266
|
+
// Only skip if it's not part of a promise chain
|
|
267
|
+
// If it's the object of a MemberExpression with .then/.catch/.finally, it's a promise
|
|
268
|
+
const grandParent = parent.parent;
|
|
269
|
+
if (!(grandParent &&
|
|
270
|
+
grandParent.type === 'MemberExpression' &&
|
|
271
|
+
grandParent.object === parent &&
|
|
272
|
+
grandParent.property.type === 'Identifier' &&
|
|
273
|
+
(grandParent.property.name === 'then' ||
|
|
274
|
+
grandParent.property.name === 'catch' ||
|
|
275
|
+
grandParent.property.name === 'finally'))) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
context.report({
|
|
280
|
+
node,
|
|
281
|
+
messageId: 'unhandledPromise',
|
|
282
|
+
suggest: [
|
|
283
|
+
{
|
|
284
|
+
messageId: 'addCatch',
|
|
285
|
+
fix: () => null, // Cannot auto-fix without context
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
messageId: 'useTryCatch',
|
|
289
|
+
fix: () => null,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
messageId: 'useAwait',
|
|
293
|
+
fix: () => null,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Check identifier expressions for unhandled promises
|
|
300
|
+
* Note: Currently unused, keeping for future implementation
|
|
301
|
+
*/
|
|
302
|
+
/*
|
|
303
|
+
function checkIdentifier(node: TSESTree.Identifier) {
|
|
304
|
+
// Skip identifiers that are inside promise chain callbacks
|
|
305
|
+
if (isInsidePromiseCallback({ type: 'CallExpression', callee: node, arguments: [], optional: false } as TSESTree.CallExpression)) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if it's a promise-like identifier
|
|
310
|
+
if (!isPromiseExpression(node)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Check if it's already handled
|
|
315
|
+
if (isPromiseHandled(node)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check if it's in a void expression
|
|
320
|
+
if (ignoreVoidExpressions) {
|
|
321
|
+
const parent = (node as TSESTree.Node & { parent?: TSESTree.Node }).parent;
|
|
322
|
+
if (parent && parent.type === 'UnaryExpression' && parent.operator === 'void') {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
context.report({
|
|
328
|
+
node,
|
|
329
|
+
messageId: 'unhandledPromise',
|
|
330
|
+
suggest: [
|
|
331
|
+
{
|
|
332
|
+
messageId: 'addCatch',
|
|
333
|
+
fix: () => null,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
messageId: 'useTryCatch',
|
|
337
|
+
fix: () => null,
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
messageId: 'useAwait',
|
|
341
|
+
fix: () => null,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
*/
|
|
347
|
+
return {
|
|
348
|
+
CallExpression: checkCallExpression,
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: no-await-in-loop
|
|
8
|
+
* Disallow await inside loops without considering concurrency implications (unicorn-inspired)
|
|
9
|
+
*/
|
|
10
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
11
|
+
type MessageIds = 'awaitInLoop' | 'suggestPromiseAll' | 'suggestConcurrent' | 'considerSequential' | 'asyncLoopPattern';
|
|
12
|
+
export interface Options {
|
|
13
|
+
/** Allow await in for-of loops */
|
|
14
|
+
allowForOf?: boolean;
|
|
15
|
+
/** Allow await in while loops */
|
|
16
|
+
allowWhile?: boolean;
|
|
17
|
+
/** Check for potential concurrent execution opportunities */
|
|
18
|
+
checkConcurrency?: boolean;
|
|
19
|
+
}
|
|
20
|
+
type RuleOptions = [Options?];
|
|
21
|
+
export declare const noAwaitInLoop: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noAwaitInLoop = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
12
|
+
name: 'no-await-in-loop',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Disallow await inside loops and suggest appropriate concurrency patterns',
|
|
17
|
+
},
|
|
18
|
+
hasSuggestions: false,
|
|
19
|
+
messages: {
|
|
20
|
+
awaitInLoop: (0, eslint_devkit_2.formatLLMMessage)({
|
|
21
|
+
icon: eslint_devkit_2.MessageIcons.WARNING,
|
|
22
|
+
issueName: 'Sequential Async Loop',
|
|
23
|
+
description: 'Await in loop forces sequential execution',
|
|
24
|
+
severity: 'MEDIUM',
|
|
25
|
+
fix: 'Consider Promise.all() for concurrent execution or extract async logic',
|
|
26
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function',
|
|
27
|
+
}),
|
|
28
|
+
suggestPromiseAll: (0, eslint_devkit_2.formatLLMMessage)({
|
|
29
|
+
icon: eslint_devkit_2.MessageIcons.INFO,
|
|
30
|
+
issueName: 'Use Promise.all',
|
|
31
|
+
description: 'Concurrent execution of independent operations',
|
|
32
|
+
severity: 'LOW',
|
|
33
|
+
fix: 'Promise.all(items.map(async (item) => await process(item)))',
|
|
34
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all',
|
|
35
|
+
}),
|
|
36
|
+
suggestConcurrent: (0, eslint_devkit_2.formatLLMMessage)({
|
|
37
|
+
icon: eslint_devkit_2.MessageIcons.INFO,
|
|
38
|
+
issueName: 'Use Promise.allSettled',
|
|
39
|
+
description: 'Concurrent execution with error handling',
|
|
40
|
+
severity: 'LOW',
|
|
41
|
+
fix: 'Promise.allSettled(items.map(async (item) => await process(item)))',
|
|
42
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled',
|
|
43
|
+
}),
|
|
44
|
+
considerSequential: (0, eslint_devkit_2.formatLLMMessage)({
|
|
45
|
+
icon: eslint_devkit_2.MessageIcons.INFO,
|
|
46
|
+
issueName: 'Sequential Control',
|
|
47
|
+
description: 'Operations require sequential execution',
|
|
48
|
+
severity: 'LOW',
|
|
49
|
+
fix: 'Add concurrency control or extract to async function',
|
|
50
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function',
|
|
51
|
+
}),
|
|
52
|
+
asyncLoopPattern: (0, eslint_devkit_2.formatLLMMessage)({
|
|
53
|
+
icon: eslint_devkit_2.MessageIcons.INFO,
|
|
54
|
+
issueName: 'Async Loop Pattern',
|
|
55
|
+
description: 'Use controlled concurrency library',
|
|
56
|
+
severity: 'LOW',
|
|
57
|
+
fix: 'Consider p-map, p-series, or similar for controlled concurrency',
|
|
58
|
+
documentationLink: 'https://github.com/sindresorhus/p-map',
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
schema: [
|
|
62
|
+
{
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
allowForOf: {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
default: false,
|
|
68
|
+
},
|
|
69
|
+
allowWhile: {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
default: false,
|
|
72
|
+
},
|
|
73
|
+
checkConcurrency: {
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
default: true,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
defaultOptions: [{ allowForOf: false, allowWhile: false, checkConcurrency: true }],
|
|
83
|
+
create(context) {
|
|
84
|
+
const [options] = context.options;
|
|
85
|
+
const { allowForOf = false, allowWhile = false } = options || {};
|
|
86
|
+
function analyzeLoop(node, loopType) {
|
|
87
|
+
// Skip allowed loop types
|
|
88
|
+
if ((loopType === 'ForOfStatement' && allowForOf) ||
|
|
89
|
+
(loopType === 'WhileStatement' && allowWhile)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Find await expressions within the loop
|
|
93
|
+
const awaitExpressions = [];
|
|
94
|
+
// Properties to skip to avoid circular references
|
|
95
|
+
const skipProperties = new Set(['parent', 'tokens', 'comments', 'loc', 'range']);
|
|
96
|
+
function findAwaits(currentNode) {
|
|
97
|
+
if (currentNode.type === 'AwaitExpression') {
|
|
98
|
+
awaitExpressions.push(currentNode);
|
|
99
|
+
}
|
|
100
|
+
// Don't traverse into nested functions (different scope)
|
|
101
|
+
if (currentNode.type !== 'FunctionDeclaration' &&
|
|
102
|
+
currentNode.type !== 'FunctionExpression' &&
|
|
103
|
+
currentNode.type !== 'ArrowFunctionExpression') {
|
|
104
|
+
for (const key in currentNode) {
|
|
105
|
+
if (skipProperties.has(key))
|
|
106
|
+
continue;
|
|
107
|
+
const child = currentNode[key];
|
|
108
|
+
if (Array.isArray(child)) {
|
|
109
|
+
child.forEach(item => {
|
|
110
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
111
|
+
findAwaits(item);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else if (child && typeof child === 'object' && 'type' in child) {
|
|
116
|
+
findAwaits(child);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
findAwaits(node);
|
|
122
|
+
if (awaitExpressions.length > 0) {
|
|
123
|
+
// Analyze the loop context to determine the best suggestion
|
|
124
|
+
const loopContext = analyzeLoopContext(node, loopType);
|
|
125
|
+
for (const awaitExpr of awaitExpressions) {
|
|
126
|
+
context.report({
|
|
127
|
+
node: awaitExpr,
|
|
128
|
+
messageId: 'awaitInLoop',
|
|
129
|
+
data: {
|
|
130
|
+
loopType: loopContext.loopType,
|
|
131
|
+
operations: loopContext.operationCount,
|
|
132
|
+
suggestion: getSuggestion(loopContext),
|
|
133
|
+
performance: loopContext.estimatedPerformance,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function analyzeLoopContext(node, loopType) {
|
|
140
|
+
const context = {
|
|
141
|
+
loopType,
|
|
142
|
+
operationCount: 1,
|
|
143
|
+
hasDependencies: false,
|
|
144
|
+
isSequential: true,
|
|
145
|
+
hasSideEffects: false,
|
|
146
|
+
estimatedPerformance: 'unknown',
|
|
147
|
+
operations: [],
|
|
148
|
+
node,
|
|
149
|
+
};
|
|
150
|
+
// Properties to skip to avoid circular references
|
|
151
|
+
const skipProps = new Set(['parent', 'tokens', 'comments', 'loc', 'range']);
|
|
152
|
+
// Analyze what operations are being performed in the loop
|
|
153
|
+
function analyzeOperations(currentNode) {
|
|
154
|
+
if (currentNode.type === 'CallExpression') {
|
|
155
|
+
if (currentNode.callee.type === 'Identifier') {
|
|
156
|
+
context.operations.push(currentNode.callee.name);
|
|
157
|
+
}
|
|
158
|
+
else if (currentNode.callee.type === 'MemberExpression' &&
|
|
159
|
+
currentNode.callee.property.type === 'Identifier') {
|
|
160
|
+
context.operations.push(currentNode.callee.property.name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Check for dependencies between iterations
|
|
164
|
+
if (currentNode.type === 'AssignmentExpression' ||
|
|
165
|
+
currentNode.type === 'UpdateExpression') {
|
|
166
|
+
context.hasDependencies = true;
|
|
167
|
+
}
|
|
168
|
+
// Check for side effects that might require sequential execution
|
|
169
|
+
if (currentNode.type === 'CallExpression' &&
|
|
170
|
+
(context.operations.includes('push') ||
|
|
171
|
+
context.operations.includes('splice') ||
|
|
172
|
+
context.operations.includes('delete'))) {
|
|
173
|
+
context.hasSideEffects = true;
|
|
174
|
+
}
|
|
175
|
+
// Count operations
|
|
176
|
+
if (currentNode.type === 'AwaitExpression') {
|
|
177
|
+
context.operationCount++;
|
|
178
|
+
}
|
|
179
|
+
// Recursively analyze
|
|
180
|
+
for (const key in currentNode) {
|
|
181
|
+
if (skipProps.has(key))
|
|
182
|
+
continue;
|
|
183
|
+
const child = currentNode[key];
|
|
184
|
+
if (Array.isArray(child)) {
|
|
185
|
+
child.forEach(item => {
|
|
186
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
187
|
+
analyzeOperations(item);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else if (child && typeof child === 'object' && 'type' in child) {
|
|
192
|
+
analyzeOperations(child);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
analyzeOperations(node);
|
|
197
|
+
// Determine if operations can be concurrent
|
|
198
|
+
if (context.hasDependencies || context.hasSideEffects) {
|
|
199
|
+
context.isSequential = true;
|
|
200
|
+
context.estimatedPerformance = `sequential (${context.operationCount} operations)`;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
context.isSequential = false;
|
|
204
|
+
context.estimatedPerformance = `potential ${context.operationCount}x speedup with concurrency`;
|
|
205
|
+
}
|
|
206
|
+
return context;
|
|
207
|
+
}
|
|
208
|
+
function getSuggestion(context) {
|
|
209
|
+
if (context.isSequential) {
|
|
210
|
+
return 'operations may need to be sequential - consider if concurrency is safe';
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
return 'operations appear independent - consider Promise.all() for concurrency';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
ForStatement(node) {
|
|
218
|
+
analyzeLoop(node, 'ForStatement');
|
|
219
|
+
},
|
|
220
|
+
ForInStatement(node) {
|
|
221
|
+
analyzeLoop(node, 'ForInStatement');
|
|
222
|
+
},
|
|
223
|
+
ForOfStatement(node) {
|
|
224
|
+
analyzeLoop(node, 'ForOfStatement');
|
|
225
|
+
},
|
|
226
|
+
WhileStatement(node) {
|
|
227
|
+
analyzeLoop(node, 'WhileStatement');
|
|
228
|
+
},
|
|
229
|
+
DoWhileStatement(node) {
|
|
230
|
+
analyzeLoop(node, 'DoWhileStatement');
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: no-missing-null-checks
|
|
8
|
+
* Detects potential null pointer dereferences
|
|
9
|
+
* CWE-476: NULL Pointer Dereference
|
|
10
|
+
*
|
|
11
|
+
* @see https://cwe.mitre.org/data/definitions/476.html
|
|
12
|
+
* @see https://rules.sonarsource.com/javascript/RSPEC-2259/
|
|
13
|
+
*/
|
|
14
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
15
|
+
type MessageIds = 'missingNullCheck' | 'useOptionalChaining' | 'useNullishCoalescing' | 'addExplicitCheck';
|
|
16
|
+
export interface Options {
|
|
17
|
+
/** Ignore in test files. Default: true */
|
|
18
|
+
ignoreInTests?: boolean;
|
|
19
|
+
/** Require explicit null checks. Default: false */
|
|
20
|
+
requireExplicitChecks?: boolean;
|
|
21
|
+
}
|
|
22
|
+
type RuleOptions = [Options?];
|
|
23
|
+
export declare const noMissingNullChecks: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
export {};
|