Expression Too Complex

ID

java.expression_too_complex

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Complexity

Language

Java

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. The expression can be an if/while/do test, a variable initializer, a method argument, an array element — anywhere a boolean or ternary chain appears, not only in an if.

Two conditions have to hold, not one. Out of the box the expression must also combine more than one kind of operator — && with ||, either with a ternary, or a ternary inside another. A flat chain of a single operator (a || b || c || d || e || f) is a list, and is no harder to read at eight terms than at two, so it is not reported however long it grows. Set requireMixedOperatorKinds to false to score every expression on its raw operator count alone.

Rationale

A dense condition is hard to read not because it is long but because every operator doubles the number of truth-value combinations the reader has to keep in mind. Precedence makes it worse: && binds tighter than ||, so a || b && c does not group the way the eye reads it, and the grouping is invisible without parentheses. This is exactly where a wrong term or a missing negation survives review — nothing in the layout marks the operand that is wrong.

The mixing requirement is what separates the two cases. Across a ten-project Java corpus, 52% of this rule’s findings were flat single-operator chains — device allow-lists, file-extension lists, status enumerations — and none of them was judged worth changing, while every finding that was worth acting on mixed at least two kinds. That measurement is the default, not a law: a code base that wants every compound condition pushed towards named intermediates turns requireMixedOperatorKinds off and gets the raw count.

Only the outermost expression of a chain is reported, so one dense condition produces one issue rather than one per nesting level; the count sums every operator reachable from it through ordinary expression composition (parens, method arguments, array elements, an expression-bodied lambda). A block-bodied lambda, or a nested type’s own member declarations, starts a fresh expression and is checked independently rather than folded into the count of whatever it sits in. Bitwise & and | are not counted — they do not short-circuit — and neither are comparisons, which carry no branching of their own.

Not reported inside an equals(Object) override, where comparing every field with a chain of && is the idiom, not a smell.

This is an expression-level measure, deliberately separate from the function-level cyclomatic complexity rule. A function can sit comfortably inside its complexity budget and still hold one unreadable condition, and a function with many simple branches can exceed the function-level budget without containing a single complex expression.

class Gate {

  boolean canEnter(boolean member, boolean paid, boolean banned, boolean invited, boolean staff,
      boolean vip) {
    // FLAW -- 5 operators, above the default 4, and of two kinds
    if (member && paid && !banned || invited && staff || vip) {
      return true;
    }

    // OK -- 5 operators, above the default 4, but all of one kind: a list, not a puzzle
    if (member && paid && !banned && invited && staff && vip) {
      return true;
    }

    if (member && paid && !banned) {                         // OK -- 2 operators
      return true;
    }

    return false;
  }

  boolean readable(boolean member, boolean paid, boolean banned, boolean invited, boolean staff) {
    boolean inGoodStanding = member && paid && !banned;      // OK -- named, 2 operators
    boolean hasOverride = invited || staff;                  // OK -- named, 1 operator
    return inGoodStanding || hasOverride;                    // OK -- 1 operator
  }
}

Remediation

Give the intermediate conditions names. Assigning each part of the condition to a well-named local turns the operator soup into a sentence, and short-circuiting is preserved. For a condition that belongs to a domain concept, promote it to a small predicate method on the type it interrogates. Where the expression selects among several results rather than testing one thing, a switch expression usually reads better than stacked ternaries.

Configuration

Property Default Description

maxLogicalOperators

4

Number of conditional operators (&&, ||, ?:) in one expression, strictly greater than which the expression is flagged. Raise it for a codebase that tolerates denser conditions; lower it to push every compound condition towards named intermediates.

requireMixedOperatorKinds

true

Report only expressions that combine more than one kind of operator. Set to false to flag any expression over maxLogicalOperators, including a flat chain of a single operator — more findings, most of them lists rather than precedence puzzles.

© 2026 Xygeni. All rights reserved