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,310 @@
|
|
|
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.identicalFunctions = 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.identicalFunctions = (0, eslint_devkit_2.createRule)({
|
|
13
|
+
name: 'identical-functions',
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'suggestion',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Detects duplicate function implementations with DRY refactoring suggestions',
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
// 🎯 Token optimization: 43% reduction (56→32 tokens) - DRY principle violation detected
|
|
21
|
+
identicalFunctions: (0, eslint_devkit_1.formatLLMMessage)({
|
|
22
|
+
icon: eslint_devkit_1.MessageIcons.DUPLICATION,
|
|
23
|
+
issueName: 'Code duplication',
|
|
24
|
+
description: '{{count}} duplicates ({{similarity}}% similar)',
|
|
25
|
+
severity: 'MEDIUM',
|
|
26
|
+
fix: 'Extract to reusable function',
|
|
27
|
+
documentationLink: 'https://en.wikipedia.org/wiki/Don%27t_repeat_yourself',
|
|
28
|
+
}),
|
|
29
|
+
extractGeneric: (0, eslint_devkit_1.formatLLMMessage)({
|
|
30
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
31
|
+
issueName: 'Extract Generic',
|
|
32
|
+
description: 'Extract to generic function',
|
|
33
|
+
severity: 'LOW',
|
|
34
|
+
fix: 'Create shared function with parameters',
|
|
35
|
+
documentationLink: 'https://en.wikipedia.org/wiki/Don%27t_repeat_yourself',
|
|
36
|
+
}),
|
|
37
|
+
useHigherOrder: (0, eslint_devkit_1.formatLLMMessage)({
|
|
38
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
39
|
+
issueName: 'Use Higher-Order',
|
|
40
|
+
description: 'Use higher-order function pattern',
|
|
41
|
+
severity: 'LOW',
|
|
42
|
+
fix: 'Create factory function that returns specialized functions',
|
|
43
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Glossary/Higher-order_function',
|
|
44
|
+
}),
|
|
45
|
+
applyInheritance: (0, eslint_devkit_1.formatLLMMessage)({
|
|
46
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
47
|
+
issueName: 'Use Composition',
|
|
48
|
+
description: 'Use inheritance/composition',
|
|
49
|
+
severity: 'LOW',
|
|
50
|
+
fix: 'Extract common behavior to base class or mixin',
|
|
51
|
+
documentationLink: 'https://en.wikipedia.org/wiki/Composition_over_inheritance',
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
schema: [
|
|
55
|
+
{
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
minLines: {
|
|
59
|
+
type: 'number',
|
|
60
|
+
default: 3,
|
|
61
|
+
minimum: 1,
|
|
62
|
+
description: 'Minimum lines to consider for duplication',
|
|
63
|
+
},
|
|
64
|
+
similarityThreshold: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
default: 0.9,
|
|
67
|
+
minimum: 0.5,
|
|
68
|
+
maximum: 1,
|
|
69
|
+
description: 'Similarity threshold (0.5-1.0)',
|
|
70
|
+
},
|
|
71
|
+
ignoreTestFiles: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
default: true,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
defaultOptions: [
|
|
81
|
+
{
|
|
82
|
+
minLines: 3,
|
|
83
|
+
similarityThreshold: 0.9,
|
|
84
|
+
ignoreTestFiles: true,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
create(context) {
|
|
88
|
+
const options = context.options[0] || {};
|
|
89
|
+
const { minLines = 3, similarityThreshold = 0.9, ignoreTestFiles = true, } = options || {};
|
|
90
|
+
const sourceCode = context.sourceCode || context.sourceCode;
|
|
91
|
+
const filename = context.filename || context.getFilename();
|
|
92
|
+
// Skip test files if configured
|
|
93
|
+
if (ignoreTestFiles && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
const functions = [];
|
|
97
|
+
/**
|
|
98
|
+
* Normalize function body for comparison
|
|
99
|
+
* Remove variable names, keep structure
|
|
100
|
+
*/
|
|
101
|
+
function normalizeBody(body) {
|
|
102
|
+
return (body
|
|
103
|
+
// Remove whitespace
|
|
104
|
+
.replace(/\s+/g, ' ')
|
|
105
|
+
// Normalize string quotes
|
|
106
|
+
.replace(/["'`]/g, '"')
|
|
107
|
+
// Normalize variable names to generic identifiers
|
|
108
|
+
.replace(/\b[a-z_$][a-zA-Z0-9_$]*\b/g, 'VAR')
|
|
109
|
+
// Remove comments
|
|
110
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
111
|
+
.replace(/\/\/.*/g, '')
|
|
112
|
+
.trim());
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Calculate similarity between two normalized strings
|
|
116
|
+
* Using Levenshtein distance ratio
|
|
117
|
+
*/
|
|
118
|
+
function calculateSimilarity(str1, str2) {
|
|
119
|
+
if (str1 === str2)
|
|
120
|
+
return 1.0;
|
|
121
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
122
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
123
|
+
if (longer.length === 0)
|
|
124
|
+
return 1.0;
|
|
125
|
+
const editDistance = levenshteinDistance(longer, shorter);
|
|
126
|
+
return (longer.length - editDistance) / longer.length;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Levenshtein distance algorithm
|
|
130
|
+
*/
|
|
131
|
+
function levenshteinDistance(str1, str2) {
|
|
132
|
+
const matrix = [];
|
|
133
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
134
|
+
matrix[i] = [i];
|
|
135
|
+
}
|
|
136
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
137
|
+
matrix[0][j] = j;
|
|
138
|
+
}
|
|
139
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
140
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
141
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
142
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return matrix[str2.length][str1.length];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Find groups of similar functions
|
|
153
|
+
*/
|
|
154
|
+
function findDuplicationGroups() {
|
|
155
|
+
const groups = [];
|
|
156
|
+
const processed = new Set();
|
|
157
|
+
for (let i = 0; i < functions.length; i++) {
|
|
158
|
+
if (processed.has(i))
|
|
159
|
+
continue;
|
|
160
|
+
const group = [functions[i]];
|
|
161
|
+
processed.add(i);
|
|
162
|
+
for (let j = i + 1; j < functions.length; j++) {
|
|
163
|
+
if (processed.has(j))
|
|
164
|
+
continue;
|
|
165
|
+
const similarity = calculateSimilarity(functions[i].normalizedBody, functions[j].normalizedBody);
|
|
166
|
+
if (similarity >= similarityThreshold) {
|
|
167
|
+
group.push(functions[j]);
|
|
168
|
+
processed.add(j);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (group.length >= 2) {
|
|
172
|
+
const avgSimilarity = group.reduce((sum, func, idx) => {
|
|
173
|
+
if (idx === 0)
|
|
174
|
+
return 0;
|
|
175
|
+
return (sum +
|
|
176
|
+
calculateSimilarity(group[0].normalizedBody, func.normalizedBody));
|
|
177
|
+
}, 0) /
|
|
178
|
+
(group.length - 1);
|
|
179
|
+
groups.push({
|
|
180
|
+
functions: group,
|
|
181
|
+
similarityScore: avgSimilarity,
|
|
182
|
+
commonPattern: functions[i].normalizedBody,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return groups;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Generate unified function suggestion
|
|
190
|
+
*/
|
|
191
|
+
function generateUnifiedFunction(group) {
|
|
192
|
+
const firstFunc = group.functions[0];
|
|
193
|
+
// Analyze parameter differences
|
|
194
|
+
const allParams = new Set();
|
|
195
|
+
group.functions.forEach((func) => func.params.forEach((p) => allParams.add(p)));
|
|
196
|
+
// Generate generic function name
|
|
197
|
+
const baseName = firstFunc.name.replace(/^(handle|process|get|set|create|update|delete)/, '');
|
|
198
|
+
const genericName = `handle${baseName || 'Generic'}`;
|
|
199
|
+
// If functions differ only in constant values, extract as parameter
|
|
200
|
+
const paramList = Array.from(allParams);
|
|
201
|
+
if (paramList.length > firstFunc.params.length) {
|
|
202
|
+
paramList.push('options');
|
|
203
|
+
}
|
|
204
|
+
return `function ${genericName}(${paramList.join(', ')}) {\n ${firstFunc.body.trim()}\n}`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Suggest refactoring approach
|
|
208
|
+
*/
|
|
209
|
+
function suggestRefactoringApproach(group) {
|
|
210
|
+
const funcNames = group.functions.map((f) => f.name);
|
|
211
|
+
const hasRolePattern = funcNames.some((name) => /user|admin|guest|customer/i.test(name));
|
|
212
|
+
const hasTypePattern = funcNames.some((name) => /payment|shipping|billing|email|sms/i.test(name));
|
|
213
|
+
if (hasRolePattern || hasTypePattern) {
|
|
214
|
+
return {
|
|
215
|
+
approach: 'Parameter Object + Strategy Pattern',
|
|
216
|
+
pattern: 'Extract discriminator as parameter',
|
|
217
|
+
complexity: 'moderate',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (group.functions[0].params.length > 0) {
|
|
221
|
+
return {
|
|
222
|
+
approach: 'Higher-Order Function',
|
|
223
|
+
pattern: 'Extract common logic, inject differences',
|
|
224
|
+
complexity: 'simple',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
approach: 'Extract Method',
|
|
229
|
+
pattern: 'DRY - Single source of truth',
|
|
230
|
+
complexity: 'simple',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Store function information
|
|
235
|
+
*/
|
|
236
|
+
function storeFunctionInfo(node) {
|
|
237
|
+
const body = node.body ? sourceCode.getText(node.body) : '';
|
|
238
|
+
const lines = body.split('\n').length;
|
|
239
|
+
if (lines < minLines)
|
|
240
|
+
return;
|
|
241
|
+
const name = (0, eslint_devkit_3.extractFunctionSignature)(node)
|
|
242
|
+
.split('(')[0]
|
|
243
|
+
.replace('function ', '');
|
|
244
|
+
const params = node.params.map((p) => p.type === 'Identifier' ? p.name : sourceCode.getText(p));
|
|
245
|
+
functions.push({
|
|
246
|
+
node,
|
|
247
|
+
name: name || 'anonymous',
|
|
248
|
+
body,
|
|
249
|
+
normalizedBody: normalizeBody(body),
|
|
250
|
+
lines,
|
|
251
|
+
location: `${filename}:${node.loc?.start.line}`,
|
|
252
|
+
params,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Report duplications after analyzing all functions
|
|
257
|
+
*/
|
|
258
|
+
function reportDuplications() {
|
|
259
|
+
const groups = findDuplicationGroups();
|
|
260
|
+
groups.forEach((group) => {
|
|
261
|
+
const refactoringApproach = suggestRefactoringApproach(group);
|
|
262
|
+
const unifiedFunction = generateUnifiedFunction(group);
|
|
263
|
+
const primaryFunction = group.functions[0];
|
|
264
|
+
const similarityPercent = Math.round(group.similarityScore * 100);
|
|
265
|
+
context.report({
|
|
266
|
+
node: primaryFunction.node,
|
|
267
|
+
messageId: 'identicalFunctions',
|
|
268
|
+
data: {
|
|
269
|
+
count: String(group.functions.length),
|
|
270
|
+
similarity: String(similarityPercent),
|
|
271
|
+
filePath: filename,
|
|
272
|
+
line: String(primaryFunction.node.loc?.start.line ?? 0),
|
|
273
|
+
},
|
|
274
|
+
suggest: [
|
|
275
|
+
{
|
|
276
|
+
messageId: 'extractGeneric',
|
|
277
|
+
data: {
|
|
278
|
+
functionName: unifiedFunction.match(/function (\w+)/)?.[1] ||
|
|
279
|
+
'handleGeneric',
|
|
280
|
+
},
|
|
281
|
+
fix: () => null,
|
|
282
|
+
},
|
|
283
|
+
...(refactoringApproach.approach.includes('Higher-Order')
|
|
284
|
+
? [
|
|
285
|
+
{
|
|
286
|
+
messageId: 'useHigherOrder',
|
|
287
|
+
fix: () => null,
|
|
288
|
+
},
|
|
289
|
+
]
|
|
290
|
+
: []),
|
|
291
|
+
...(refactoringApproach.approach.includes('Strategy')
|
|
292
|
+
? [
|
|
293
|
+
{
|
|
294
|
+
messageId: 'applyInheritance',
|
|
295
|
+
fix: () => null,
|
|
296
|
+
},
|
|
297
|
+
]
|
|
298
|
+
: []),
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
FunctionDeclaration: storeFunctionInfo,
|
|
305
|
+
FunctionExpression: storeFunctionInfo,
|
|
306
|
+
ArrowFunctionExpression: storeFunctionInfo,
|
|
307
|
+
'Program:exit': reportDuplications,
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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: max-parameters
|
|
8
|
+
* Detects functions with too many parameters
|
|
9
|
+
*
|
|
10
|
+
* Note: ESLint has max-params, but this rule provides LLM-optimized messages
|
|
11
|
+
* and additional context about refactoring to object parameters
|
|
12
|
+
*
|
|
13
|
+
* @see https://rules.sonarsource.com/javascript/RSPEC-107/
|
|
14
|
+
*/
|
|
15
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
16
|
+
type MessageIds = 'tooManyParameters' | 'useObjectParameter' | 'extractToClass' | 'splitFunction';
|
|
17
|
+
export interface Options {
|
|
18
|
+
/** Maximum allowed parameters. Default: 4 */
|
|
19
|
+
max?: number;
|
|
20
|
+
/** Ignore constructors. Default: false */
|
|
21
|
+
ignoreConstructors?: boolean;
|
|
22
|
+
/** Ignore overridden methods. Default: false */
|
|
23
|
+
ignoreOverriddenMethods?: boolean;
|
|
24
|
+
}
|
|
25
|
+
type RuleOptions = [Options?];
|
|
26
|
+
export declare const maxParameters: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
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.maxParameters = 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
|
+
/**
|
|
13
|
+
* Count function parameters
|
|
14
|
+
*/
|
|
15
|
+
function countParameters(node) {
|
|
16
|
+
return node.params.length;
|
|
17
|
+
}
|
|
18
|
+
exports.maxParameters = (0, eslint_devkit_2.createRule)({
|
|
19
|
+
name: 'max-parameters',
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'suggestion',
|
|
22
|
+
docs: {
|
|
23
|
+
description: 'Detects functions with too many parameters',
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
tooManyParameters: (0, eslint_devkit_1.formatLLMMessage)({
|
|
27
|
+
icon: eslint_devkit_1.MessageIcons.COMPLEXITY,
|
|
28
|
+
issueName: 'Too many parameters',
|
|
29
|
+
description: '{{functionName}}: {{count}} parameters (max: {{max}})',
|
|
30
|
+
severity: 'MEDIUM',
|
|
31
|
+
fix: 'Refactor to use object parameter or split function',
|
|
32
|
+
documentationLink: 'https://rules.sonarsource.com/javascript/RSPEC-107/',
|
|
33
|
+
}),
|
|
34
|
+
useObjectParameter: (0, eslint_devkit_1.formatLLMMessage)({
|
|
35
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
36
|
+
issueName: 'Use Object Parameter',
|
|
37
|
+
description: 'Use object parameter pattern',
|
|
38
|
+
severity: 'LOW',
|
|
39
|
+
fix: 'function({ param1, param2, param3 })',
|
|
40
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment',
|
|
41
|
+
}),
|
|
42
|
+
extractToClass: (0, eslint_devkit_1.formatLLMMessage)({
|
|
43
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
44
|
+
issueName: 'Extract to Class',
|
|
45
|
+
description: 'Extract to class with properties',
|
|
46
|
+
severity: 'LOW',
|
|
47
|
+
fix: 'Create class to hold related parameters',
|
|
48
|
+
documentationLink: 'https://refactoring.guru/introduce-parameter-object',
|
|
49
|
+
}),
|
|
50
|
+
splitFunction: (0, eslint_devkit_1.formatLLMMessage)({
|
|
51
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
52
|
+
issueName: 'Split Function',
|
|
53
|
+
description: 'Split into smaller functions',
|
|
54
|
+
severity: 'LOW',
|
|
55
|
+
fix: 'Extract logic into separate focused functions',
|
|
56
|
+
documentationLink: 'https://refactoring.guru/smells/long-parameter-list',
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
schema: [
|
|
60
|
+
{
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
max: {
|
|
64
|
+
type: 'number',
|
|
65
|
+
default: 4,
|
|
66
|
+
minimum: 1,
|
|
67
|
+
},
|
|
68
|
+
ignoreConstructors: {
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
default: false,
|
|
71
|
+
},
|
|
72
|
+
ignoreOverriddenMethods: {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
default: false,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
defaultOptions: [
|
|
82
|
+
{
|
|
83
|
+
max: 4,
|
|
84
|
+
ignoreConstructors: false,
|
|
85
|
+
ignoreOverriddenMethods: false,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
create(context, [options = {}]) {
|
|
89
|
+
const { max = 4, ignoreConstructors = false,
|
|
90
|
+
// ignoreOverriddenMethods = false, // Not used
|
|
91
|
+
} = options || {};
|
|
92
|
+
/**
|
|
93
|
+
* Check function parameters
|
|
94
|
+
*/
|
|
95
|
+
function checkFunction(node) {
|
|
96
|
+
// Check if it's a constructor
|
|
97
|
+
if (ignoreConstructors) {
|
|
98
|
+
if (node.type === 'FunctionDeclaration' &&
|
|
99
|
+
node.id &&
|
|
100
|
+
node.id.name &&
|
|
101
|
+
/^[A-Z]/.test(node.id.name)) {
|
|
102
|
+
// Likely a constructor
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const paramCount = countParameters(node);
|
|
107
|
+
if (paramCount <= max) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const functionSignature = (0, eslint_devkit_3.extractFunctionSignature)(node);
|
|
111
|
+
const overBy = paramCount - max;
|
|
112
|
+
context.report({
|
|
113
|
+
node,
|
|
114
|
+
messageId: 'tooManyParameters',
|
|
115
|
+
data: {
|
|
116
|
+
functionName: functionSignature,
|
|
117
|
+
count: String(paramCount),
|
|
118
|
+
max: String(max),
|
|
119
|
+
overBy: String(overBy),
|
|
120
|
+
},
|
|
121
|
+
suggest: [
|
|
122
|
+
{
|
|
123
|
+
messageId: 'useObjectParameter',
|
|
124
|
+
fix: () => null, // Complex refactoring, cannot auto-fix
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
messageId: 'extractToClass',
|
|
128
|
+
fix: () => null,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
messageId: 'splitFunction',
|
|
132
|
+
fix: () => null,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
FunctionDeclaration: checkFunction,
|
|
139
|
+
FunctionExpression: checkFunction,
|
|
140
|
+
ArrowFunctionExpression: checkFunction,
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
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: nested-complexity-hotspots
|
|
8
|
+
* Identifies nested control structures that harm readability
|
|
9
|
+
* Priority 3: Enhanced Complexity Analysis
|
|
10
|
+
*
|
|
11
|
+
* @see https://en.wikipedia.org/wiki/Cyclomatic_complexity
|
|
12
|
+
*/
|
|
13
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
14
|
+
type MessageIds = 'nestedComplexity' | 'useEarlyReturn' | 'useGuardClauses' | 'extractMethod';
|
|
15
|
+
export interface Options {
|
|
16
|
+
/** Maximum nesting depth. Default: 4 */
|
|
17
|
+
maxDepth?: number;
|
|
18
|
+
/** Count nested conditionals. Default: true */
|
|
19
|
+
countConditionals?: boolean;
|
|
20
|
+
/** Count nested loops. Default: true */
|
|
21
|
+
countLoops?: boolean;
|
|
22
|
+
}
|
|
23
|
+
type RuleOptions = [Options?];
|
|
24
|
+
/**
|
|
25
|
+
* Calculate nesting depth for a node
|
|
26
|
+
* Note: Currently unused, keeping for future implementation
|
|
27
|
+
*/
|
|
28
|
+
export declare const nestedComplexityHotspots: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
29
|
+
name: string;
|
|
30
|
+
};
|
|
31
|
+
export {};
|