-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cognitive versus Cyclomatic Complexity #924
Comments
Thank you for raising this issue! It really challenged me to think about how we can improve the approach to measuring code complexity. I completely understand your remarks on the early exits and case-statements, where this significantly increases the cyclomatic complexity. The cyclomatic complexity is a established standard, which represents a quantitative measure of the number of linearly independent paths. Interestingly, both of the examples you provided return the same Cyclomatic Complexity score of 8. If you would have to write automated test and want to archive a 100% coverage, the amount of test's are the same between the both examples. This highlights an important point: Cyclomatic Complexity isn’t always the right metric when it comes to code readability. The good news is that I believe I have a another metric which is a better solution for your question. Cognitive ComplexityCognitive 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. A Cognitive Complexity score is assessed according to three basic rules:
When applying this metric to your examples, the difference in complexity becomes much clearer: I’ll be diving deeper into this and plan to introduce Cognitive Complexity as a new rule in LinterCop in the coming weeks. If you have feedback that would been incredibly helpful shaping this direction, and I appreciate the insights you’ve shared. Thanks again for starting this discussion, I’ll keep you updated on the progress. |
Thanks for weighing in Arthur, that looks about right! Nesting should be punished a lot more. I did read about the cognetive complexity in issue #566 but since this topic was closed I thought it has lost traction. Also I didn't really understand the metric, now I think I do, thank you for this clear example. And you are right cyclomatic complexity is an established metric that is why I proposed a setting, but it looks like cognetive complexity is even better. In that case there is no need for improved cyclomatic complexity. In my opinion this would really be usefull and a fair metric, and can be made mandatory and possibly integrated into build pipelines. Somehow functions always have a tendancy to grow, and we need to avoid raising thresholds or pragma warnings because the metric is not optimal. Looking forward for the release. |
Two new rules are available in the v0.34.1 Pre-Release version of the LinterCop:
It would be great if you could give it a try and I’d love to hear your feedback on it! |
I would like to try this, but somehow it does not pick up the pre-release. I'm sure it is me being stupid but what am I doing wrong?
|
Would also love to the test the new rule but also struggling with getting the pre-release version: When setting |
Oops 😳 The Pre-release was missing assets, so the VS Code extension couldn't find an version to download. I'm not sure yet what went wrong, as some versions failed on the automated tests. I'm guessing I hit an thread safety issue, but I'm unsure how I can test this. I have now created an updated v0.34.1 Pre-release with Assets(!), so I would expect that the VS Code extension will now work as expected. |
Got it now! One of our major (very large) vertical:
And had a quick look 95% of these functions could use a diet. Thank you for this effort! |
Great to hear that the results are looking promising! I’ll need to look into some last thing, like else and else if constructs, so the calculations can slightly change before it will be in the release version of the LinterCop. |
Yeah I do see the main reason why we still have many high cognetive complexity (despite our best efforts ofcourse) because of the following guard clause pattern we still have a lot in our code. We do not use begin end anymore for single statement if conditions, but there is still a lot of legacy code programmed this way. This counts as 1:
This one is for free:
This is probably by design and doesn't bother me as much, is easy to refactor. So I guess I will be busy for a while. :) |
Also we may have to treat an error the same as an exit in my opinion. this guard clause is for free:
this error guard clause is punished with 1:
|
@lv-janpieter Thank you for trying it out and providing feedback. It's been really helpful while working on this new rule and trying to convert the information of the whitepaper to the constructs of the AL Language. I've update the v0.34.1 Pre-release of the LinterCop today. Currently the rule will analyze the following;
There's also something on recursion in the whitepaper, where I'm unsure if I'm able to implement this.
procedure selfRecursive()
begin
this.selfRecursive();
end;
procedure indirectRecursiveA()
begin
this.indirectRecursiveB();
end;
procedure indirectRecursiveB()
begin
this.indirectRecursiveA();
end; I'm doubting if it's feasible on the AL Language to analyze this. I'll have a look into this, but I'm thinking of Dependency Injection on Interfaces combined with Enums it could get extremely complicated. I could do the selfRecursive, but not sure if implementing only a part of the recursion is an good idea. Would be great to hear some idea's on this. I wanted also to provide some feedback on your findings;
You're right, this is indeed by design. The code analyzer can detect, in the second example without the
In the documentation I found on Guard Clauses, the context focuses on if (SalesLine.Type = SalesLine.Type::Item) then begin
EmitTelemetry('Wrong Sales Line Type', SalesLine);
Error('I cannot be an item!');
end; Personally I prefer to use the Guard Clauses to return an boolean to the calling method, to deligate the ErrorHandling back to the calling method. local procedure IsValidRecord(var SalesHeader: Record "Sales Header"): Boolean
begin
if SalesHeader.IsTemporary() then
exit(false);
if not (SalesHeader."Document Type" in [
Enum::"Sales Document Type"::"Quote",
Enum::"Sales Document Type"::"Order"])
then
exit(false);
if SalesHeader."Order Date" = 0D then
exit(false);
exit(true);
end; Would be great to gather more viewpoint on this. |
using begin/end counting error AL does not know such thing as a try catch. Only a TryFunction might resemble something simular. Also in this case the complexity is added to the calling function and not the tryfunction itself. An Error will always halt the execution of a function, just like an exit. You might argue that raising an Error is just calling another function. Just like raising a FieldError. And maybe some more functions i'm missing. A TestField might raise an error but it might continue. So I agree it is a bit of a gray area. I don't know what the documentation sais about throwing an error without try / catch within a single statement if. That would be the simular case. I cannot find any documentation however how this situation is handled. I personally feel it would make sense not make it count. If however this is counted it would not be a big deal, most guard clauses are exits. |
Also just a wild idea. Some functions are difficult to refactor (or we are just unwilling to) and we might need to pragma the congnitive complexity. However when doing this everone is free to expand the function. And we don't want this. Would it be feasable to have a way of allowing a higher score for a specific function? Maybe add a comment before the function: Or With a system like this we are able to keep enforcing cognetive complexity and forbid pragma it, while in the code review we can review every raise in complexity and reject it if we don't agree with the justification of raising it. |
In the Cognitive Complexity whitepaper I find this on early exits: Jumps to labels I've read this multiple times, but still didn't fully comprehend it. I asked help of ChatGTP to break it down for me; 1. "goto adds a fundamental increment to Cognitive Complexity"
Example (if AL had
|
Wow indeed there are many more scenario's to consider. I did not realize. Well done. I agree is should be well thought through. Althoug officially lintercop is still in preview so users have to expect some surprises. But when it is integrated in build pipelines you don't want additional warnings pop up because of changed rules. Exactly. It is equilavant to a throw error in c#. It stop's execution of the function (unless there is a try / catch, but in that case I guess the try catch is already counted). ChatGPT is much better in explaining than me. Multi level jump Your link to the documentation doesn't work, but I found another explanation (targeted at java). |
Some more reading into jumps and labels, I've found this the cognitive complexity whitepaper is a bit vague/ambiguous, which helped me better understanding this.
Where the AL Language doesn't support break or continue to a number, makes that we can handle the
In the same post as I was referring to, I find;
If I now understand the "..an early return can often make code much clearer, no other jumps or early exits cause an increment.." correct, it means that a "no other jumps" mean a jump without an label or number. By this logic a I think I now have everything covered, and the rule is ready for a release candidate. |
We have been starting to use LC for a few months now, and really appreciate it. And consider it now mandatory for our projects.
We think LC0010 is really interesting. We are convinced functions should be easy to understand. And having a metric to determine this is really handy. We want it to be part of the process for all of our developers.
However LC0010 is producing too much false positives. We know we can raise the treshold but that would grant too much coplexity for other type of functions. So I would like to make a few suggestions which in my opinion would produce better results. This tweaks could be included as additional settings depending on preference just like the treshold.
Wondering how you all feel about this.
Tweak 1:
View case statements as only 1 added complexity.
Case options are easy to read and should not raise the complexity.
I know this issue was raised before a time ago #566
Tweak 2:
Do not consider early exits (also known as guard clauses) as additional complexity.
We use early exit's extensively to avoid a lot of nesting. But this quickly raises cyclomatic complexity.
Would be nice to have a setting where you can say "ignore unnested if statements which are immediatly followed by an exit".
For example, we have a lot of functions styled like the one below (imagine this a function being on the sales line).
This function already has 8 points in score, but we consider this a well designed function. In reality we would only want the main section counted.
This variant of the function however should definitly have full complexity count:
Thanks a lot!
The text was updated successfully, but these errors were encountered: