Created
May 26, 2020 11:48
-
-
Save SteveGilham/c10608eb7894fc6bc6f185da489f244b 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 : 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