Modernize tests, remove Tester project, and automate amalgamation pipeline
This commit is contained in:
@@ -1,192 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AmalgamationTool
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
List<string> usings = new List<string>();
|
||||
Dictionary<string, List<string>> classes = new Dictionary<string, List<string>>();
|
||||
|
||||
// Build a file using string builder.
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
foreach (var f in new DirectoryInfo(Path.GetFullPath(args[0].Trim('"', '\''))).GetFiles("*.cs", SearchOption.AllDirectories))
|
||||
{
|
||||
string content = File.ReadAllText(f.FullName);
|
||||
|
||||
string namespaceName = string.Empty;
|
||||
|
||||
// Deal with usings
|
||||
foreach (var u in content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
|
||||
.Where(l => l.Trim().StartsWith("using ") && !l.Trim().StartsWith("using ("))
|
||||
.Select(l => l.Trim()))
|
||||
if (!usings.Contains(u))
|
||||
usings.Add(u);
|
||||
|
||||
// Extract namespace
|
||||
|
||||
//if (args.Length > 2)
|
||||
//{
|
||||
// var tcontent = Regex.Replace(content, @"^\s*using\s+.*\s*;$", string.Empty);
|
||||
// tcontent = Regex.Replace(content, @"^\s*namespace\s+.*\s*", string.Empty).Trim();
|
||||
|
||||
// var ns = Regex.Match(content, @"^\s*namespace\s+(?<ns>.*)\s*");
|
||||
|
||||
// if (ns.Success)
|
||||
// {
|
||||
// if (!classes.ContainsKey(ns.Groups["ns"].Value))
|
||||
// classes.Add(ns.Groups["ns"].Value, new List<string>());
|
||||
|
||||
// classes[ns.Groups["ns"].Value].Add(tcontent);
|
||||
// }
|
||||
//}
|
||||
//else
|
||||
{
|
||||
if (content.Trim().Length == 0)
|
||||
continue;
|
||||
|
||||
var nstart = content.IndexOf("namespace ") + "namespace ".Length;
|
||||
var bbrace = content.IndexOf("{", nstart);
|
||||
var nlen = bbrace - nstart;
|
||||
|
||||
if (nstart < "namespace ".Length)
|
||||
{
|
||||
if (f.Name.ToLower() == "assemblyinfo.cs")
|
||||
{
|
||||
var hs = content.IndexOf("/*");
|
||||
var es = content.IndexOf("*/", hs) + 2;
|
||||
if (es > hs)
|
||||
{
|
||||
sb.AppendLine(content.Substring(hs, es - hs));
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
string ns = content.Substring(nstart, nlen).Trim();
|
||||
|
||||
// Add namespace if not exist
|
||||
if (!classes.ContainsKey(ns))
|
||||
classes.Add(ns, new List<string>());
|
||||
|
||||
var ebrace = content.LastIndexOf('}');
|
||||
|
||||
// Cut content as class/enum
|
||||
classes[ns].Add(content.Substring(bbrace + 1, ebrace - bbrace - 1));
|
||||
}
|
||||
}
|
||||
|
||||
usings.Sort();
|
||||
|
||||
foreach (var u in usings)
|
||||
sb.AppendLine(u);
|
||||
|
||||
sb.AppendLine(@"
|
||||
[module: System.Diagnostics.CodeAnalysis.SuppressMessage(""StyleCop.CSharp.MaintainabilityRules"", ""SA1402:FileMayOnlyContainASingleClass"", Justification = ""This is a generated file which generates all the necessary support classes."")]
|
||||
[module: System.Diagnostics.CodeAnalysis.SuppressMessage(""StyleCop.CSharp.MaintainabilityRules"", ""SA1403:FileMayOnlyContainASingleNamespace"", Justification = ""This is a generated file which generates all the necessary support classes."")]");
|
||||
|
||||
FillClassesAndNamespacesIddented(classes, sb);
|
||||
|
||||
string amalgamation = sb.ToString();
|
||||
|
||||
sb = new StringBuilder();
|
||||
|
||||
string prevTrimmed = null;
|
||||
|
||||
string[] array = amalgamation.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
|
||||
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
string l = array[i];
|
||||
var currentTrimmed = l.Trim();
|
||||
var nextTrimmed = (i + 1 == array.Length) ? null : array[i + 1].Trim();
|
||||
|
||||
if (prevTrimmed != null)
|
||||
{
|
||||
switch (prevTrimmed)
|
||||
{
|
||||
case "":
|
||||
if (currentTrimmed == string.Empty)
|
||||
continue;
|
||||
break;
|
||||
|
||||
case "{":
|
||||
case "}":
|
||||
if (currentTrimmed == string.Empty && (nextTrimmed == prevTrimmed || nextTrimmed == string.Empty))
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine(l);
|
||||
prevTrimmed = currentTrimmed;
|
||||
}
|
||||
|
||||
File.WriteAllText(Path.GetFullPath(args[1].Trim('"', '\'')), sb.ToString());
|
||||
}
|
||||
|
||||
private static void FillClassesAndNamespaces(Dictionary<string, List<string>> classes, StringBuilder sb)
|
||||
{
|
||||
foreach (var n in classes)
|
||||
{
|
||||
sb.AppendFormat("namespace {0}{1}{{", n.Key, Environment.NewLine);
|
||||
n.Value.ForEach(c => sb.Append(c));
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillClassesAndNamespacesIddented(Dictionary<string, List<string>> classes, StringBuilder sb)
|
||||
{
|
||||
var min = classes.Min(k => k.Key.Split('.').Count());
|
||||
|
||||
foreach (var n in classes.Where(nc => nc.Key.Split('.').Count() == min))
|
||||
{
|
||||
sb.AppendFormat("namespace {0}{1}{{", n.Key, Environment.NewLine);
|
||||
n.Value.ForEach(c => sb.Append(c));
|
||||
|
||||
SubNamespaces(classes, n.Key, sb, min);
|
||||
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SubNamespaces(Dictionary<string, List<string>> classes, string p, StringBuilder sb, int ident)
|
||||
{
|
||||
sb.AppendLine(string.Empty);
|
||||
|
||||
foreach (var n in classes.Where(nc => nc.Key.Split('.').Count() == ident + 1 && nc.Key.StartsWith(p)))
|
||||
{
|
||||
for (int i = 0; i < ident; i++) sb.Append(" ");
|
||||
sb.AppendFormat("namespace {0}{1}", n.Key.Substring(p.Length + 1), Environment.NewLine);
|
||||
|
||||
for (int i = 0; i < ident; i++) sb.Append(" ");
|
||||
sb.Append("{");
|
||||
n.Value.ForEach(c =>
|
||||
{
|
||||
foreach (var l in c.Split(new string[] { Environment.NewLine }, StringSplitOptions.None))
|
||||
{
|
||||
for (int i = 0; i < ident; i++) sb.Append(" ");
|
||||
sb.AppendLine(l);
|
||||
}
|
||||
});
|
||||
|
||||
SubNamespaces(classes, n.Key, sb, ident + 1);
|
||||
|
||||
for (int i = 0; i < ident; i++) sb.Append(" ");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine(string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AmalgamationTool;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private const string NamespaceToken = "namespace ";
|
||||
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: AmalgamationTool <sourceDir> <outputFile>");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var sourceDir = Path.GetFullPath(args[0].Trim('"', '\''));
|
||||
var outputFile = Path.GetFullPath(args[1].Trim('"', '\''));
|
||||
|
||||
if (!Directory.Exists(sourceDir))
|
||||
{
|
||||
Console.Error.WriteLine($"Source directory not found: {sourceDir}");
|
||||
return 2;
|
||||
}
|
||||
|
||||
var allUsings = new SortedSet<string>(StringComparer.Ordinal);
|
||||
var namespaces = new SortedDictionary<string, List<string>>(StringComparer.Ordinal);
|
||||
var headerComment = string.Empty;
|
||||
|
||||
foreach (var file in EnumerateInputFiles(sourceDir))
|
||||
{
|
||||
var content = File.ReadAllText(file);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(headerComment))
|
||||
{
|
||||
headerComment = TryGetFileHeaderComment(content);
|
||||
}
|
||||
|
||||
foreach (var @using in ExtractUsingLines(content))
|
||||
{
|
||||
allUsings.Add(@using);
|
||||
}
|
||||
|
||||
var nsData = TryExtractNamespaceBody(content);
|
||||
if (nsData == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!namespaces.TryGetValue(nsData.Value.Namespace, out var nodes))
|
||||
{
|
||||
nodes = new List<string>();
|
||||
namespaces[nsData.Value.Namespace] = nodes;
|
||||
}
|
||||
|
||||
nodes.Add(nsData.Value.Body);
|
||||
}
|
||||
|
||||
var output = BuildAmalgamation(headerComment, allUsings, namespaces);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputFile) ?? ".");
|
||||
File.WriteAllText(outputFile, output, new UTF8Encoding(false));
|
||||
|
||||
Console.WriteLine($"Amalgamation generated: {outputFile}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateInputFiles(string sourceDir)
|
||||
{
|
||||
var root = new DirectoryInfo(sourceDir);
|
||||
return root.EnumerateFiles("*.cs", SearchOption.AllDirectories)
|
||||
.Where(f => !f.FullName.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => !f.FullName.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => !f.Name.Equals("AssemblyInfo.cs", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => !f.Name.Equals("DynamORM.Amalgamation.cs", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(f => f.FullName)
|
||||
.OrderBy(f => f, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ExtractUsingLines(string content)
|
||||
{
|
||||
var matches = Regex.Matches(content, @"^\s*using\s+[^;]+;", RegexOptions.Multiline);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var line = match.Value.Trim();
|
||||
if (!line.StartsWith("using (", StringComparison.Ordinal))
|
||||
{
|
||||
yield return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (string Namespace, string Body)? TryExtractNamespaceBody(string content)
|
||||
{
|
||||
var nsIndex = content.IndexOf(NamespaceToken, StringComparison.Ordinal);
|
||||
if (nsIndex < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nsStart = nsIndex + NamespaceToken.Length;
|
||||
var braceStart = content.IndexOf('{', nsStart);
|
||||
if (braceStart < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var namespaceName = content.Substring(nsStart, braceStart - nsStart).Trim();
|
||||
if (string.IsNullOrWhiteSpace(namespaceName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var braceEnd = FindMatchingBrace(content, braceStart);
|
||||
if (braceEnd <= braceStart)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var body = content.Substring(braceStart + 1, braceEnd - braceStart - 1).Trim('\r', '\n');
|
||||
return (namespaceName, body);
|
||||
}
|
||||
|
||||
private static int FindMatchingBrace(string content, int openBrace)
|
||||
{
|
||||
var depth = 0;
|
||||
for (var i = openBrace; i < content.Length; i++)
|
||||
{
|
||||
switch (content[i])
|
||||
{
|
||||
case '{':
|
||||
depth++;
|
||||
break;
|
||||
case '}':
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string TryGetFileHeaderComment(string content)
|
||||
{
|
||||
var match = Regex.Match(content, @"^\s*/\*.*?\*/", RegexOptions.Singleline);
|
||||
return match.Success ? match.Value.Trim() : string.Empty;
|
||||
}
|
||||
|
||||
private static string BuildAmalgamation(
|
||||
string headerComment,
|
||||
IEnumerable<string> allUsings,
|
||||
SortedDictionary<string, List<string>> namespaces)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
if (!string.IsNullOrWhiteSpace(headerComment))
|
||||
{
|
||||
sb.AppendLine(headerComment);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var @using in allUsings)
|
||||
{
|
||||
sb.AppendLine(@using);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("[module: System.Diagnostics.CodeAnalysis.SuppressMessage(\"StyleCop.CSharp.MaintainabilityRules\", \"SA1402:FileMayOnlyContainASingleClass\", Justification = \"This is a generated file which generates all the necessary support classes.\")]");
|
||||
sb.AppendLine("[module: System.Diagnostics.CodeAnalysis.SuppressMessage(\"StyleCop.CSharp.MaintainabilityRules\", \"SA1403:FileMayOnlyContainASingleNamespace\", Justification = \"This is a generated file which generates all the necessary support classes.\")]");
|
||||
sb.AppendLine();
|
||||
|
||||
FillNamespaceTree(namespaces, sb);
|
||||
return CleanupWhitespace(sb.ToString());
|
||||
}
|
||||
|
||||
private static void FillNamespaceTree(SortedDictionary<string, List<string>> classes, StringBuilder sb)
|
||||
{
|
||||
var minDepth = classes.Keys.Min(k => k.Split('.').Length);
|
||||
|
||||
foreach (var root in classes.Where(c => c.Key.Split('.').Length == minDepth))
|
||||
{
|
||||
sb.AppendLine($"namespace {root.Key}");
|
||||
sb.AppendLine("{");
|
||||
|
||||
foreach (var code in root.Value)
|
||||
{
|
||||
sb.AppendLine(code);
|
||||
}
|
||||
|
||||
FillSubNamespaces(classes, root.Key, minDepth, sb);
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillSubNamespaces(
|
||||
SortedDictionary<string, List<string>> classes,
|
||||
string parent,
|
||||
int depth,
|
||||
StringBuilder sb)
|
||||
{
|
||||
foreach (var child in classes.Where(c => c.Key.StartsWith(parent + ".", StringComparison.Ordinal) && c.Key.Split('.').Length == depth + 1))
|
||||
{
|
||||
var indent = new string(' ', depth * 4);
|
||||
var shortNamespace = child.Key.Substring(parent.Length + 1);
|
||||
sb.Append(indent).AppendLine($"namespace {shortNamespace}");
|
||||
sb.Append(indent).AppendLine("{");
|
||||
|
||||
foreach (var block in child.Value)
|
||||
{
|
||||
foreach (var line in block.Replace("\r\n", "\n").Split('\n'))
|
||||
{
|
||||
sb.Append(indent).Append(" ").AppendLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
FillSubNamespaces(classes, child.Key, depth + 1, sb);
|
||||
sb.Append(indent).AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
private static string CleanupWhitespace(string content)
|
||||
{
|
||||
var lines = content.Replace("\r\n", "\n").Split('\n');
|
||||
var output = new StringBuilder();
|
||||
string? previous = null;
|
||||
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var current = lines[i];
|
||||
var trimmed = current.Trim();
|
||||
var nextTrimmed = i + 1 < lines.Length ? lines[i + 1].Trim() : null;
|
||||
|
||||
if (string.IsNullOrEmpty(trimmed))
|
||||
{
|
||||
if (string.IsNullOrEmpty(previous))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previous == "{" || previous == "}" || nextTrimmed == "}" || string.IsNullOrEmpty(nextTrimmed))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
output.AppendLine(current.TrimEnd());
|
||||
previous = trimmed;
|
||||
}
|
||||
|
||||
return output.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user