Skip to content

Commit

Permalink
dev: code metrics analysis report
Browse files Browse the repository at this point in the history
  • Loading branch information
swharden committed Mar 9, 2023
1 parent 5bdb051 commit 6e920c5
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/dev/www/metrics
*.Metrics.xml
/dev/www/cookbook
src/ScottPlot4/ScottPlot.Cookbook/CookbookOutput
Expand Down
10 changes: 10 additions & 0 deletions dev/CodeAnalysis/CodeAnalysis.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
25 changes: 25 additions & 0 deletions dev/CodeAnalysis/CodeAnalysis.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeAnalysis", "CodeAnalysis.csproj", "{720EC31A-1F74-47A1-96D2-36EAB1803CC3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{720EC31A-1F74-47A1-96D2-36EAB1803CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{720EC31A-1F74-47A1-96D2-36EAB1803CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{720EC31A-1F74-47A1-96D2-36EAB1803CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{720EC31A-1F74-47A1-96D2-36EAB1803CC3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {25D3A5B0-EEC3-4388-AF70-061D5C24DD8E}
EndGlobalSection
EndGlobal
76 changes: 76 additions & 0 deletions dev/CodeAnalysis/CodeReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using CodeAnalysis.HtmlReports;
using System.Linq;
using System.Text;

namespace CodeAnalysis;

public static class CodeReport
{
public static void Generate(string repoRootPath, string saveAs = "code-report.html")
{
string[] csFilePaths = Directory.GetFiles(repoRootPath, "*.cs", SearchOption.AllDirectories)
.Where(x => !x.EndsWith("Designer.cs"))
.Where(x => !x.Contains(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar))
.Where(x => !x.EndsWith("AssemblyInfo.cs"))
.OrderBy(x => x)
.ToArray();

Dictionary<string, int> linesByFile = csFilePaths.ToDictionary(x => x, x => File.ReadAllLines(x).Length);

StringBuilder sb = new();

sb.AppendLine("<h1 align='center' style='margin-bottom: 0px;'>ScottPlot Code Metrics</h1>");
sb.AppendLine($"<div align='center'>Together, all projects contain {Total(linesByFile):N0} total lines of code</div>");
sb.AppendLine($"<hr style='margin: 50px;' />");

var sp4 = linesByFile.Where(x => x.Key.Contains(Path.DirectorySeparatorChar + "ScottPlot4" + Path.DirectorySeparatorChar));
sb.AppendLine("<h2 style='margin-bottom: 0px;'>ScottPlot 4</h2>");
sb.AppendLine($"<ul>");
sb.AppendLine($"<li>Core Library: {Total(sp4, "ScottPlot4"):N0} lines</li>");
sb.AppendLine($"<li>Tests: {Total(sp4, "ScottPlot.Tests"):N0} lines</li>");
sb.AppendLine($"<li>Cookbook: {Total(sp4, "ScottPlot.Cookbook"):N0} lines</li>");
sb.AppendLine($"<li>Demos: {Total(sp4, "ScottPlot.Demo"):N0} lines</li>");
sb.AppendLine($"<li>All: {Total(sp4):N0}</li>");
sb.AppendLine($"</ul>");

var sp5 = linesByFile.Where(x => x.Key.Contains(Path.DirectorySeparatorChar + "ScottPlot5" + Path.DirectorySeparatorChar));
sb.AppendLine("<h2 style='margin-bottom: 0px;'>ScottPlot 5</h2>");
sb.AppendLine($"<ul>");
sb.AppendLine($"<li>Core Library: {Total(sp5, "ScottPlot5"):N0}</li>");
sb.AppendLine($"<li>Tests: {Total(sp5, "ScottPlot5 Tests"):N0} lines</li>");
sb.AppendLine($"<li>Cookbook: {Total(sp5, "ScottPlot5 Cookbook"):N0} lines</li>");
sb.AppendLine($"<li>Demos: {Total(sp5, "ScottPlot5 Demos"):N0} lines</li>");
sb.AppendLine($"<li>All: {Total(sp5):N0}</li>");
sb.AppendLine($"</ul>");

var shared = linesByFile.Where(x => x.Key.Contains(Path.DirectorySeparatorChar + "Shared" + Path.DirectorySeparatorChar));
sb.AppendLine("<h2 style='margin-bottom: 0px;'>Shared Code</h2>");
sb.AppendLine($"<ul>");
sb.AppendLine($"<li>All: {Total(shared):N0} lines</li>");
sb.AppendLine($"</ul>");

sb.AppendLine($"<div align='center' style='margin-top: 100px;'>Generated {DateTime.Now}</div>");

/*
foreach ((string filePath, int lineCount) in linesByFile)
sb.AppendLine($"<div>{filePath} {lineCount}</div>");
*/

saveAs = Path.GetFullPath(saveAs);
string html = HtmlTemplate.WrapInPico(sb.ToString());
Directory.CreateDirectory(Path.GetDirectoryName(saveAs)!);
File.WriteAllText(saveAs, html);
Console.WriteLine($"Wrote: {saveAs}");
}

private static int Total(IEnumerable<KeyValuePair<string, int>> linesByFile, string? midFolderName = null)
{
if (string.IsNullOrEmpty(midFolderName))
return linesByFile.Select(x => x.Value).Sum();

return linesByFile
.Where(x => x.Key.Contains(Path.DirectorySeparatorChar + midFolderName + Path.DirectorySeparatorChar))
.Select(x => x.Value)
.Sum();
}
}
39 changes: 39 additions & 0 deletions dev/CodeAnalysis/HtmlReports/HtmlTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace CodeAnalysis.HtmlReports;

public static class HtmlTemplate
{
public static string WrapInPico(string content, string? title = null)
{
return @"<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='stylesheet' href='https://unpkg.com/@picocss/pico@1.*/css/pico.min.css'>
<title>{{TITLE}}</title>
</head>
<body>
<main class='container'>
{{CONTENT}}
</main>
</body>
</html>".Replace("{{TITLE}}", title).Replace("{{CONTENT}}", content);
}

public static string WrapInBootstrap(string content, string? title = null)
{
return @"<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>{{TITLE}}</title>
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'>
</head>
<body>
<div class='container'>{{CONTENT}}</div>
<script src='https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js'></script>
</body>
</html>".Replace("{{TITLE}}", title).Replace("{{CONTENT}}", content);
}
}
51 changes: 51 additions & 0 deletions dev/CodeAnalysis/HtmlReports/ProjectSummaries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Text;

namespace CodeAnalysis.HtmlReports;

public class ProjectSummaries
{
MultiProjectReport Report { get; }

public ProjectSummaries(MultiProjectReport report)
{
Report = report;
}

private string GetContent()
{
StringBuilder sb = new();

sb.AppendLine("<h1>ScottPlot Code Metrics</h1>");

sb.AppendLine("<h3>All Projects</h3>");

TableBuilder tb = new();

tb.AddHeader(new string[] {
"Project",
"Lines of Code",
"Types",
"Cyclomatic Complexity" });

foreach (ProjectReport project in Report.Projects)
{
tb.AddRow(new string[] {
project.ProjectName,
$"{project.LinesOfCode:N0}",
$"{project.Types:N0}",
$"{project.CyclomaticComplexity:N0}" });
}

sb.AppendLine(tb.GetHtml());

return sb.ToString();
}

public void Save(string htmlFilePath)
{
string html = HtmlTemplate.WrapInPico(GetContent(), "ScottPlot Code Metrics");
string saveAs = Path.GetFullPath(htmlFilePath);
File.WriteAllText(saveAs, html);
Console.WriteLine($"Saved: {saveAs}");
}
}
33 changes: 33 additions & 0 deletions dev/CodeAnalysis/HtmlReports/TableBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Text;

namespace CodeAnalysis.HtmlReports;

internal class TableBuilder
{
StringBuilder Body = new();

public void AddHeader(string[] items)
{
Body.AppendLine("<tr>");
foreach (string item in items)
{
Body.AppendLine($"<th><strong>{item}</strong></th>");
}
Body.AppendLine("</tr>");
}

public void AddRow(string[] items)
{
Body.AppendLine("<tr>");
foreach (string item in items)
{
Body.AppendLine($"<td>{item}</td>");
}
Body.AppendLine("</tr>");
}

public string GetHtml()
{
return $"<table>{Body}</table>";
}
}
36 changes: 36 additions & 0 deletions dev/CodeAnalysis/Metrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Xml.Linq;

namespace CodeAnalysis;

public class Metrics
{
public string? NamespaceName { get; set; } = null;
public string? TypeName { get; set; } = null;
public string FullTypeName => $"{NamespaceName}.{TypeName}";

public int? CyclomaticComplexity { get; set; } = null;
public int? LinesOfCode { get; set; } = null;
public int? ExecutableLines { get; set; } = null;
public int? Maintainability { get; set; } = null;

public static Metrics FromElement(XElement metricsElement, string namespaceName, string typeName)
{
Metrics ms = new();

foreach (XElement metric in metricsElement.Elements("Metric"))
{
string name = metric.Attribute("Name")!.Value.ToString();
int value = int.Parse(metric.Attribute("Value")!.Value);
if (name == "CyclomaticComplexity")
ms.CyclomaticComplexity = value;
else if (name == "MaintainabilityIndex")
ms.Maintainability = value;
else if (name == "SourceLines")
ms.LinesOfCode = value;
else if (name == "ExecutableLines")
ms.ExecutableLines = value;
}

return ms;
}
}
18 changes: 18 additions & 0 deletions dev/CodeAnalysis/MultiProjectReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace CodeAnalysis;

public class MultiProjectReport
{
public Metrics[] Metrics { get; }
public ProjectReport[] Projects { get; }

public MultiProjectReport(string[] metricsFilePaths)
{
Projects = metricsFilePaths.Select(x => new ProjectReport(x)).ToArray();
Metrics = Projects.SelectMany(x => x.Metrics).ToArray();
}

public override string ToString()
{
return $"Report spanning {Projects.Length} projects and {Metrics.Length} analyzed types";
}
}
19 changes: 19 additions & 0 deletions dev/CodeAnalysis/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using CodeAnalysis;

string repoRoot = Path.GetFullPath(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "../../../../../"));

/*
string sourceFolder = Path.Combine(repoRoot, "src");
if (!Directory.Exists(sourceFolder))
throw new DirectoryNotFoundException(sourceFolder);
string[] metricsFiles = Directory.GetFiles(sourceFolder, "*.Metrics.xml", SearchOption.AllDirectories);
MultiProjectReport report = new(metricsFiles);
Console.WriteLine(report);
CodeAnalysis.HtmlReports.ProjectSummaries s = new(report);
s.Save("summaries.html");
*/

string saveAs = Path.Combine(repoRoot, "dev/www/metrics/index.html");
CodeReport.Generate(repoRoot, saveAs);
44 changes: 44 additions & 0 deletions dev/CodeAnalysis/ProjectReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Xml.Linq;

namespace CodeAnalysis;

public class ProjectReport
{
public string ProjectName { get; }
public List<Metrics> Metrics { get; } = new();

public int LinesOfCode => Metrics.Select(x => x.LinesOfCode).Where(x => x.HasValue).Select(x => x!.Value).Sum();
public int CyclomaticComplexity => Metrics.Select(x => x.CyclomaticComplexity).Where(x => x.HasValue).Select(x => x!.Value).Sum();

public int Types => Metrics.Count;

public ProjectReport(string xmlFilePath)
{
ProjectName = Path.GetFileName(xmlFilePath).Replace(".Metrics.xml", "");

string xmlText = File.ReadAllText(xmlFilePath);
XDocument doc = XDocument.Parse(xmlText);
XElement assembly = doc.Descendants("Assembly").First();

foreach (XElement namespaceElement in assembly.Element("Namespaces")!.Elements("Namespace"))
{
string namespaceName = namespaceElement.Attribute("Name")!.Value;

foreach (XElement namedType in namespaceElement.Elements("Types").Elements("NamedType"))
{
string typeName = namedType.Attribute("Name")!.Value;

var metricsElement = namedType.Element("Metrics");
if (metricsElement is not null)
{
Metrics.Add(CodeAnalysis.Metrics.FromElement(metricsElement, namespaceName, typeName));
}
}
}
}

public override string ToString()
{
return $"{ProjectName} with {Metrics.Count} metrics";
}
}

0 comments on commit 6e920c5

Please sign in to comment.