eslint-plugin-maintainability 3.0.9 → 3.0.11
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 +7 -18
- package/src/index.d.ts +15 -250
- package/src/index.js +1 -17
- 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 -21
- package/src/rules/error-handling/no-silent-errors.js +1 -21
- package/src/rules/error-handling/no-unhandled-promise.js +4 -100
- package/src/rules/maintainability/cognitive-complexity.js +3 -46
- package/src/rules/maintainability/consistent-function-scoping.js +0 -45
- package/src/rules/maintainability/identical-functions.js +0 -43
- package/src/rules/maintainability/max-parameters.js +2 -17
- package/src/rules/maintainability/nested-complexity-hotspots.js +1 -55
- package/src/rules/maintainability/no-lonely-if.js +0 -15
- package/src/rules/maintainability/no-nested-ternary.js +0 -27
- package/src/rules/maintainability/no-unreadable-iife.js +1 -21
- package/src/types/index.js +0 -5
- package/CHANGELOG.md +0 -192
- 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 -41
- package/src/rules/maintainability/cognitive-complexity.d.ts +0 -28
- package/src/rules/maintainability/consistent-function-scoping.d.ts +0 -20
- package/src/rules/maintainability/identical-functions.d.ts +0 -33
- package/src/rules/maintainability/max-parameters.d.ts +0 -29
- package/src/rules/maintainability/nested-complexity-hotspots.d.ts +0 -31
- package/src/rules/maintainability/no-lonely-if.d.ts +0 -19
- package/src/rules/maintainability/no-nested-ternary.d.ts +0 -19
- package/src/rules/maintainability/no-unreadable-iife.d.ts +0 -24
|
@@ -1,41 +1,26 @@
|
|
|
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.noSilentErrors = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
5
|
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
-
/**
|
|
12
|
-
* Check if catch block is empty or only has comments
|
|
13
|
-
*/
|
|
14
6
|
function isEmptyCatchBlock(catchClause) {
|
|
15
7
|
const body = catchClause.body;
|
|
16
8
|
if (!body || body.type !== 'BlockStatement') {
|
|
17
9
|
return true;
|
|
18
10
|
}
|
|
19
|
-
// Check if body only has comments or is empty
|
|
20
11
|
const statements = body.body;
|
|
21
12
|
if (statements.length === 0) {
|
|
22
13
|
return true;
|
|
23
14
|
}
|
|
24
|
-
// Check if all statements are just comments (not possible in AST, but check for empty statements)
|
|
25
15
|
const nonEmptyStatements = statements.filter((stmt) => stmt.type !== 'EmptyStatement');
|
|
26
16
|
return nonEmptyStatements.length === 0;
|
|
27
17
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Check if catch block has a comment explaining why it's empty
|
|
30
|
-
*/
|
|
31
18
|
function hasExplanatoryComment(catchClause, sourceCode) {
|
|
32
|
-
// Check for comments before the catch clause
|
|
33
19
|
const comments = sourceCode.getAllComments();
|
|
34
20
|
const catchStart = catchClause.loc?.start;
|
|
35
21
|
if (!catchStart || !comments.length) {
|
|
36
22
|
return false;
|
|
37
23
|
}
|
|
38
|
-
// Look for explanatory comments near the catch clause
|
|
39
24
|
const explanatoryPatterns = [
|
|
40
25
|
/intentional/i,
|
|
41
26
|
/expected/i,
|
|
@@ -53,7 +38,6 @@ function hasExplanatoryComment(catchClause, sourceCode) {
|
|
|
53
38
|
/todo/i,
|
|
54
39
|
/fixme/i,
|
|
55
40
|
];
|
|
56
|
-
// Check comments before the catch clause (within 2 lines)
|
|
57
41
|
for (const comment of comments) {
|
|
58
42
|
if (comment.loc && catchStart.line - comment.loc.end.line <= 2) {
|
|
59
43
|
const commentText = comment.value.toLowerCase();
|
|
@@ -140,14 +124,10 @@ exports.noSilentErrors = (0, eslint_devkit_2.createRule)({
|
|
|
140
124
|
return {};
|
|
141
125
|
}
|
|
142
126
|
const sourceCode = context.sourceCode;
|
|
143
|
-
/**
|
|
144
|
-
* Check catch clauses
|
|
145
|
-
*/
|
|
146
127
|
function checkCatchClause(node) {
|
|
147
128
|
if (!isEmptyCatchBlock(node)) {
|
|
148
129
|
return;
|
|
149
130
|
}
|
|
150
|
-
// Check if comment explains why it's empty
|
|
151
131
|
if (allowWithComment && hasExplanatoryComment(node, sourceCode)) {
|
|
152
132
|
return;
|
|
153
133
|
}
|
|
@@ -157,7 +137,7 @@ exports.noSilentErrors = (0, eslint_devkit_2.createRule)({
|
|
|
157
137
|
suggest: [
|
|
158
138
|
{
|
|
159
139
|
messageId: 'addErrorLogging',
|
|
160
|
-
fix: () => null,
|
|
140
|
+
fix: () => null,
|
|
161
141
|
},
|
|
162
142
|
{
|
|
163
143
|
messageId: 'addErrorHandling',
|
|
@@ -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.noUnhandledPromise = void 0;
|
|
9
4
|
exports.isPromiseExpression = isPromiseExpression;
|
|
@@ -11,26 +6,15 @@ exports.isInsidePromiseCallback = isInsidePromiseCallback;
|
|
|
11
6
|
exports.isPromiseHandled = isPromiseHandled;
|
|
12
7
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
13
8
|
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
14
|
-
/**
|
|
15
|
-
* Check if a node is a Promise-like expression
|
|
16
|
-
* For now, we check all CallExpressions since we can't statically determine
|
|
17
|
-
* which functions return promises. The isPromiseHandled function will filter out
|
|
18
|
-
* non-promise calls that are inside handled promise chains.
|
|
19
|
-
*/
|
|
20
9
|
function isPromiseExpression(node) {
|
|
21
|
-
// Function calls that might return promises
|
|
22
10
|
if (node.type === 'CallExpression') {
|
|
23
11
|
return true;
|
|
24
12
|
}
|
|
25
|
-
// Await expressions (already handled)
|
|
26
13
|
if (node.type === 'AwaitExpression') {
|
|
27
|
-
return false;
|
|
14
|
+
return false;
|
|
28
15
|
}
|
|
29
16
|
return false;
|
|
30
17
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Check if a CallExpression is inside a promise chain callback
|
|
33
|
-
*/
|
|
34
18
|
function isInsidePromiseCallback(node) {
|
|
35
19
|
let current = node;
|
|
36
20
|
let depth = 0;
|
|
@@ -40,10 +24,8 @@ function isInsidePromiseCallback(node) {
|
|
|
40
24
|
.parent;
|
|
41
25
|
if (!parent)
|
|
42
26
|
break;
|
|
43
|
-
// Check if we're inside an arrow function or function expression
|
|
44
27
|
if (parent.type === 'ArrowFunctionExpression' ||
|
|
45
28
|
parent.type === 'FunctionExpression') {
|
|
46
|
-
// Check if this function is an argument to a promise method (.then, .catch, .finally)
|
|
47
29
|
const funcParent = parent
|
|
48
30
|
.parent;
|
|
49
31
|
if (funcParent &&
|
|
@@ -56,7 +38,6 @@ function isInsidePromiseCallback(node) {
|
|
|
56
38
|
if (methodName === 'then' ||
|
|
57
39
|
methodName === 'catch' ||
|
|
58
40
|
methodName === 'finally') {
|
|
59
|
-
// We're inside a promise chain callback
|
|
60
41
|
return true;
|
|
61
42
|
}
|
|
62
43
|
}
|
|
@@ -67,11 +48,7 @@ function isInsidePromiseCallback(node) {
|
|
|
67
48
|
}
|
|
68
49
|
return false;
|
|
69
50
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Check if promise is handled (has .catch, .then, or is in try/catch)
|
|
72
|
-
*/
|
|
73
51
|
function isPromiseHandled(node) {
|
|
74
|
-
// For identifiers, check if they're used in a promise chain
|
|
75
52
|
if (node.type === 'Identifier') {
|
|
76
53
|
const parent = node.parent;
|
|
77
54
|
if (parent &&
|
|
@@ -82,19 +59,16 @@ function isPromiseHandled(node) {
|
|
|
82
59
|
if (methodName === 'catch' ||
|
|
83
60
|
methodName === 'then' ||
|
|
84
61
|
methodName === 'finally') {
|
|
85
|
-
// Check if this MemberExpression is used as a callee (called)
|
|
86
62
|
const memberParent = parent.parent;
|
|
87
63
|
if (memberParent &&
|
|
88
64
|
memberParent.type === 'CallExpression' &&
|
|
89
65
|
memberParent.callee === parent) {
|
|
90
|
-
// Promise is handled by .then(), .catch(), or .finally()
|
|
91
66
|
return true;
|
|
92
67
|
}
|
|
93
68
|
}
|
|
94
69
|
}
|
|
95
70
|
}
|
|
96
71
|
}
|
|
97
|
-
// For CallExpressions, traverse up the AST to find if this promise is part of a handled chain
|
|
98
72
|
let current = node;
|
|
99
73
|
let depth = 0;
|
|
100
74
|
const maxDepth = 10;
|
|
@@ -103,29 +77,24 @@ function isPromiseHandled(node) {
|
|
|
103
77
|
.parent;
|
|
104
78
|
if (!parent)
|
|
105
79
|
break;
|
|
106
|
-
// Check if parent is a MemberExpression with .catch/.then/.finally
|
|
107
80
|
if (parent.type === 'MemberExpression' && parent.object === current) {
|
|
108
81
|
if (parent.property.type === 'Identifier') {
|
|
109
82
|
const methodName = parent.property.name;
|
|
110
83
|
if (methodName === 'catch' ||
|
|
111
84
|
methodName === 'then' ||
|
|
112
85
|
methodName === 'finally') {
|
|
113
|
-
// Check if this MemberExpression is used as a callee (called)
|
|
114
86
|
const memberParent = parent.parent;
|
|
115
87
|
if (memberParent &&
|
|
116
88
|
memberParent.type === 'CallExpression' &&
|
|
117
89
|
memberParent.callee === parent) {
|
|
118
|
-
// Promise is handled by .then(), .catch(), or .finally()
|
|
119
90
|
return true;
|
|
120
91
|
}
|
|
121
92
|
}
|
|
122
93
|
}
|
|
123
94
|
}
|
|
124
|
-
// Check if in try/catch block
|
|
125
95
|
if (parent.type === 'TryStatement') {
|
|
126
96
|
return true;
|
|
127
97
|
}
|
|
128
|
-
// Check if in await expression
|
|
129
98
|
if (parent.type === 'AwaitExpression') {
|
|
130
99
|
return true;
|
|
131
100
|
}
|
|
@@ -212,49 +181,37 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
212
181
|
if (isTestFile) {
|
|
213
182
|
return {};
|
|
214
183
|
}
|
|
215
|
-
// const sourceCode = context.sourceCode; // Not used
|
|
216
|
-
/**
|
|
217
|
-
* Check call expressions for unhandled promises
|
|
218
|
-
*/
|
|
219
184
|
function checkCallExpression(node) {
|
|
220
|
-
// Skip CallExpressions that are inside promise chain callbacks
|
|
221
185
|
if (isInsidePromiseCallback(node)) {
|
|
222
186
|
return;
|
|
223
187
|
}
|
|
224
|
-
// Skip calls to promise methods (.then, .catch, .finally) as they are handled by definition
|
|
225
|
-
// But only if they have meaningful callbacks
|
|
226
188
|
if (node.callee.type === 'MemberExpression' &&
|
|
227
189
|
node.callee.property.type === 'Identifier') {
|
|
228
190
|
const methodName = node.callee.property.name;
|
|
229
191
|
if (methodName === 'then' ||
|
|
230
192
|
methodName === 'catch' ||
|
|
231
193
|
methodName === 'finally') {
|
|
232
|
-
// Check if the callback is empty or meaningless
|
|
233
194
|
if (node.arguments.length > 0 &&
|
|
234
195
|
node.arguments[0].type === 'ArrowFunctionExpression') {
|
|
235
196
|
const callback = node.arguments[0];
|
|
236
197
|
if (callback.body.type === 'BlockStatement' &&
|
|
237
198
|
callback.body.body.length === 0) {
|
|
238
|
-
// Empty callback - don't skip, this should be flagged
|
|
239
199
|
}
|
|
240
200
|
else {
|
|
241
|
-
return;
|
|
201
|
+
return;
|
|
242
202
|
}
|
|
243
203
|
}
|
|
244
204
|
else {
|
|
245
|
-
return;
|
|
205
|
+
return;
|
|
246
206
|
}
|
|
247
207
|
}
|
|
248
208
|
}
|
|
249
|
-
// Check if it's a promise-returning function
|
|
250
209
|
if (!isPromiseExpression(node)) {
|
|
251
210
|
return;
|
|
252
211
|
}
|
|
253
|
-
// Check if it's already handled
|
|
254
212
|
if (isPromiseHandled(node)) {
|
|
255
213
|
return;
|
|
256
214
|
}
|
|
257
|
-
// Check if it's in a void expression
|
|
258
215
|
if (ignoreVoidExpressions) {
|
|
259
216
|
const parent = node
|
|
260
217
|
.parent;
|
|
@@ -264,13 +221,9 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
264
221
|
return;
|
|
265
222
|
}
|
|
266
223
|
}
|
|
267
|
-
// Skip if this CallExpression is an argument to another CallExpression
|
|
268
|
-
// (e.g., console.log(fetch(url)) - we don't want to flag fetch(url) here)
|
|
269
224
|
const parent = node
|
|
270
225
|
.parent;
|
|
271
226
|
if (parent && parent.type === 'CallExpression') {
|
|
272
|
-
// Only skip if it's not part of a promise chain
|
|
273
|
-
// If it's the object of a MemberExpression with .then/.catch/.finally, it's a promise
|
|
274
227
|
const grandParent = parent.parent;
|
|
275
228
|
if (!(grandParent &&
|
|
276
229
|
grandParent.type === 'MemberExpression' &&
|
|
@@ -288,7 +241,7 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
288
241
|
suggest: [
|
|
289
242
|
{
|
|
290
243
|
messageId: 'addCatch',
|
|
291
|
-
fix: () => null,
|
|
244
|
+
fix: () => null,
|
|
292
245
|
},
|
|
293
246
|
{
|
|
294
247
|
messageId: 'useTryCatch',
|
|
@@ -301,55 +254,6 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
301
254
|
],
|
|
302
255
|
});
|
|
303
256
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Check identifier expressions for unhandled promises
|
|
306
|
-
* Note: Currently unused, keeping for future implementation
|
|
307
|
-
*/
|
|
308
|
-
/*
|
|
309
|
-
function checkIdentifier(node: TSESTree.Identifier) {
|
|
310
|
-
// Skip identifiers that are inside promise chain callbacks
|
|
311
|
-
if (isInsidePromiseCallback({ type: 'CallExpression', callee: node, arguments: [], optional: false } as TSESTree.CallExpression)) {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Check if it's a promise-like identifier
|
|
316
|
-
if (!isPromiseExpression(node)) {
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Check if it's already handled
|
|
321
|
-
if (isPromiseHandled(node)) {
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Check if it's in a void expression
|
|
326
|
-
if (ignoreVoidExpressions) {
|
|
327
|
-
const parent = (node as TSESTree.Node & { parent?: TSESTree.Node }).parent;
|
|
328
|
-
if (parent && parent.type === 'UnaryExpression' && parent.operator === 'void') {
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
context.report({
|
|
334
|
-
node,
|
|
335
|
-
messageId: 'unhandledPromise',
|
|
336
|
-
suggest: [
|
|
337
|
-
{
|
|
338
|
-
messageId: 'addCatch',
|
|
339
|
-
fix: () => null,
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
messageId: 'useTryCatch',
|
|
343
|
-
fix: () => null,
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
messageId: 'useAwait',
|
|
347
|
-
fix: () => null,
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
*/
|
|
353
257
|
return {
|
|
354
258
|
CallExpression: checkCallExpression,
|
|
355
259
|
};
|
|
@@ -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.cognitiveComplexity = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -20,7 +15,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
20
15
|
cvss: 7.5,
|
|
21
16
|
},
|
|
22
17
|
messages: {
|
|
23
|
-
// 🎯 Token optimization: 40% reduction (60→36 tokens) - keeps complexity metrics inline
|
|
24
18
|
highCognitiveComplexity: (0, eslint_devkit_1.formatLLMMessage)({
|
|
25
19
|
icon: eslint_devkit_1.MessageIcons.COMPLEXITY,
|
|
26
20
|
issueName: 'High cognitive complexity',
|
|
@@ -82,10 +76,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
82
76
|
create(context) {
|
|
83
77
|
const { maxComplexity = 15 } = context.options[0] || {};
|
|
84
78
|
const filename = context.filename;
|
|
85
|
-
/**
|
|
86
|
-
* Calculate cognitive complexity for a function
|
|
87
|
-
* Based on SonarQube's cognitive complexity algorithm
|
|
88
|
-
*/
|
|
89
79
|
function calculateCognitiveComplexity(node) {
|
|
90
80
|
let complexity = 0;
|
|
91
81
|
const breakdown = {
|
|
@@ -101,27 +91,22 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
101
91
|
? node.id.name
|
|
102
92
|
: 'anonymous';
|
|
103
93
|
function traverse(n, currentNesting) {
|
|
104
|
-
// Increment for conditionals
|
|
105
94
|
if (n.type === 'IfStatement') {
|
|
106
95
|
complexity += 1 + currentNesting;
|
|
107
96
|
breakdown.conditionals++;
|
|
108
|
-
// Traverse the test condition to count logical operators
|
|
109
97
|
traverse(n.test, currentNesting);
|
|
110
98
|
traverse(n.consequent, currentNesting + 1);
|
|
111
99
|
if (n.alternate) {
|
|
112
100
|
if (n.alternate.type === 'IfStatement') {
|
|
113
|
-
// else if doesn't increase nesting
|
|
114
101
|
traverse(n.alternate, currentNesting);
|
|
115
102
|
}
|
|
116
103
|
else {
|
|
117
|
-
// else increases nesting
|
|
118
104
|
complexity += 1;
|
|
119
105
|
traverse(n.alternate, currentNesting + 1);
|
|
120
106
|
}
|
|
121
107
|
}
|
|
122
108
|
return;
|
|
123
109
|
}
|
|
124
|
-
// Loops
|
|
125
110
|
if (n.type === 'ForStatement' ||
|
|
126
111
|
n.type === 'ForInStatement' ||
|
|
127
112
|
n.type === 'ForOfStatement' ||
|
|
@@ -152,7 +137,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
152
137
|
}
|
|
153
138
|
return;
|
|
154
139
|
}
|
|
155
|
-
// Switch
|
|
156
140
|
if (n.type === 'SwitchStatement') {
|
|
157
141
|
complexity += 1 + currentNesting;
|
|
158
142
|
breakdown.switches++;
|
|
@@ -160,7 +144,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
160
144
|
n.cases.forEach((c) => traverse(c, currentNesting + 1));
|
|
161
145
|
return;
|
|
162
146
|
}
|
|
163
|
-
// Logical operators (short-circuiting)
|
|
164
147
|
if (n.type === 'LogicalExpression') {
|
|
165
148
|
if (n.operator === '&&' ||
|
|
166
149
|
n.operator === '||' ||
|
|
@@ -169,19 +152,16 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
169
152
|
breakdown.logicalOperators++;
|
|
170
153
|
}
|
|
171
154
|
}
|
|
172
|
-
// Catch clauses
|
|
173
155
|
if (n.type === 'CatchClause') {
|
|
174
156
|
complexity += 1 + currentNesting;
|
|
175
157
|
breakdown.catches++;
|
|
176
158
|
traverse(n.body, currentNesting + 1);
|
|
177
159
|
return;
|
|
178
160
|
}
|
|
179
|
-
// Ternary operators
|
|
180
161
|
if (n.type === 'ConditionalExpression') {
|
|
181
162
|
complexity += 1 + currentNesting;
|
|
182
163
|
breakdown.conditionals++;
|
|
183
164
|
}
|
|
184
|
-
// Recursion
|
|
185
165
|
if (n.type === 'CallExpression') {
|
|
186
166
|
if (n.callee.type === 'Identifier' &&
|
|
187
167
|
n.callee.name === functionName) {
|
|
@@ -189,17 +169,12 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
189
169
|
breakdown.recursion++;
|
|
190
170
|
}
|
|
191
171
|
}
|
|
192
|
-
// Update max nesting
|
|
193
172
|
if (n.type === 'BlockStatement' ||
|
|
194
173
|
n.type === 'FunctionDeclaration' ||
|
|
195
174
|
n.type === 'FunctionExpression' ||
|
|
196
175
|
n.type === 'ArrowFunctionExpression') {
|
|
197
176
|
breakdown.nesting = Math.max(breakdown.nesting, currentNesting);
|
|
198
177
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Traverse children - use a visited set to prevent infinite recursion
|
|
201
|
-
* Only traverse specific AST child properties, not all object properties
|
|
202
|
-
*/
|
|
203
178
|
const visited = new Set();
|
|
204
179
|
function traverseChild(child) {
|
|
205
180
|
if (child && typeof child === 'object' && 'type' in child) {
|
|
@@ -210,7 +185,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
210
185
|
}
|
|
211
186
|
}
|
|
212
187
|
}
|
|
213
|
-
// Known child properties based on ESTree spec
|
|
214
188
|
const childKeys = [
|
|
215
189
|
'body',
|
|
216
190
|
'test',
|
|
@@ -238,7 +212,7 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
238
212
|
'handler',
|
|
239
213
|
'block',
|
|
240
214
|
'finalizer',
|
|
241
|
-
];
|
|
215
|
+
];
|
|
242
216
|
for (const key of childKeys) {
|
|
243
217
|
const child = n[key];
|
|
244
218
|
if (child) {
|
|
@@ -256,12 +230,8 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
256
230
|
}
|
|
257
231
|
return { total: complexity, breakdown };
|
|
258
232
|
}
|
|
259
|
-
/**
|
|
260
|
-
* Analyze function and suggest extractions
|
|
261
|
-
*/
|
|
262
233
|
function suggestExtractions(node, breakdown) {
|
|
263
234
|
const suggestions = [];
|
|
264
|
-
// Suggest extracting deeply nested logic
|
|
265
235
|
if (breakdown.nesting >= 4 && node.loc) {
|
|
266
236
|
suggestions.push({
|
|
267
237
|
name: 'extractNestedLogic',
|
|
@@ -270,7 +240,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
270
240
|
estimatedComplexityReduction: Math.floor(breakdown.nesting * 1.5),
|
|
271
241
|
});
|
|
272
242
|
}
|
|
273
|
-
// Suggest extracting switch/case logic
|
|
274
243
|
if (breakdown.switches >= 2 && node.loc) {
|
|
275
244
|
suggestions.push({
|
|
276
245
|
name: 'refactorSwitchToStrategy',
|
|
@@ -279,7 +248,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
279
248
|
estimatedComplexityReduction: breakdown.switches * 2,
|
|
280
249
|
});
|
|
281
250
|
}
|
|
282
|
-
// Suggest extracting loop logic
|
|
283
251
|
if (breakdown.loops >= 3 && node.loc) {
|
|
284
252
|
suggestions.push({
|
|
285
253
|
name: 'extractLoopLogic',
|
|
@@ -288,7 +256,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
288
256
|
estimatedComplexityReduction: breakdown.loops * 2,
|
|
289
257
|
});
|
|
290
258
|
}
|
|
291
|
-
// Suggest simplifying conditionals
|
|
292
259
|
if (breakdown.conditionals >= 5 && node.loc) {
|
|
293
260
|
suggestions.push({
|
|
294
261
|
name: 'simplifyConditionals',
|
|
@@ -299,10 +266,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
299
266
|
}
|
|
300
267
|
return suggestions;
|
|
301
268
|
}
|
|
302
|
-
/**
|
|
303
|
-
* Suggest architectural patterns
|
|
304
|
-
*/
|
|
305
|
-
// oxlint-disable-next-line consistent-function-scoping
|
|
306
269
|
function suggestPattern(breakdown) {
|
|
307
270
|
if (breakdown.switches >= 2)
|
|
308
271
|
return 'Strategy Pattern';
|
|
@@ -314,11 +277,8 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
314
277
|
return 'Extract Method + Composed Functions';
|
|
315
278
|
return 'Extract Method';
|
|
316
279
|
}
|
|
317
|
-
/**
|
|
318
|
-
* Calculate estimated refactoring time
|
|
319
|
-
*/
|
|
320
280
|
function estimateRefactoringTime(complexity, breakdown) {
|
|
321
|
-
const baseTime = Math.floor((complexity - maxComplexity) * 3);
|
|
281
|
+
const baseTime = Math.floor((complexity - maxComplexity) * 3);
|
|
322
282
|
const nestingPenalty = breakdown.nesting >= 4 ? 15 : 0;
|
|
323
283
|
const totalMinutes = baseTime + nestingPenalty;
|
|
324
284
|
if (totalMinutes < 30)
|
|
@@ -327,9 +287,6 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
327
287
|
return '30-60 minutes';
|
|
328
288
|
return `${Math.ceil(totalMinutes / 60)} hours`;
|
|
329
289
|
}
|
|
330
|
-
/**
|
|
331
|
-
* Check function complexity
|
|
332
|
-
*/
|
|
333
290
|
function checkFunction(node) {
|
|
334
291
|
const { total: complexity, breakdown } = calculateCognitiveComplexity(node);
|
|
335
292
|
if (complexity <= maxComplexity)
|
|
@@ -369,7 +326,7 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
|
369
326
|
pattern,
|
|
370
327
|
reduction: String(suggestion.estimatedComplexityReduction),
|
|
371
328
|
},
|
|
372
|
-
fix: () => null,
|
|
329
|
+
fix: () => null,
|
|
373
330
|
};
|
|
374
331
|
})
|
|
375
332
|
: undefined,
|
|
@@ -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.consistentFunctionScoping = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -52,7 +47,6 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
52
47
|
create(context) {
|
|
53
48
|
const [options] = context.options;
|
|
54
49
|
const { checkArrowFunctions = true } = options || {};
|
|
55
|
-
// Track variables declared in each scope
|
|
56
50
|
const scopeStack = [new Set()];
|
|
57
51
|
function enterScope() {
|
|
58
52
|
scopeStack.push(new Set());
|
|
@@ -77,28 +71,13 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
77
71
|
return outerVars;
|
|
78
72
|
}
|
|
79
73
|
function analyzeFunction(node) {
|
|
80
|
-
// Skip module-level functions (direct children of Program or ExportNamedDeclaration)
|
|
81
74
|
if (node.parent?.type === 'Program' || node.parent?.type === 'ExportNamedDeclaration' || node.parent?.type === 'ExportDefaultDeclaration') {
|
|
82
75
|
return;
|
|
83
76
|
}
|
|
84
|
-
// Class methods / class field initializers — these are bound to the
|
|
85
|
-
// instance and cannot be moved to module scope. The parent chain is
|
|
86
|
-
// `MethodDefinition` (regular methods) or `PropertyDefinition` (class
|
|
87
|
-
// fields). Without this exemption, every method that doesn't reference
|
|
88
|
-
// `this` is wrongly flagged.
|
|
89
77
|
const p = node.parent;
|
|
90
78
|
if (p?.type === 'MethodDefinition' || p?.type === 'PropertyDefinition') {
|
|
91
79
|
return;
|
|
92
80
|
}
|
|
93
|
-
// Inline callbacks passed to higher-order methods. These functions are
|
|
94
|
-
// inline BY DESIGN — moving `arr.reduce((a, b) => a + b)` to a
|
|
95
|
-
// top-level named function loses inline locality without improving
|
|
96
|
-
// anything. Common high-arity hosts:
|
|
97
|
-
// - Array methods: map, filter, reduce, forEach, find, some, every, sort, flatMap
|
|
98
|
-
// - Promise: .then, .catch, .finally
|
|
99
|
-
// - Event hosts: .on('event', ...), addEventListener('event', ...)
|
|
100
|
-
// - Scheduler: setTimeout, setInterval, requestAnimationFrame
|
|
101
|
-
// - Promise constructor: `new Promise((resolve, reject) => ...)`
|
|
102
81
|
if (p?.type === 'CallExpression') {
|
|
103
82
|
const callee = p.callee;
|
|
104
83
|
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
@@ -120,16 +99,13 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
120
99
|
return;
|
|
121
100
|
}
|
|
122
101
|
}
|
|
123
|
-
// `new Promise((resolve, reject) => ...)` — executor must stay inline.
|
|
124
102
|
if (p?.type === 'NewExpression' &&
|
|
125
103
|
p.callee.type === 'Identifier' &&
|
|
126
104
|
p.callee.name === 'Promise') {
|
|
127
105
|
return;
|
|
128
106
|
}
|
|
129
|
-
// Get all variables referenced in the function body
|
|
130
107
|
const referencedVars = new Set();
|
|
131
108
|
function collectReferences(astNode, depth = 0, visited = new Set()) {
|
|
132
|
-
// Prevent infinite recursion
|
|
133
109
|
if (depth > 10 || visited.has(astNode)) {
|
|
134
110
|
return;
|
|
135
111
|
}
|
|
@@ -137,7 +113,6 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
137
113
|
if (astNode.type === 'Identifier') {
|
|
138
114
|
referencedVars.add(astNode.name);
|
|
139
115
|
}
|
|
140
|
-
// Recursively check all child nodes with depth limit
|
|
141
116
|
if (depth < 10) {
|
|
142
117
|
for (const key in astNode) {
|
|
143
118
|
const child = astNode[key];
|
|
@@ -150,31 +125,24 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
150
125
|
});
|
|
151
126
|
}
|
|
152
127
|
else if ('type' in child) {
|
|
153
|
-
// Outer `child && typeof child === 'object'` already guarantees
|
|
154
|
-
// a non-null object here (CodeQL: `js/comparison-between-incompatible-types`).
|
|
155
128
|
collectReferences(child, depth + 1, visited);
|
|
156
129
|
}
|
|
157
130
|
}
|
|
158
131
|
}
|
|
159
132
|
}
|
|
160
133
|
}
|
|
161
|
-
// Collect all references in the function body
|
|
162
134
|
if (node.body.type === 'BlockStatement') {
|
|
163
135
|
node.body.body.forEach((stmt) => collectReferences(stmt));
|
|
164
136
|
}
|
|
165
137
|
else {
|
|
166
|
-
// Arrow function with expression body
|
|
167
138
|
collectReferences(node.body);
|
|
168
139
|
}
|
|
169
|
-
// Check function parameters
|
|
170
140
|
node.params.forEach((param) => {
|
|
171
141
|
if (param.type === 'Identifier') {
|
|
172
142
|
referencedVars.add(param.name);
|
|
173
143
|
}
|
|
174
144
|
});
|
|
175
|
-
// Get variables from outer scopes
|
|
176
145
|
const outerVars = getOuterScopeVariables();
|
|
177
|
-
// Check if function captures any outer variables
|
|
178
146
|
let capturesOuterVar = false;
|
|
179
147
|
for (const ref of referencedVars) {
|
|
180
148
|
if (outerVars.has(ref)) {
|
|
@@ -182,9 +150,7 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
182
150
|
break;
|
|
183
151
|
}
|
|
184
152
|
}
|
|
185
|
-
// If function doesn't capture any outer variables, it can be moved up
|
|
186
153
|
if (!capturesOuterVar) {
|
|
187
|
-
// Additional check: ensure function name doesn't conflict at module scope
|
|
188
154
|
const functionName = node.type === 'FunctionDeclaration' ? node.id?.name : undefined;
|
|
189
155
|
const moduleScope = scopeStack[0];
|
|
190
156
|
if (!functionName || !moduleScope.has(functionName)) {
|
|
@@ -198,11 +164,6 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
198
164
|
{
|
|
199
165
|
messageId: 'moveToModuleScope',
|
|
200
166
|
fix(fixer) {
|
|
201
|
-
// This is a complex fix that would require:
|
|
202
|
-
// 1. Finding the module scope location
|
|
203
|
-
// 2. Moving the function declaration33 3
|
|
204
|
-
// 3. Updating any references
|
|
205
|
-
// For now, just provide a suggestion
|
|
206
167
|
return fixer.insertTextBefore(node, '// TODO: Move this function to module scope - it doesn\'t capture outer variables\n');
|
|
207
168
|
},
|
|
208
169
|
},
|
|
@@ -220,7 +181,6 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
220
181
|
},
|
|
221
182
|
FunctionDeclaration(node) {
|
|
222
183
|
enterScope();
|
|
223
|
-
// Add function parameters to the current scope
|
|
224
184
|
node.params.forEach((param) => {
|
|
225
185
|
if (param.type === 'Identifier') {
|
|
226
186
|
addVariableToCurrentScope(param.name);
|
|
@@ -233,14 +193,11 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
233
193
|
},
|
|
234
194
|
FunctionExpression(node) {
|
|
235
195
|
enterScope();
|
|
236
|
-
// Add function parameters to the current scope
|
|
237
196
|
node.params.forEach((param) => {
|
|
238
197
|
if (param.type === 'Identifier') {
|
|
239
198
|
addVariableToCurrentScope(param.name);
|
|
240
199
|
}
|
|
241
200
|
});
|
|
242
|
-
// Only check function expressions if they are assigned to variables
|
|
243
|
-
// (not just used as callbacks)
|
|
244
201
|
analyzeFunction(node);
|
|
245
202
|
},
|
|
246
203
|
'FunctionExpression:exit'() {
|
|
@@ -248,7 +205,6 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
248
205
|
},
|
|
249
206
|
ArrowFunctionExpression(node) {
|
|
250
207
|
enterScope();
|
|
251
|
-
// Add function parameters to the current scope
|
|
252
208
|
node.params.forEach((param) => {
|
|
253
209
|
if (param.type === 'Identifier') {
|
|
254
210
|
addVariableToCurrentScope(param.name);
|
|
@@ -262,7 +218,6 @@ exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
|
262
218
|
exitScope();
|
|
263
219
|
},
|
|
264
220
|
VariableDeclaration(node) {
|
|
265
|
-
// Add variables to current scope
|
|
266
221
|
node.declarations.forEach((decl) => {
|
|
267
222
|
if (decl.id.type === 'Identifier') {
|
|
268
223
|
addVariableToCurrentScope(decl.id.name);
|