This script analyzes the dependencies between top level types in a .NET Assembly. It is then used to compare the dependency relationships in some F# projects with those in some C# projects.
(* | |
This script analyzes the dependencies between top level types in a .NET Assembly. | |
It is then used to compare the dependency relationships in some F# projects with those in some C# projects. | |
Note that no attempt has been made to optimize the code yet! | |
REQUIRES: | |
* Mono.Cecil for code analysis | |
From http://www.mono-project.com/Cecil#Download | |
or via NuGet | |
Install-Package Mono.Cecil -Version 0.9.5.4 | |
* QuickGraph for graph algorithm | |
or via NuGet | |
Install-Package QuickGraph -Version 3.6.61119.7 | |
* GraphViz for graph rendering. | |
From http://www.graphviz.org/Download.php | |
Alternatives are: GLEE (http://research.microsoft.com/en-us/downloads/f1303e46-965f-401a-87c3-34e1331d32c5/default.aspx) | |
or Canviz (https://code.google.com/p/canviz/) to render DOT files with JS in the browser. | |
USING NUGET | |
NuGet requires a project, so I just create an empty project and solution and add this script to it. | |
Once the project exists, you can run the NuGet commands from the "Package Manager Console" . | |
*) | |
// make Visual Studio use the script directory | |
System.IO.Directory.SetCurrentDirectory(__SOURCE_DIRECTORY__) | |
#r @"packages\Mono.Cecil.0.9.5.4\lib\net40\Mono.Cecil.dll" | |
#r @"packages\Mono.Cecil.0.9.5.4\lib\net40\Mono.Cecil.Rocks.dll" | |
#r @"packages\QuickGraph.3.6.61119.7\lib\net4\QuickGraph.dll" | |
open System | |
open System.IO | |
// ================================ | |
// The domain types - note there are no dependencies on Cecil | |
// ================================ | |
/// Distinguish a Type name from normal strings. | |
/// Use a "name" rather than a Cecil.TypeDefinition/TypeReference to satisfy the comparison constraint of sets | |
type TypeName = TypeName of string | |
/// Distinguish top level types from normal types | |
type TltName = TltName of string | |
/// a dependency to another type can be internal (same assembly) or external (different assembly) | |
type TypeDependency = | |
| Internal of TypeName | |
| External of TypeName | |
/// a type with its dependencies -- references exposed publically and references used in the implementation | |
type TypeWithDependencies = | |
{ typeDef: TypeName; isAuthored: bool; publicDeps: TypeDependency Set; implDeps: TypeDependency Set} | |
/// a top level class/module along with all the types it contains | |
type TopLevelType = | |
{ name: TltName; types: TypeWithDependencies Set} | |
/// a top level class/module along with the top level types it depends on | |
type DependencySet = | |
{ dependent: TltName; dependencies: TltName Set} | |
// ================================ | |
// Module for extracting types from an assembly (using Cecil) | |
// ================================ | |
module AssemblyTypes = | |
open Mono.Cecil | |
/// Extract the full name from the TypeDefinition or TypeReference | |
let inline typeName td = TypeName ( ^a : (member FullName : string) td) | |
/// A type is "core" if it is in the Microsoft.FSharp namespace | |
/// This stops projects with Fsharp.Core built in from double counting | |
let isCoreName (s:string) = | |
s.StartsWith("Microsoft.FSharp") | |
// used to filter out types that should not be included | |
let isNotCoreType excludeCore (td:TypeDefinition) = | |
not (isCoreName td.FullName && excludeCore) | |
/// get a named attribute of the specified type, or None | |
let getCustomAttribute (td:TypeDefinition) attrName = | |
td.CustomAttributes | |
|> Seq.tryFind (fun a -> a.AttributeType.FullName = attrName ) | |
/// get a named attribute constructor arg of the specified type, or None | |
let getCustomAttributeArg (td:TypeDefinition) attrName argName = | |
getCustomAttribute td attrName |> Option.bind (fun attr -> | |
attr.ConstructorArguments |> Seq.tryFind (fun arg -> arg.Type.FullName = argName ) | |
) | |
// is the type a F# sum type? | |
let isFsSumType (td:TypeDefinition) = | |
let attrName = "Microsoft.FSharp.Core.CompilationMappingAttribute" | |
let argName = "Microsoft.FSharp.Core.SourceConstructFlags" | |
getCustomAttributeArg td attrName argName | |
|> Option.map (fun arg -> arg.Value = box (int SourceConstructFlags.SumType)) | |
|> defaultArg <| false | |
// is the type a case in a F# sum type? | |
let isFsCaseType (td:TypeDefinition) = | |
(td.DeclaringType <> null) && (isFsSumType td.DeclaringType) | |
// does the type have the GeneratedCodeAttribute? | |
let hasGeneratedCodeAttribute (td:TypeDefinition) = | |
let attrName = "System.CodeDom.Compiler.GeneratedCodeAttribute" | |
getCustomAttribute td attrName |> Option.isSome | |
/// Return true if the type name was generated by the compiler | |
let hasGeneratedTypeName (s:string) = | |
s.Contains("__") // C# | |
|| s.Contains("<") // C# | |
|| s.Contains("@") // F# | |
|| s.Contains("$") // F# | |
/// Return true if the type was authored by a human | |
let isAuthored td = | |
(td |> hasGeneratedCodeAttribute |> not) && | |
(td |> isFsCaseType |> not) && | |
(td.FullName |> hasGeneratedTypeName |> not) | |
/// a filter for nulls | |
let isNotNull o = (o<>null) | |
/// True if type reference is external to the dependant | |
/// This is caused by it being in a different assembly, or if excludeCore is true, by being in the Microsoft namespace | |
let isExternalRef (dependant:TypeDefinition) excludeCore (tr:TypeReference) = | |
// hack to handle nulls | |
let trScopeName = | |
if tr = null then "" | |
else if tr.Scope = null then "" | |
else tr.Scope.Name | |
match dependant.Scope.Name with | |
| s when s <> trScopeName -> true | |
| s when excludeCore && isCoreName s -> true | |
| _ -> false | |
/// Make a dependency from a TypeReference | |
let toDependency isExternal (tr:TypeReference) = | |
if isExternal tr | |
then typeName tr |> External | |
else typeName tr |> Internal | |
/// Given a TypeReference, return it and its generic arguments as well (recursively) | |
/// E.g IList<Option<String>> expands to { IList<>; Option<>; String } | |
let rec refWithGenericParams (tr:TypeReference) = seq { | |
match tr with | |
| :? GenericInstanceType as g -> | |
yield tr | |
for arg in g.GenericArguments do yield! refWithGenericParams arg | |
| _ -> | |
yield tr // just the typeRef | |
} | |
/// Get a list of all types directly referenced by public elements of a TypeDefinition | |
/// Not expanded to include generic params | |
let directPublicRefs (td:TypeDefinition) = seq { | |
yield td.BaseType | |
for int in td.Interfaces do | |
yield int | |
for fd in td.Fields do | |
if fd.IsPublic then yield fd.FieldType | |
// properties are included in methods | |
for md in td.Methods do | |
if md.IsPublic then | |
for parmd in md.Parameters do yield parmd.ParameterType | |
yield md.ReturnType | |
} | |
/// Get a list of all types directly or indirectly referenced by public elements of a TypeDefinition | |
let publicRefs (td:TypeDefinition) = | |
if td.IsPublic | |
then | |
td |> directPublicRefs |> Seq.collect refWithGenericParams | |
else | |
Seq.empty // nothing for private types | |
/// Create a visitor interface for the Cecil.Rocks.ILParser | |
/// It accumulates type references in a typeRefs param | |
let implVisitor typeRefs = | |
{new Mono.Cecil.Rocks.IILVisitor with | |
member this.OnInlineNone (_) = () | |
member this.OnInlineSByte (_, _) = () | |
member this.OnInlineByte (_, _) = () | |
member this.OnInlineInt32 (_, _) = () | |
member this.OnInlineInt64 (_, _) = () | |
member this.OnInlineSingle (_, _) = () | |
member this.OnInlineDouble (_, _) = () | |
member this.OnInlineString (_, _) = () | |
member this.OnInlineBranch (_, _) = () | |
member this.OnInlineSwitch (_, _) = () | |
member this.OnInlineSignature (_, _) = () | |
member this.OnInlineVariable (_, variable) = | |
if variable <> null then | |
typeRefs := variable.VariableType :: !typeRefs; () | |
member this.OnInlineArgument (_, parameter) = | |
if parameter <> null then | |
typeRefs := parameter.ParameterType :: !typeRefs; () | |
member this.OnInlineType (_, t) = | |
if t <> null then | |
typeRefs := t :: !typeRefs; () | |
member this.OnInlineField (_, field) = | |
if field <> null then | |
typeRefs := field.DeclaringType :: !typeRefs; () | |
member this.OnInlineMethod (_, m)= | |
if m <> null then | |
typeRefs := m.DeclaringType :: !typeRefs; () | |
} | |
/// Get a list of all types directly referenced by the implementation of a method | |
let directMethodRefs (md:MethodDefinition) = | |
let typeRefs = ref [] | |
let visitor = implVisitor typeRefs | |
try | |
Mono.Cecil.Rocks.ILParser.Parse(md,visitor) | |
with | |
| ex -> () // ILParser can throw NRE -- ignore for our purposes | |
!typeRefs | |
/// Get a list of all types directly referenced by the implementation of all methods of a TypeDefinition | |
/// Not expanded to include generic params | |
let directImplRefs (td:TypeDefinition) = seq { | |
for fd in td.Fields do | |
yield fd.FieldType | |
for md in td.Methods do | |
yield! directMethodRefs md | |
} | |
/// Get a set of all types directly or indirectly referenced in the implementation of the methods of a TypeDefinition | |
let implRefs td = | |
td |> directImplRefs |> Seq.collect refWithGenericParams | |
/// Create a TypeWithDependencies from a TypeDefinition by gathering all its dependencies | |
let toTypeWithDependencies excludeCore td = | |
let isExternal = isExternalRef td excludeCore | |
let makeDep = toDependency isExternal | |
{ | |
typeDef = td |> typeName | |
isAuthored = td |> isAuthored | |
publicDeps = td |> publicRefs |> Seq.filter isNotNull |> Seq.map makeDep |> Set.ofSeq | |
implDeps = td |> implRefs |> Seq.filter isNotNull |> Seq.map makeDep |> Set.ofSeq | |
} | |
// recursively get all the types under a parent type | |
let rec withAllChildTypes (parent:TypeDefinition) = [ | |
yield parent | |
for child in parent.NestedTypes do | |
yield! child |> withAllChildTypes | |
] | |
/// Construct a TopLevelType from a top level TypeDefinition | |
let topLevelType excludeCore td = | |
// convert the typeDefs to Dependants | |
let nestedTypes = | |
td | |
|> withAllChildTypes | |
|> List.map (toTypeWithDependencies excludeCore) | |
|> Set.ofList | |
{ | |
name = td.FullName |> TltName | |
types = nestedTypes | |
} | |
/// Get all authored top level types from an assembly | |
let topLevelTypes excludeCore assemblyFileName = | |
Mono.Cecil.AssemblyDefinition.ReadAssembly(fileName=assemblyFileName).MainModule.Types | |
|> Seq.filter (isNotCoreType excludeCore) | |
|> Seq.filter isAuthored | |
|> Seq.map (topLevelType excludeCore) | |
/// Get the code size for an assembly. | |
/// Count instructions in non-core methods only. | |
let codeSize excludeCore assemblyFileName = | |
Mono.Cecil.AssemblyDefinition.ReadAssembly(fileName=assemblyFileName).MainModule.Types | |
|> Seq.filter (isNotCoreType excludeCore) | |
|> Seq.collect withAllChildTypes | |
|> Seq.collect (fun td-> td.Methods) | |
|> Seq.filter (fun md-> md.HasBody) | |
|> Seq.map (fun md-> md.Body.CodeSize) | |
|> Seq.sum | |
// ================================ | |
// Analyze dependencies between top level types | |
// ================================ | |
module TopLevelTypeDependencies = | |
/// Create a reverse lookup from type to top level type as a prerequisite for dependency analysis. | |
/// Each TypeName points to the TopLevelType it is contained in. | |
let createLookup tlts = | |
let childParentTuples tlt = | |
tlt.types | |
|> Seq.map (fun t -> t.typeDef,tlt.name) | |
tlts | |
|> Seq.collect childParentTuples | |
|> Map.ofSeq | |
/// Map the low-level types to corresponding top-level types | |
/// low-level types that are external are ignored | |
let mapDependency lookup = | |
function | |
| Internal lowLevelType -> | |
Map.tryFind lowLevelType lookup | |
| External lowLevelType -> | |
None | |
/// Map a top-level type to a DependencySet | |
let toDependencySet lookup getDeps tlt = | |
let allDeps = | |
tlt.types | |
|> Set.map getDeps // get each low-level type's dependencies | |
|> Set.unionMany // combine into one set, removing dups | |
|> Seq.map (mapDependency lookup) // lookup to get corresponding top level type (maybe) | |
|> Seq.choose id // convert from option to TltName | |
|> Set.ofSeq // remove dups again | |
|> Set.remove tlt.name // remove any self-reference | |
{dependent=tlt.name; dependencies=allDeps} | |
/// Convert a list of top level types to a list of DependencySets, using the public dependencies | |
let publicDependencies topLevelTypes = | |
let lookup = createLookup topLevelTypes | |
let getDeps t = t.publicDeps | |
let map = toDependencySet lookup getDeps | |
topLevelTypes |> Seq.map map | |
/// Convert a list of top level types to a list of DependencySets, using the all dependencies, including private and implementation | |
let allDependencies topLevelTypes = | |
let lookup = createLookup topLevelTypes | |
let getDeps t = Set.union t.publicDeps t.implDeps | |
let map = toDependencySet lookup getDeps | |
topLevelTypes |> Seq.map map | |
// ================================ | |
// Analyze the cyclic dependencies using QuickGraph | |
// ================================ | |
module CyclicDependencies = | |
open QuickGraph | |
open QuickGraph.Algorithms | |
open System.Collections.Generic | |
/// Convert a list of depSets into a graph | |
let toGraph depSets = | |
let createEdge source target = | |
new QuickGraph.SEdge<_>(source, target) | |
let edges (depSet:DependencySet) = | |
depSet.dependencies | |
|> Set.toSeq // must convert back to seq type | |
|> Seq.map (createEdge depSet.dependent) | |
let allEdges = depSets |> Seq.collect edges | |
QuickGraph.GraphExtensions.ToAdjacencyGraph(edges=allEdges) | |
/// get the strongly connected components of the dependencySets as a collection of TopLevelType sets | |
let scc depSets = | |
let graph = depSets |> toGraph | |
// The components are returned as a (type->index) dictionary, | |
// where all types in a component have the same index | |
let componentDict = new Dictionary<TltName,int>() :> IDictionary<_,_> | |
let componentDictRef = ref componentDict | |
let componentCount = graph.StronglyConnectedComponents(componentDictRef) | |
let components = | |
!componentDictRef | |
|> Seq.groupBy (fun kvp -> kvp.Value) // the index | |
|> Seq.map (fun (k,seq) -> | |
seq | |
|> Seq.map (fun kvp -> kvp.Key) // extract the type | |
|> Set.ofSeq // convert to a set | |
) | |
components | |
/// get the set of other types connected to T, or empty | |
let connectedTo components t = | |
let whereContainsT = Set.contains t | |
components | |
|> Seq.filter whereContainsT // by definition, only one component contains t | |
|> Seq.collect id // extract the data, if any | |
|> Set.ofSeq // convert back into a set | |
/// For each dependency set, remove all types that are not in the same | |
/// strongly connected component, leaving only the cyclic dependencies | |
/// Also remove trivial sets (size=1) | |
let toCyclicDependencies depSets = | |
// get all the components | |
let components = scc depSets | |
// remove non-connected | |
let intersectWithScc depSet = | |
let connected = connectedTo components depSet.dependent | |
let dependencies' = Set.intersect depSet.dependencies connected | |
{depSet with dependencies= dependencies'} | |
let depSets' = depSets |> Seq.map intersectWithScc | |
depSets' | |
// ================================ | |
// Generate the graph using GraphViz | |
// ================================ | |
module GraphViz = | |
// change this as needed for your local environment | |
let graphVizPath = @"C:\Program Files (x86)\Graphviz2.30\bin\" | |
let getName (TltName n) = | |
sprintf "\"%s\"" n // be sure to quote the type name! | |
let toCsv sep strList = | |
match strList with | |
| [] -> "" | |
| _ -> List.reduce (fun s1 s2 -> s1 + sep + s2) strList | |
let writeDepSet writer depSet = | |
let fromNode = getName depSet.dependent | |
let toNodes = | |
depSet.dependencies | |
|> Seq.map getName | |
|> Seq.sort // make it more human readable | |
|> Seq.toList | |
|> toCsv "; " | |
fprintfn writer " %s -> { rank=none; %s }" fromNode toNodes | |
// Create a DOT file for graphviz to read. | |
let createDotFile dotFilename depSets = | |
use writer = new System.IO.StreamWriter(path=dotFilename) | |
fprintfn writer "digraph G {" | |
fprintfn writer " page=\"40,60\"; " | |
fprintfn writer " ratio=auto;" | |
fprintfn writer " rankdir=LR;" | |
fprintfn writer " fontsize=10;" | |
depSets | |
|> Seq.filter (fun ds -> ds.dependencies.Count > 0) // ignore empty | |
|> Seq.sort // make it more human readable | |
|> Seq.iter (writeDepSet writer) | |
fprintfn writer " }" | |
// shell out to run a command line program | |
let startProcessAndCaptureOutput cmd cmdParams = | |
let debug = false | |
if debug then | |
printfn "Process: %s %s" cmd cmdParams | |
let si = new System.Diagnostics.ProcessStartInfo(cmd, cmdParams) | |
si.UseShellExecute <- false | |
si.RedirectStandardOutput <- true | |
use p = new System.Diagnostics.Process() | |
p.StartInfo <- si | |
if p.Start() then | |
if debug then | |
use stdOut = p.StandardOutput | |
stdOut.ReadToEnd() |> printfn "%s" | |
printfn "Process complete" | |
else | |
printfn "Process failed" | |
/// Generate an image file from a DOT file | |
/// algo = dot, neato | |
/// format = gif, png, jpg, svg | |
let generateImageFile dotFilename algo format imgFilename = | |
let cmd = sprintf @"""%s%s.exe""" graphVizPath algo | |
let inFile = System.IO.Path.Combine(__SOURCE_DIRECTORY__,dotFilename) | |
let outFile = System.IO.Path.Combine(__SOURCE_DIRECTORY__,imgFilename) | |
let cmdParams = sprintf "-T%s -o\"%s\" \"%s\"" format outFile inFile | |
startProcessAndCaptureOutput cmd cmdParams | |
// ================================ | |
// Various statistics | |
// ================================ | |
module Stats = | |
/// a record to hold some stats | |
type TypeStats = { | |
topLevelTypeCount:int // number of top level types | |
authoredTypeCount:int // number of types explicitly authored | |
allTypeCount: int // number of all internal types | |
} | |
let typeStats topLevelTypes = | |
let topLevelTypeCount = topLevelTypes |> Seq.length | |
let authoredTypeCount = | |
topLevelTypes | |
|> Seq.collect (fun tlt -> tlt.types ) | |
|> Seq.filter (fun td -> td.isAuthored) | |
|> Seq.length | |
let allTypeCount = | |
topLevelTypes | |
|> Seq.map (fun tlt -> Set.count tlt.types) | |
|> Seq.sum | |
{ topLevelTypeCount=topLevelTypeCount; authoredTypeCount=authoredTypeCount; allTypeCount=allTypeCount} | |
/// a record to hold some stats | |
type DependencyStats = { | |
depCount: int // number of dependency sets (same as number of top level types) | |
totalDepCount: int // sum of number of (internal) dependencies for all types | |
oneOrMoreDeps: int // number of top level types with one or more deps | |
threeOrMoreDeps: int // number of top level types with three or more deps | |
fiveOrMoreDeps: int // number of top level types with five or more deps | |
tenOrMoreDeps: int // number of top level types with ten or more deps | |
cycleCount:int // number of cycles | |
cycleParticipants: int // number of participants in cycles | |
maxComponentSize: int // max size of a strongly connected component | |
} | |
/// return the size of the largest component | |
let maxComponentSize components = | |
if Seq.isEmpty components | |
then 0 // max won't work on an empty set | |
else | |
components | |
|> Seq.map (fun set -> Set.count set) | |
|> Seq.max | |
let dependencyStats depSets = | |
let depCount = depSets |> Seq.length | |
let totalDepCount = depSets |> Seq.sumBy (fun depSet -> depSet.dependencies.Count) | |
let getCount max = | |
depSets | |
|> Seq.map (fun depSet -> depSet.dependencies.Count) | |
|> Seq.filter (fun count -> count >= max) | |
|> Seq.length | |
let oneOrMoreDeps = getCount 1 | |
let threeOrMoreDeps = getCount 3 | |
let fiveOrMoreDeps = getCount 5 | |
let tenOrMoreDeps = getCount 10 | |
let nonTrivialComponents = CyclicDependencies.scc depSets |> Seq.filter (fun set -> set.Count > 1) | |
let cycleCount = nonTrivialComponents |> Seq.length | |
let maxComponentSize = maxComponentSize nonTrivialComponents | |
let cyclicDeps = CyclicDependencies.toCyclicDependencies depSets |> Seq.filter (fun ds -> ds.dependencies.Count > 0) // ignore empty | |
let cycleParticipants = cyclicDeps |> Seq.length | |
{ depCount=depCount; | |
totalDepCount=totalDepCount; oneOrMoreDeps=oneOrMoreDeps; | |
threeOrMoreDeps=threeOrMoreDeps; fiveOrMoreDeps=fiveOrMoreDeps; tenOrMoreDeps=tenOrMoreDeps; | |
cycleCount=cycleCount; cycleParticipants=cycleParticipants; maxComponentSize=maxComponentSize } | |
/// a record to hold some stats | |
type FrequencyStats = { | |
value:int | |
count:int | |
} | |
let typeFrequency topLevelTypes = | |
let authCount tds = | |
tds | |
|> Seq.filter (fun td -> td.isAuthored) | |
|> Seq.length | |
let authoredTypeFrequencies = | |
topLevelTypes | |
|> Seq.groupBy (fun tlt -> authCount tlt.types) | |
|> Seq.map (fun (k,v) -> {value=k;count=Seq.length v} ) | |
let allTypeFrequencies = | |
topLevelTypes | |
|> Seq.groupBy (fun tlt -> Set.count tlt.types) | |
|> Seq.map (fun (k,v) -> {value=k;count=Seq.length v} ) | |
authoredTypeFrequencies,allTypeFrequencies | |
let depFrequency depSets = | |
depSets | |
|> Seq.groupBy (fun depSet -> Set.count depSet.dependencies) | |
|> Seq.map (fun (k,v) -> {value=k;count=Seq.length v} ) | |
// ================================ | |
// For REPL or debugging | |
// ================================ | |
module Output = | |
open Stats | |
let printDependencies depSets = | |
let printDependencySet depSet = | |
depSet.dependent |> printfn "%A" | |
depSet.dependencies |> Seq.toList |> List.sort |> List.iter (printfn "\t%A") | |
depSets | |
|> Seq.sortBy (fun depSet -> depSet.dependent) | |
|> Seq.iter printDependencySet | |
let printStats projectName codeSize typeStats publicDepStats allDepStats = | |
let codeToTopTypeRatio = codeSize / typeStats.topLevelTypeCount | |
let codeToAuthoredTypeRatio = codeSize / typeStats.authoredTypeCount | |
let codeToAllTypeRatio = codeSize / typeStats.allTypeCount | |
let topToAllRatio = float typeStats.topLevelTypeCount / float typeStats.allTypeCount | |
let topToAuthoredRatio = float typeStats.topLevelTypeCount / float typeStats.authoredTypeCount | |
printfn "===========================================" | |
printfn "Stats for %s" projectName | |
printfn "===========================================" | |
printfn " - CodeSize=%i TopLevelTypes=%i AllTypes=%i" codeSize typeStats.topLevelTypeCount typeStats.allTypeCount | |
printfn " - Instructions per top level type=%i (Instructions per authored type=%i)" codeToTopTypeRatio codeToAuthoredTypeRatio | |
printfn " - Ratio of top level types to authored types = %.2f " topToAuthoredRatio | |
printfn " - Ratio of top level types to all types = %.2f " topToAllRatio | |
let avgDepCount stats = float stats.totalDepCount / float typeStats.topLevelTypeCount | |
let ( <%> )top bottom = float top * 100.0 / float bottom | |
let pcOneOrMoreDeps stats = stats.oneOrMoreDeps <%> typeStats.topLevelTypeCount | |
let pcThreeOrMoreDeps stats = stats.threeOrMoreDeps <%> typeStats.topLevelTypeCount | |
let pcFiveOrMoreDeps stats = stats.fiveOrMoreDeps <%> typeStats.topLevelTypeCount | |
let pcTenOrMoreDeps stats = stats.tenOrMoreDeps <%> typeStats.topLevelTypeCount | |
printfn "Analysis of implementation dependencies for %s" projectName | |
printfn " - Avg # of dependencies=%.2f One or more=%.1f%%, 3 or more=%.1f%%, 5 or more=%.1f%%, 10 or more=%.1f%%" (avgDepCount allDepStats) (pcOneOrMoreDeps allDepStats) (pcThreeOrMoreDeps allDepStats) (pcFiveOrMoreDeps allDepStats) (pcTenOrMoreDeps allDepStats) | |
printfn " - Cycle count=%i cycleParticipants=%i maxComponentSize=%i" allDepStats.cycleCount allDepStats.cycleParticipants allDepStats.maxComponentSize | |
printfn "Analysis of public dependencies for %s" projectName | |
printfn " - Avg # of dependencies=%.2f One or more=%.1f%%, 3 or more=%.1f%%, 5 or more=%.1f%%, 10 or more=%.1f%%" (avgDepCount publicDepStats) (pcOneOrMoreDeps publicDepStats) (pcThreeOrMoreDeps publicDepStats) (pcFiveOrMoreDeps publicDepStats) (pcTenOrMoreDeps publicDepStats) | |
printfn " - Cycle count=%i cycleParticipants=%i maxComponentSize=%i" publicDepStats.cycleCount publicDepStats.cycleParticipants publicDepStats.maxComponentSize | |
let writeFrequenciesToFile projectName extension frequencies = | |
let filename = projectName + extension | |
use writer = new System.IO.StreamWriter(path=filename) | |
fprintfn writer "Project,Value,Count" | |
let writeFreq freq = | |
fprintfn writer "%s,%i,%i" projectName freq.value freq.count | |
frequencies | |
|> Seq.sortBy (fun f -> f.value) | |
|> Seq.iter writeFreq | |
// ================================ | |
// Main functions | |
// ================================ | |
/// Analysis should not include the F# core types except | |
/// for the projects explicitly listed | |
let ignoreCoreTypes projectName = | |
projectName <> "fsCore" && projectName <> "fsPowerPack" | |
let analyzeAndGenerate projectName assemblyName = | |
let excludeCore = ignoreCoreTypes projectName | |
// analyze | |
let topLevelTypes = AssemblyTypes.topLevelTypes excludeCore assemblyName | |
let publicDeps = TopLevelTypeDependencies.publicDependencies topLevelTypes | |
let publicCycles = CyclicDependencies.toCyclicDependencies publicDeps | |
let allDeps = TopLevelTypeDependencies.allDependencies topLevelTypes | |
let allCycles = CyclicDependencies.toCyclicDependencies allDeps | |
// stats | |
let codeSize = AssemblyTypes.codeSize excludeCore assemblyName | |
let typeStats = Stats.typeStats topLevelTypes | |
let publicDepStats = Stats.dependencyStats publicDeps | |
let allDepStats = Stats.dependencyStats allDeps | |
do Output.printStats projectName codeSize typeStats publicDepStats allDepStats | |
let generateFile depSet extension = | |
// create DOT file | |
let dotFilename = projectName + extension | |
GraphViz.createDotFile dotFilename depSet | |
// create SVG file | |
let svgFilename = dotFilename + ".svg" | |
GraphViz.generateImageFile dotFilename "dot" "svg" svgFilename | |
do generateFile publicDeps ".public.dot" | |
do generateFile publicCycles ".public.cycles.dot" | |
do generateFile allDeps ".all.dot" | |
do generateFile allCycles ".all.cycles.dot" | |
let tabularStats projects = | |
printfn "Project,CodeSize,TopLevelTypes,AuthoredTypes,AllTypes,AllTotalDepCount,AllOneOrMoreDepCount,AllThreeOrMoreDepCount,AllFiveOrMoreDepCount,AllTenOrMoreDepCount,AllMaxComponentSize,AllCycleCount,AllCycleParticipants,PubTotalDepCount,PubOneOrMoreDepCount,PubThreeOrMoreDepCount,PubFiveOrMoreDepCount,PubTenOrMoreDepCount,PubMaxComponentSize,PubCycleCount,PubCycleParticipants" | |
for (projectName,assemblyName) in projects do | |
let excludeCore = ignoreCoreTypes projectName | |
// analyze | |
let topLevelTypes = AssemblyTypes.topLevelTypes excludeCore assemblyName | |
let publicDeps = TopLevelTypeDependencies.publicDependencies topLevelTypes | |
let publicCycles = CyclicDependencies.toCyclicDependencies publicDeps | |
let allDeps = TopLevelTypeDependencies.allDependencies topLevelTypes | |
let allCycles = CyclicDependencies.toCyclicDependencies allDeps | |
// stats | |
let codeSize = AssemblyTypes.codeSize excludeCore assemblyName | |
let typeStats = Stats.typeStats topLevelTypes | |
let publicDepStats = Stats.dependencyStats publicDeps | |
let allDepStats = Stats.dependencyStats allDeps | |
printfn "%s,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i,%i" projectName codeSize typeStats.topLevelTypeCount typeStats.authoredTypeCount typeStats.allTypeCount allDepStats.totalDepCount allDepStats.oneOrMoreDeps allDepStats.threeOrMoreDeps allDepStats.fiveOrMoreDeps allDepStats.tenOrMoreDeps allDepStats.maxComponentSize allDepStats.cycleCount allDepStats.cycleParticipants publicDepStats.totalDepCount publicDepStats.oneOrMoreDeps publicDepStats.threeOrMoreDeps publicDepStats.fiveOrMoreDeps publicDepStats.tenOrMoreDeps publicDepStats.maxComponentSize publicDepStats.cycleCount publicDepStats.cycleParticipants | |
let frequenciesStats projects = | |
for (projectName,assemblyName) in projects do | |
let excludeCore = ignoreCoreTypes projectName | |
// analyze | |
let topLevelTypes = AssemblyTypes.topLevelTypes excludeCore assemblyName | |
let allDeps = TopLevelTypeDependencies.allDependencies topLevelTypes | |
// stats | |
let authored,all = Stats.typeFrequency topLevelTypes | |
let deps = Stats.depFrequency allDeps | |
Output.writeFrequenciesToFile projectName ".authored.freq.csv" authored | |
Output.writeFrequenciesToFile projectName ".types.freq.csv" all | |
Output.writeFrequenciesToFile projectName ".deps.freq.csv" deps | |
// =============================================== | |
// C# Projects | |
// =============================================== | |
// C# Project - Mono.Cecil | |
// Install-Package Mono.Cecil -Version 0.9.5.4 | |
let cecil = @"packages\Mono.Cecil.0.9.5.4\lib\net40\Mono.Cecil.dll" | |
// C# Project - NUnit.Framework | |
// Install-Package NUnit -Version 2.6.2 | |
let nunit = @"packages\NUnit.2.6.2\lib\nunit.framework.dll" | |
// C# Project - Nancy | |
// Install-Package Nancy -Version 0.17.1 | |
let nancy = @"packages\Nancy.0.17.1\lib\net40\Nancy.dll" | |
// C# Project - YamlDotNet | |
// Install-Package YamlDotNet.Core -Version 1.3.0 | |
let yamlDotNet = @"packages\YamlDotNet.Core.1.3.0\lib\YamlDotNet.Core.dll" | |
// C# Project - SpecFlow | |
// Install-Package SpecFlow -Version 1.9.0 | |
let specFlow = @"packages\SpecFlow.1.9.0\lib\net35\TechTalk.SpecFlow.dll" | |
// C# Project - SignalR | |
// Install-Package Microsoft.AspNet.SignalR.Core -Version 1.1.1 | |
let signalR = @"packages\Microsoft.AspNet.SignalR.Core.1.1.1\lib\net40\Microsoft.AspNet.SignalR.Core.dll" | |
// C# Project - JSON.NET | |
// Install-Package Newtonsoft.Json -Version 5.0.5 | |
let jsonDotNet = @"packages\Newtonsoft.Json.5.0.5\lib\net40\Newtonsoft.Json.dll" | |
// C# Project - EntityFramework | |
// Install-Package EntityFramework -Version 5.0.0 | |
let ef = @"packages\EntityFramework.5.0.0\lib\net40\EntityFramework.dll" | |
// C# Project - Elmah | |
// Install-Package elmah -Version 1.2.2 | |
let elmah = @"packages\elmah.corelibrary.1.2.2\lib\Elmah.dll" | |
// C# Project - Nuget | |
// Install-Package Nuget.Core -Version 2.5.0 | |
let nuget = @"packages\Nuget.Core.2.5.0\lib\net40-Client\NuGet.Core.dll" | |
// C# Project - FParsecCS | |
// Install-Package FParsec -Version 0.9.2.0 | |
let fparsecCS = @"packages\FParsec.0.9.2.0\lib\net40\FParsecCS.dll" | |
// C# Project - Moq | |
// Install-Package Moq -Version 4.0.10827 | |
let moq = @"packages\Moq.4.0.10827\lib\NET40\Moq.dll" | |
// C# Project - NDepend | |
// Manual install needed from http://ndepend.com | |
let ndepend = @"packages\NDepend\NDepend.Framework.dll" | |
let ndependPlat = @"packages\NDepend\NDepend.Platform.DotNet.dll" | |
// =============================================== | |
// F# Projects | |
// =============================================== | |
// F# Project - FSharp.Core | |
// Install-Package FSharp.Core -Version 4.0.0 | |
let fsCore = @"packages\FSharp.Core.4.0.0\lib\FSharp.Core.dll" | |
// F# Project - FSharp.PowerPack | |
// Install-Package FSPowerPack.Community -Version 3.0.0.0 | |
let fsPowerPack = @"packages\FSPowerPack.Core.Community.3.0.0.0\lib\net40\Fsharp.PowerPack.dll" | |
// F# Project - FParsec | |
// Install-Package FParsec -Version 0.9.2.0 | |
let fParsec = @"packages\FParsec.0.9.2.0\lib\net40\FParsec.dll" | |
// F# Project - canopy | |
// Install-Package canopy -Version 0.7.5 | |
let canopy = @"packages\canopy.0.7.5\lib\canopy.dll" | |
// F# Project - FsSql | |
// Install-Package FsSql -Version 0.1.0 | |
let fsSql = @"packages\FsSql.0.1.0\lib\FsSql.dll" | |
// F# Project - FsYaml | |
// Install-Package FsYaml -Version 1.1.2 | |
let fsYaml = @"packages\FsYaml.1.1.2\lib\net40\FsYaml.dll" | |
// F# Project - Storm | |
// Download from http://storm.codeplex.com/releases/view/18871 | |
let storm = @"Storm.exe" | |
// F# Project - FsharpxCore | |
// Install-Package FSharpx.Core -Version 1.8.23 | |
let fsxCore = @"packages\FSharpx.Core.1.8.23\lib\40\FSharpx.Core.dll" | |
// F# Project - TickSpec | |
// Install-Package TickSpec -Version 1.0.0.1 | |
let tickSpec = @"packages\TickSpec.1.0.0.1\Lib\net40\TickSpec.dll" | |
// F# Project - Websharper | |
// Install-Package WebSharper -Version 2.4.85.235 | |
let websharper = @"packages\WebSharper.2.4.85.235\IntelliFactory.WebSharper.dll" | |
let websharperCore = @"packages\WebSharper.2.4.85.235\IntelliFactory.WebSharper.Core.dll" | |
let websharperFormlet = @"packages\WebSharper.2.4.85.235\IntelliFactory.WebSharper.Formlet.dll" | |
let websharperHtml = @"packages\WebSharper.2.4.85.235\IntelliFactory.WebSharper.Html.dll" | |
// F# Project - FsUnit | |
// Install-Package FsUnit -Version 1.2.1.0 | |
let fsUnit = @"packages\FsUnit.1.2.1.0\Lib\Net40\FsUnit.NUnit.dll" | |
// F# Project - Foq | |
// Install-Package Foq -Version 0.9 | |
let foq = @"packages\Foq.0.9\Lib\net40\Foq.dll" | |
let csProjects = [ | |
("ef",ef) | |
("jsonDotNet",jsonDotNet) | |
("nancy",nancy) | |
("cecil",cecil) | |
("nuget",nuget) | |
("signalR",signalR) | |
("nunit",nunit) | |
("specFlow",specFlow) | |
("elmah",elmah) | |
("yamlDotNet",yamlDotNet) | |
("fparsecCS",fparsecCS) | |
("moq",moq) | |
("ndepend",ndepend) | |
("ndependPlat",ndependPlat) | |
] | |
let fsProjects = [ | |
("fsxCore",fsxCore) | |
("fsCore",fsCore) | |
("fsPowerPack",fsPowerPack) | |
("storm",storm) | |
("fParsec",fParsec) | |
("websharper",websharper) | |
("tickSpec",tickSpec) | |
("websharperHtml", websharperHtml) | |
("canopy",canopy) | |
("fsYaml",fsYaml) | |
("fsSql",fsSql) | |
("fsUnit",fsUnit) | |
("foq",foq) | |
] | |
// =============================================== | |
// Generate SVG files | |
// =============================================== | |
analyzeAndGenerate "ef" ef | |
analyzeAndGenerate "jsonDotNet" jsonDotNet | |
analyzeAndGenerate "nancy" nancy | |
analyzeAndGenerate "cecil" cecil | |
analyzeAndGenerate "nuget" nuget | |
analyzeAndGenerate "signalR" signalR | |
analyzeAndGenerate "nunit" nunit | |
analyzeAndGenerate "specFlow" specFlow | |
analyzeAndGenerate "elmah" elmah | |
analyzeAndGenerate "yamlDotNet" yamlDotNet | |
analyzeAndGenerate "fparsecCS" fparsecCS | |
analyzeAndGenerate "moq" moq | |
analyzeAndGenerate "ndepend" ndepend | |
analyzeAndGenerate "ndependPlat" ndependPlat | |
analyzeAndGenerate "fsxCore" fsxCore | |
analyzeAndGenerate "fsCore" fsCore | |
analyzeAndGenerate "fsPowerPack" fsPowerPack | |
analyzeAndGenerate "storm" storm | |
analyzeAndGenerate "fParsec" fParsec | |
analyzeAndGenerate "websharper" websharper | |
analyzeAndGenerate "websharperCore" websharperCore | |
analyzeAndGenerate "websharperFormlet" websharperFormlet | |
analyzeAndGenerate "websharperHtml" websharperHtml | |
analyzeAndGenerate "tickSpec" tickSpec | |
analyzeAndGenerate "canopy" canopy | |
analyzeAndGenerate "fsYaml" fsYaml | |
analyzeAndGenerate "fsSql" fsSql | |
analyzeAndGenerate "fsUnit" fsUnit | |
analyzeAndGenerate "foq" foq | |
// =============================================== | |
// All stats tabulated | |
// =============================================== | |
tabularStats csProjects | |
tabularStats fsProjects | |
frequenciesStats csProjects | |
frequenciesStats fsProjects |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment