From aef77b7302d4421350b4f37ab7979089cbc5c978 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 04:31:46 +0000 Subject: [PATCH 1/2] Keep a preprocessor directive in an expression body off the arrow line and out of the semicolon [patch] WriteExpressionBody wrote the first content line straight after " => ", even a directive (CS1040), and appended the semicolon to the last line with code, which could be #endif (CS1025). A body that opens with a directive now starts on the line after the arrow, and when a directive follows the last line of code the semicolon goes on its own line after it, so every #if branch reaches it. Covers PropertyTemplate ExpressionBodyFactory and AccessorKind.Expression. Fixes ktsu-dev/CodeBlocker#158 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3C2EQ5hMiLw63t4Aa --- CodeBlocker.Test/TemplateTests.cs | 57 ++++++++++++++++++++++ CodeBlocker/Templates/TemplateRendering.cs | 40 +++++++++++++-- 2 files changed, 93 insertions(+), 4 deletions(-) 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..089aa8d 100644 --- a/CodeBlocker/Templates/TemplateRendering.cs +++ b/CodeBlocker/Templates/TemplateRendering.cs @@ -341,26 +341,50 @@ internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action "); if (last < 0) { - codeBlocker.WriteLine(";"); + codeBlocker.WriteLine(" => ;"); return; } + // 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])) + { + codeBlocker.WriteLine(" =>"); + } + else + { + codeBlocker.Write(" => "); + } + int[] codeEnds = FindCodeEnds(lines); int terminated = last; - while (terminated > first && string.IsNullOrWhiteSpace(lines[terminated][..codeEnds[terminated]])) + while (terminated > first && !IsCode(terminated)) { terminated--; } + // When a directive follows the last line of code, that line may sit in one branch of an + // #if, and terminating it would leave every other branch unterminated. The semicolon then + // goes on a line of its own after the last directive, where every branch reaches it. + bool directiveFollows = false; + for (int i = terminated + 1; i <= last; i++) + { + directiveFollows |= IsDirective(lines[i]); + } + for (int i = first; i <= last; i++) { - if (i == terminated) + if (i == terminated && !directiveFollows) { codeBlocker.WriteLine(Terminate(lines[i], codeEnds[i])); } + else if (i == last && directiveFollows) + { + codeBlocker.WriteLine(lines[i]); + codeBlocker.WriteLine(";"); + } else if (lines[i].Length == 0) { codeBlocker.NewLine(); @@ -370,6 +394,14 @@ internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action line.TrimStart().StartsWith('#'); } /// From 6835c4c1bf3c586c3d32a9e200a30a7d0dec846f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 04:43:45 +0000 Subject: [PATCH 2/2] Split the semicolon placement out of WriteExpressionBody SonarCloud S3776 flagged the method's cognitive complexity (19 > 15) after the directive handling. The choice of which line takes the semicolon moves to FindTerminatedLine, and the code/directive checks become private helpers; behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3C2EQ5hMiLw63t4Aa --- CodeBlocker/Templates/TemplateRendering.cs | 77 +++++++++++++++------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/CodeBlocker/Templates/TemplateRendering.cs b/CodeBlocker/Templates/TemplateRendering.cs index 089aa8d..6fc9cc4 100644 --- a/CodeBlocker/Templates/TemplateRendering.cs +++ b/CodeBlocker/Templates/TemplateRendering.cs @@ -359,32 +359,13 @@ internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action first && !IsCode(terminated)) - { - terminated--; - } - - // When a directive follows the last line of code, that line may sit in one branch of an - // #if, and terminating it would leave every other branch unterminated. The semicolon then - // goes on a line of its own after the last directive, where every branch reaches it. - bool directiveFollows = false; - for (int i = terminated + 1; i <= last; i++) - { - directiveFollows |= IsDirective(lines[i]); - } - + int terminated = FindTerminatedLine(lines, codeEnds, first, last); for (int i = first; i <= last; i++) { - if (i == terminated && !directiveFollows) + if (i == terminated) { codeBlocker.WriteLine(Terminate(lines[i], codeEnds[i])); } - else if (i == last && directiveFollows) - { - codeBlocker.WriteLine(lines[i]); - codeBlocker.WriteLine(";"); - } else if (lines[i].Length == 0) { codeBlocker.NewLine(); @@ -395,15 +376,61 @@ 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++) { - string code = lines[i][..codeEnds[i]].Trim(); - return code.Length != 0 && code[0] != '#'; + if (IsDirective(lines[i])) + { + return -1; + } } - static bool IsDirective(string line) => line.TrimStart().StartsWith('#'); + 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.