eslint-plugin-maintainability 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/CHANGELOG.md +63 -0
- package/LICENSE +23 -0
- package/README.md +116 -0
- package/package.json +65 -0
- package/src/index.d.ts +180 -0
- package/src/index.js +56 -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/maintainability/cognitive-complexity.d.ts +28 -0
- package/src/rules/maintainability/cognitive-complexity.js +381 -0
- package/src/rules/maintainability/consistent-function-scoping.d.ts +20 -0
- package/src/rules/maintainability/consistent-function-scoping.js +226 -0
- package/src/rules/maintainability/identical-functions.d.ts +27 -0
- package/src/rules/maintainability/identical-functions.js +310 -0
- package/src/rules/maintainability/max-parameters.d.ts +29 -0
- package/src/rules/maintainability/max-parameters.js +143 -0
- package/src/rules/maintainability/nested-complexity-hotspots.d.ts +31 -0
- package/src/rules/maintainability/nested-complexity-hotspots.js +190 -0
- package/src/rules/maintainability/no-lonely-if.d.ts +19 -0
- package/src/rules/maintainability/no-lonely-if.js +120 -0
- package/src/rules/maintainability/no-nested-ternary.d.ts +19 -0
- package/src/rules/maintainability/no-nested-ternary.js +165 -0
- package/src/rules/maintainability/no-unreadable-iife.d.ts +24 -0
- package/src/rules/maintainability/no-unreadable-iife.js +239 -0
- package/src/types/index.d.ts +48 -0
- package/src/types/index.js +7 -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,28 @@
|
|
|
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: cognitive-complexity
|
|
8
|
+
* Detects high cognitive complexity with refactoring suggestions
|
|
9
|
+
* Inspired by SonarQube RSPEC-3776
|
|
10
|
+
*
|
|
11
|
+
* @see https://rules.sonarsource.com/javascript/RSPEC-3776/
|
|
12
|
+
*/
|
|
13
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
14
|
+
/**
|
|
15
|
+
* Message IDs for cognitive complexity violations and suggestions
|
|
16
|
+
*/
|
|
17
|
+
type MessageIds = 'highCognitiveComplexity' | 'extractMethod' | 'simplifyLogic' | 'useStrategy';
|
|
18
|
+
export interface Options {
|
|
19
|
+
/** Maximum allowed cognitive complexity score. Default: 15 */
|
|
20
|
+
maxComplexity?: number;
|
|
21
|
+
/** Include complexity metrics in error message. Default: false */
|
|
22
|
+
includeMetrics?: boolean;
|
|
23
|
+
}
|
|
24
|
+
type RuleOptions = [Options?];
|
|
25
|
+
export declare const cognitiveComplexity: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
export {};
|