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,381 @@
|
|
|
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.cognitiveComplexity = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
const eslint_devkit_3 = require("@interlace/eslint-devkit");
|
|
12
|
+
exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
|
|
13
|
+
name: 'cognitive-complexity',
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'suggestion',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Enforces a maximum cognitive complexity threshold with refactoring guidance',
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
// 🎯 Token optimization: 40% reduction (60→36 tokens) - keeps complexity metrics inline
|
|
21
|
+
highCognitiveComplexity: (0, eslint_devkit_1.formatLLMMessage)({
|
|
22
|
+
icon: eslint_devkit_1.MessageIcons.COMPLEXITY,
|
|
23
|
+
issueName: 'High cognitive complexity',
|
|
24
|
+
cwe: 'CWE-1104',
|
|
25
|
+
description: '{{functionName}}: {{complexity}}/{{max}} ({{overBy}} over)',
|
|
26
|
+
severity: 'HIGH',
|
|
27
|
+
fix: 'Extract logic to helpers',
|
|
28
|
+
documentationLink: 'https://en.wikipedia.org/wiki/Cognitive_complexity',
|
|
29
|
+
}),
|
|
30
|
+
extractMethod: (0, eslint_devkit_1.formatLLMMessage)({
|
|
31
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
32
|
+
issueName: 'Extract Method',
|
|
33
|
+
description: 'Extract nested logic to separate method',
|
|
34
|
+
severity: 'LOW',
|
|
35
|
+
fix: 'Extract to "{{methodName}}" (reduces complexity by ~{{reduction}})',
|
|
36
|
+
documentationLink: 'https://refactoring.guru/extract-method',
|
|
37
|
+
}),
|
|
38
|
+
simplifyLogic: (0, eslint_devkit_1.formatLLMMessage)({
|
|
39
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
40
|
+
issueName: 'Simplify Logic',
|
|
41
|
+
description: 'Simplify conditional logic',
|
|
42
|
+
severity: 'LOW',
|
|
43
|
+
fix: 'Use guard clauses and early returns',
|
|
44
|
+
documentationLink: 'https://refactoring.guru/replace-nested-conditional-with-guard-clauses',
|
|
45
|
+
}),
|
|
46
|
+
useStrategy: (0, eslint_devkit_1.formatLLMMessage)({
|
|
47
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
48
|
+
issueName: 'Use Strategy Pattern',
|
|
49
|
+
description: 'Apply design pattern to reduce complexity',
|
|
50
|
+
severity: 'LOW',
|
|
51
|
+
fix: 'Apply {{pattern}} pattern',
|
|
52
|
+
documentationLink: 'https://refactoring.guru/design-patterns/strategy',
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
schema: [
|
|
56
|
+
{
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
maxComplexity: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
default: 15,
|
|
62
|
+
minimum: 1,
|
|
63
|
+
},
|
|
64
|
+
includeMetrics: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
default: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
defaultOptions: [
|
|
74
|
+
{
|
|
75
|
+
maxComplexity: 15,
|
|
76
|
+
includeMetrics: true,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
create(context) {
|
|
80
|
+
const options = context.options[0] || {};
|
|
81
|
+
const { maxComplexity = 15 } = options || {};
|
|
82
|
+
const filename = context.filename || context.getFilename();
|
|
83
|
+
/**
|
|
84
|
+
* Calculate cognitive complexity for a function
|
|
85
|
+
* Based on SonarQube's cognitive complexity algorithm
|
|
86
|
+
*/
|
|
87
|
+
function calculateCognitiveComplexity(node) {
|
|
88
|
+
let complexity = 0;
|
|
89
|
+
const breakdown = {
|
|
90
|
+
conditionals: 0,
|
|
91
|
+
loops: 0,
|
|
92
|
+
switches: 0,
|
|
93
|
+
nesting: 0,
|
|
94
|
+
logicalOperators: 0,
|
|
95
|
+
catches: 0,
|
|
96
|
+
recursion: 0,
|
|
97
|
+
};
|
|
98
|
+
const functionName = node.type === 'FunctionDeclaration' && node.id
|
|
99
|
+
? node.id.name
|
|
100
|
+
: 'anonymous';
|
|
101
|
+
function traverse(n, currentNesting) {
|
|
102
|
+
// Increment for conditionals
|
|
103
|
+
if (n.type === 'IfStatement') {
|
|
104
|
+
complexity += 1 + currentNesting;
|
|
105
|
+
breakdown.conditionals++;
|
|
106
|
+
// Traverse the test condition to count logical operators
|
|
107
|
+
traverse(n.test, currentNesting);
|
|
108
|
+
traverse(n.consequent, currentNesting + 1);
|
|
109
|
+
if (n.alternate) {
|
|
110
|
+
if (n.alternate.type === 'IfStatement') {
|
|
111
|
+
// else if doesn't increase nesting
|
|
112
|
+
traverse(n.alternate, currentNesting);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// else increases nesting
|
|
116
|
+
complexity += 1;
|
|
117
|
+
traverse(n.alternate, currentNesting + 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Loops
|
|
123
|
+
if (n.type === 'ForStatement' ||
|
|
124
|
+
n.type === 'ForInStatement' ||
|
|
125
|
+
n.type === 'ForOfStatement' ||
|
|
126
|
+
n.type === 'WhileStatement' ||
|
|
127
|
+
n.type === 'DoWhileStatement') {
|
|
128
|
+
complexity += 1 + currentNesting;
|
|
129
|
+
breakdown.loops++;
|
|
130
|
+
if (n.type === 'ForStatement') {
|
|
131
|
+
if (n.init)
|
|
132
|
+
traverse(n.init, currentNesting);
|
|
133
|
+
if (n.test)
|
|
134
|
+
traverse(n.test, currentNesting);
|
|
135
|
+
if (n.update)
|
|
136
|
+
traverse(n.update, currentNesting);
|
|
137
|
+
traverse(n.body, currentNesting + 1);
|
|
138
|
+
}
|
|
139
|
+
else if (n.type === 'WhileStatement' ||
|
|
140
|
+
n.type === 'DoWhileStatement') {
|
|
141
|
+
traverse(n.test, currentNesting);
|
|
142
|
+
traverse(n.body, currentNesting + 1);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if ('left' in n)
|
|
146
|
+
traverse(n.left, currentNesting);
|
|
147
|
+
if ('right' in n)
|
|
148
|
+
traverse(n.right, currentNesting);
|
|
149
|
+
traverse(n.body, currentNesting + 1);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Switch
|
|
154
|
+
if (n.type === 'SwitchStatement') {
|
|
155
|
+
complexity += 1 + currentNesting;
|
|
156
|
+
breakdown.switches++;
|
|
157
|
+
traverse(n.discriminant, currentNesting);
|
|
158
|
+
n.cases.forEach((c) => traverse(c, currentNesting + 1));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Logical operators (short-circuiting)
|
|
162
|
+
if (n.type === 'LogicalExpression') {
|
|
163
|
+
if (n.operator === '&&' ||
|
|
164
|
+
n.operator === '||' ||
|
|
165
|
+
n.operator === '??') {
|
|
166
|
+
complexity += 1;
|
|
167
|
+
breakdown.logicalOperators++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Catch clauses
|
|
171
|
+
if (n.type === 'CatchClause') {
|
|
172
|
+
complexity += 1 + currentNesting;
|
|
173
|
+
breakdown.catches++;
|
|
174
|
+
traverse(n.body, currentNesting + 1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Ternary operators
|
|
178
|
+
if (n.type === 'ConditionalExpression') {
|
|
179
|
+
complexity += 1 + currentNesting;
|
|
180
|
+
breakdown.conditionals++;
|
|
181
|
+
}
|
|
182
|
+
// Recursion
|
|
183
|
+
if (n.type === 'CallExpression') {
|
|
184
|
+
if (n.callee.type === 'Identifier' &&
|
|
185
|
+
n.callee.name === functionName) {
|
|
186
|
+
complexity += 1;
|
|
187
|
+
breakdown.recursion++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Update max nesting
|
|
191
|
+
if (n.type === 'BlockStatement' ||
|
|
192
|
+
n.type === 'FunctionDeclaration' ||
|
|
193
|
+
n.type === 'FunctionExpression' ||
|
|
194
|
+
n.type === 'ArrowFunctionExpression') {
|
|
195
|
+
breakdown.nesting = Math.max(breakdown.nesting, currentNesting);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Traverse children - use a visited set to prevent infinite recursion
|
|
199
|
+
* Only traverse specific AST child properties, not all object properties
|
|
200
|
+
*/
|
|
201
|
+
const visited = new Set();
|
|
202
|
+
function traverseChild(child) {
|
|
203
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
204
|
+
const childNode = child;
|
|
205
|
+
if (!visited.has(childNode)) {
|
|
206
|
+
visited.add(childNode);
|
|
207
|
+
traverse(childNode, currentNesting);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Known child properties based on ESTree spec
|
|
212
|
+
const childKeys = [
|
|
213
|
+
'body',
|
|
214
|
+
'test',
|
|
215
|
+
'consequent',
|
|
216
|
+
'alternate',
|
|
217
|
+
'init',
|
|
218
|
+
'update',
|
|
219
|
+
'left',
|
|
220
|
+
'right',
|
|
221
|
+
'argument',
|
|
222
|
+
'arguments',
|
|
223
|
+
'callee',
|
|
224
|
+
'object',
|
|
225
|
+
'property',
|
|
226
|
+
'elements',
|
|
227
|
+
'properties',
|
|
228
|
+
'expression',
|
|
229
|
+
'expressions',
|
|
230
|
+
'declarations',
|
|
231
|
+
'declaration',
|
|
232
|
+
'specifiers',
|
|
233
|
+
'source',
|
|
234
|
+
'key',
|
|
235
|
+
'value',
|
|
236
|
+
'handler',
|
|
237
|
+
'block',
|
|
238
|
+
'finalizer',
|
|
239
|
+
]; // handler for TryStatement.catch, block/finalizer for TryStatement
|
|
240
|
+
for (const key of childKeys) {
|
|
241
|
+
const child = n[key];
|
|
242
|
+
if (child) {
|
|
243
|
+
if (Array.isArray(child)) {
|
|
244
|
+
child.forEach(traverseChild);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
traverseChild(child);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (node.body) {
|
|
253
|
+
traverse(node.body, 0);
|
|
254
|
+
}
|
|
255
|
+
return { total: complexity, breakdown };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Analyze function and suggest extractions
|
|
259
|
+
*/
|
|
260
|
+
function suggestExtractions(node, breakdown) {
|
|
261
|
+
const suggestions = [];
|
|
262
|
+
// Suggest extracting deeply nested logic
|
|
263
|
+
if (breakdown.nesting >= 4 && node.loc) {
|
|
264
|
+
suggestions.push({
|
|
265
|
+
name: 'extractNestedLogic',
|
|
266
|
+
reason: `Nesting depth of ${breakdown.nesting} makes code hard to follow`,
|
|
267
|
+
lineRange: [node.loc.start.line, node.loc.end.line],
|
|
268
|
+
estimatedComplexityReduction: Math.floor(breakdown.nesting * 1.5),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// Suggest extracting switch/case logic
|
|
272
|
+
if (breakdown.switches >= 2 && node.loc) {
|
|
273
|
+
suggestions.push({
|
|
274
|
+
name: 'refactorSwitchToStrategy',
|
|
275
|
+
reason: `${breakdown.switches} switch statements suggest strategy pattern`,
|
|
276
|
+
lineRange: [node.loc.start.line, node.loc.end.line],
|
|
277
|
+
estimatedComplexityReduction: breakdown.switches * 2,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Suggest extracting loop logic
|
|
281
|
+
if (breakdown.loops >= 3 && node.loc) {
|
|
282
|
+
suggestions.push({
|
|
283
|
+
name: 'extractLoopLogic',
|
|
284
|
+
reason: `${breakdown.loops} loops can be extracted to separate methods`,
|
|
285
|
+
lineRange: [node.loc.start.line, node.loc.end.line],
|
|
286
|
+
estimatedComplexityReduction: breakdown.loops * 2,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// Suggest simplifying conditionals
|
|
290
|
+
if (breakdown.conditionals >= 5 && node.loc) {
|
|
291
|
+
suggestions.push({
|
|
292
|
+
name: 'simplifyConditionals',
|
|
293
|
+
reason: `${breakdown.conditionals} conditional branches could use Guard Clauses`,
|
|
294
|
+
lineRange: [node.loc.start.line, node.loc.end.line],
|
|
295
|
+
estimatedComplexityReduction: Math.floor(breakdown.conditionals * 0.8),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return suggestions;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Suggest architectural patterns
|
|
302
|
+
*/
|
|
303
|
+
function suggestPattern(breakdown) {
|
|
304
|
+
if (breakdown.switches >= 2)
|
|
305
|
+
return 'Strategy Pattern';
|
|
306
|
+
if (breakdown.conditionals >= 5)
|
|
307
|
+
return 'Guard Clauses + Early Return';
|
|
308
|
+
if (breakdown.loops >= 3)
|
|
309
|
+
return 'Extract Method + Pipeline';
|
|
310
|
+
if (breakdown.nesting >= 4)
|
|
311
|
+
return 'Extract Method + Composed Functions';
|
|
312
|
+
return 'Extract Method';
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Calculate estimated refactoring time
|
|
316
|
+
*/
|
|
317
|
+
function estimateRefactoringTime(complexity, breakdown) {
|
|
318
|
+
const baseTime = Math.floor((complexity - maxComplexity) * 3); // 3 minutes per point
|
|
319
|
+
const nestingPenalty = breakdown.nesting >= 4 ? 15 : 0;
|
|
320
|
+
const totalMinutes = baseTime + nestingPenalty;
|
|
321
|
+
if (totalMinutes < 30)
|
|
322
|
+
return `${totalMinutes} minutes`;
|
|
323
|
+
if (totalMinutes < 60)
|
|
324
|
+
return '30-60 minutes';
|
|
325
|
+
return `${Math.ceil(totalMinutes / 60)} hours`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Check function complexity
|
|
329
|
+
*/
|
|
330
|
+
function checkFunction(node) {
|
|
331
|
+
const { total: complexity, breakdown } = calculateCognitiveComplexity(node);
|
|
332
|
+
if (complexity <= maxComplexity)
|
|
333
|
+
return;
|
|
334
|
+
const functionSignature = (0, eslint_devkit_3.extractFunctionSignature)(node);
|
|
335
|
+
const suggestions = suggestExtractions(node, breakdown);
|
|
336
|
+
const pattern = suggestPattern(breakdown);
|
|
337
|
+
const estimatedTime = estimateRefactoringTime(complexity, breakdown);
|
|
338
|
+
context.report({
|
|
339
|
+
node,
|
|
340
|
+
messageId: 'highCognitiveComplexity',
|
|
341
|
+
data: {
|
|
342
|
+
functionName: functionSignature,
|
|
343
|
+
complexity: String(complexity),
|
|
344
|
+
max: String(maxComplexity),
|
|
345
|
+
overBy: String(complexity - maxComplexity),
|
|
346
|
+
current: String(complexity),
|
|
347
|
+
filePath: filename,
|
|
348
|
+
line: String(node.loc?.start.line ?? 0),
|
|
349
|
+
conditionals: String(breakdown.conditionals),
|
|
350
|
+
loops: String(breakdown.loops),
|
|
351
|
+
nesting: String(breakdown.nesting),
|
|
352
|
+
pattern,
|
|
353
|
+
estimatedTime,
|
|
354
|
+
},
|
|
355
|
+
suggest: suggestions.length > 0
|
|
356
|
+
? suggestions.map((suggestion, index) => {
|
|
357
|
+
const messageId = index === 0
|
|
358
|
+
? 'extractMethod'
|
|
359
|
+
: index === 1
|
|
360
|
+
? 'useStrategy'
|
|
361
|
+
: 'simplifyLogic';
|
|
362
|
+
return {
|
|
363
|
+
messageId,
|
|
364
|
+
data: {
|
|
365
|
+
methodName: suggestion.name,
|
|
366
|
+
pattern,
|
|
367
|
+
reduction: String(suggestion.estimatedComplexityReduction),
|
|
368
|
+
},
|
|
369
|
+
fix: () => null, // Complex refactoring, cannot auto-fix
|
|
370
|
+
};
|
|
371
|
+
})
|
|
372
|
+
: undefined,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
FunctionDeclaration: checkFunction,
|
|
377
|
+
FunctionExpression: checkFunction,
|
|
378
|
+
ArrowFunctionExpression: checkFunction,
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
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: consistent-function-scoping
|
|
8
|
+
* Disallow functions that are declared in a scope which does not capture any variables from the outer scope
|
|
9
|
+
*/
|
|
10
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
11
|
+
type MessageIds = 'inconsistentFunctionScoping' | 'moveToModuleScope';
|
|
12
|
+
export interface Options {
|
|
13
|
+
/** Check arrow functions for scoping issues */
|
|
14
|
+
checkArrowFunctions?: boolean;
|
|
15
|
+
}
|
|
16
|
+
type RuleOptions = [Options?];
|
|
17
|
+
export declare const consistentFunctionScoping: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,226 @@
|
|
|
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.consistentFunctionScoping = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
exports.consistentFunctionScoping = (0, eslint_devkit_1.createRule)({
|
|
12
|
+
name: 'consistent-function-scoping',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Move function definitions to the highest possible scope to improve readability and performance',
|
|
17
|
+
},
|
|
18
|
+
hasSuggestions: true,
|
|
19
|
+
messages: {
|
|
20
|
+
inconsistentFunctionScoping: (0, eslint_devkit_2.formatLLMMessage)({
|
|
21
|
+
icon: eslint_devkit_2.MessageIcons.ARCHITECTURE,
|
|
22
|
+
issueName: 'Inconsistent Function Scoping',
|
|
23
|
+
description: 'Function can be moved to higher scope as it doesn\'t capture outer variables',
|
|
24
|
+
severity: 'MEDIUM',
|
|
25
|
+
fix: 'Move function declaration to module scope',
|
|
26
|
+
documentationLink: 'https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-function-scoping.md',
|
|
27
|
+
}),
|
|
28
|
+
moveToModuleScope: (0, eslint_devkit_2.formatLLMMessage)({
|
|
29
|
+
icon: eslint_devkit_2.MessageIcons.ARCHITECTURE,
|
|
30
|
+
issueName: 'Function Scoping Optimization',
|
|
31
|
+
description: 'Function does not use variables from its containing scope and can be moved to module level',
|
|
32
|
+
severity: 'MEDIUM',
|
|
33
|
+
fix: 'Move function outside current scope: extract `function helper() { return "value"; }` to module level before the containing function/class',
|
|
34
|
+
documentationLink: 'https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-function-scoping.md',
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
schema: [
|
|
38
|
+
{
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
checkArrowFunctions: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
default: true,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
defaultOptions: [{ checkArrowFunctions: true }],
|
|
51
|
+
create(context) {
|
|
52
|
+
const [options] = context.options;
|
|
53
|
+
const { checkArrowFunctions = true } = options || {};
|
|
54
|
+
// Track variables declared in each scope
|
|
55
|
+
const scopeStack = [new Set()];
|
|
56
|
+
function enterScope() {
|
|
57
|
+
scopeStack.push(new Set());
|
|
58
|
+
}
|
|
59
|
+
function exitScope() {
|
|
60
|
+
scopeStack.pop();
|
|
61
|
+
}
|
|
62
|
+
function addVariableToCurrentScope(name) {
|
|
63
|
+
const currentScope = scopeStack[scopeStack.length - 1];
|
|
64
|
+
if (currentScope) {
|
|
65
|
+
currentScope.add(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function getOuterScopeVariables() {
|
|
69
|
+
const outerScopes = scopeStack.slice(0, -1);
|
|
70
|
+
const outerVars = new Set();
|
|
71
|
+
for (const scope of outerScopes) {
|
|
72
|
+
for (const varName of scope) {
|
|
73
|
+
outerVars.add(varName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return outerVars;
|
|
77
|
+
}
|
|
78
|
+
function analyzeFunction(node) {
|
|
79
|
+
// Skip module-level functions (direct children of Program or ExportNamedDeclaration)
|
|
80
|
+
if (node.parent?.type === 'Program' || node.parent?.type === 'ExportNamedDeclaration' || node.parent?.type === 'ExportDefaultDeclaration') {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Get all variables referenced in the function body
|
|
84
|
+
const referencedVars = new Set();
|
|
85
|
+
function collectReferences(node, depth = 0, visited = new Set()) {
|
|
86
|
+
// Prevent infinite recursion
|
|
87
|
+
if (depth > 10 || visited.has(node)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
visited.add(node);
|
|
91
|
+
if (node.type === 'Identifier') {
|
|
92
|
+
referencedVars.add(node.name);
|
|
93
|
+
}
|
|
94
|
+
// Recursively check all child nodes with depth limit
|
|
95
|
+
if (depth < 10) {
|
|
96
|
+
for (const key in node) {
|
|
97
|
+
const child = node[key];
|
|
98
|
+
if (child && typeof child === 'object') {
|
|
99
|
+
if (Array.isArray(child)) {
|
|
100
|
+
child.forEach(item => {
|
|
101
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
102
|
+
collectReferences(item, depth + 1, visited);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else if ('type' in child && typeof child === 'object' && child !== null) {
|
|
107
|
+
collectReferences(child, depth + 1, visited);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Collect all references in the function body
|
|
114
|
+
if (node.body.type === 'BlockStatement') {
|
|
115
|
+
node.body.body.forEach((stmt) => collectReferences(stmt));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Arrow function with expression body
|
|
119
|
+
collectReferences(node.body);
|
|
120
|
+
}
|
|
121
|
+
// Check function parameters
|
|
122
|
+
node.params.forEach((param) => {
|
|
123
|
+
if (param.type === 'Identifier') {
|
|
124
|
+
referencedVars.add(param.name);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Get variables from outer scopes
|
|
128
|
+
const outerVars = getOuterScopeVariables();
|
|
129
|
+
// Check if function captures any outer variables
|
|
130
|
+
let capturesOuterVar = false;
|
|
131
|
+
for (const ref of referencedVars) {
|
|
132
|
+
if (outerVars.has(ref)) {
|
|
133
|
+
capturesOuterVar = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// If function doesn't capture any outer variables, it can be moved up
|
|
138
|
+
if (!capturesOuterVar) {
|
|
139
|
+
// Additional check: ensure function name doesn't conflict at module scope
|
|
140
|
+
const functionName = node.type === 'FunctionDeclaration' ? node.id?.name : undefined;
|
|
141
|
+
const moduleScope = scopeStack[0];
|
|
142
|
+
if (!functionName || !moduleScope.has(functionName)) {
|
|
143
|
+
context.report({
|
|
144
|
+
node,
|
|
145
|
+
messageId: 'inconsistentFunctionScoping',
|
|
146
|
+
data: {
|
|
147
|
+
functionName: functionName || 'anonymous function',
|
|
148
|
+
},
|
|
149
|
+
suggest: [
|
|
150
|
+
{
|
|
151
|
+
messageId: 'moveToModuleScope',
|
|
152
|
+
fix(fixer) {
|
|
153
|
+
// This is a complex fix that would require:
|
|
154
|
+
// 1. Finding the module scope location
|
|
155
|
+
// 2. Moving the function declaration33 3
|
|
156
|
+
// 3. Updating any references
|
|
157
|
+
// For now, just provide a suggestion
|
|
158
|
+
return fixer.insertTextBefore(node, '// TODO: Move this function to module scope - it doesn\'t capture outer variables\n');
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
Program() {
|
|
168
|
+
enterScope();
|
|
169
|
+
},
|
|
170
|
+
'Program:exit'() {
|
|
171
|
+
exitScope();
|
|
172
|
+
},
|
|
173
|
+
FunctionDeclaration(node) {
|
|
174
|
+
enterScope();
|
|
175
|
+
// Add function parameters to the current scope
|
|
176
|
+
node.params.forEach((param) => {
|
|
177
|
+
if (param.type === 'Identifier') {
|
|
178
|
+
addVariableToCurrentScope(param.name);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
analyzeFunction(node);
|
|
182
|
+
},
|
|
183
|
+
'FunctionDeclaration:exit'() {
|
|
184
|
+
exitScope();
|
|
185
|
+
},
|
|
186
|
+
FunctionExpression(node) {
|
|
187
|
+
enterScope();
|
|
188
|
+
// Add function parameters to the current scope
|
|
189
|
+
node.params.forEach((param) => {
|
|
190
|
+
if (param.type === 'Identifier') {
|
|
191
|
+
addVariableToCurrentScope(param.name);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// Only check function expressions if they are assigned to variables
|
|
195
|
+
// (not just used as callbacks)
|
|
196
|
+
analyzeFunction(node);
|
|
197
|
+
},
|
|
198
|
+
'FunctionExpression:exit'() {
|
|
199
|
+
exitScope();
|
|
200
|
+
},
|
|
201
|
+
ArrowFunctionExpression(node) {
|
|
202
|
+
enterScope();
|
|
203
|
+
// Add function parameters to the current scope
|
|
204
|
+
node.params.forEach((param) => {
|
|
205
|
+
if (param.type === 'Identifier') {
|
|
206
|
+
addVariableToCurrentScope(param.name);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
if (checkArrowFunctions) {
|
|
210
|
+
analyzeFunction(node);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
'ArrowFunctionExpression:exit'() {
|
|
214
|
+
exitScope();
|
|
215
|
+
},
|
|
216
|
+
VariableDeclaration(node) {
|
|
217
|
+
// Add variables to current scope
|
|
218
|
+
node.declarations.forEach((decl) => {
|
|
219
|
+
if (decl.id.type === 'Identifier') {
|
|
220
|
+
addVariableToCurrentScope(decl.id.name);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
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: identical-functions
|
|
8
|
+
* Detects functions with identical implementations and suggests DRY refactoring
|
|
9
|
+
* Inspired by SonarQube RSPEC-4144
|
|
10
|
+
*
|
|
11
|
+
* @see https://rules.sonarsource.com/javascript/RSPEC-4144/
|
|
12
|
+
*/
|
|
13
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
14
|
+
type MessageIds = 'identicalFunctions' | 'extractGeneric' | 'useHigherOrder' | 'applyInheritance';
|
|
15
|
+
export interface Options {
|
|
16
|
+
/** Minimum lines to consider for duplicate detection. Default: 3 */
|
|
17
|
+
minLines?: number;
|
|
18
|
+
/** Similarity percentage threshold (0-100). Default: 90 */
|
|
19
|
+
similarityThreshold?: number;
|
|
20
|
+
/** Ignore test files. Default: false */
|
|
21
|
+
ignoreTestFiles?: boolean;
|
|
22
|
+
}
|
|
23
|
+
type RuleOptions = [Options?];
|
|
24
|
+
export declare const identicalFunctions: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
25
|
+
name: string;
|
|
26
|
+
};
|
|
27
|
+
export {};
|