262 lines
8.5 KiB
C#
262 lines
8.5 KiB
C#
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();
|
|
}
|
|
}
|