Managing Code quality in enterprise software with nDepend static analysis

By Joshua Holden

Preamble


Not so long ago whilst working on a large monolithic mission-critical web application renowned for being a pain to work with I was asked by the organisation's CTO, and I paraphrase:

"We are going to be depending on the functionality of this software a lot more going forwards, do you think It needs a total rewrite or can we get by with using the existing codebase and improving it"

This is a common scenario which could be avoided by adding static analysis into the build pipeline to detect and block changes with "code smells" preventing the growth of issues into the codebase until the point it becomes unmaintainable.
Often many developers work on a single application stack and it ends up growing arms and legs, becoming unmaintainable, but the question is, "How do I work out if it's going to be more cost-effective to rewrite the application or try and fix it?", fortunately, there is a way of working this out by calculating technical-debt and annual-interest.

Technical-debt is an abstract term and quite hard to explain without using a metaphorical scenario, so I will do exactly that, let's imagine you own an  old house in the country which you bought for £400,000, you notice it is creaking a bit  and wood is flaking from the ceiling so you call in an expert to look at the house which he examines and comes back to you with the following report:

"I have examined the house, it has a serious termite infestation and it will fall down in about 4 years, but the cost to sure up the wood and remove the termites will be in the region of  £50,000".
In this scenario, the termites are the bugs in the software and the house is the software, after speaking to the expert we can now make the following assertions:

1: If we let the house fall in 4 years it has a £100,000 a year annual-interest, and as with monetary debt will get more expensive to repair the longer is left unaddressed.

2: If you repair it now, it will cost £50,000 but will increase the habitability and resale value of the house by at least the time and money costs, furthermore it will save you having to completely rebuild or abandon it in the long term. 

Using NDepend to calculate technical debt

When managing software and development teams the expert here could be a solutions architect or a lead-developer etc using a tool such as static analysis to help them examine the code base and calculate the technical debt.

Below is an example of NDepend static analysis on the aforementioned solution:

Here, you can see that to rebuild the entire application of 13,704  lines it would take 365 days, and to fix all of the issues (technical debt) addressed by static analysis it would take  28 days (28.908 but NDepend floors the number),  therefore the debt level is 7.92%  (Days to rebuild / debt days * 100).

This can be further reported on showing monetary costs as well, however, obviously, to make these calculations NDepend needs to be aware of how many hours per day a developer works, and that can be set as shown below:

You can see from this, calculating the debt-level is highly configurable depending on developer velocity, hours worked per day and cost per hour, furthermore you can set a threshold on the SQALE debt rating, this can be especially useful in a CI/CD pipeline failing a build if the debt-level crosses the threshold to maintain quality software deployments without slippage.

Okay, I get it technical debt can be managed, but what else can nDepend do?

Quite a lot more actually, to calculate technical debt NDepend also needs to be aware of issues in your code and classify them accordingly in a ranking from low to critical (and blockers which need immediate address), to do this NDepend has a huge list of common and not so common errors it uses its static analysis engine to detect, furthermore other checks can be easily added, and existing rules can be modified if they do not suit your development shop by using C# LINQ queries.

I'm not going to go into to much detail because the rules are discussed in detail here, instead, I will discuss at a high level below:

Built-in issue rules

Hotspots
Hot spot rules allow you to quickly identify issues with the most technical debt allowing you to address them first and cut down the code debt quicker.

Code smells
Covered under code smells are 9 rules detecting common code smells such as a lack of comments, overly complex methods, too many local variables in methods and much more.

Code smells regression
Regression rules only fire when a baseline has already been done (previously ran the analysis on an older version), these rules check if the number of code smells has increased/decreased since the previous analysis (diff checks).

Object orientated design
Rules specifically focusing around SOLID and OOP paradigms, for example: avoid empty interfaces and classes with no descendants should be sealed.

Design
This ruleset addresses design flaws which increase technical debt such as usage of obsoleted methods and detecting classes that should be structs.

Architecture
The ruleset for detecting architectural issues such as accessing database types within a UI layer and not using view models preventing overposting.

API breaking changes
The ruleset is dependant on a previously ran analysis, does a diff check and makes sure nothing has been introduced which could cause breaking changes for anyone or anything consuming the API.

Code coverage
The ruleset for the detection of code not covered by unit tests, a number of the tests here depend on code coverage files being imported from software such as visual studio's own code coverage report or ReSharper dotCover.

Dead code
Detects if a class, method or field is never used anywhere and flags it for removal.

Security
Possibly my favourite test ruleset detects security flaws such as code vulnerable to SQL injection, usage of weaker cyphers such as DES and 3DES and disabling certificate validation.

Visibility
Detection for classes and fields with higher accessibility modifiers than required.

Immutability
Checks such as not marking structs as immutable and arrays marked as read-only.

Naming conventions
Enforces naming conventions such as ensuring types are always capitalised and preventing multiple types with the same name which happens and is extremely annoying.

Source file organisation
Several checks to ensure source code is well organised such as declaring namespaces that do not match the solution structure.

.Net framework utilisation
A huge set of rules making sure when using .net framework classes and functionality such as not throwing generic ApplicationErrors when a more explicit exception type could be used.

NDepend can be run separately to visual studio as a stand-alone app, however, I prefer to run it with the Visual Studio plugin as it detects issues as you code in realtime:


Above you can see on the far left, the different rulesets, in the middle it displays a count of violations of each rule within the ruleset, clicking a rule shows all areas in the application which violate the rule which can be clicked on to jump to the area with the issue for address.

Creation of new rules is extremely simple, take for example the below rule that enforces namespaces start with company name, in this case, ACME Ltd:

warnif count > 0 from n in Application.Namespaces where
!n.NameLike (@"^AcmeLTD")
select new {
n,
n.NbLinesOfCode,
Debt = 10.ToMinutes().ToDebt(),
Severity = Severity.High
}

the above rule asserts that namespaces must start with AcmeLtd and any that do not add 10 minutes to the technical dept and flag as a high priority issue.

Reporting

One of the strongest points of Resharper is the reports it can kick out as standard, as such, below i'm going to go into a little detail of the reporting functionality.

The dependency graph and matrix offer 2 different dimensions of the same data and allow you to take a closer look at how the solution code is actually structured and highlights coding flaws such as large dependency cycles as well as helping with refactoring analysis as you can see at a glance if you alter a specific dependency what impact it will have on other components within the code, shown below is an example of a dependency matrix:

This is just a slice of my application focusing on the controllers only but you can clearly see that GetPostById method is called by many of the controller methods and making changes to it could be considered risky but it also helps with regression testing should you make a change as you know exactly what controllers will need to be checked extra carefully during the testing cycle.

Taking another view on the same data using the dependency graph shown below allows you to visualise the couplings and relations between classes and structures within your application.

Each node in this dependency chart can be double-clicked into to zoom into it and view at an even finer level for example shown below double-clicking on the user services node within the lager Core.Services namespace

As you can see this is an extremely powerful tool for visualising large applications, how they work and the impacts of any changes if you don't have the original UML documents to hand (or more likely they were never created).

We also get a treemap report which by default looks at cyclomatic complexity of methods, wherein the larger (in lines of code) a method is the larger the tree, and the more complex a method  the more it will move towards the red scale, shown below is an example showing the cyclomatic complexity dimension: