Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -119,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
```
<type>[optional scope]: <description>

[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.
3 changes: 2 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
};

Expand Down
3 changes: 2 additions & 1 deletion rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
export { default as noComplexConditionals } from './no-complex-conditionals';
export { default as maxNestingDepth } from './max-nesting-depth';
161 changes: 161 additions & 0 deletions rules/max-nesting-depth.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading