Skip to content
Arthur van de Vondervoort edited this page Feb 14, 2025 · 9 revisions

Show Cognitive Complexity diagnostics for methods above threshold

Introduction

When we say that someone's code quality is bad, in most of the time, what we really mean is that the code is hard to understand. Many developers prefer to start a new project from scratch rather than add features to a legacy project, because the mental cost of understanding the code is much higher than creating something new. As the features of the project increases, it inevitably becomes more difficult to understand. Therefore, it's important to have a way to measure and control the code's complexity and understandability.

Cognitive load

Cognitive load is how much a developer needs to think in order to complete a task.

Cognitive load is what matters

What is Cognitive Complexity?

Cognitive Complexity is the difficulty level in understanding concepts or solving problems based on the interaction of multiple elements.

In software engineering, cognitive complexity quantifies developers challenges in comprehending code or software systems. Unlike traditional metrics focusing only on code structure, cognitive complexity accounts for the human cognitive load in navigating a program's logic. It evaluates the mental effort required for understanding, debugging, and modifying software, making it a vital measure of software quality.

The cognitive complexity metric assesses the intricacies of control structures, conditional nesting, and program flow, identifying code sections that may need simplification or refactoring to improve code quality and maintainability.

An illustration of the problem

procedure SumOfPrimes(Max: Integer): Integer
var
    Total: Integer;
    I, J : Integer;
    IsPrime: Boolean;
begin
    Total := 0;
    for I := 1 to Max do begin
        IsPrime := true;
        for J := 2 to I - 1 do begin
            if (I mod J = 0) then begin
                IsPrime := false;
                break;
            end;
        end;
        if IsPrime then
            Total += I;
    end;
    exit(Total);
end;               // Cyclomatic Complexity: 5
procedure GetWords(Number: Integer): Text
begin
    case Number of
        1:
            exit('one');
        2:
            exit('a couple');
        3:
            exit('a few');
        4:
            exit('a quadruple');
        else
            exit('lots');
    end;
end;               // Cyclomatic Complexity: 5

While Cyclomatic Complexity gives equal weight to both the SumOfPrimes and GetWords methods, it is apparent that SumOfPrimes is much more complex and difficult to understand than GetWords. This illustrates that measuring understandability based solely on the paths of a program may not be sufficient.

Intuitively 'right' complexity scores

procedure SumOfPrimes(Max: Integer): Integer
var
    Total: Integer;
    I, J : Integer;
    IsPrime: Boolean;
begin
    Total := 0;
    for I := 1 to Max do begin          // +1
        IsPrime := true;
        for J := 2 to I - 1 do begin    // +3
            if (I mod J = 0) then begin // +3
                IsPrime := false;
                break;
            end;
        end;
        if IsPrime then                 // +2
            Total += I;
    end;
    exit(Total);
end;               // Cognitive Complexity: 8
procedure GetWords(Number: Integer): Text
begin
    case Number of                      // +1
        1:
            exit('one');
        2:
            exit('a couple');
        3:
            exit('a few');
        4:
            exit('a quadruple');
        else
            exit('lots');
    end;
end;               // Cognitive Complexity: 1

Let's look again at the examples, where Cyclomatic Complexity give them the same score. The Cognitive Complexity algorithm gives these two methods markedly different scores, ones that are far more reflective of their relative understandability.

With Cyclomatic Complexity, an method with early exits and case statement, can have the same number of decision points. However, Cognitive Complexity addresses this limitation by not incrementing for each decision point, making it easier to compare the metric values.

Basic criteria and methodology

A Cognitive Complexity score is assessed according to three basic rules:

  1. Ignore structures that allow multiple statements to be readably shorthanded into one
  2. Increment (add one) for each break in the linear flow of the code
  3. Increment when flow-breaking structures are nested

