From 06658352a38df53c27cdd7cf73f321ca79efec30 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 10:30:17 +0000 Subject: [PATCH] Brace a member body that holds only a block comment [patch] HasCode treated only // comments and directives as "not code", so a BodyFactory that wrote only a /* ... */ comment took the single-line branch and was placed on the declaration line: "void M() /* nothing to do */" (CS1002). The scanner now reports the characters inside every top-level comment, line or block, honouring string and character literals, and HasCode looks for code outside them, so a block-comment-only body is braced like a //-only one has been since #161. Fixes ktsu-dev/CodeBlocker#185 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW1zLzNveHEn69h7Vn3 --- CodeBlocker.Test/TemplateTests.cs | 59 +++++++++++++++++++ CodeBlocker/Templates/TemplateRendering.cs | 37 ++++++++++-- .../Templates/VerbatimStringScanner.cs | 48 ++++++++++++++- 3 files changed, 136 insertions(+), 8 deletions(-) diff --git a/CodeBlocker.Test/TemplateTests.cs b/CodeBlocker.Test/TemplateTests.cs index 3fd8237..532d287 100644 --- a/CodeBlocker.Test/TemplateTests.cs +++ b/CodeBlocker.Test/TemplateTests.cs @@ -886,6 +886,65 @@ public void AConstructorBodyThatWritesOnlyALineCommentIsBraced() => BodyFactory = codeBlocker => codeBlocker.WriteLine("// no state"), })); + [TestMethod] + public void ABodyThatWritesOnlyABlockCommentIsBraced() => + // "void M() /* nothing to do */" has no body at all (CS1002). + Assert.AreEqual( + "void M()\n{\n\t/* nothing to do */\n}\n", + Render(new MethodTemplate + { + Type = "void", + Name = "M", + BodyFactory = codeBlocker => codeBlocker.WriteLine("/* nothing to do */"), + })); + + [TestMethod] + public void ABodyThatWritesOnlyAMultiLineBlockCommentIsBraced() => + Assert.AreEqual( + "void M()\n{\n\t/* nothing\n\t to do */\n}\n", + Render(new MethodTemplate + { + Type = "void", + Name = "M", + BodyFactory = codeBlocker => + { + codeBlocker.WriteLine("/* nothing"); + codeBlocker.WriteLine(" to do */"); + }, + })); + + [TestMethod] + public void ABodyThatWritesOnlyBlockAndLineCommentsIsBraced() => + Assert.AreEqual( + "C()\n{\n\t/* a */ // b\n}\n", + Render(new ConstructorTemplate + { + Name = "C", + BodyFactory = codeBlocker => codeBlocker.WriteLine("/* a */ // b"), + })); + + [TestMethod] + public void ABodyWithCodeAfterABlockCommentStaysOnTheDeclarationLine() => + Assert.AreEqual( + "int M() /* note */ => 1;\n", + Render(new MethodTemplate + { + Type = "int", + Name = "M", + BodyFactory = codeBlocker => codeBlocker.WriteLine("/* note */ => 1;"), + })); + + [TestMethod] + public void ABlockCommentOpenerInsideAStringIsCode() => + Assert.AreEqual( + "string M() => \"/* not a comment */\";\n", + Render(new MethodTemplate + { + Type = "string", + Name = "M", + BodyFactory = codeBlocker => codeBlocker.WriteLine("=> \"/* not a comment */\";"), + })); + [TestMethod] public void ABodyThatWritesOnlyAnEmptyPragmaScopeIsBraced() => Assert.AreEqual( diff --git a/CodeBlocker/Templates/TemplateRendering.cs b/CodeBlocker/Templates/TemplateRendering.cs index f993ece..68bf01c 100644 --- a/CodeBlocker/Templates/TemplateRendering.cs +++ b/CodeBlocker/Templates/TemplateRendering.cs @@ -463,17 +463,44 @@ private static bool IsCode(string code) private static bool IsDirective(string line) => line.TrimStart().StartsWith('#'); /// - /// Reports whether any line of a fragment holds code: something other than a line comment or a - /// preprocessor directive. + /// Reports whether any line of a fragment holds code: something other than a comment, line or + /// block, or a preprocessor directive. /// /// The lines of a fragment, as returns them. /// if at least one line has code on it. + /// + /// The lines are scanned together, so a block comment that spans several lines is not code on + /// any of them, and a /* or // inside a string literal is code, not a comment. + /// private static bool HasCode(string[] lines) { - int[] codeEnds = FindCodeEnds(lines); - for (int i = 0; i < lines.Length; i++) + bool[] inComment = VerbatimStringScanner.FindCommentText(string.Join("\n", lines)); + int lineStart = 0; + foreach (string line in lines) + { + if (!IsDirective(line) && HasCodeOutsideComments(line, inComment, lineStart)) + { + return true; + } + + lineStart += line.Length + 1; + } + + return false; + } + + /// + /// Reports whether a line has anything other than whitespace outside its comments. + /// + /// The line to check. + /// The comment flags for the whole fragment. + /// Where starts in the fragment. + /// if the line has code on it. + private static bool HasCodeOutsideComments(string line, bool[] inComment, int lineStart) + { + for (int i = 0; i < line.Length; i++) { - if (IsCode(lines[i][..codeEnds[i]])) + if (!inComment[lineStart + i] && !char.IsWhiteSpace(line[i])) { return true; } diff --git a/CodeBlocker/Templates/VerbatimStringScanner.cs b/CodeBlocker/Templates/VerbatimStringScanner.cs index 56caf0b..7158b09 100644 --- a/CodeBlocker/Templates/VerbatimStringScanner.cs +++ b/CodeBlocker/Templates/VerbatimStringScanner.cs @@ -33,6 +33,24 @@ internal static bool[] FindVerbatimText(string text) return inVerbatimText; } + /// + /// Marks each character of that lies inside a comment belonging to the + /// code itself: a // line comment or a /* */ block comment. + /// + /// The C# fragment to scan. + /// + /// One flag per character of , true where that character is part of a + /// comment, delimiters included. A // or /* inside a string or character literal + /// starts no comment. An unterminated block comment runs to the end of the text. + /// + internal static bool[] FindCommentText(string text) + { + bool[] inCommentText = new bool[text.Length]; + int position = 0; + ScanCode(text, ref position, new bool[text.Length], lineCommentStarts: null, inCommentText: inCommentText); + return inCommentText; + } + /// /// Finds the line comments in that belong to the code itself. /// @@ -61,7 +79,11 @@ internal static List FindLineComments(string text) /// they are not wanted, and for an interpolation hole. /// /// Whether the walk is inside an interpolation hole. - private static void ScanCode(string text, ref int position, bool[] inVerbatimText, List? lineCommentStarts, bool isInterpolationHole = false) + /// + /// Receives the comment flags, at the top level only; when they are not + /// wanted, and for an interpolation hole. + /// + private static void ScanCode(string text, ref int position, bool[] inVerbatimText, List? lineCommentStarts, bool isInterpolationHole = false, bool[]? inCommentText = null) { int braceDepth = 0; int bracketDepth = 0; @@ -72,7 +94,7 @@ private static void ScanCode(string text, ref int position, bool[] inVerbatimTex lineCommentStarts.Add(position); } - if (TrySkipCommentOrLiteral(text, ref position, inVerbatimText)) + if (TrySkipCommentOrLiteral(text, ref position, inVerbatimText, inCommentText)) { continue; } @@ -139,12 +161,18 @@ private static void SkipFormatSpecifier(string text, ref int position) /// /// Moves past the comment or literal that starts at , if one does. /// - private static bool TrySkipCommentOrLiteral(string text, ref int position, bool[] inVerbatimText) + /// The C# fragment to scan. + /// Where to start, and on return where the skip stopped. + /// Receives the verbatim literal flags. + /// Receives the comment flags, or when they are not wanted. + private static bool TrySkipCommentOrLiteral(string text, ref int position, bool[] inVerbatimText, bool[]? inCommentText) { char c = text[position]; + int start = position; if (c == '/' && At(text, position + 1) == '/') { SkipLineComment(text, ref position); + MarkComment(inCommentText, start, position); return true; } @@ -152,6 +180,7 @@ private static bool TrySkipCommentOrLiteral(string text, ref int position, bool[ { int end = text.IndexOf("*/", position + 2, StringComparison.Ordinal); position = end < 0 ? text.Length : end + 2; + MarkComment(inCommentText, start, position); return true; } @@ -187,6 +216,19 @@ private static bool IsClosingBrace(char c, ref int braceDepth) return false; } + private static void MarkComment(bool[]? inCommentText, int start, int end) + { + if (inCommentText is null) + { + return; + } + + for (int i = start; i < end; i++) + { + inCommentText[i] = true; + } + } + private static char At(string text, int position) => position < text.Length ? text[position] : '\0'; private static bool IsLineBreak(char c) => c is '\n' or '\r' or '\u0085' or '\u2028' or '\u2029';