Skip to content

Instantly share code, notes, and snippets.

@SteveGilham
Last active May 26, 2020 11:50
Show Gist options
  • Save SteveGilham/92f965534e1df3beceb1b90a88f68b6f to your computer and use it in GitHub Desktop.
Save SteveGilham/92f965534e1df3beceb1b90a88f68b6f to your computer and use it in GitHub Desktop.
(*
.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