Skip to content

Commit e4c7f46

Browse files
committed
feat: add max-nesting-depth rule to enforce code readability
Implements a new ESLint rule that enforces maximum nesting depth for control structures (if, for, while, switch, try, etc.). Deep nesting makes code difficult to read and maintain. Features: - Configurable max depth (default: 3) - Functions reset nesting depth (encourages extraction) - Optional IIFE handling with ignoreTopLevelIIFE option - Comprehensive test coverage with 24 test cases This rule complements existing rules like no-complex-conditionals and low-function-cohesion to promote maintainable, readable code.
1 parent 510d89d commit e4c7f46

5 files changed

Lines changed: 551 additions & 2 deletions

File tree

index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export default {
1414
'no-late-variable-usage': rules.noLateVariableUsage,
1515
'low-function-cohesion': rules.lowFunctionCohesion,
1616
'low-class-cohesion': rules.lowClassCohesion,
17-
'no-complex-conditionals': rules.noComplexConditionals
17+
'no-complex-conditionals': rules.noComplexConditionals,
18+
'max-nesting-depth': rules.maxNestingDepth
1819
}
1920
};
2021

rules/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export { default as noLateArgumentUsage } from './no-late-argument-usage';
55
export { default as noLateVariableUsage } from './no-late-variable-usage';
66
export { default as lowFunctionCohesion } from './low-function-cohesion';
77
export { default as lowClassCohesion } from './low-class-cohesion';
8-
export { default as noComplexConditionals } from './no-complex-conditionals';
8+
export { default as noComplexConditionals } from './no-complex-conditionals';
9+
export { default as maxNestingDepth } from './max-nesting-depth';

rules/max-nesting-depth.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* @fileoverview Rule to enforce maximum nesting depth for control structures
3+
* @author eslint-plugin-code-complete
4+
*/
5+
6+
import { Rule } from 'eslint';
7+
import { MaxNestingDepthOptions } from '../types/rule-options.js';
8+
import { createRuleMeta, RULE_CATEGORIES } from '../utils/rule-meta.js';
9+
10+
const rule: Rule.RuleModule = {
11+
meta: createRuleMeta('max-nesting-depth', {
12+
description: 'Enforce a maximum nesting depth for control structures to improve code readability',
13+
category: RULE_CATEGORIES.BEST_PRACTICES,
14+
recommended: true,
15+
schema: [
16+
{
17+
type: 'object',
18+
properties: {
19+
maxDepth: {
20+
type: 'number',
21+
minimum: 1,
22+
default: 3
23+
},
24+
ignoreTopLevelIIFE: {
25+
type: 'boolean',
26+
default: true
27+
}
28+
},
29+
additionalProperties: false
30+
}
31+
],
32+
messages: {
33+
maxNestingDepth: 'Nesting depth of {{depth}} exceeds maximum allowed depth of {{maxDepth}}. Consider refactoring with guard clauses or extracting to helper functions.'
34+
}
35+
}),
36+
37+
create(context: Rule.RuleContext): Rule.RuleListener {
38+
const options = context.options[0] || {} as MaxNestingDepthOptions;
39+
const maxDepth = options.maxDepth !== undefined ? options.maxDepth : 3;
40+
const ignoreTopLevelIIFE = options.ignoreTopLevelIIFE !== undefined ? options.ignoreTopLevelIIFE : true;
41+
42+
// Stack to track nesting depth and context
43+
const depthStack: number[] = [];
44+
let currentDepth = 0;
45+
46+
/**
47+
* Check if a node is an immediately invoked function expression (IIFE)
48+
* and if it's at the top level (not nested in another function)
49+
* @param {Object} node - The node to check
50+
* @returns {boolean} - True if the node is a top-level IIFE
51+
*/
52+
function isTopLevelIIFE(node: any): boolean {
53+
// Check if it's an IIFE
54+
if (node.parent && node.parent.type === 'CallExpression' && node.parent.callee === node) {
55+
// Check if we're at the top level (no function context saved)
56+
return depthStack.length === 0;
57+
}
58+
return false;
59+
}
60+
61+
/**
62+
* Increment depth when entering a nesting structure
63+
* @param {Object} node - The node being entered
64+
*/
65+
function enterNestingStructure(node: any): void {
66+
currentDepth++;
67+
68+
if (currentDepth > maxDepth) {
69+
context.report({
70+
node,
71+
messageId: 'maxNestingDepth',
72+
data: {
73+
depth: currentDepth.toString(),
74+
maxDepth: maxDepth.toString()
75+
}
76+
});
77+
}
78+
}
79+
80+
/**
81+
* Decrement depth when exiting a nesting structure
82+
*/
83+
function exitNestingStructure(): void {
84+
currentDepth--;
85+
}
86+
87+
/**
88+
* Handle entering a function - saves current depth and starts fresh
89+
* @param {Object} node - The function node being entered
90+
*/
91+
function enterFunction(node: any): void {
92+
// If it's a top-level IIFE and we should ignore it, don't reset depth
93+
if (isTopLevelIIFE(node) && !ignoreTopLevelIIFE) {
94+
// Treat IIFE as a nesting structure
95+
enterNestingStructure(node);
96+
return;
97+
}
98+
99+
// Save current depth and reset for the new function scope
100+
depthStack.push(currentDepth);
101+
currentDepth = 0;
102+
}
103+
104+
/**
105+
* Handle exiting a function - restores previous depth
106+
* @param {Object} node - The function node being exited
107+
*/
108+
function exitFunction(node: any): void {
109+
// If it's a top-level IIFE that we're treating as nesting, decrement
110+
if (isTopLevelIIFE(node) && !ignoreTopLevelIIFE) {
111+
exitNestingStructure();
112+
return;
113+
}
114+
115+
// Restore previous depth
116+
if (depthStack.length > 0) {
117+
currentDepth = depthStack.pop()!;
118+
}
119+
}
120+
121+
return {
122+
// Function boundaries - reset depth or count as nesting for IIFEs
123+
FunctionDeclaration(node) { enterFunction(node); },
124+
FunctionExpression(node) { enterFunction(node); },
125+
ArrowFunctionExpression(node) { enterFunction(node); },
126+
'FunctionDeclaration:exit'(node) { exitFunction(node); },
127+
'FunctionExpression:exit'(node) { exitFunction(node); },
128+
'ArrowFunctionExpression:exit'(node) { exitFunction(node); },
129+
130+
// Control structures that increase nesting
131+
IfStatement: enterNestingStructure,
132+
'IfStatement:exit': exitNestingStructure,
133+
134+
ForStatement: enterNestingStructure,
135+
'ForStatement:exit': exitNestingStructure,
136+
137+
ForInStatement: enterNestingStructure,
138+
'ForInStatement:exit': exitNestingStructure,
139+
140+
ForOfStatement: enterNestingStructure,
141+
'ForOfStatement:exit': exitNestingStructure,
142+
143+
WhileStatement: enterNestingStructure,
144+
'WhileStatement:exit': exitNestingStructure,
145+
146+
DoWhileStatement: enterNestingStructure,
147+
'DoWhileStatement:exit': exitNestingStructure,
148+
149+
SwitchStatement: enterNestingStructure,
150+
'SwitchStatement:exit': exitNestingStructure,
151+
152+
TryStatement: enterNestingStructure,
153+
'TryStatement:exit': exitNestingStructure,
154+
155+
WithStatement: enterNestingStructure,
156+
'WithStatement:exit': exitNestingStructure
157+
};
158+
}
159+
};
160+
161+
export default rule;

0 commit comments

Comments
 (0)