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