Last active
May 26, 2020 11:50
-
-
Save SteveGilham/92f965534e1df3beceb1b90a88f68b6f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
.SYNOPSIS | |
This script estimates cyclomatic complexities over a folder full of assemblies. | |
.DESCRIPTION | |
It loads the assemblies, and then introspects 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 : ComputeComplexity.fsx | |
Requires : F# v5.0/.net 5.0 | |
While .net 5.0 is in preview, use : dotnet fsi /langversion:preview ComputeComplexity.fsx | |
After that, use : dotnet fsi ComputeComplexity.fsx | |
.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 print out the whole analysis | |
*) | |
#r "nuget: Mono.Cecil" | |
#r "nuget: Mono.Options" | |
open System | |
open System.IO | |
open Mono.Cecil | |
open Mono.Cecil.Cil | |
open Mono.Cecil.Rocks | |
open Mono.Options | |
let mutable assemblyPath = String.Empty | |
let mutable ReportLevel = 0 | |
let options = | |
[ | |
("AssemblyPath=", | |
(fun x -> assemblyPath <- x |> Path.GetFullPath | |
if assemblyPath |> Directory.Exists |> not | |
then assemblyPath |> FileNotFoundException |> raise)) | |
("ReportLevel=", | |
(fun x -> let (ok, n) = Int32.TryParse(x) | |
if ok | |
then ReportLevel <- n | |
else (x + " : Not a number") |> InvalidOperationException |> raise) | |
) | |
] | |
|> List.fold | |
(fun (o : OptionSet) (p, a) -> | |
o.Add(p, p.Trim('='), new System.Action<string>(a))) | |
(OptionSet()) | |
try | |
fsi.CommandLineArgs | |
|> options.Parse | |
|> ignore | |
// IL branch opcodes | |
let branchOpCodes = [ | |
OpCodes.Beq | |
OpCodes.Beq_S | |
OpCodes.Bge | |
OpCodes.Bge_S | |
OpCodes.Bge_Un | |
OpCodes.Bge_Un_S | |
OpCodes.Bgt | |
OpCodes.Bgt_S | |
OpCodes.Bgt_Un | |
OpCodes.Bgt_Un_S | |
OpCodes.Ble | |
OpCodes.Ble_S | |
OpCodes.Ble_Un | |
OpCodes.Ble_Un_S | |
OpCodes.Blt | |
OpCodes.Blt_S | |
OpCodes.Blt_Un | |
OpCodes.Blt_Un_S | |
OpCodes.Bne_Un | |
OpCodes.Bne_Un_S | |
OpCodes.Br | |
OpCodes.Br_S | |
OpCodes.Brtrue | |
OpCodes.Brtrue_S | |
OpCodes.Brfalse | |
OpCodes.Brfalse_S | |
] | |
// Compute method complexity (based on the algorithm of NDepend 1.3.2 by Patrick Smacchia) | |
let ComputeComplexity body = | |
let mutable offsets = Set.empty<int> | |
body |> | |
Seq.iter (fun (instruction:Instruction) -> | |
if instruction.OpCode.OperandType = OperandType.InlineSwitch | |
then instruction.Operand :?> Instruction array | |
|> Seq.filter (fun i -> (instruction.Next <> null) && | |
(i.Offset <> instruction.Next.Offset)) | |
|> Seq.iter (fun i -> offsets <- Set.add i.Offset offsets) | |
else if (branchOpCodes | |
|> Seq.exists (fun op -> instruction.OpCode = op)) | |
then | |
let target = instruction.Operand :?> Instruction | |
if (instruction.Next <> null) && | |
(target.Offset <> instruction.Next.Offset) | |
then offsets <- Set.add target.Offset offsets) | |
Set.count offsets | |
// load assemblies (no symbols needed) and explore methods | |
let assemblies = assemblyPath | |
|> Directory.GetFiles | |
|> Seq.filter (fun n -> n.EndsWith(".exe") || n.EndsWith(".dll")) | |
|> Seq.map (fun assembly -> | |
let a = AssemblyDefinition.ReadAssembly assembly | |
{| | |
Name = a.Name.FullName | |
Types = a.MainModule.GetAllTypes() | |
|> Seq.map (fun t -> {| | |
Name = t.FullName | |
Methods = t.Methods | |
|> Seq.filter (fun m -> m.CustomAttributes // skip compiler generated code | |
|> Seq.exists (fun a -> a.AttributeType.FullName | |
= "System.Runtime.CompilerServices.CompilerGeneratedAttribute") | |
|> not) | |
|> Seq.filter (fun m -> m.HasBody) | |
|> Seq.map (fun m -> {| | |
Name = m.Name | |
Complexity = ComputeComplexity m.Body.Instructions | |
|}) | |
|> Seq.toList | |
|} | |
) | |
|> Seq.toList | |
|} | |
) | |
|> Seq.toList | |
if ReportLevel <> 0 then | |
let mutable aname = String.Empty | |
let mutable cname = String.Empty | |
assemblies | |
|> List.iter (fun a -> aname <- a.Name | |
a.Types | |
|> List.iter (fun t -> cname <- t.Name | |
t.Methods | |
|> List.iter (fun m -> if m.Complexity >= ReportLevel | |
then | |
if aname |> String.IsNullOrEmpty |> not | |
then printfn "%s" aname | |
aname <- String.Empty | |
if cname |> String.IsNullOrEmpty |> not | |
then printfn "\t%s" cname | |
cname <- String.Empty | |
printfn "\t\t%s => %d" m.Name m.Complexity | |
) | |
) | |
) | |
else | |
assemblies | |
|> List.iter (fun a -> printfn "%s" a.Name | |
a.Types | |
|> List.iter (fun t -> printfn "\t%s" t.Name | |
t.Methods | |
|> List.iter (fun m -> printfn "\t\t%s => %d" m.Name m.Complexity))) | |
with | |
| x -> printfn "%s" <| x.ToString() //x.Message |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment