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 "); 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(StringComparer.Ordinal); var namespaces = new SortedDictionary>(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(); 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 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 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 allUsings, SortedDictionary> 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> 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> 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(); } }