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 |
|---|---|---|
|
|
Number of conditional operators ( |
|
|
Report only expressions that combine more than one kind
of operator. Set to |