Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
37 changes: 32 additions & 5 deletions CodeBlocker/Templates/TemplateRendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,17 +463,44 @@ private static bool IsCode(string code)
private static bool IsDirective(string line) => line.TrimStart().StartsWith('#');

/// <summary>
/// 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.
/// </summary>
/// <param name="lines">The lines of a fragment, as <see cref="SplitLines"/> returns them.</param>
/// <returns><see langword="true"/> if at least one line has code on it.</returns>
/// <remarks>
/// The lines are scanned together, so a block comment that spans several lines is not code on
/// any of them, and a <c>/*</c> or <c>//</c> inside a string literal is code, not a comment.
/// </remarks>
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;
}

/// <summary>
/// Reports whether a line has anything other than whitespace outside its comments.
/// </summary>
/// <param name="line">The line to check.</param>
/// <param name="inComment">The comment flags for the whole fragment.</param>
/// <param name="lineStart">Where <paramref name="line"/> starts in the fragment.</param>
/// <returns><see langword="true"/> if the line has code on it.</returns>
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;
}
Expand Down
48 changes: 45 additions & 3 deletions CodeBlocker/Templates/VerbatimStringScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ internal static bool[] FindVerbatimText(string text)
return inVerbatimText;
}

/// <summary>
/// Marks each character of <paramref name="text"/> that lies inside a comment belonging to the
/// code itself: a <c>//</c> line comment or a <c>/* */</c> block comment.
/// </summary>
/// <param name="text">The C# fragment to scan.</param>
/// <returns>
/// One flag per character of <paramref name="text"/>, true where that character is part of a
/// comment, delimiters included. A <c>//</c> or <c>/*</c> inside a string or character literal
/// starts no comment. An unterminated block comment runs to the end of the text.
/// </returns>
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;
}

/// <summary>
/// Finds the line comments in <paramref name="text"/> that belong to the code itself.
/// </summary>
Expand Down Expand Up @@ -61,7 +79,11 @@ internal static List<int> FindLineComments(string text)
/// they are not wanted, and for an interpolation hole.
/// </param>
/// <param name="isInterpolationHole">Whether the walk is inside an interpolation hole.</param>
private static void ScanCode(string text, ref int position, bool[] inVerbatimText, List<int>? lineCommentStarts, bool isInterpolationHole = false)
/// <param name="inCommentText">
/// Receives the comment flags, at the top level only; <see langword="null"/> when they are not
/// wanted, and for an interpolation hole.
/// </param>
private static void ScanCode(string text, ref int position, bool[] inVerbatimText, List<int>? lineCommentStarts, bool isInterpolationHole = false, bool[]? inCommentText = null)
{
int braceDepth = 0;
int bracketDepth = 0;
Expand All @@ -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;
}
Expand Down Expand Up @@ -139,19 +161,26 @@ private static void SkipFormatSpecifier(string text, ref int position)
/// <summary>
/// Moves past the comment or literal that starts at <paramref name="position"/>, if one does.
/// </summary>
private static bool TrySkipCommentOrLiteral(string text, ref int position, bool[] inVerbatimText)
/// <param name="text">The C# fragment to scan.</param>
/// <param name="position">Where to start, and on return where the skip stopped.</param>
/// <param name="inVerbatimText">Receives the verbatim literal flags.</param>
/// <param name="inCommentText">Receives the comment flags, or <see langword="null"/> when they are not wanted.</param>
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;
}

if (c == '/' && At(text, position + 1) == '*')
{
int end = text.IndexOf("*/", position + 2, StringComparison.Ordinal);
position = end < 0 ? text.Length : end + 2;
MarkComment(inCommentText, start, position);
return true;
}

Expand Down Expand Up @@ -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';
Expand Down
Loading