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';