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
62 changes: 62 additions & 0 deletions docs/rules/no-complex-conditionals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# no-complex-conditionals

Disallows complex conditionals with too many logical operators (`&&`, `||`, `??`).

## Rule Details

Complex conditionals are hard to read and understand. This rule checks `if`, `while`, `do-while` loops, and `ternary` operators for conditions that use more than the allowed number of logical operators.

Examples of **incorrect** code for this rule (with default options):

```js
// ❌ Bad - too many operators (3 operators, default max is 2)
if (isValid && hasPermission && !isBlocked && isActive) {
doSomething();
}

// ❌ Bad - complex condition in ternary
const status = (isReady && !hasError && isLoaded && user.loggedIn) ? 'active' : 'inactive';
```

Examples of **correct** code for this rule:

```js
// ✅ Good - simple conditions (within limit)
if (isValid && hasPermission && isActive) {
doSomething();
}

// ✅ Good - extracted to variable
const canAccess = isValid && hasPermission && !isBlocked && isActive;
if (canAccess) {
doSomething();
}

// ✅ Good - extracted to function
if (canUserAccess(user)) {
doSomething();
}
```

## Options

This rule has an object option:

- `"maxOperators"`: (default: `2`) The maximum number of logical operators allowed in a condition.

### maxOperators

Set the maximum number of logical operators allowed.

```js
// "code-complete/no-complex-conditionals": ["error", { "maxOperators": 3 }]

// ✅ OK with maxOperators: 3
if (a && b && c && d) {
// ...
}
```

## When Not To Use It

If you prefer to have all logic inline regardless of complexity, you can disable this rule. However, extracting complex logic usually improves readability.
3 changes: 2 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export default {
'no-late-argument-usage': rules.noLateArgumentUsage,
'no-late-variable-usage': rules.noLateVariableUsage,
'low-function-cohesion': rules.lowFunctionCohesion,
'low-class-cohesion': rules.lowClassCohesion
'low-class-cohesion': rules.lowClassCohesion,
'no-complex-conditionals': rules.noComplexConditionals
}
};

Expand Down
3 changes: 2 additions & 1 deletion rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { default as enforceMeaningfulNames } from './enforce-meaningful-names';
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 lowClassCohesion } from './low-class-cohesion';
export { default as noComplexConditionals } from './no-complex-conditionals';
87 changes: 87 additions & 0 deletions rules/no-complex-conditionals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* @fileoverview Rule to warn about complex conditionals
* @author eslint-plugin-code-complete
*/

import { Rule } from 'eslint';
import { ComplexConditionalsOptions } from '../types/rule-options.js';
import { createRuleMeta, RULE_CATEGORIES } from '../utils/rule-meta.js';

const rule: Rule.RuleModule = {
meta: createRuleMeta('no-complex-conditionals', {
description: 'Avoid complex conditionals with too many logical operators',
category: RULE_CATEGORIES.BEST_PRACTICES,
recommended: true,
schema: [
{
type: 'object',
properties: {
maxOperators: {
type: 'number',
default: 2
}
},
additionalProperties: false
}
],
messages: {
complexConditional: 'This condition is too complex ({{count}} operators). Maximum allowed is {{max}}. Consider extracting it to a function or variable.'
}
}),

create(context: Rule.RuleContext): Rule.RuleListener {
const options = context.options[0] || {} as ComplexConditionalsOptions;
const maxOperators = options.maxOperators !== undefined ? options.maxOperators : 2;

const countLogicalOperators = (node: any): number => {
if (!node) return 0;

if (node.type === 'LogicalExpression') {
return 1 + countLogicalOperators(node.left) + countLogicalOperators(node.right);
}

if (node.type === 'UnaryExpression' && node.operator === '!') {
return countLogicalOperators(node.argument);
}

return 0;
};

const checkCondition = (node: any, testNode: any) => {
if (!testNode) return;

const operatorCount = countLogicalOperators(testNode);

if (operatorCount > maxOperators) {
context.report({
node: testNode,
messageId: 'complexConditional',
data: {
count: operatorCount.toString(),
max: maxOperators.toString()
}
});
}
};

return {
IfStatement(node) {
checkCondition(node, node.test);
},
WhileStatement(node) {
checkCondition(node, node.test);
},
DoWhileStatement(node) {
checkCondition(node, node.test);
},
ForStatement(node) {
checkCondition(node, node.test);
},
ConditionalExpression(node) {
checkCondition(node, node.test);
}
};
}
};

export default rule;
54 changes: 54 additions & 0 deletions tests/no-complex-conditionals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @fileoverview Tests for no-complex-conditionals rule
* @author eslint-plugin-code-complete
*/

import { ruleTester } from './config';
import rule from '../rules/no-complex-conditionals';

ruleTester.run('no-complex-conditionals', rule, {
valid: [
// Simple conditions
'if (a) {}',
'if (a && b) {}',
'if (a && b && c) {}', // 2 operators
'while (a || b) {}',
'const x = a ? b : c',

// Mixed operators within limit
'if (a && b || c) {}',

// Configurable limit
{
code: 'if (a && b && c && d) {}', // 3 operators
options: [{ maxOperators: 3 }]
}
],
invalid: [
{
code: 'if (a && b && c && d) {}', // 3 operators, default limit 2
errors: [{ messageId: 'complexConditional', data: { count: '3', max: '2' } }]
},
{
code: 'while (a || b || c || d) {}',
errors: [{ messageId: 'complexConditional', data: { count: '3', max: '2' } }]
},
{
code: 'const x = (a && b && c && d) ? 1 : 0',
errors: [{ messageId: 'complexConditional', data: { count: '3', max: '2' } }]
},
{
code: 'if (a && b) {}',
options: [{ maxOperators: 0 }],
errors: [{ messageId: 'complexConditional', data: { count: '1', max: '0' } }]
},
{
code: 'for (let i = 0; a && b && c && d; i++) {}',
errors: [{ messageId: 'complexConditional', data: { count: '3', max: '2' } }]
},
{
code: 'do {} while (a && b && c && d)',
errors: [{ messageId: 'complexConditional', data: { count: '3', max: '2' } }]
}
]
});
4 changes: 4 additions & 0 deletions types/rule-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ export interface FunctionCohesionOptions extends BaseRuleOptions {

export interface ClassCohesionOptions extends BaseRuleOptions {
minClassLength?: number;
}

export interface ComplexConditionalsOptions extends BaseRuleOptions {
maxOperators?: number;
}