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,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 {};