Skip to content

Instantly share code, notes, and snippets.

@SteveGilham
Created May 26, 2020 11:48
Show Gist options
  • Save SteveGilham/c10608eb7894fc6bc6f185da489f244b to your computer and use it in GitHub Desktop.
Save SteveGilham/c10608eb7894fc6bc6f185da489f244b 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 : ComputeComplexity2.fsx
Requires : F# v5.0/.net 5.0
While .net 5.0 is in preview, use : dotnet fsi /langversion:preview ComputeComplexity2.fsx
After that, use : dotnet fsi ComputeComplexity2.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 return the whole analysis to the pipeline
*)
#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
// Gendarme's algorithm to compute Cyclomatic Complexity values.
let mask = [ 0xFFFF6C3FCUL; 0x1B0300000000FFE0UL; 0x400100FFF800UL; 0xDE0UL ]
let findFirstUnconditionalBranchTarget(ins : Cil.Instruction) =
Seq.unfold
(fun (state : Cil.Instruction) ->
if isNull state then None else Some(state, state.Next)) ins
|> Seq.tryFind (fun i -> i.OpCode.FlowControl = FlowControl.Branch)
|> Option.map (fun i -> i.Operand :?> Cil.Instruction)
let accumulateSwitchTargets (ins : Cil.Instruction)
(targets : System.Collections.Generic.HashSet<Cil.Instruction>) =
let cases = ins.Operand :?> Cil.Instruction []
cases
|> Seq.iter (fun target ->
if target <> ins.Next then
target
|> targets.Add
|> ignore)
// add 'default' branch (if one exists)
let next = ins.Next
if next.OpCode.FlowControl = FlowControl.Branch then
let operand = next.Operand :?> Cil.Instruction
match cases
|> Seq.head
|> findFirstUnconditionalBranchTarget with
| Some unc when unc = operand -> ()
| _ ->
operand
|> targets.Add
|> ignore
let ``detect ternary pattern`` (code : Code option) =
// look-up into a bit-string to get
// +1 for any Load instruction, basically
let index = int (if Option.isSome code then Option.get code else Code.Nop)
((mask
|> Seq.skip (index >>> 6)
|> Seq.head
&&& (1UL <<< (index &&& 63))
<> 0UL) :> IConvertible).ToInt32(System.Globalization.CultureInfo.InvariantCulture)
let switchCyclomaticComplexity(instructions : Cil.Instruction seq) =
let targets = System.Collections.Generic.HashSet<Cil.Instruction>()
let complexity (c:int) (i:Instruction) =
match i.OpCode.FlowControl with
| FlowControl.Branch ->
c + ((if i.Previous |> isNull then None else Some i.Previous)
|> Option.map (fun (previous : Instruction) ->
do if previous.OpCode.FlowControl = FlowControl.Cond_Branch then
match previous.Operand with
| :? Cil.Instruction as branch ->
if targets.Contains branch then
i
|> targets.Add
|> ignore
| _ -> ()
previous.OpCode.Code)
|> ``detect ternary pattern``)
| FlowControl.Cond_Branch ->
if i.OpCode = OpCodes.Switch then
accumulateSwitchTargets i targets
c
else
let branch = i.Operand :?> Cil.Instruction
c + ((if branch.Previous |> isNull then None else Some branch.Previous)
|> Option.filter (fun (previous : Instruction) ->
previous.Previous.OpCode.Code <> OpCodes.Switch.Code && branch
|> targets.Contains
|> not)
|> (fun x -> if Option.isSome x then 1 else 0))
| _ -> c
let fast =
instructions
|> Seq.fold complexity 1
fast + targets.Count
let cyclomaticComplexity(m : MethodDefinition) =
if m.HasBody then
let instructions = m.Body.Instructions |> Seq.cast<Cil.Instruction>
match instructions |> Seq.tryFind (fun i -> i.OpCode = OpCodes.Switch) with
| None ->
instructions
|> Seq.fold (fun c i ->
match i.OpCode.FlowControl with
| FlowControl.Cond_Branch -> c + 1
| FlowControl.Branch ->
c + ((if i.Previous |> isNull then None else Some i.Previous)
|> Option.map
(fun (previous : Instruction) -> previous.OpCode.Code)
|> ``detect ternary pattern``)
| _ -> c) 1
| _ -> switchCyclomaticComplexity instructions
else
1
// 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 = cyclomaticComplexity m
|})
|> 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