From e4c7f46ecf40ab63f79e1446028d45cfaf252d2b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Dec 2025 05:16:04 +0000 Subject: [PATCH 1/3] 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. --- index.ts | 3 +- rules/index.ts | 3 +- rules/max-nesting-depth.ts | 161 ++++++++++++++ tests/max-nesting-depth.test.ts | 381 ++++++++++++++++++++++++++++++++ types/rule-options.ts | 5 + 5 files changed, 551 insertions(+), 2 deletions(-) create mode 100644 rules/max-nesting-depth.ts create mode 100644 tests/max-nesting-depth.test.ts diff --git a/index.ts b/index.ts index b53cc21..b0958b4 100644 --- a/index.ts +++ b/index.ts @@ -14,7 +14,8 @@ export default { 'no-late-variable-usage': rules.noLateVariableUsage, 'low-function-cohesion': rules.lowFunctionCohesion, 'low-class-cohesion': rules.lowClassCohesion, - 'no-complex-conditionals': rules.noComplexConditionals + 'no-complex-conditionals': rules.noComplexConditionals, + 'max-nesting-depth': rules.maxNestingDepth } }; diff --git a/rules/index.ts b/rules/index.ts index 5aa7dd2..e551606 100644 --- a/rules/index.ts +++ b/rules/index.ts @@ -5,4 +5,5 @@ export { default as noLateArgumentUsage } from './no-late-argument-usage'; export { default as noLateVariableUsage } from './no-late-variable-usage'; export { default as lowFunctionCohesion } from './low-function-cohesion'; export { default as lowClassCohesion } from './low-class-cohesion'; -export { default as noComplexConditionals } from './no-complex-conditionals'; \ No newline at end of file +export { default as noComplexConditionals } from './no-complex-conditionals'; +export { default as maxNestingDepth } from './max-nesting-depth'; \ No newline at end of file diff --git a/rules/max-nesting-depth.ts b/rules/max-nesting-depth.ts new file mode 100644 index 0000000..1cc721c --- /dev/null +++ b/rules/max-nesting-depth.ts @@ -0,0 +1,161 @@ +/** + * @fileoverview Rule to enforce maximum nesting depth for control structures + * @author eslint-plugin-code-complete + */ + +import { Rule } from 'eslint'; +import { MaxNestingDepthOptions } from '../types/rule-options.js'; +import { createRuleMeta, RULE_CATEGORIES } from '../utils/rule-meta.js'; + +const rule: Rule.RuleModule = { + meta: createRuleMeta('max-nesting-depth', { + description: 'Enforce a maximum nesting depth for control structures to improve code readability', + category: RULE_CATEGORIES.BEST_PRACTICES, + recommended: true, + schema: [ + { + type: 'object', + properties: { + maxDepth: { + type: 'number', + minimum: 1, + default: 3 + }, + ignoreTopLevelIIFE: { + type: 'boolean', + default: true + } + }, + additionalProperties: false + } + ], + messages: { + maxNestingDepth: 'Nesting depth of {{depth}} exceeds maximum allowed depth of {{maxDepth}}. Consider refactoring with guard clauses or extracting to helper functions.' + } + }), + + create(context: Rule.RuleContext): Rule.RuleListener { + const options = context.options[0] || {} as MaxNestingDepthOptions; + const maxDepth = options.maxDepth !== undefined ? options.maxDepth : 3; + const ignoreTopLevelIIFE = options.ignoreTopLevelIIFE !== undefined ? options.ignoreTopLevelIIFE : true; + + // Stack to track nesting depth and context + const depthStack: number[] = []; + let currentDepth = 0; + + /** + * Check if a node is an immediately invoked function expression (IIFE) + * and if it's at the top level (not nested in another function) + * @param {Object} node - The node to check + * @returns {boolean} - True if the node is a top-level IIFE + */ + function isTopLevelIIFE(node: any): boolean { + // Check if it's an IIFE + if (node.parent && node.parent.type === 'CallExpression' && node.parent.callee === node) { + // Check if we're at the top level (no function context saved) + return depthStack.length === 0; + } + return false; + } + + /** + * Increment depth when entering a nesting structure + * @param {Object} node - The node being entered + */ + function enterNestingStructure(node: any): void { + currentDepth++; + + if (currentDepth > maxDepth) { + context.report({ + node, + messageId: 'maxNestingDepth', + data: { + depth: currentDepth.toString(), + maxDepth: maxDepth.toString() + } + }); + } + } + + /** + * Decrement depth when exiting a nesting structure + */ + function exitNestingStructure(): void { + currentDepth--; + } + + /** + * Handle entering a function - saves current depth and starts fresh + * @param {Object} node - The function node being entered + */ + function enterFunction(node: any): void { + // If it's a top-level IIFE and we should ignore it, don't reset depth + if (isTopLevelIIFE(node) && !ignoreTopLevelIIFE) { + // Treat IIFE as a nesting structure + enterNestingStructure(node); + return; + } + + // Save current depth and reset for the new function scope + depthStack.push(currentDepth); + currentDepth = 0; + } + + /** + * Handle exiting a function - restores previous depth + * @param {Object} node - The function node being exited + */ + function exitFunction(node: any): void { + // If it's a top-level IIFE that we're treating as nesting, decrement + if (isTopLevelIIFE(node) && !ignoreTopLevelIIFE) { + exitNestingStructure(); + return; + } + + // Restore previous depth + if (depthStack.length > 0) { + currentDepth = depthStack.pop()!; + } + } + + return { + // Function boundaries - reset depth or count as nesting for IIFEs + FunctionDeclaration(node) { enterFunction(node); }, + FunctionExpression(node) { enterFunction(node); }, + ArrowFunctionExpression(node) { enterFunction(node); }, + 'FunctionDeclaration:exit'(node) { exitFunction(node); }, + 'FunctionExpression:exit'(node) { exitFunction(node); }, + 'ArrowFunctionExpression:exit'(node) { exitFunction(node); }, + + // Control structures that increase nesting + IfStatement: enterNestingStructure, + 'IfStatement:exit': exitNestingStructure, + + ForStatement: enterNestingStructure, + 'ForStatement:exit': exitNestingStructure, + + ForInStatement: enterNestingStructure, + 'ForInStatement:exit': exitNestingStructure, + + ForOfStatement: enterNestingStructure, + 'ForOfStatement:exit': exitNestingStructure, + + WhileStatement: enterNestingStructure, + 'WhileStatement:exit': exitNestingStructure, + + DoWhileStatement: enterNestingStructure, + 'DoWhileStatement:exit': exitNestingStructure, + + SwitchStatement: enterNestingStructure, + 'SwitchStatement:exit': exitNestingStructure, + + TryStatement: enterNestingStructure, + 'TryStatement:exit': exitNestingStructure, + + WithStatement: enterNestingStructure, + 'WithStatement:exit': exitNestingStructure + }; + } +}; + +export default rule; diff --git a/tests/max-nesting-depth.test.ts b/tests/max-nesting-depth.test.ts new file mode 100644 index 0000000..cb700e9 --- /dev/null +++ b/tests/max-nesting-depth.test.ts @@ -0,0 +1,381 @@ +/** + * @fileoverview Tests for max-nesting-depth rule + * @author eslint-plugin-code-complete + */ + +import { ruleTester } from './config'; +import rule from '../rules/max-nesting-depth'; + +ruleTester.run('max-nesting-depth', rule, { + valid: [ + // Depth 1 - should pass + `function test() { + if (a) { + console.log('depth 1'); + } + }`, + + // Depth 2 - should pass + `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + console.log('depth 2'); + } + } + }`, + + // Depth 3 (exactly at limit) - should pass + `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + console.log('depth 3'); + } + } + } + }`, + + // Multiple structures at same depth level - should pass + `function test() { + if (a) { + console.log('depth 1'); + } + for (let i = 0; i < 10; i++) { + console.log('depth 1'); + } + while (b) { + console.log('depth 1'); + } + }`, + + // Function resets depth - should pass + `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + function inner() { + if (c) { + for (let j = 0; j < 5; j++) { + while (d) { + console.log('new function resets depth'); + } + } + } + } + } + } + } + }`, + + // Arrow function resets depth + `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + const fn = () => { + if (c) { + for (let j = 0; j < 5; j++) { + while (d) { + console.log('arrow function resets depth'); + } + } + } + }; + } + } + }`, + + // Different control structures within limit + `function test() { + try { + if (a) { + switch (b) { + case 1: + console.log('depth 3'); + break; + } + } + } catch (e) { + console.log('error'); + } + }`, + + // Custom max depth of 5 + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + try { + if (c) { + console.log('depth 5'); + } + } catch (e) {} + } + } + } + }`, + options: [{ maxDepth: 5 }] + }, + + // Top-level IIFE ignored by default + `(function() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + console.log('IIFE ignored, so depth 3'); + } + } + } + })();`, + + // For-of loop + `function test() { + for (const item of items) { + if (item.valid) { + console.log('depth 2'); + } + } + }`, + + // For-in loop + `function test() { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + console.log('depth 2'); + } + } + }`, + + // Do-while loop + `function test() { + if (a) { + do { + console.log('depth 2'); + } while (b); + } + }` + ], + + invalid: [ + // Depth 4 - exceeds default limit of 3 + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + if (c) { + console.log('depth 4'); + } + } + } + } + }`, + errors: [{ messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }] + }, + + // Depth 5 - multiple violations + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + try { + if (c) { + console.log('depth 5'); + } + } catch (e) {} + } + } + } + }`, + errors: [ + { messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }, + { messageId: 'maxNestingDepth', data: { depth: '5', maxDepth: '3' } } + ] + }, + + // Switch statement exceeding depth + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + switch (b) { + case 1: + if (c) { + console.log('depth 4'); + } + break; + } + } + } + }`, + errors: [{ messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }] + }, + + // Try-catch exceeding depth + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + try { + console.log('depth 4'); + } catch (e) {} + } + } + } + }`, + errors: [{ messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }] + }, + + // Catch clause also counts + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + try { + console.log('ok'); + } catch (e) { + if (d) { + console.log('depth 4'); + } + } + } + } + }`, + errors: [{ messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }] + }, + + // Custom max depth of 2 + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + if (b) { + console.log('depth 3, exceeds custom limit of 2'); + } + } + } + }`, + options: [{ maxDepth: 2 }], + errors: [{ messageId: 'maxNestingDepth', data: { depth: '3', maxDepth: '2' } }] + }, + + // IIFE not ignored when configured + { + code: `(function() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + if (c) { + console.log('depth 5 with IIFE counted'); + } + } + } + } + })();`, + options: [{ ignoreTopLevelIIFE: false }], + errors: [ + { messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }, + { messageId: 'maxNestingDepth', data: { depth: '5', maxDepth: '3' } } + ] + }, + + // For-of loop exceeding depth + { + code: `function test() { + if (a) { + for (const item of items) { + while (b) { + if (c) { + console.log('depth 4'); + } + } + } + } + }`, + errors: [{ messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }] + }, + + // For-in loop exceeding depth + { + code: `function test() { + if (a) { + for (const key in obj) { + while (b) { + if (c) { + console.log('depth 4'); + } + } + } + } + }`, + errors: [{ messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }] + }, + + // Do-while exceeding depth + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + do { + if (c) { + console.log('depth 4'); + } + } while (b); + } + } + }`, + errors: [{ messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }] + }, + + // With statement (deprecated but still in spec) + { + code: `function test() { + if (a) { + for (let i = 0; i < 10; i++) { + with (obj) { + if (c) { + console.log('depth 4'); + } + } + } + } + }`, + errors: [{ messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }] + }, + + // Multiple functions, each with violations + { + code: ` + function test1() { + if (a) { + for (let i = 0; i < 10; i++) { + while (b) { + if (c) { + console.log('depth 4 in test1'); + } + } + } + } + } + + function test2() { + if (x) { + for (let j = 0; j < 5; j++) { + while (y) { + if (z) { + console.log('depth 4 in test2'); + } + } + } + } + } + `, + errors: [ + { messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } }, + { messageId: 'maxNestingDepth', data: { depth: '4', maxDepth: '3' } } + ] + } + ] +}); diff --git a/types/rule-options.ts b/types/rule-options.ts index fb4672f..810c5ee 100644 --- a/types/rule-options.ts +++ b/types/rule-options.ts @@ -39,4 +39,9 @@ export interface ClassCohesionOptions extends BaseRuleOptions { export interface ComplexConditionalsOptions extends BaseRuleOptions { maxOperators?: number; +} + +export interface MaxNestingDepthOptions extends BaseRuleOptions { + maxDepth?: number; + ignoreTopLevelIIFE?: boolean; } \ No newline at end of file From f46570c2374397287a72080b0847c9be009e8ddb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Dec 2025 05:43:48 +0000 Subject: [PATCH 2/3] docs: update CLAUDE.md with max-nesting-depth rule --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 2489b88..6162e9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,7 @@ This plugin supports ESLint 9's flat config system: 6. **low-function-cohesion** - Detect functions doing unrelated tasks 7. **low-class-cohesion** - Detect classes with unrelated methods 8. **no-complex-conditionals** - Limit conditional complexity +9. **max-nesting-depth** - Enforce maximum nesting depth for control structures (default: 3 levels) ## Development Workflow From acbf8923ceac03009301e7ecee48bea23182da54 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Dec 2025 05:48:43 +0000 Subject: [PATCH 3/3] docs: add commit message conventions for release-please --- CLAUDE.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 6162e9f..85e650e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,3 +120,43 @@ This plugin supports ESLint 9's flat config system: - ESLint's `RuleTester` is used for rule testing - All tests must pass before builds are considered successful - The `pretest` script ensures the project is built before tests run + +## Commit Message Conventions + +This project uses **Conventional Commits** format for release-please automation. + +### Format +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +### Common Types +- `feat:` - New feature (triggers minor version bump, e.g., 1.3.1 → 1.4.0) +- `fix:` - Bug fix (triggers patch version bump, e.g., 1.3.1 → 1.3.2) +- `docs:` - Documentation changes only +- `chore:` - Maintenance tasks, dependency updates +- `refactor:` - Code changes that neither fix bugs nor add features +- `test:` - Adding or updating tests +- `perf:` - Performance improvements + +### Examples +```bash +feat: add max-nesting-depth rule to enforce code readability +fix: correct depth calculation in max-nesting-depth rule +docs: update README with new rule examples +chore(deps): update eslint to 9.30.1 +``` + +### Breaking Changes +Add `BREAKING CHANGE:` in the commit body or append `!` after type: +```bash +feat!: change default maxDepth from 3 to 2 + +BREAKING CHANGE: The default value for maxDepth option has changed from 3 to 2. +``` + +**Important:** Always use conventional commits format as release-please uses these to automatically generate changelogs and version bumps.