diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGenerator.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGenerator.cs index 9d1b9f01c85..0f0a3b8e4b3 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGenerator.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGenerator.cs @@ -4,6 +4,7 @@ using HotChocolate.Language; using HotChocolate.Validation; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.Extensions.DependencyInjection; using StrawberryShake.CodeGeneration.Analyzers; @@ -262,13 +263,31 @@ private static void GenerateSingleCSharpDocument( code.AppendLine("#nullable enable annotations"); code.AppendLine("#nullable disable warnings"); - var compilationUnit = CompilationUnit(); + code.AppendLine(); + + // Each namespace declaration is normalized on its own and the pieces are joined, + // rather than normalizing one giant compilation unit of every namespace. Building + // that single tree was super-linear in the number of types and dominated generation + // for large clients (it was also the out-of-memory source, since it materialized one + // huge tree). Normalizing per namespace keeps the cost far lower and never holds the + // whole client in a single tree. + // + // The assembled text is byte-identical to normalizing the whole compilation unit. A + // standalone namespace declaration normalizes with no leading or trailing trivia, and + // the whole-tree normalizer separates the top-level namespace members with a blank + // line (one line ending to close the previous namespace's brace, then one blank + // line), so we reproduce exactly that separator between namespaces. Normalizing each + // namespace as a real subtree (rather than re-indenting per-type text) preserves the + // normalizer's own handling of nested constructs such as preprocessor directives. + var firstNamespace = true; foreach (var group in results.GroupBy(t => t.Result.Namespace).OrderBy(t => t.Key)) { var namespaceDeclaration = NamespaceDeclaration(IdentifierName(group.Key)); + var members = new List(); + foreach (var item in group) { var typeDeclaration = item.Result.TypeDeclaration; @@ -279,16 +298,37 @@ private static void GenerateSingleCSharpDocument( typeDeclaration = typeDeclaration.WithLeadingTrivia(trivia); #endif - namespaceDeclaration = namespaceDeclaration.AddMembers(typeDeclaration); + members.Add(typeDeclaration); } - compilationUnit = compilationUnit.AddMembers(namespaceDeclaration); - } + // Adding the members one at a time copies the growing member list on every call, + // which is quadratic for namespaces with tens of thousands of types. Setting the + // whole list once keeps namespace construction linear. + namespaceDeclaration = namespaceDeclaration.WithMembers(List(members)); - compilationUnit = compilationUnit.NormalizeWhitespace(elasticTrivia: true); + var normalized = namespaceDeclaration + .NormalizeWhitespace(elasticTrivia: true) + .ToFullString(); - code.AppendLine(); - code.AppendLine(compilationUnit.ToFullString()); + if (firstNamespace) + { + firstNamespace = false; + } + else + { + code.Append("\r\n\r\n"); + } + + code.Append(normalized); + } + + // The original whole-tree path appended a trailing line ending after the compilation + // unit (AppendLine of ToFullString()). Preserve it so the emitted text stays + // byte-identical. + if (!firstNamespace) + { + code.Append(Environment.NewLine); + } documents.Add( new(