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.
Files changed (32) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +23 -0
  3. package/README.md +116 -0
  4. package/package.json +65 -0
  5. package/src/index.d.ts +180 -0
  6. package/src/index.js +56 -0
  7. package/src/rules/error-handling/error-message.d.ts +20 -0
  8. package/src/rules/error-handling/error-message.js +146 -0
  9. package/src/rules/error-handling/no-missing-error-context.d.ts +26 -0
  10. package/src/rules/error-handling/no-missing-error-context.js +183 -0
  11. package/src/rules/error-handling/no-silent-errors.d.ts +24 -0
  12. package/src/rules/error-handling/no-silent-errors.js +178 -0
  13. package/src/rules/error-handling/no-unhandled-promise.d.ts +26 -0
  14. package/src/rules/error-handling/no-unhandled-promise.js +351 -0
  15. package/src/rules/maintainability/cognitive-complexity.d.ts +28 -0
  16. package/src/rules/maintainability/cognitive-complexity.js +381 -0
  17. package/src/rules/maintainability/consistent-function-scoping.d.ts +20 -0
  18. package/src/rules/maintainability/consistent-function-scoping.js +226 -0
  19. package/src/rules/maintainability/identical-functions.d.ts +27 -0
  20. package/src/rules/maintainability/identical-functions.js +310 -0
  21. package/src/rules/maintainability/max-parameters.d.ts +29 -0
  22. package/src/rules/maintainability/max-parameters.js +143 -0
  23. package/src/rules/maintainability/nested-complexity-hotspots.d.ts +31 -0
  24. package/src/rules/maintainability/nested-complexity-hotspots.js +190 -0
  25. package/src/rules/maintainability/no-lonely-if.d.ts +19 -0
  26. package/src/rules/maintainability/no-lonely-if.js +120 -0
  27. package/src/rules/maintainability/no-nested-ternary.d.ts +19 -0
  28. package/src/rules/maintainability/no-nested-ternary.js +165 -0
  29. package/src/rules/maintainability/no-unreadable-iife.d.ts +24 -0
  30. package/src/rules/maintainability/no-unreadable-iife.js +239 -0
  31. package/src/types/index.d.ts +48 -0
  32. 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 {};