Cyclomatic Complexity Calculation for Overall Project Code

Hello everyone,

I’d like to discuss whether cyclomatic complexity should be calculated for the overall codebase of a project. In my experience, I’ve noticed that the cyclomatic complexity for the overall code is quite high, with a value of 300. This high complexity seems to be due to numerous boundary conditions and essential checks spread across different modules.

Given this, I’m wondering if it’s advisable to consider the cyclomatic complexity at the project level rather than just individual components, as it reflects the overall structure and maintainability of the code.

What are your thoughts on this? How do you handle high complexity in large projects?

Looking forward to your insights!

Best,
Shubhashini

Hi @Shubhashini_Patil,

Disclaimer: I’m sharing my personal opinion here, it is not an official point of view at Sonar.

First, cyclomatic complexity does not map very well to the actual complexity of the source code. I think our cognitive complexity metric is a much better fit for how developers think about source code.

Secondly, the main goal of cyclomatic complexity at the function level is to detect when a function becomes too large to be testable and, by extension, too large to understand and improve. It is actionable: After a threshold, the function should be split into smaller and more manageable units.

On the other hand, since cognitive complexity is monotonically increasing, a large project will always have a larger complexity than a small one. What is the meaning of a total complexity of 9000? Does it mean the code is badly written, badly architectured, or too complex, or does it just mean that the project has many features? I don’t think a complexity value (cyclomatic or cognitive) at the project level is very actionable.

We could be tempted to compute an average complexity for a project, but it is usually not very meaningful, like all averages. A ton of one-liners would hide problematic very complex functions. Raising an issue on all complex functions works better.

Of course, even if all individual functions have a low complexity, the project as a whole can still be too complex for what it is doing. But I think that at the project level, it is probably better to look at how the different functions (and higher-level structures, such as classes) interact with each other to get a grasp of the project’s health. Many such metrics exist (for instance, coupling between components or component cohesion). These metrics are not as easy to act on as complexity metrics (this is why they are not a strong focus of our products for now), but I think they better represent the complexity of a project.

We recently introduced rules targeting cyclic dependencies, and it goes in the direction of improving our offering for rules that work at the scale of the full project (and there is more to come in the future).

Would you agree with this point of view?

1 Like

@JolyLoic Do you think there are some cases where comparing a total complexity figure (cognitive or cyclomatic) could make sense?

For example, if you have a bunch of microservices and one is far more complex than the rest, maybe it signals that it’s taking on too many responsibilities and should be broken down further. Of course, this would need to be considered alongside other architectural metrics like the ones you mentioned.

Maybe it could be used in that case, but I think what you really want is a measure of the size of each microservice (and by size, I mean the essential size, the number of features/use cases covered by the module).

This essential size is not easy to measure from the source code, which is more closely related to the accidental size. In other words: If a module has a large size, is it because it does too much (essential size), or is it because it is written in a non-efficient way that unnecessarily increases its size (accidental size)?

A good (mostly because it is particularly easy to compute and understand) measure of the accidental size is the number of lines of code. There are several measures that try to get closer to the essential size (for instance, function points which are much more complex/obscure, and IMO only work well for a certain class of simplistic projects).

The complexity might be another proxy to get closer to this essential size, but I doubt it is a great one: When a function is too complex, you should refactor the code to decrease its complexity, often by splitting the function. While doing so, I see no reason for the total complexity to increase, and I see some cases where it can decrease (by removing some duplication or, in the case of cognitive complexity, by reducing nesting). But the essential size remains the same, because this refactoring was purely technical.

In my experience, lines of code, while highly inaccurate to measure the essential size, are usually good enough, and the simplicity of this metric outweighs its shortcomings. To paraphrase Churchill:

Indeed it has been said that counting lines of code is the worst form of metric except for all those other forms that have been tried

1 Like

Very interesting topic, and one I think where there are no definitive “right” answers.

LoC has it’s own problems in that, if you consider more lines to be more complex then that encourages fewer lines…which can lead to more obscure code that increases the cognitive complexity.
Sometimes more lines making the code clearer are better than fewer which implement clever tricks that take the reader time to wrap their head around - after all, if you can’t understand the code, you can’t really maintain it, so cognitive complexity should have greater importance (IMO) than the number of lines.

Of course this is a highy subjective measure too - an Engineer fresh out of college may find struggle to understand something that an Engineer with 10 years experience would recognise instantly - so the cognitive complexity is dependent on the skills of the reader surely? Though I guess one might reasonably assume the relative cognitive complexity is probably about the same for both, just that the starting threshold for “not complex at all” should be higher for the more experienced Engineer.

Comparing across microservices/project in general has even more challenges - what if one is written in C++ and another in python? Or one uses some framework that abstracts away a lot of the boilerplate and the other does not? The C++ one may have far fewer lines of code because it uses a snazzy framework where the python one does everything from scratch.
Or indeed how do you compare the complexity of a python program to the complexity of a C++ one?
And of course how “micro” should a microservice be? One service might be doing something incredibly simple - say serving up a file from the file system.
Another may need to obtain an access token and then access a database and output the results in JSON
The service boundaries are correct - the services do what they need to, and are independently deployable etc…but one is likely far more complex than the other, necessarily so, and not because the service is “too complex”.

Maybe we need to throw some AI at the problem to determine if the code is too complex for what it needs to do :slight_smile: