From 0ebef6dfc12008e5f55719257cea45254cefad5b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:27:30 +0000 Subject: [PATCH] Ignore a trailing line terminator in comment text [patch] LineTerminators.Split kept the empty tail that a trailing terminator leaves, so a copyright notice ending in a newline got a bare // line before the auto-generated marker, and a one-line doc summary ending in one was laid out over several lines with an empty ///. Drop that one empty tail, as TemplateRendering.SplitLines already does. A blank line inside the text is still written. Fixes ktsu-dev/CodeBlocker#170 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5LkA4KtEKYNbjk5tz --- .../CodeBlockerExtensionsTests.cs | 26 +++++++++++++++++++ CodeBlocker.Test/DocCommentTests.cs | 24 +++++++++++++++++ CodeBlocker/LineTerminators.cs | 18 ++++++++++--- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/CodeBlocker.Test/CodeBlockerExtensionsTests.cs b/CodeBlocker.Test/CodeBlockerExtensionsTests.cs index 0a14c3d..b6bfae1 100644 --- a/CodeBlocker.Test/CodeBlockerExtensionsTests.cs +++ b/CodeBlocker.Test/CodeBlockerExtensionsTests.cs @@ -65,6 +65,32 @@ public void AutoGeneratedHeaderWritesABlankCopyrightLineAsABareComment() codeBlocker.ToString()); } + [TestMethod] + [DataRow("\n", DisplayName = "LF")] + [DataRow("\r\n", DisplayName = "CRLF")] + [DataRow("\r", DisplayName = "CR")] + [DataRow("\u0085", DisplayName = "NEL")] + [DataRow("\u2028", DisplayName = "LINE SEPARATOR")] + [DataRow("\u2029", DisplayName = "PARAGRAPH SEPARATOR")] + public void AutoGeneratedHeaderIgnoresATrailingLineTerminatorOnTheCopyright(string terminator) + { + using CodeBlocker codeBlocker = Create(); + + codeBlocker.WriteAutoGeneratedHeader($"Copyright (c) X{terminator}"); + + Assert.AreEqual("// Copyright (c) X\n// \n\n", codeBlocker.ToString()); + } + + [TestMethod] + public void AutoGeneratedHeaderKeepsABlankLineBeforeATrailingLineTerminator() + { + using CodeBlocker codeBlocker = Create(); + + codeBlocker.WriteAutoGeneratedHeader("A\n\nB\n"); + + Assert.AreEqual("// A\n//\n// B\n// \n\n", codeBlocker.ToString()); + } + [TestMethod] public void NullableDirectivesAreWrittenVerbatim() { diff --git a/CodeBlocker.Test/DocCommentTests.cs b/CodeBlocker.Test/DocCommentTests.cs index f802433..67d85a2 100644 --- a/CodeBlocker.Test/DocCommentTests.cs +++ b/CodeBlocker.Test/DocCommentTests.cs @@ -56,6 +56,30 @@ public void AMultiLineSummaryPrefixesEveryLine() Render(documentation)); } + [TestMethod] + [DataRow("\n", DisplayName = "LF")] + [DataRow("\r\n", DisplayName = "CRLF")] + [DataRow("\u2028", DisplayName = "LINE SEPARATOR")] + public void ASingleLineSummaryWithATrailingLineTerminatorStaysOnOneLine(string terminator) => + Assert.AreEqual( + "/// Hello.\n", + Render(new DocComment { Summary = $"Hello.{terminator}" })); + + [TestMethod] + public void AMultiLineSummaryWithATrailingLineTerminatorHasNoEmptyLine() => + Assert.AreEqual( + "/// \n/// A\n/// B\n/// \n", + Render(new DocComment { Summary = "A\nB\n" })); + + [TestMethod] + public void ANamedTagWithATrailingLineTerminatorStaysOnOneLine() + { + DocComment documentation = new(); + documentation.Params.Add(new DocTag { Name = "a", Text = "The value.\n" }); + + Assert.AreEqual("/// The value.\n", Render(documentation)); + } + [TestMethod] public void ABlankLineInsideATagIsWrittenWithoutTrailingWhitespace() { diff --git a/CodeBlocker/LineTerminators.cs b/CodeBlocker/LineTerminators.cs index d7b8fb3..68a376d 100644 --- a/CodeBlocker/LineTerminators.cs +++ b/CodeBlocker/LineTerminators.cs @@ -23,9 +23,21 @@ internal static class LineTerminators /// the comment and into the code. /// /// The text to split. - /// The lines, without their terminators. Text with no terminator is a single line. - internal static string[] Split(string text) => - text.Replace("\r\n", "\n").Split(Characters); + /// + /// The lines, without their terminators. Text with no terminator is a single line. A trailing + /// terminator leaves an empty tail, which is not a line of its own, so "A\n" is the one + /// line A; a blank line inside the text, as in "A\n\nB", is kept. + /// + internal static string[] Split(string text) + { + string[] lines = text.Replace("\r\n", "\n").Split(Characters); + if (lines.Length > 1 && lines[^1].Length == 0) + { + Array.Resize(ref lines, lines.Length - 1); + } + + return lines; + } /// /// Reports whether text contains any line terminator the C# compiler recognises.