7. Code complexity metrics. Cyclomatic vs Cognitive complexity. What is clear code for you?
Video version
(Leave your feedback on YouTube)
Hard-skill answer for the question "What is a clear code for you?"
The code has clear metrics that can be calculated.
Cyclomatic complexity
I'll start with the classic one - cyclomatic complexity. This is a metric developed back in the 1980s to determine the complexity of a project, a specific class, or a function.
I will try to explain in very simple words, and if that's not enough, you can google it.
The cyclomatic complexity algorithm builds a graph where each code in statements like
if
, else
, for
, do-while
, each switch-case
option, try-catch
, and so on is a node.
It calculates how many paths can be taken from the beginning to the end of the graph. The number of possible paths is the complexity indicator. The higher it is, the more complex your program is.
But I mentally prefer another definition. It has more practical sense. Cyclomatic complexity shows how many tests are needed to 100% cover the entire code. A very simple example, to cover a division function, you need 2 tests: the first passes 2 regular numbers, and the second passes a divisor equal to zero and expects an error.
function division($numerator, $denominator) {
if ($denominator === 0) {
throw new Exception("Error: Division by zero is not allowed.");
}
return $numerator / $denominator;
}
This algorithm also shows the test coverage of your program. And this is a great example that shows 100% test coverage doesn't mean the code is 100% tested, it only shows that all lines of code are involved in the tests.
And cyclomatic complexity was set up automatically in the previous video about PHP Insights. There, the metric is a bit hidden and shows what percentage of classes have a complexity higher than 5 by default.
I definitely recommend using it, and I use it myself. It really helps to see if you are doing something wrong... too complex.
But cyclomatic complexity has a problem. I'll try to illustrate it.
for ($i = 0; $i < count($array); $i++) { // +1
for ($j = 0; $j < count($array[$i]); $j++) { // +1
for ($k = 0; $k < count($array[$i][$j]); $k++) { // +1
for ($p = 0; $p < count($array[$i][$j][$k]); $p++) { // +1
if ($array[$i][$j][$k][$p] === 1) { // +1
$result = 2;
}
}
}
}
}
This is an example of code with a cyclomatic complexity of 5. Not a high complexity. But here we are dealing with a 4-dimensional array. Any developer would say this is very complex code and should be refactored.
On the other hand, look at this code.
switch($number) {
case 1: return 'One'; // +1
case 2: return 'Two'; // +1
case 3: return 'Tree'; // +1
case 4: return 'Four'; // +1
default: return 'More'; // +1
}
And this is also complexity 5. Not very difficult, agree.
So we conclude. Cyclomatic complexity is useful, but does not always indicate real difficulty.
Cognitive complexity
Understanding this issue, in 2017, the company Sonar (SonarQube) introduced its metric.
Cognitive Complexity. Essentially, it's the same as cyclomatic complexity, but, firstly, each nested operator adds one more unit of complexity. So, for example, the code with a 4-dimensional array would have a complexity of 15.
for ($i = 0; $i < count($array); $i++) { // +1
for ($j = 0; $j < count($array[$i]); $j++) { // +2
for ($k = 0; $k < count($array[$i][$j]); $k++) { // +3
for ($p = 0; $p < count($array[$i][$j][$k]); $p++) { // +4
if ($array[$i][$j][$k][$p] === 1) { // +5
$result = 2;
}
}
}
}
}
Secondly, cognitive complexity takes into account language constructs such as ternary operators, match expressions, case-of statements, and so on. Therefore, the complexity of the example with a switch statement would be 1.
You can read the official documentation about cognitive complexity here.
Of course, there are other metrics, such as depth of inheritance or coupling between classes.
But I'm more focused on the practical side here. So cyclomatic complexity is already set up, and I have experience with it, so I highly recommend it.
Cognitive complexity setup
I haven't worked with cognitive complexity yet, but I really want to. Therefore, I put package It requires phpstan, but it's worth it.
And make config file
parameters:
level: 0
paths:
- app/
- tests/
cognitive_complexity:
class: 50
function: 8
level: 0
- PHPStan check level. I set it to 0 (weakest) because PHP Insights already handles code editing and analysis. There's no need to duplicate checks.paths
- Directories to scan; I'll also add modules here.cognitive_complexity
- Maximum allowable cognitive complexity for a class and a specific function.
Add command to .husky/pre-commit
and gitlab-ci.yml
docker exec -t bh-app php artisan insights --fix --no-interaction
docker exec -t bh-app vendor/bin/phpstan
git add $(git diff --cached --name-only --diff-filter=ACM)
script:
- docker compose exec app php artisan insights --no-interaction
- docker compose exec app vendor/bin/phpstan