Expression Too Complex

ID

javascript.expression_too_complex

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Complexity

Language

JavaScript

Tags

complexity, control-flow, readability

Description

Reports one expression that combines more short-circuit and conditional operators than a reader can follow. The complexity of an expression is the number of &&, || and ternary ?: operators it contains; a count above the threshold — maxLogicalOperators, 4 by default — is reported, and only when the expression mixes more than one kind of operator. The expression can be an if/while test, a variable initializer, a call argument, a JSX attribute value — anywhere a boolean or ternary chain appears, not only in an if.

Only the outermost node of a chain is reported, so one dense expression produces one issue rather than one per nesting level. A nested function — a callback, an arrow, a method — starts a fresh expression: its operators are counted against it, and it is checked on its own.

Markup is the other boundary, and for the same reason. In cond && <Panel>{a && <X/>}{b ? <Y/> : <Z/>}</Panel> the render guards inside the panel are siblings in a tree, not operands of cond &&: no precedence relates them, and a reader takes them one line at a time. So a JSX element, fragment, attribute value or child expression ends the count, and whatever is inside it is measured on its own — a guard or an attribute expression dense enough on its own terms is still reported, at its own line.

Rationale

// Bad
if (user.active && user.verified && !user.banned && hasQuota(user) && isOpen(now) || user.isAdmin) {
  allow();
}
  • Every operator adds another combination of truth values the reader has to keep in mind, and && binds tighter than ||, so a || b && c does not group the way the eye reads it. The grouping is invisible without parentheses, which is exactly where a wrong term or a missing negation survives review.

  • A long chain of a single operator is not what costs a reader: a || b || c || d || e is a list, as easy to follow at eight terms as at two. That is why the rule stays silent on flat single-operator chains and reports only a mix — a device blacklist or a file-extension list is not worth rewriting.

  • Nullish coalescing ?? is not counted: the grammar rejects mixing it with && or || unparenthesised, so the precedence trap cannot arise from it, and a chain of ?? alone is a default-value list. Bitwise & and | are not counted either — they do not short-circuit, so they add no branching of their own.

This is an expression-level measure, deliberately separate from javascript.cyclomatic_complexity, which scores a whole function body from its data-flow graph. A function can be inside its complexity budget and still contain one unreadable condition, and a function of many simple branches can exceed the function-level budget with no complex expression in it.

Remediation

Name the intermediate conditions, or parenthesise the groups so the precedence is explicit.

// Good
const isEligible = user.active && user.verified && !user.banned;
const canServe = hasQuota(user) && isOpen(now);

if ((isEligible && canServe) || user.isAdmin) {
  allow();
}

Naming the parts also gives the condition somewhere to be tested and somewhere to put a comment. When the groups genuinely belong together, at least make the grouping visible: (a && b) || c reads the same to the parser and to the reader.

© 2026 Xygeni. All rights reserved