Additionally, a complexity score is made up of four different types of increments:

  • Nesting - assessed for nesting control flow structures inside each other
  • Structural - assessed on control flow structures that are subject to a nesting increment, and that increase the nesting count
  • Fundamental - assessed on statements not subject to a nesting increment
  • Hybrid - assessed on control flow structures that are not subject to a nesting increment, but which do increase the nesting count

While the type of an increment makes no difference in the math - each increment adds one to the final score - making a distinction among the categories of features being counted makes it easier to understand where nesting increments do and do not apply.

Not all types of increments are supported in the AL Language extension for Microsoft Dynamics 365 Business Central.

Category Increment Nesting Level Nesting Penalty AL
if, ternary operator X X X YES
else if, else X X YES*
case X X X YES
for, foreach X X X YES
while, repeat X X X YES
catch X X X NO
goto LABEL, break LABEL, continue LABEL, break NUMBER, continue NUMBER X X X NO
sequences of binary logical operators X   YES
each method in a recursion cycle X X X NO
nested methods and method like structures such as lambdas X X X NO

* See Compensating Usages

Example

procedure HandleAllTheQuantities()
begin
    if Type = Type::Item then begin                 // +1 
        if "Unit of Measure Code" <> '' then        // +2
            if Status = Status::Open then           // +3
                repeat                              // +4
                    if Quantity < 0 then            // +5
                        HandleNegativeQuantity();
                    if Quantity = 0 then            // +5
                        HandleZeroQuantity();
                    if Quantity > 0 then            // +5
                        HandlePositiveQuantity();
                until AllHandled();
    end;
end;                                                // Cognitive Complexity: 25
procedure VerifyLineQuantity()
begin
    if Type <> Type::Item then
        exit;

    if "Unit of Measure Code" = '' then
        exit;

    if Status <> Status::Open then
        exit;
        
    repeat                                          // +1
        if Quantity < 0 then                        // +2
            HandleNegativeQuantity();
        if Quantity = 0 then                        // +2
            HandleZeroQuantity();
        if Quantity > 0 then                        // +2
            HandlePositiveQuantity();
    until AllHandled();
end;                                                // Cognitive Complexity: 7

Diving deeper into Cognitive Complexity, you can start with this blog post from Sonar, which includes a link to the whitepaper by the primary author. Koh Hom's blog also features a great article, Introducing Code Complexity Metric: Cognitive Complexity. Additionally, the SciTools blog has an excellent article on the Cognitive Complexity Metric Plugin to give you a detailed understanding of Cognitive Complexity metric.

Threshold

What should the limit be?

I would say there shouldn't be one. Because the essential complexity for a simple calculator app is far, far lower than for a program on the Space Shuttle. And if you try to make the Space Shuttle program fit inside the calculator threshold, you're absolutely going to break something.

Primary author of Cognitive Complexity on Stack Overflow

If you're uncertain about the right threshold, the matrix below could serve as a starting point.

Cognitive Complexity Code Quality Readability Maintainability
1-5 Simple and easy to follow High Easy
6-10 Somewhat complex Medium Moderate
11-20 Complex Low Difficult
21+ Very complex Poor Very difficul

With the configuration of the LinterCop.json, the threshold for the LC0090 diagnostic can be adjusted.

{
  "cognitiveComplexityThreshold": 15
}

To always display the Cognitive Complexity metric, a second diagnostic is available: LC0089. This will always show the Cognitive Complexity metric, regardless of the threshold.

Compensating Usages

For AL, which lacks an else if structure, an if as the only statement in an else clause does not incur a nesting penalty. Additionally, there is no increment for the else itself. That is, an else followed immediately by an if is treated as an else if, even though syntactically it is not.

if condition1 then                  // +1 structure, +0 for nesting
    ...
else
    if condition2 then              // +1 structure, +0 for nesting
        ...
    else
        if condition3 then begin    // +1 structure, +0 for nesting
            statement1
            if condition4 then      // +1 structure, +1 for nesting
                ...
        end;

Read more

Clone this wiki locally