Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CodeBlocker.Test/SplitLinesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ private static string Split(string fragment, string newLine = NewLines.Lf)
[DataRow("s = \"\"\"a\"\"b\"\"\" + @\"p\nq\";\nb", "s = \"\"\"a\"\"b\"\"\" + @\"p\nq\";|b", DisplayName = "Raw string with a shorter run of quotes inside")]
[DataRow("c = '\\'' + @\"p\nq\";\nb", "c = '\\'' + @\"p\nq\";|b", DisplayName = "Escaped quote in a char literal")]
[DataRow("@class = 1; // @\"\nb", "@class = 1; // @\"|b", DisplayName = "Verbatim identifier, and a quote in a line comment")]
[DataRow("s = $\"{d:yyyy//MM}\" + @\"p\nq\";\nb", "s = $\"{d:yyyy//MM}\" + @\"p\nq\";|b", DisplayName = "Slash pair in a format specifier")]
[DataRow("s = $\"{d:h' o''clock'}\" + @\"p\nq\";\nb", "s = $\"{d:h' o''clock'}\" + @\"p\nq\";|b", DisplayName = "Apostrophes in a format specifier")]
[DataRow("s = $\"{(a ? b : @\"p\nq\")}\";\nb", "s = $\"{(a ? b : @\"p\nq\")}\";|b", DisplayName = "Conditional in a hole is not a format specifier")]
[DataRow("s = $\"{global::X.Y:@\"}\" + @\"p\nq\";\nb", "s = $\"{global::X.Y:@\"}\" + @\"p\nq\";|b", DisplayName = "Alias qualifier, then a quote in a format specifier")]
[DataRow("c = '\nb", "c = '|b", DisplayName = "Unterminated char literal")]
[DataRow("s = \"p\nb", "s = \"p|b", DisplayName = "Unterminated regular string")]
[DataRow("/* @\"\nb", "/* @\"|b", DisplayName = "Unterminated block comment")]
Expand Down
34 changes: 34 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,18 @@ public void ASlashPairInsideAStringLiteralIsNotTakenForAComment() =>
"public int X => \"http://x\";\n",
RenderExpressionBody(codeBlocker => codeBlocker.Write("\"http://x\"")));

[TestMethod]
public void ASlashPairInAnInterpolationFormatSpecifierIsNotTakenForAComment() =>
Assert.AreEqual(
"public int X => $\"{d:yyyy//MM}\"; // c\n",
RenderExpressionBody(codeBlocker => codeBlocker.Write("$\"{d:yyyy//MM}\" // c")));

[TestMethod]
public void ASlashPairInAFormatSpecifierAfterAnAliasQualifierIsNotTakenForAComment() =>
Assert.AreEqual(
"public int X => $\"{global::System.DateTime.Now:yyyy//MM}\"; // c\n",
RenderExpressionBody(codeBlocker => codeBlocker.Write("$\"{global::System.DateTime.Now:yyyy//MM}\" // c")));

[TestMethod]
public void ASlashPairInsideAVerbatimStringOpenedOnAnEarlierLineIsNotTakenForAComment() =>
Assert.AreEqual(
Expand Down Expand Up @@ -1292,6 +1304,28 @@ public void AMethodBodyKeepsAVerbatimStringsContinuationLinesAsWritten()
RenderInClass(method));
}

[TestMethod]
public void AMethodBodyKeepsAVerbatimStringsContinuationLinesAsWrittenAfterASlashPairInAFormatSpecifier()
{
MethodTemplate method = new()
{
Name = "M",
Type = "string",
BodyFactory = b =>
{
b.WriteLine("{");
b.Indent();
b.WriteLine("return $@\"{d:yyyy//MM}\nline2\";");
b.Outdent();
b.WriteLine("}");
},
};

Assert.AreEqual(
"class C\n{\n\tstring M()\n\t{\n\t\treturn $@\"{d:yyyy//MM}\nline2\";\n\t}\n}\n",
RenderInClass(method));
}

[TestMethod]
public void ABlockAccessorKeepsAnInterpolatedVerbatimStringsContinuationLinesAsWritten()
{
Expand Down
57 changes: 54 additions & 3 deletions CodeBlocker/Templates/VerbatimStringScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ internal static List<int> FindLineComments(string text)
private static void ScanCode(string text, ref int position, bool[] inVerbatimText, List<int>? lineCommentStarts, bool isInterpolationHole = false)
{
int braceDepth = 0;
int bracketDepth = 0;
while (position < text.Length)
{
if (lineCommentStarts is not null && text[position] == '/' && At(text, position + 1) == '/')
Expand All @@ -76,15 +77,65 @@ private static void ScanCode(string text, ref int position, bool[] inVerbatimTex
continue;
}

if (isInterpolationHole && IsClosingBrace(text[position], ref braceDepth))
if (isInterpolationHole)
{
return;
char c = text[position];
if (IsClosingBrace(c, ref braceDepth))
{
return;
}

if (braceDepth == 0 && IsFormatSpecifierColon(text, position, ref bracketDepth))
{
SkipFormatSpecifier(text, ref position);
return;
}
}

position++;
}
}

/// <summary>
/// Tracks parenthesis and bracket nesting inside an interpolation hole, and reports whether the
/// character at <paramref name="position"/> is the colon that starts the hole's format specifier.
/// </summary>
/// <remarks>
/// Only a colon outside every nested parenthesis, bracket and brace starts one: a conditional
/// expression or a named argument is always nested. The <c>::</c> of an alias qualifier is not
/// one either.
/// </remarks>
private static bool IsFormatSpecifierColon(string text, int position, ref int bracketDepth)
{
char c = text[position];
if (c is '(' or '[')
{
bracketDepth++;
}
else if (c is ')' or ']')
{
bracketDepth = Math.Max(bracketDepth - 1, 0);
}

return c == ':'
&& bracketDepth == 0
&& At(text, position + 1) != ':'
&& (position == 0 || text[position - 1] != ':');
}

/// <summary>
/// Moves from a format specifier's colon up to the brace that closes the interpolation hole,
/// which is left for the caller to consume. A format specifier is literal text: a <c>//</c>,
/// quote or apostrophe in it starts nothing.
/// </summary>
private static void SkipFormatSpecifier(string text, ref int position)
{
while (position < text.Length && text[position] != '}' && !IsLineBreak(text[position]))
{
position++;
}
}

/// <summary>
/// Moves past the comment or literal that starts at <paramref name="position"/>, if one does.
/// </summary>
Expand Down Expand Up @@ -344,7 +395,7 @@ private static void SkipInterpolationHole(string text, ref int position, bool[]
{
position++;
ScanCode(text, ref position, inVerbatimText, lineCommentStarts: null, isInterpolationHole: true);
if (position < text.Length)
if (At(text, position) == '}')
{
position++;
}
Expand Down
Loading