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,190 @@
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.nestedComplexityHotspots = void 0;
9
+ const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ const eslint_devkit_2 = require("@interlace/eslint-devkit");
11
+ /**
12
+ * Calculate nesting depth for a node
13
+ * Note: Currently unused, keeping for future implementation
14
+ */
15
+ /*
16
+ function calculateNestingDepth(
17
+ node: TSESTree.Node,
18
+ sourceCode: TSESLint.SourceCode
19
+ ): number {
20
+ let depth = 0;
21
+ let current: TSESTree.Node | null = node;
22
+ const maxDepth = 20;
23
+
24
+ // Start from the node itself and traverse up
25
+ while (current && depth < maxDepth) {
26
+ const parent = (current as any).parent;
27
+
28
+ if (!parent) break;
29
+
30
+ // Count nested control structures
31
+ if (
32
+ parent.type === 'IfStatement' ||
33
+ parent.type === 'ForStatement' ||
34
+ parent.type === 'ForInStatement' ||
35
+ parent.type === 'ForOfStatement' ||
36
+ parent.type === 'WhileStatement' ||
37
+ parent.type === 'DoWhileStatement' ||
38
+ parent.type === 'SwitchStatement' ||
39
+ parent.type === 'TryStatement'
40
+ ) {
41
+ depth++;
42
+ }
43
+
44
+ current = parent as TSESTree.Node;
45
+ }
46
+
47
+ return depth;
48
+ }
49
+ */
50
+ exports.nestedComplexityHotspots = (0, eslint_devkit_2.createRule)({
51
+ name: 'nested-complexity-hotspots',
52
+ meta: {
53
+ type: 'suggestion',
54
+ docs: {
55
+ description: 'Identifies nested control structures that harm readability',
56
+ },
57
+ messages: {
58
+ nestedComplexity: (0, eslint_devkit_1.formatLLMMessage)({
59
+ icon: eslint_devkit_1.MessageIcons.COMPLEXITY,
60
+ issueName: 'Nested complexity hotspot',
61
+ description: 'Nesting depth {{depth}} exceeds maximum {{max}}',
62
+ severity: 'MEDIUM',
63
+ fix: 'Use early returns, guard clauses, or extract methods',
64
+ documentationLink: 'https://en.wikipedia.org/wiki/Cyclomatic_complexity',
65
+ }),
66
+ useEarlyReturn: (0, eslint_devkit_1.formatLLMMessage)({
67
+ icon: eslint_devkit_1.MessageIcons.INFO,
68
+ issueName: 'Use Early Return',
69
+ description: 'Use early return to reduce nesting',
70
+ severity: 'LOW',
71
+ fix: 'if (!condition) return; // Continue with main logic',
72
+ documentationLink: 'https://refactoring.guru/smells/long-method',
73
+ }),
74
+ useGuardClauses: (0, eslint_devkit_1.formatLLMMessage)({
75
+ icon: eslint_devkit_1.MessageIcons.INFO,
76
+ issueName: 'Use Guard Clauses',
77
+ description: 'Use guard clauses for validation',
78
+ severity: 'LOW',
79
+ fix: 'if (!isValid(input)) throw new Error()',
80
+ documentationLink: 'https://refactoring.guru/replace-nested-conditional-with-guard-clauses',
81
+ }),
82
+ extractMethod: (0, eslint_devkit_1.formatLLMMessage)({
83
+ icon: eslint_devkit_1.MessageIcons.INFO,
84
+ issueName: 'Extract Method',
85
+ description: 'Extract nested logic to separate method',
86
+ severity: 'LOW',
87
+ fix: 'Extract to private method',
88
+ documentationLink: 'https://refactoring.guru/extract-method',
89
+ }),
90
+ },
91
+ schema: [
92
+ {
93
+ type: 'object',
94
+ properties: {
95
+ maxDepth: {
96
+ type: 'number',
97
+ default: 4,
98
+ minimum: 1,
99
+ description: 'Maximum nesting depth',
100
+ },
101
+ countConditionals: {
102
+ type: 'boolean',
103
+ default: true,
104
+ description: 'Count nested conditionals',
105
+ },
106
+ countLoops: {
107
+ type: 'boolean',
108
+ default: true,
109
+ description: 'Count nested loops',
110
+ },
111
+ },
112
+ additionalProperties: false,
113
+ },
114
+ ],
115
+ },
116
+ defaultOptions: [
117
+ {
118
+ maxDepth: 4,
119
+ countConditionals: true,
120
+ countLoops: true,
121
+ },
122
+ ],
123
+ create(context, [options = {}]) {
124
+ const { maxDepth = 4, countConditionals = true, countLoops = true, } = options || {};
125
+ // const sourceCode = context.sourceCode || context.sourceCode; // Not used
126
+ /**
127
+ * Check control structures
128
+ */
129
+ function checkControlStructure(node) {
130
+ // Count how many control structures are nested above this node
131
+ let depth = 0;
132
+ let current = node;
133
+ const maxDepthCheck = 20;
134
+ // Start from the node itself and traverse up
135
+ while (current && depth < maxDepthCheck) {
136
+ const parent = current
137
+ .parent;
138
+ if (!parent)
139
+ break;
140
+ // Count nested control structures above this node
141
+ if (parent.type === 'IfStatement' ||
142
+ parent.type === 'ForStatement' ||
143
+ parent.type === 'ForInStatement' ||
144
+ parent.type === 'ForOfStatement' ||
145
+ parent.type === 'WhileStatement' ||
146
+ parent.type === 'DoWhileStatement' ||
147
+ parent.type === 'SwitchStatement' ||
148
+ parent.type === 'TryStatement') {
149
+ depth++;
150
+ }
151
+ current = parent;
152
+ }
153
+ // depth now represents how many control structures are nested above this node
154
+ // For 5 nested ifs, the innermost if will have depth = 4 (4 ifs above it)
155
+ // So we check if depth >= maxDepth (not >)
156
+ if (depth >= maxDepth) {
157
+ context.report({
158
+ node,
159
+ messageId: 'nestedComplexity',
160
+ data: {
161
+ depth: String(depth + 1), // +1 to include the current node
162
+ max: String(maxDepth),
163
+ },
164
+ suggest: [
165
+ {
166
+ messageId: 'useEarlyReturn',
167
+ fix: () => null,
168
+ },
169
+ {
170
+ messageId: 'useGuardClauses',
171
+ fix: () => null,
172
+ },
173
+ {
174
+ messageId: 'extractMethod',
175
+ fix: () => null,
176
+ },
177
+ ],
178
+ });
179
+ }
180
+ }
181
+ return {
182
+ IfStatement: countConditionals ? checkControlStructure : undefined,
183
+ ForStatement: countLoops ? checkControlStructure : undefined,
184
+ ForInStatement: countLoops ? checkControlStructure : undefined,
185
+ ForOfStatement: countLoops ? checkControlStructure : undefined,
186
+ WhileStatement: countLoops ? checkControlStructure : undefined,
187
+ SwitchStatement: countConditionals ? checkControlStructure : undefined,
188
+ };
189
+ },
190
+ });
@@ -0,0 +1,19 @@
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: no-lonely-if
8
+ * Prevent lone if statements inside else blocks
9
+ */
10
+ import type { TSESLint } from '@interlace/eslint-devkit';
11
+ export interface Options {
12
+ /** Allow lonely if in specific contexts */
13
+ allow?: string[];
14
+ }
15
+ type RuleOptions = [Options?];
16
+ export declare const noLonelyIf: TSESLint.RuleModule<"noLonelyIf", RuleOptions, unknown, TSESLint.RuleListener> & {
17
+ name: string;
18
+ };
19
+ export {};
@@ -0,0 +1,120 @@
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.noLonelyIf = void 0;
9
+ const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ const eslint_devkit_2 = require("@interlace/eslint-devkit");
11
+ exports.noLonelyIf = (0, eslint_devkit_1.createRule)({
12
+ name: 'no-lonely-if',
13
+ meta: {
14
+ type: 'suggestion',
15
+ docs: {
16
+ description: 'Prevent lone if statements inside else blocks - use else if instead',
17
+ },
18
+ hasSuggestions: true,
19
+ messages: {
20
+ noLonelyIf: (0, eslint_devkit_2.formatLLMMessage)({
21
+ icon: eslint_devkit_2.MessageIcons.WARNING,
22
+ issueName: 'Lonely If',
23
+ description: 'Unexpected if statement inside else block',
24
+ severity: 'MEDIUM',
25
+ fix: 'Replace with else if for better readability',
26
+ documentationLink: 'https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-lonely-if.md',
27
+ }),
28
+ },
29
+ schema: [
30
+ {
31
+ type: 'object',
32
+ properties: {
33
+ allow: {
34
+ type: 'array',
35
+ items: { type: 'string' },
36
+ default: [],
37
+ },
38
+ },
39
+ additionalProperties: false,
40
+ },
41
+ ],
42
+ },
43
+ defaultOptions: [{ allow: [] }],
44
+ create(context) {
45
+ const [options] = context.options;
46
+ const { allow = [] } = options || {};
47
+ const allowedContexts = new Set(allow);
48
+ function isInAllowedContext() {
49
+ // Check if we're in an allowed context
50
+ // This is a simple implementation - could be extended for more complex cases
51
+ for (const allowedContext of allowedContexts) {
52
+ // For now, just check if the context string appears anywhere in the source
53
+ const sourceCode = context.sourceCode;
54
+ const sourceText = sourceCode.getText();
55
+ if (sourceText.includes(allowedContext)) {
56
+ return true;
57
+ }
58
+ }
59
+ return false;
60
+ }
61
+ function isLonelyIf(node) {
62
+ // Check if this if statement is inside an else block (not a proper else if)
63
+ const parent = node.parent;
64
+ // If parent is a BlockStatement, check if that block is an else block
65
+ if (parent?.type === 'BlockStatement') {
66
+ const grandParent = parent.parent;
67
+ // Check if the block is the alternate (else) of an if statement
68
+ return (grandParent?.type === 'IfStatement' &&
69
+ grandParent.alternate === parent);
70
+ }
71
+ // If not in a block, it's not a lonely if (it's either a proper else if or top-level)
72
+ return false;
73
+ }
74
+ return {
75
+ IfStatement(node) {
76
+ if (isLonelyIf(node) && !isInAllowedContext()) {
77
+ context.report({
78
+ node,
79
+ messageId: 'noLonelyIf',
80
+ data: {
81
+ current: 'if statement in else block',
82
+ fix: 'else if',
83
+ },
84
+ suggest: [
85
+ {
86
+ messageId: 'noLonelyIf',
87
+ fix(fixer) {
88
+ const sourceCode = context.sourceCode;
89
+ // Find the else keyword
90
+ let elseToken = null;
91
+ const tokens = sourceCode.getTokensBefore(node, 10);
92
+ for (let i = tokens.length - 1; i >= 0; i--) {
93
+ if (tokens[i].value === 'else') {
94
+ elseToken = tokens[i];
95
+ break;
96
+ }
97
+ }
98
+ if (elseToken) {
99
+ // Remove 'else' and replace 'if' with 'else if'
100
+ const ifToken = sourceCode.getTokenAfter(elseToken);
101
+ if (ifToken && ifToken.value === 'if') {
102
+ return [
103
+ fixer.removeRange([
104
+ elseToken.range[0],
105
+ ifToken.range[1],
106
+ ]),
107
+ fixer.insertTextBefore(ifToken, 'else '),
108
+ ];
109
+ }
110
+ }
111
+ return null;
112
+ },
113
+ },
114
+ ],
115
+ });
116
+ }
117
+ },
118
+ };
119
+ },
120
+ });
@@ -0,0 +1,19 @@
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: no-nested-ternary
8
+ * Prevent nested ternary expressions
9
+ */
10
+ import type { TSESLint } from '@interlace/eslint-devkit';
11
+ export interface Options {
12
+ /** Allow nested ternaries in specific contexts */
13
+ allow?: string[];
14
+ }
15
+ type RuleOptions = [Options?];
16
+ export declare const noNestedTernary: TSESLint.RuleModule<"noNestedTernary", RuleOptions, unknown, TSESLint.RuleListener> & {
17
+ name: string;
18
+ };
19
+ export {};
@@ -0,0 +1,165 @@
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.noNestedTernary = void 0;
9
+ const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ const eslint_devkit_2 = require("@interlace/eslint-devkit");
11
+ exports.noNestedTernary = (0, eslint_devkit_1.createRule)({
12
+ name: 'no-nested-ternary',
13
+ meta: {
14
+ type: 'suggestion',
15
+ docs: {
16
+ description: 'Prevent nested ternary expressions for better readability',
17
+ },
18
+ hasSuggestions: true,
19
+ messages: {
20
+ noNestedTernary: (0, eslint_devkit_2.formatLLMMessage)({
21
+ icon: eslint_devkit_2.MessageIcons.WARNING,
22
+ issueName: 'Nested Ternary',
23
+ description: 'Avoid nested ternary expressions',
24
+ severity: 'MEDIUM',
25
+ fix: 'Extract to helper variable or use if-else',
26
+ documentationLink: 'https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-nested-ternary.md',
27
+ }),
28
+ },
29
+ schema: [
30
+ {
31
+ type: 'object',
32
+ properties: {
33
+ allow: {
34
+ type: 'array',
35
+ items: { type: 'string' },
36
+ default: [],
37
+ },
38
+ },
39
+ additionalProperties: false,
40
+ },
41
+ ],
42
+ },
43
+ defaultOptions: [{ allow: [] }],
44
+ create(context) {
45
+ const [options] = context.options;
46
+ const { allow = [] } = options || {};
47
+ /**
48
+ * Check if node is in an allowed context based on the allow option.
49
+ * Supported contexts:
50
+ * - 'jsx': Allow nested ternaries in JSX expressions
51
+ * - 'variable': Allow nested ternaries in variable declarations
52
+ * - 'return': Allow nested ternaries in return statements
53
+ * - 'argument': Allow nested ternaries in function arguments
54
+ */
55
+ function isInAllowedContext(node) {
56
+ if (allow.length === 0) {
57
+ return false;
58
+ }
59
+ let current = node.parent;
60
+ while (current) {
61
+ // Check for JSX context
62
+ if (allow.includes('jsx') &&
63
+ (current.type === 'JSXExpressionContainer' ||
64
+ current.type === 'JSXElement' ||
65
+ current.type === 'JSXFragment')) {
66
+ return true;
67
+ }
68
+ // Check for variable declaration context
69
+ if (allow.includes('variable') &&
70
+ current.type === 'VariableDeclarator') {
71
+ return true;
72
+ }
73
+ // Check for return statement context
74
+ if (allow.includes('return') && current.type === 'ReturnStatement') {
75
+ return true;
76
+ }
77
+ // Check for function argument context
78
+ if (allow.includes('argument') && current.type === 'CallExpression') {
79
+ return true;
80
+ }
81
+ current = current.parent;
82
+ }
83
+ return false;
84
+ }
85
+ function hasNestedTernary(node) {
86
+ // Check if the consequent or alternate contains another ternary
87
+ function containsTernary(expr) {
88
+ if (expr.type === 'ConditionalExpression') {
89
+ return true;
90
+ }
91
+ // For other expression types, check their child expressions
92
+ switch (expr.type) {
93
+ case 'ArrayExpression':
94
+ return expr.elements.some((element) => element &&
95
+ element.type !== 'SpreadElement' &&
96
+ containsTernary(element));
97
+ case 'ObjectExpression':
98
+ return expr.properties.some((prop) => prop.type === 'Property' &&
99
+ prop.value &&
100
+ containsTernary(prop.value));
101
+ case 'CallExpression':
102
+ return (expr.arguments.some((arg) => arg.type !== 'SpreadElement' && containsTernary(arg)) ||
103
+ (expr.callee.type !== 'Super' && containsTernary(expr.callee)));
104
+ case 'MemberExpression':
105
+ return (containsTernary(expr.object) ||
106
+ (expr.property.type !== 'Identifier' &&
107
+ expr.property.type !== 'PrivateIdentifier' &&
108
+ containsTernary(expr.property)));
109
+ case 'BinaryExpression':
110
+ case 'LogicalExpression':
111
+ return containsTernary(expr.left) || containsTernary(expr.right);
112
+ case 'UnaryExpression':
113
+ case 'UpdateExpression':
114
+ return containsTernary(expr.argument);
115
+ case 'AssignmentExpression':
116
+ return containsTernary(expr.right);
117
+ // For literals and identifiers, no nested expressions
118
+ case 'Literal':
119
+ case 'Identifier':
120
+ case 'ThisExpression':
121
+ case 'Super':
122
+ case 'MetaProperty':
123
+ return false;
124
+ // For template literals, check expressions
125
+ case 'TemplateLiteral':
126
+ return expr.expressions.some((exp) => containsTernary(exp));
127
+ // For tagged templates, check tag and expressions
128
+ case 'TaggedTemplateExpression':
129
+ return (containsTernary(expr.tag) ||
130
+ expr.quasi.expressions.some((exp) => containsTernary(exp)));
131
+ // Default: assume no nested expressions for unknown types
132
+ default:
133
+ return false;
134
+ }
135
+ }
136
+ return (containsTernary(node.consequent) || containsTernary(node.alternate));
137
+ }
138
+ return {
139
+ ConditionalExpression(node) {
140
+ if (hasNestedTernary(node) && !isInAllowedContext(node)) {
141
+ context.report({
142
+ node,
143
+ messageId: 'noNestedTernary',
144
+ data: {
145
+ current: 'nested ternary expression',
146
+ fix: 'extract or use if-else',
147
+ },
148
+ suggest: [
149
+ {
150
+ messageId: 'noNestedTernary',
151
+ fix() {
152
+ // This is a complex fix that would require:
153
+ // 1. Extracting the nested ternary to a variable
154
+ // 2. Replacing the nested part
155
+ // For now, just provide the suggestion
156
+ return null;
157
+ },
158
+ },
159
+ ],
160
+ });
161
+ }
162
+ },
163
+ };
164
+ },
165
+ });
@@ -0,0 +1,24 @@
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: no-unreadable-iife
8
+ * Prevent unreadable Immediately Invoked Function Expressions (unicorn-inspired)
9
+ */
10
+ import type { TSESLint } from '@interlace/eslint-devkit';
11
+ type MessageIds = 'unreadableIIFE' | 'suggestNamedFunction' | 'suggestBlockScope' | 'complexIIFE';
12
+ export interface Options {
13
+ /** Maximum number of statements allowed in IIFE */
14
+ maxStatements?: number;
15
+ /** Maximum depth of nesting allowed in IIFE */
16
+ maxDepth?: number;
17
+ /** Allow IIFEs that return values */
18
+ allowReturningIIFE?: boolean;
19
+ }
20
+ type RuleOptions = [Options?];
21
+ export declare const noUnreadableIife: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
22
+ name: string;
23
+ };
24
+ export {};