From 3819819d27f25864b5ccc97eb37e523efe627fe5 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:21:18 +0100 Subject: [PATCH] perf: avoid splitting raw generated source into line strings --- src/TUnit.Core.SourceGenerator/CodeWriter.cs | 63 ++++++++++++------- .../CodeWriterTests.cs | 52 +++++++++++++++ 2 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 tests/TUnit.Core.SourceGenerator.Tests/CodeWriterTests.cs diff --git a/src/TUnit.Core.SourceGenerator/CodeWriter.cs b/src/TUnit.Core.SourceGenerator/CodeWriter.cs index f8d77b5bcf2..1bbcb50b7c3 100644 --- a/src/TUnit.Core.SourceGenerator/CodeWriter.cs +++ b/src/TUnit.Core.SourceGenerator/CodeWriter.cs @@ -215,35 +215,56 @@ public ICodeWriter AppendRaw(string multilineText) return this; } - var lines = multilineText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None); - - // Skip leading empty lines - var startIndex = 0; - while (startIndex < lines.Length && string.IsNullOrWhiteSpace(lines[startIndex])) + var position = 0; + var hasContent = false; + var pendingBlankLines = 0; + while (position < multilineText.Length) { - startIndex++; - } + var lineStart = position; + while (position < multilineText.Length && multilineText[position] is not '\r' and not '\n') + { + position++; + } - // Skip trailing empty lines - var endIndex = lines.Length - 1; - while (endIndex >= startIndex && string.IsNullOrWhiteSpace(lines[endIndex])) - { - endIndex--; - } + var lineEnd = position; + if (position < multilineText.Length) + { + var newline = multilineText[position++]; + if (newline == '\r' && position < multilineText.Length && multilineText[position] == '\n') + { + position++; + } + } - // Process remaining lines, preserving blank lines within the content - for (var i = startIndex; i <= endIndex; i++) - { - var line = lines[i].TrimEnd(); - if (line.Length == 0) + while (lineEnd > lineStart && char.IsWhiteSpace(multilineText[lineEnd - 1])) + { + lineEnd--; + } + + if (lineEnd == lineStart) + { + if (hasContent) + { + pendingBlankLines++; + } + continue; + } + + // Delay blank lines so leading/trailing ones are omitted, while interior ones survive. + while (pendingBlankLines > 0) { - // Preserve blank lines by forcing a newline even when already at line start _builder.AppendLine(); + pendingBlankLines--; } - else + + if (_isNewLine) { - AppendLine(line); + _builder.Append(GetIndentation(_indentLevel)); } + _builder.Append(multilineText, lineStart, lineEnd - lineStart); + _builder.AppendLine(); + _isNewLine = true; + hasContent = true; } return this; diff --git a/tests/TUnit.Core.SourceGenerator.Tests/CodeWriterTests.cs b/tests/TUnit.Core.SourceGenerator.Tests/CodeWriterTests.cs new file mode 100644 index 00000000000..79e24f5c628 --- /dev/null +++ b/tests/TUnit.Core.SourceGenerator.Tests/CodeWriterTests.cs @@ -0,0 +1,52 @@ +namespace TUnit.Core.SourceGenerator.Tests; + +public class CodeWriterTests +{ + [Test] + [Arguments("", "")] + [Arguments(" \t\r\n\u00a0\r\u2003\n", "")] + [Arguments("first", " first\n")] + [Arguments("first\nsecond", " first\n second\n")] + [Arguments("first\rsecond", " first\n second\n")] + [Arguments("first\r\nsecond", " first\n second\n")] + [Arguments("\r\n \t\n first \t\r\n\r\n\t\rsecond\n \t\n", " first\n\n\n second\n")] + [Arguments("first\u00a0\u2003\r\n\u00a0\nsecond\u0085", " first\n\n second\n")] + [Arguments("first\r\r\nsecond\n\n", " first\n\n second\n")] + [Arguments("first\u2028middle\nsecond", " first\u2028middle\n second\n")] + public async Task AppendRaw_PreservesFormatting(string input, string expected) + { + var writer = new CodeWriter(includeHeader: false); + writer.Indent(); + writer.AppendRaw(input); + writer.Append("tail"); + + await Assert.That(writer.ToString()).IsEqualTo(expected.Replace("\n", Environment.NewLine) + " tail"); + } + + [Test] + public async Task AppendRaw_PreservesPartialLineAndSubsequentWrites() + { + var writer = new CodeWriter(indentString: "--", includeHeader: false); + writer.Indent(); + writer.Append("prefix:"); + writer.AppendRaw("\n \r\nfirst \t\r\n\t\n second\r\n\n"); + writer.AppendRaw("third\n"); + writer.Append("tail"); + + await Assert.That(writer.ToString()).IsEqualTo( + "--prefix:first\n\n-- second\n--third\n--tail".Replace("\n", Environment.NewLine)); + } + + [Test] + public async Task AppendRaw_EmptyInputDoesNotEndPartialLine() + { + var writer = new CodeWriter(includeHeader: false); + writer.Append("prefix:"); + writer.AppendRaw(null!); + writer.AppendRaw(""); + writer.AppendRaw(" \t\r\n\u00a0\n"); + writer.Append("tail"); + + await Assert.That(writer.ToString()).IsEqualTo("prefix:tail"); + } +}