eslint-plugin-maintainability 3.0.5 → 3.0.6

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [3.0.3] - 2026-02-08
2
2
 
3
+ ## 3.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [#225](https://github.com/ofri-peretz/eslint/pull/225) [`34ff5a8`](https://github.com/ofri-peretz/eslint/commit/34ff5a8e6f5126c5d1c0a524759e0af2b5476b46) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - CI-only: pin all coverage thresholds at 100% (integration target, merges last).
8
+
3
9
  ## 3.0.5
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <a href="https://www.npmjs.com/package/eslint-plugin-maintainability" target="_blank"><img src="https://img.shields.io/npm/v/eslint-plugin-maintainability.svg" alt="NPM Version" /></a>
11
11
  <a href="https://www.npmjs.com/package/eslint-plugin-maintainability" target="_blank"><img src="https://img.shields.io/npm/dm/eslint-plugin-maintainability.svg" alt="NPM Downloads" /></a>
12
12
  <a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="Package License" /></a>
13
- <a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=maintainability" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=maintainability" alt="Codecov" /></a>
13
+ <a href="https://app.codecov.io/gh/ofri-peretz/eslint/tree/main" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?flag=eslint-plugin-maintainability" alt="Codecov" /></a>
14
14
  <a href="https://github.com/ofri-peretz/eslint" target="_blank"><img src="https://img.shields.io/badge/Since-Dec_2025-blue?logo=rocket&logoColor=white" alt="Since Dec 2025" /></a>
15
15
  </p>
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-maintainability",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "ESLint rules for reducing cognitive load and ensuring code readability.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -58,7 +58,7 @@ exports.plugin = {
58
58
  // `@interlace` scope; a scoped name here drifts from how consumers install
59
59
  // and reference the plugin. Locked in index.test.ts.
60
60
  name: 'eslint-plugin-maintainability',
61
- version: '3.0.5',
61
+ version: '3.0.6',
62
62
  },
63
63
  rules: exports.rules,
64
64
  };
@@ -13,7 +13,6 @@ const eslint_devkit_2 = require("@interlace/eslint-devkit");
13
13
  */
