Skip to content
Arthur van de Vondervoort edited this page Feb 20, 2025 · 27 revisions

Show Cognitive Complexity diagnostics for methods above threshold

This rule will show a diagnostic when the Cognitive Complexity of a procedure or trigger is equal to or above the threshold.

Because Testability != Understandability

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.

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.

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    // +2
            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

Looking again again at the examples where 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 procedure 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.

What is Cognitive?

To understand why the cognitive aspect is so important, check out the excellent article Cognitive load is what matters, which provides a great introduction to the topic.

Diving deeper into Cognitive Complexity, you can start with this blog post from Sonar, which includes a link to the whitepaper.

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.

The AL Language

The following increments are supported for the AL Language extension for Microsoft Dynamics 365 Business Central.

Category Increment Nesting Level Nesting Penalty
if, ternary operator X X X
else, else if * X X
case X X X
for, foreach X X X
while, repeat X X X
sequences of binary logical operators X
each method in a recursion cycle X

Example

procedure VerifyLineQuantity()
begin
    if Type = Type::Item then begin                 // +1 (1 increment + 0 nesting penalty)
        if "Unit of Measure Code" <> '' then        // +2 (1 increment + 1 nesting penalty)
            if Status = Status::Open then           // +3 (1 increment + 2 nesting penalty)
                repeat                              // +4 (1 increment + 3 nesting penalty)
                    if Quantity < 0 then            // +5 (1 increment + 4 nesting penalty)
                        HandleNegativeQuantity();
                    if Quantity = 0 then            // +5 (1 increment + 4 nesting penalty)
                        HandleZeroQuantity();
                    if Quantity > 0 then            // +5 (1 increment + 4 nesting penalty)
                        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 (1 increment + 0 nesting penalty)
        if Quantity < 0 then                        // +2 (1 increment + 1 nesting penalty)
            HandleNegativeQuantity();
        if Quantity = 0 then                        // +2 (1 increment + 1 nesting penalty)
            HandleZeroQuantity();
        if Quantity > 0 then                        // +2 (1 increment + 1 nesting penalty)
            HandlePositiveQuantity();
    until AllHandled();
end;                                                // Cognitive Complexity: 7

Increments

image

To view the details of the Cognitive Complexity increments, you can enable the LC0089i diagnostic. Together with the Error Lens extension for VS Code, you can easily see how the Cognitive Complexity is calculated.

{
    "id": "LC0089i",
    "action": "Info",
    "justification": "Show every individual increment metric of the Cognitive Complexity"
}

Logical operators

Cognitive Complexity does not increment for each binary logical operator. Instead, it assesses a fundamental increment for each sequence of binary logical operators. For instance, consider the following pairs

a and b
a and b and c and d

a or b
a or b or c or d

Understanding the second line in each pair isn’t that much harder than understanding the first. On the other hand, there is a marked difference in the effort to understand the following two lines:

a and b and c and d
a or b and c or d

Because boolean expressions become more difficult to understand with mixed operators, Cognitive complexity increments for each new sequence of like operators. For instance

if                  // +1 for 'if'
 a and b and        // +1
   c or d or        // +1
     e and f        // +1
then;


if                  // +1 for 'if'
 a and              // +1
   not (b and c)    // +1
then;

While Cognitive Complexity offers a "discount" for like operators relative to Cyclomatic Complexity, it does increment for all sequences of binary boolean operators such as those in variable assignments, method invocations, and return statements.

Note: The AL Language does not support Lazy Evaluation. While using a sequence of binary logical operators can improve readability, it may come at a performance cost during runtime.

Recursion

Unlike Cyclomatic Complexity, Cognitive Complexity adds a fundamental increment for each method in a recursion cycle, whether direct or indirect. Because Recursion contribute very similar complexity like Loop.

Nesting penalties do not apply to recursions, so only increments are applied regardless of the nesting level.

procedure SelfRecursive()
begin
    SelfRecursive();        // +1 (increment)
end;
procedure IndirectRecursiveA()
begin
    IndirectRecursiveB();   // +1 (increment)
end;

procedure IndirectRecursiveB()
begin
    IndirectRecursiveC();   // +1 (increment)
end;

procedure IndirectRecursiveC()
begin
    IndirectRecursiveA();   // +1 (increment)
end;

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 (1 increment + 0 nesting penalty)
    ...
else
    if condition2 then              // +1 (1 increment + 0 nesting penalty)
        ...
    else
        if condition3 then begin    // +1 (1 increment + 0 nesting penalty)
            statement1
            if condition4 then      // +2 (1 increment + 1 nesting penalty)
                ...
        end;

Read more

Clone this wiki locally