eslint-plugin-maintainability 3.0.9 → 3.0.10

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