Friday, October 28, 2011

Computing cyclomatic complexity with PowerShell and FxCop

A measure of the cyclomatic complexity of a .net method can be gauged by counting the number of IL branch instructions that do nor branch to the next instruction, and which have a distinct target from any other branch -- this is essentially the algorithm used in NDepend 1.x to make the computation. The introspection mechanism of FxCop provides enough decompilation of the IL that getting the instruction type and offset, and the target offset of a branch. An actual FxCop rule, though feasible to write, however, would be less useful than one could hope.

For one thing, FxCop itself doesn't provide any convenient way of passing parameters to custom rules (to e.g. set a trigger threshold); and for another, any analysis is likely to be run over a debug build (DEBUG and CODE_ANALYSIS variables defined there to not contaminate the released code with [SuppressMessage] annotations), which is likely to have a different underlying complexity to a release build (this is especially true in F# debug builds where there are a lot of sequences that look like

IL_00ec: brtrue.s IL_00f0
IL_00ee: br.s IL_00f2
IL_00f0: br.s IL_010a
IL_00f2: ...
view raw gistfile1.cs hosted with ❤ by GitHub

which can be replaced by

IL_00ec: brtrue IL_010a
[enough nop opcodes]
IL_00f2: ...
view raw gistfile1.cs hosted with ❤ by GitHub

which, as it turns out, is pretty much what the release build does.

A more configurable and flexible way of performing the operation is to drive the FxCop facilities from a script -- these days I'm doing a lot of my .net scripting in PowerShell, but it could equally well be done with IronPython or similar .net scripting language. The result looks like this:

