|
(* |
|
.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 |