diff --git a/CodeBlocker.Test/TemplateTests.cs b/CodeBlocker.Test/TemplateTests.cs index cbe4b4a..859f6d8 100644 --- a/CodeBlocker.Test/TemplateTests.cs +++ b/CodeBlocker.Test/TemplateTests.cs @@ -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() { diff --git a/CodeBlocker/Templates/TemplateRendering.cs b/CodeBlocker/Templates/TemplateRendering.cs index a2625bd..6fc9cc4 100644 --- a/CodeBlocker/Templates/TemplateRendering.cs +++ b/CodeBlocker/Templates/TemplateRendering.cs @@ -341,20 +341,25 @@ internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action "); 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) @@ -370,8 +375,62 @@ internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action + /// 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. + /// + /// The lines of the expression, as returns them. + /// Where the code on each line ends, as found. + /// The first line with content. + /// The last line with content. + /// + /// The line's index, or -1 when a directive follows it. That line may sit in one branch of an + /// #if, 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. + /// + 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; + } + + /// + /// Reports whether the code part of a line holds code: anything but whitespace or a + /// preprocessor directive. + /// + /// The line up to where its code ends. + /// if the line has code on it. + private static bool IsCode(string code) + { + string trimmed = code.Trim(); + return trimmed.Length != 0 && trimmed[0] != '#'; } + /// Reports whether a line is a preprocessor directive. + /// The line to check. + /// if the line's first non-whitespace character is #. + private static bool IsDirective(string line) => line.TrimStart().StartsWith('#'); + /// /// 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.