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: ... |
which can be replaced by
IL_00ec: brtrue IL_010a | |
[enough nop opcodes] | |
IL_00f2: ... |
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 } |
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 :
Post a Comment