Cognitive Complexity

Cognitive Complexity is a measure of how difficult a unit of code is to intuitively understand. Unlike Cyclomatic Complexity, which determines how difficult your code will be to test, Cognitive Complexity tells you how difficult your code will be to read and understand.

Now, Code Climate can help you identify which methods are overly difficult to understand and prevent introducing them into your code.

📘

Cognitive Complexity

The Code Climate maintainability check for Cognitive Complexity is listed as "Method Complexity".

Difference from Cyclomatic Complexity

As an example of code which is easy to understand, but difficult to test, consider this PHP example:

<?php
switch ($meal) {
    case "breakfast":
        echo "Most important meal of the day! Enjoy.";
        break;
    case "lunch":
        echo "A reasonably important meal of the day.";
        break;
    case "dinner":
        echo "Alright, rounding out your day, very nice.";
        break;
    default:
       echo "Snacking is important!";
}
?>

This code is perfectly intuitive to understand, but if you wanted to test it exhaustively, you would need to write at least four test cases. This is what we mean when we say its Cyclomatic Complexity is higher than its Cognitive Complexity.

The Idea

A method's cognitive complexity is based on a few simple rules:

  1. Code is considered more complex when it uses shorthand that the language provides for collapsing multiple statements into one
  2. Code is considered more complex for each "break in the linear flow of the code"
  3. Code is considered more complex when "flow breaking structures are nested"

Let's break those down:

Shorthand

Let's say you're writing Ruby, and you write:

def destroy(post)
  if current_user && current_user.admin?
    post.destroy
  end
end

This uses &&, which does contribute to the method's Cognitive Complexity. If, however, you were to use the "safe navigation" operator and write:

def destroy(post)
  if current_user&.admin?
    post.destroy
  end
end

... this is a bit more intuitive, and doesn't contribute to the method's Cognitive Complexity.

Breaks in flow

When a method's logic flows from top to bottom, it is very easy to understand. Take this JavaScript snippet for example:

function count(a, b, c) {
  var total = 0;

  total += a;
  total += b;
  total += c;
  
  return total;
}

It flows from top to bottom with no breaks. Compare to this alternative implementation:

function count(a, b, c) {
  var total = 0;
  var nums = [a, b, c];
  
  for (var i = 0; i < nums.length; i++) {
    total += nums[i];
  }
  
  return total;
}

The for loop changes the function so it no longer flows directly from top to bottom, but now loops in circles a few times in the middle, which contributes to the Cognitive Complexity for the reader. It's a small thing, but it adds up.

Other examples of breaks in flow:

  • loops
  • conditionals
  • catching/rescuing exceptions
  • switch or case statements
  • sequences of logical operators (e.g. a || b && c || d)
  • recursion
  • jumps to labels

Nesting

The more deeply-nested your code gets, the harder it can be to reason about. This line of Java is pretty straight-forward:

if (env.debugMode()) {
  System.out.println("Hello, world!");
}

But when you're nested within multiple layers of conditionals or loops, a simple conditional isn't just a simple conditional, and leads to a greater Cognitive Complexity.

Here's the same conditional, in a totally different context:

while(theWorldTurns) {
  if(isMorning) {
    try {
      if (env.debugMode()) {
        System.out.println("Hello, world!");
      }
    } catch (BadDay e) {
      System.out.println("Yikes!");
    }
  }
}

In this example, the same conditional adds more complexity. Context matters.

The following count as nesting:

  • conditionals
  • loops
  • try/catch blocks

Further reading

Please see the Cognitive Complexity: A new way of measuring understandability white paper by G. Ann Campbell of SonarSource for further detail and many examples.