14
14
  function isEmptyCatchBlock(catchClause) {
15
15
  const body = catchClause.body;
16
- /* v8 ignore next 3 -- defensive: catch clause body is always BlockStatement in valid JS */
17
16
  if (!body || body.type !== 'BlockStatement') {
18
17
  return true;
19
18
  }
@@ -33,7 +32,6 @@ function hasExplanatoryComment(catchClause, sourceCode) {
33
32
  // Check for comments before the catch clause
34
33
  const comments = sourceCode.getAllComments();
35
34
  const catchStart = catchClause.loc?.start;
36
- /* v8 ignore next 3 -- defensive: catch clause always has location, and no comments = no match */
37
35
  if (!catchStart || !comments.length) {
38
36
  return false;
39
37
  }
@@ -11,7 +11,7 @@
11
11
  * @see https://cwe.mitre.org/data/definitions/1024.html
12
12
  * @see https://rules.sonarsource.com/javascript/RSPEC-4635/
13
13
  */
14
- import type { TSESLint } from '@interlace/eslint-devkit';
14
+ import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
15
15
  type MessageIds = 'unhandledPromise' | 'addCatch' | 'useTryCatch' | 'useAwait';
16
16
  export interface Options {
17
17
  /** Ignore promises in test files. Default: true */
@@ -20,6 +20,21 @@ export interface Options {
20
20
  ignoreVoidExpressions?: boolean;
21
21
  }
22
22
  type RuleOptions = [Options?];
23
+ /**
24
+ * Check if a node is a Promise-like expression
25
+ * For now, we check all CallExpressions since we can't statically determine
26
+ * which functions return promises. The isPromiseHandled function will filter out
27
+ * non-promise calls that are inside handled promise chains.
28
+ */
29
+ export declare function isPromiseExpression(node: TSESTree.Node): boolean;
30
+ /**
31
+ * Check if a CallExpression is inside a promise chain callback
32
+ */
33
+ export declare function isInsidePromiseCallback(node: TSESTree.CallExpression): boolean;
34
+ /**
35
+ * Check if promise is handled (has .catch, .then, or is in try/catch)
36
+ */
37
+ export declare function isPromiseHandled(node: TSESTree.Node): boolean;
23
38
  export declare const noUnhandledPromise: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
24
39
  name: string;
25
40
  };
@@ -6,6 +6,9 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.noUnhandledPromise = void 0;
9
+ exports.isPromiseExpression = isPromiseExpression;
10
+ exports.isInsidePromiseCallback = isInsidePromiseCallback;
11
+ exports.isPromiseHandled = isPromiseHandled;
9
12
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
13
  const eslint_devkit_2 = require("@interlace/eslint-devkit");
11
14
  /**
@@ -80,8 +80,7 @@ exports.cognitiveComplexity = (0, eslint_devkit_2.createRule)({
80
80
  },
81
81
  ],
82
82
  create(context) {
83
- const options = context.options[0] || {};
84
- const { maxComplexity = 15 } = options || {};
83
+ const { maxComplexity = 15 } = context.options[0] || {};
85
84
  const filename = context.filename;
86
85
  /**
87
86
  * Calculate cognitive complexity for a function
@@ -21,6 +21,12 @@ export interface Options {
21
21
  ignoreTestFiles?: boolean;
22
22
  }
23
23
  type RuleOptions = [Options?];
24
+ /**
25
+ * Build the generic extracted-function name suggested for a duplication group.
26
+ * Shared by the unified-function template and the report data so the two can
27
+ * never drift apart.
28
+ */
29
+ export declare function buildGenericName(firstFunctionName: string): string;
24
30
  export declare const identicalFunctions: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
25
31
  name: string;
26
32
  };
@@ -6,9 +6,19 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.identicalFunctions = void 0;
9
+ exports.buildGenericName = buildGenericName;
9
10
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
11
  const eslint_devkit_2 = require("@interlace/eslint-devkit");
11
12
  const eslint_devkit_3 = require("@interlace/eslint-devkit");
13
+ /**
14
+ * Build the generic extracted-function name suggested for a duplication group.
15
+ * Shared by the unified-function template and the report data so the two can
16
+ * never drift apart.
17
+ */
18
+ function buildGenericName(firstFunctionName) {
19
+ const baseName = firstFunctionName.replace(/^(handle|process|get|set|create|update|delete)/, '');
20
+ return `handle${baseName || 'Generic'}`;
21
+ }
12
22
  exports.identicalFunctions = (0, eslint_devkit_2.createRule)({
13
23
  name: 'identical-functions',
14
24
  meta: {
@@ -86,8 +96,7 @@ exports.identicalFunctions = (0, eslint_devkit_2.createRule)({
86
96
  },
87
97
  ],
88
98
  create(context) {
89
- const options = context.options[0] || {};
90
- const { minLines = 3, similarityThreshold = 0.9, ignoreTestFiles = true, } = options || {};
99
+ const { minLines = 3, similarityThreshold = 0.9, ignoreTestFiles = true, } = context.options[0] || {};
91
100
  const sourceCode = context.sourceCode;
92
101
  const filename = context.filename;
93
102
  // Skip test files if configured
@@ -120,10 +129,9 @@ exports.identicalFunctions = (0, eslint_devkit_2.createRule)({
120
129
  function calculateSimilarity(str1, str2) {
121
130
  if (str1 === str2)
122
131
  return 1.0;
132
+ // str1 !== str2 here, so the longer string is never empty.
123
133
  const longer = str1.length > str2.length ? str1 : str2;
124
134
  const shorter = str1.length > str2.length ? str2 : str1;
125
- if (longer.length === 0)
126
- return 1.0;
127
135
  const editDistance = levenshteinDistance(longer, shorter);
128
136
  return (longer.length - editDistance) / longer.length;
129
137
  }
@@ -188,25 +196,6 @@ exports.identicalFunctions = (0, eslint_devkit_2.createRule)({
188
196
  }
189
197
  return groups;
190
198
  }
191
- /**
192
- * Generate unified function suggestion
193
- */
194
- // oxlint-disable-next-line consistent-function-scoping
195
- function generateUnifiedFunction(group) {
196
- const firstFunc = group.functions[0];
197
- // Analyze parameter differences
198
- const allParams = new Set();
199
- group.functions.forEach((func) => func.params.forEach((p) => allParams.add(p)));
200
- // Generate generic function name
201
- const baseName = firstFunc.name.replace(/^(handle|process|get|set|create|update|delete)/, '');
202
- const genericName = `handle${baseName || 'Generic'}`;
203
- // If functions differ only in constant values, extract as parameter
204
- const paramList = Array.from(allParams);
205
- if (paramList.length > firstFunc.params.length) {
206
- paramList.push('options');
207
- }
208
- return `function ${genericName}(${paramList.join(', ')}) {\n ${firstFunc.body.trim()}\n}`;
209
- }
210
199
  /**
211
200
  * Suggest refactoring approach
212
201
  */
@@ -264,7 +253,6 @@ exports.identicalFunctions = (0, eslint_devkit_2.createRule)({
264
253
  const groups = findDuplicationGroups();
265
254
  groups.forEach((group) => {
266
255
  const refactoringApproach = suggestRefactoringApproach(group);
267
- const unifiedFunction = generateUnifiedFunction(group);
268
256
  const primaryFunction = group.functions[0];
269
257
  const similarityPercent = Math.round(group.similarityScore * 100);
270
258
  context.report({
@@ -280,8 +268,7 @@ exports.identicalFunctions = (0, eslint_devkit_2.createRule)({
280
268
  {
281
269
  messageId: 'extractGeneric',
282
270
  data: {
283
- functionName: unifiedFunction.match(/function (\w+)/)?.[1] ||
284
- 'handleGeneric',
271
+ functionName: buildGenericName(primaryFunction.name),
285
272
  },
286
273
  fix: () => null,
287
274
  },
@@ -79,21 +79,21 @@ exports.noUnreadableIife = (0, eslint_devkit_1.createRule)({
79
79
  const [options] = context.options;
80
80
  const { maxStatements = 3, maxDepth = 2, allowReturningIIFE = true } = options || {};
81
81
  // oxlint-disable-next-line consistent-function-scoping
82
- function isIIFE(node) {
82
+ function getIifeFunctionNode(node) {
83
83
  // Check if this is a call expression with a function expression or arrow function
84
84
  // Note: In typescript-eslint AST, parentheses don't create a separate node type,
85
85
  // so (function(){})() will have callee.type === 'FunctionExpression' directly
86
86
  if (node.callee.type === 'FunctionExpression' ||
87
87
  node.callee.type === 'ArrowFunctionExpression') {
88
- return true;
88
+ return node.callee;
89
89
  }
90
- // Check for unary operator IIFEs like !function(){}(), +function(){}(), void function(){}()
90
+ // Check for unary operator IIFEs like (!function(){})(), (+function(){})(), (void function(){})()
91
91
  if (node.callee.type === 'UnaryExpression' &&
92
92
  (node.callee.argument.type === 'FunctionExpression' ||
93
93
  node.callee.argument.type === 'ArrowFunctionExpression')) {
94
- return true;
94
+ return node.callee.argument;
95
95
  }
96
- return false;
96
+ return null;
97
97
  }
98
98
  // oxlint-disable-next-line consistent-function-scoping
99
99
  function countStatements(body) {
@@ -161,20 +161,7 @@ exports.noUnreadableIife = (0, eslint_devkit_1.createRule)({
161
161
  }
162
162
  return false;
163
163
  }
164
- function analyzeIIFE(node) {
165
- let functionNode = null;
166
- if (node.callee.type === 'FunctionExpression' ||
167
- node.callee.type === 'ArrowFunctionExpression') {
168
- functionNode = node.callee;
169
- }
170
- else if (node.callee.type === 'UnaryExpression' &&
171
- (node.callee.argument.type === 'FunctionExpression' ||
172
- node.callee.argument.type === 'ArrowFunctionExpression')) {
173
- functionNode = node.callee.argument;
174
- }
175
- if (!functionNode) {
176
- return;
177
- }
164
+ function analyzeIIFE(node, functionNode) {
178
165
  const body = functionNode.body;
179
166
  const statementCount = countStatements(body);
180
167
  const nestingDepth = calculateDepth(body);
@@ -229,8 +216,9 @@ exports.noUnreadableIife = (0, eslint_devkit_1.createRule)({
229
216
  }
230
217
  return {
231
218
  CallExpression(node) {
232
- if (isIIFE(node)) {
233
- analyzeIIFE(node);
219
+ const functionNode = getIifeFunctionNode(node);
220
+ if (functionNode) {
221
+ analyzeIIFE(node, functionNode);
234
222
  }
235
223
  },
236
224
  };