-
Notifications
You must be signed in to change notification settings - Fork 32
LC0090
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 is how much a developer needs to think in order to complete a task.
Cognitive load is what matters
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.
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.
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.
A Cognitive Complexity score is assessed according to three basic rules:
- Ignore structures that allow multiple statements to be readably shorthanded into one
- Increment (add one) for each break in the linear flow of the code
- 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
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.
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.
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;