eslint-plugin-maintainability 3.0.4 → 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 +32 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/rules/error-handling/no-silent-errors.js +0 -2
- package/src/rules/error-handling/no-unhandled-promise.d.ts +16 -1
- package/src/rules/error-handling/no-unhandled-promise.js +3 -0
- package/src/rules/maintainability/cognitive-complexity.js +1 -2
- package/src/rules/maintainability/identical-functions.d.ts +6 -0
- package/src/rules/maintainability/identical-functions.js +13 -26
- package/src/rules/maintainability/no-unreadable-iife.js +9 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
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
|
+
|
|
9
|
+
## 3.0.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#200](https://github.com/ofri-peretz/eslint/pull/200) [`02e0baf`](https://github.com/ofri-peretz/eslint/commit/02e0baf7a4e8ba83e8b2ec2b82169f733e4f4d87) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - fix: republish `recommended` preset with the correct plugin namespace
|
|
14
|
+
|
|
15
|
+
The published builds of `eslint-plugin-maintainability` and
|
|
16
|
+
`eslint-plugin-operability` shipped a `recommended` config whose plugin KEY
|
|
17
|
+
(`@interlace/maintainability`) did not match its rule PREFIX
|
|
18
|
+
(`@interlace/maintainability/maintainability/…` — doubled). ESLint cannot
|
|
19
|
+
resolve that, so spreading `...configs.recommended` throws
|
|
20
|
+
"could not find plugin" the moment a consumer lints a file — under both
|
|
21
|
+
ESLint 9 and 10.
|
|
22
|
+
|
|
23
|
+
The source was corrected in the 2026-05-16 namespace cleanup (alongside
|
|
24
|
+
`react-features`, which has since been republished via other changesets), but
|
|
25
|
+
these two plugins were never bumped — so npm still serves the broken builds and
|
|
26
|
+
they are the only two doubled-namespace plugins still unfixed downstream. This
|
|
27
|
+
republishes them from the corrected source.
|
|
28
|
+
|
|
29
|
+
Regression lock: `packages/eslint-config-interlace/src/ecosystem-integrity.test.ts`
|
|
30
|
+
loads every plugin's every config preset into a real ESLint instance and fails
|
|
31
|
+
if any rule→plugin reference cannot be resolved. Run it against the built
|
|
32
|
+
`dist/` in the release pipeline (pre-publish) to also catch a stale-artifact
|
|
33
|
+
publish — the failure mode that let these two ship broken.
|
|
34
|
+
|
|
3
35
|
## 3.0.4
|
|
4
36
|
|
|
5
37
|
### 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/
|
|
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
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.
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
94
|
+
return node.callee.argument;
|
|
95
95
|
}
|
|
96
|
-
return
|
|
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
|
-
|
|
233
|
-
|
|
219
|
+
const functionNode = getIifeFunctionNode(node);
|
|
220
|
+
if (functionNode) {
|
|
221
|
+
analyzeIIFE(node, functionNode);
|
|
234
222
|
}
|
|
235
223
|
},
|
|
236
224
|
};
|