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
57 changes: 57 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,63 @@ public void AnExpressionBodiedAccessorEndingInALineCommentGetsItsSemicolonBefore
Assert.AreEqual("public int X\n{\n\tget => x; // c\n}\n", Render(property));
}

private static void WriteDebugConditional(CodeBlocker codeBlocker)
{
codeBlocker.WriteLine("#if DEBUG");
codeBlocker.WriteLine("1");
codeBlocker.WriteLine("#else");
codeBlocker.WriteLine("2");
codeBlocker.WriteLine("#endif");
}

[TestMethod]
public void AnExpressionBodyOpeningWithADirectiveStartsBelowTheArrowAndEndsAfterTheLastDirective() =>
// Used to render "int P => #if DEBUG" (CS1040) and "#endif;" (CS1025).
Assert.AreEqual(
"int P =>\n#if DEBUG\n1\n#else\n2\n#endif\n;\n",
Render(new PropertyTemplate { Type = "int", Name = "P", ExpressionBodyFactory = WriteDebugConditional }));

[TestMethod]
public void AnExpressionBodiedAccessorWithADirectiveIsTerminatedAfterIt() =>
Assert.AreEqual(
"int P\n{\n\tget =>\n\t#if DEBUG\n\t1\n\t#else\n\t2\n\t#endif\n\t;\n}\n",
Render(new PropertyTemplate { Type = "int", Name = "P", Getter = AccessorTemplate.Expression(WriteDebugConditional) }));

[TestMethod]
public void AnExpressionBodyEndingInADirectiveAfterCodeOnTheArrowLineIsTerminatedAfterIt() =>
Assert.AreEqual(
"int P => 1\n#if DEBUG\n+ 2\n#endif\n;\n",
Render(new PropertyTemplate
{
Type = "int",
Name = "P",
ExpressionBodyFactory = codeBlocker =>
{
codeBlocker.WriteLine("1");
codeBlocker.WriteLine("#if DEBUG");
codeBlocker.WriteLine("+ 2");
codeBlocker.WriteLine("#endif");
},
}));

[TestMethod]
public void AnExpressionBodyWithCodeAfterItsLastDirectiveTerminatesThatCode() =>
Assert.AreEqual(
"int P => 1\n#if DEBUG\n+ 2\n#endif\n+ 3;\n",
Render(new PropertyTemplate
{
Type = "int",
Name = "P",
ExpressionBodyFactory = codeBlocker =>
{
codeBlocker.WriteLine("1");
codeBlocker.WriteLine("#if DEBUG");
codeBlocker.WriteLine("+ 2");
codeBlocker.WriteLine("#endif");
codeBlocker.WriteLine("+ 3");
},
}));

[TestMethod]
public void ABlockBodiedAccessorIsBracedAndIndented()
{
Expand Down
71 changes: 65 additions & 6 deletions CodeBlocker/Templates/TemplateRendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,20 +341,25 @@ internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action<CodeBlo
first++;
}

codeBlocker.Write(" => ");
if (last < 0)
{
codeBlocker.WriteLine(";");
codeBlocker.WriteLine(" => ;");
return;
}

int[] codeEnds = FindCodeEnds(lines);
int terminated = last;
while (terminated > first && string.IsNullOrWhiteSpace(lines[terminated][..codeEnds[terminated]]))
// A directive must be the first thing on its line (CS1040), so one that opens the
// expression goes on the line after the arrow rather than on the declaration line.
if (IsDirective(lines[first]))
{
terminated--;
codeBlocker.WriteLine(" =>");
}
else
{
codeBlocker.Write(" => ");
}

int[] codeEnds = FindCodeEnds(lines);
int terminated = FindTerminatedLine(lines, codeEnds, first, last);
for (int i = first; i <= last; i++)
{
if (i == terminated)
Expand All @@ -370,8 +375,62 @@ internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action<CodeBlo
codeBlocker.WriteLine(lines[i]);
}
}

if (terminated < 0)
{
codeBlocker.WriteLine(";");
}
}

/// <summary>
/// Finds the line of an expression body that takes the terminating semicolon: the last line
/// with code on it, skipping lines that hold only a comment or a directive.
/// </summary>
/// <param name="lines">The lines of the expression, as <see cref="SplitLines"/> returns them.</param>
/// <param name="codeEnds">Where the code on each line ends, as <see cref="FindCodeEnds"/> found.</param>
/// <param name="first">The first line with content.</param>
/// <param name="last">The last line with content.</param>
/// <returns>
/// The line's index, or -1 when a directive follows it. That line may sit in one branch of an
/// <c>#if</c>, and terminating it would leave every other branch unterminated, so the semicolon
/// then goes on a line of its own after the last directive, where every branch reaches it.
/// </returns>
private static int FindTerminatedLine(string[] lines, int[] codeEnds, int first, int last)
{
int terminated = last;
while (terminated > first && !IsCode(lines[terminated][..codeEnds[terminated]]))
{
terminated--;
}

for (int i = terminated + 1; i <= last; i++)
{
if (IsDirective(lines[i]))
{
return -1;
}
}

return terminated;
}

/// <summary>
/// Reports whether the code part of a line holds code: anything but whitespace or a
/// preprocessor directive.
/// </summary>
/// <param name="code">The line up to where its code ends.</param>
/// <returns><see langword="true"/> if the line has code on it.</returns>
private static bool IsCode(string code)
{
string trimmed = code.Trim();
return trimmed.Length != 0 && trimmed[0] != '#';
}

/// <summary>Reports whether a line is a preprocessor directive.</summary>
/// <param name="line">The line to check.</param>
/// <returns><see langword="true"/> if the line's first non-whitespace character is <c>#</c>.</returns>
private static bool IsDirective(string line) => line.TrimStart().StartsWith('#');

/// <summary>
/// Finds where the code on each line ends: at the start of the line comment the line ends in,
/// or at the end of the line when it has none.
Expand Down
Loading