<#
.SYNOPSIS
This script estimates cyclomatic complexities over a folder full of assemblies.
.DESCRIPTION
It loads the assemblies, and then introspeccts over each method, estimating the
complexity as number of branch instructions which do not target the next instruction
and which have a unique target instruction.
.NOTES
File Name : Get-Complexity.ps1
Requires : PowerShell Version 2.0 (3.0 or alternative launcher for .net 4/FxCop 10.0)
.PARAMETER FxCopPath
The path to the directory where FxCop has been installed
.PARAMETER AssemblyPath
The path to the folder containging the assemblies to inspect
.PARAMETER ReportLevel
If given, print out methods matching or exceeding this complexity,
otherwise return the whole analysis to the pipeline
#>
param (
[string] $FxCopPath,
[Parameter(Mandatory = $true)] [string] $AssemblyPath,
[int] $ReportLevel,
[switch] $Help)
$fxcopVersion = @{ "v2"='Microsoft FxCop 1.36'; "v4"='Microsoft FxCop 10.0' }
if (-not $FxCopPath) {
$programFiles = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFiles)
$dotNetVersion = [System.Runtime.InteropServices.RuntimeEnvironment]::GetSystemVersion().Split('.') | Select-Object -First 1
$FxCopPath = Join-Path -Path $programFiles -ChildPath ($fxcopVersion[$dotNetVersion])
}
if ($help -or (-not (Test-Path $FxCopPath -PathType Container)) -or
(-not (Test-Path $AssemblyPath -PathType Container)) -or
(-not (Test-Path $(Join-Path -Path $FxCopPath -ChildPath 'FxCopSdk.dll') -PathType Leaf)))
{
Get-Help $MyInvocation.MyCommand.Definition
return
}
Add-Type -Path $(Join-Path -Path $FxCopPath -ChildPath 'FxCopSdk.dll')
## IL branch opcodes
$branchOpCodes = @([Microsoft.FxCop.Sdk.OpCode]::Beq,
[Microsoft.FxCop.Sdk.OpCode]::Beq_S,
[Microsoft.FxCop.Sdk.OpCode]::Bge,
[Microsoft.FxCop.Sdk.OpCode]::Bge_S,
[Microsoft.FxCop.Sdk.OpCode]::Bge_Un,
[Microsoft.FxCop.Sdk.OpCode]::Bge_Un_S,
[Microsoft.FxCop.Sdk.OpCode]::Bgt,
[Microsoft.FxCop.Sdk.OpCode]::Bgt_S,
[Microsoft.FxCop.Sdk.OpCode]::Bgt_Un,
[Microsoft.FxCop.Sdk.OpCode]::Bgt_Un_S,
[Microsoft.FxCop.Sdk.OpCode]::Ble,
[Microsoft.FxCop.Sdk.OpCode]::Ble_S,
[Microsoft.FxCop.Sdk.OpCode]::Ble_Un,
[Microsoft.FxCop.Sdk.OpCode]::Ble_Un_S,
[Microsoft.FxCop.Sdk.OpCode]::Blt,
[Microsoft.FxCop.Sdk.OpCode]::Blt_S,
[Microsoft.FxCop.Sdk.OpCode]::Blt_Un,
[Microsoft.FxCop.Sdk.OpCode]::Blt_Un_S,
[Microsoft.FxCop.Sdk.OpCode]::Bne_Un,
[Microsoft.FxCop.Sdk.OpCode]::Bne_Un_S,
[Microsoft.FxCop.Sdk.OpCode]::Br,
[Microsoft.FxCop.Sdk.OpCode]::Br_S,
[Microsoft.FxCop.Sdk.OpCode]::Brtrue,
[Microsoft.FxCop.Sdk.OpCode]::Brtrue_S,
[Microsoft.FxCop.Sdk.OpCode]::Brfalse,
[Microsoft.FxCop.Sdk.OpCode]::Brfalse_S)
# Compute method complexity (based on the algorithm of NDepend 1.3.2 by Patrick Smacchia)
Function Compute-Complexity ([Microsoft.FxCop.Sdk.InstructionCollection] $body)
{
$offsets = @()
$body | ? { $branchOpCodes -contains $_.OpCode } | ? {
$_.Value -ne ($_.Offset + 2) } | ? { ## don't count branch to next instruction
-not ($offsets -contains $_.Offset) } | % {
$offsets += $_.Offset } | Out-Null
$offsets.Length
}
# load assemblies (no symbols needed) and explore methods
$assemblies = dir $AssemblyPath | ? { $_.Name.EndsWith(".exe") -or $_.Name.EndsWith(".dll") } | % {
$assembly = [Microsoft.FxCop.Sdk.AssemblyNode]::GetAssembly($_.FullName)
$compilerGenerated = $assembly.GetType([Microsoft.FxCop.Sdk.Identifier]::For("System.Runtime.CompilerServices"), [Microsoft.FxCop.Sdk.Identifier]::For("CompilerGeneratedAttribute"), $true)
New-Object PSObject -Property @{
Name = $_.Name;
Types = $assembly.Types | % {
New-Object PSObject -Property @{
Name = $_.FullName;
Methods = $_.Members | ? { $_.NodeType -eq [Microsoft.FxCop.Sdk.NodeType]::Method } | ? {
-not (($_.Attributes | % { $_.Type }) -contains $compilerGenerated) } | % { ## skip compiler generated code
New-Object PSObject -Property @{
Name = $_.GetUnmangledNameWithTypeParameters();
Complexity = Compute-Complexity($_.Instructions); }}}}}}
if ($ReportLevel) {
$assemblies | % { $aname = $_.Name
$_.Types | % { $cname = "`t$($_.Name)"
$_.Methods | ? { $_.Complexity -ge $ReportLevel } | % {
if ($aname) { Write-Host $aname; $aname = $null }
if ($cname) { Write-Host $cname; $cname = $null }
Write-Host "`t`t$_" }}}} else { $assemblies }
view raw gistfile1.ps1 hosted with ❤ by GitHub

and running it over a set of F# code shows that the reported complexity of the release build of a method halves (or more) what is reported for a debug build. I haven't made a comparison over an significant amount of C# as yet, to see how much complexity the compiler removes between the two configurations.

No comments :