From 8b0372eb7c7f6a67dc4ebc1f63f94b8b637f73a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 19:26:07 +0000 Subject: [PATCH] Reject an expression-bodied property that also has a default value [patch] PropertyTemplate wrote the expression body and silently dropped DefaultValue, since C# cannot give an expression-bodied property an initialiser. Validate now refuses the combination before anything is written, so a class holding such a property is refused by AddClass up front too. Fixes ktsu-dev/CodeBlocker#190 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5kkYGVswnWUL9YgPE --- CodeBlocker.Test/TemplateTests.cs | 42 +++++++++++++++++++++++ CodeBlocker/Templates/PropertyTemplate.cs | 13 ++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CodeBlocker.Test/TemplateTests.cs b/CodeBlocker.Test/TemplateTests.cs index 3fd8237..384105a 100644 --- a/CodeBlocker.Test/TemplateTests.cs +++ b/CodeBlocker.Test/TemplateTests.cs @@ -530,6 +530,48 @@ public void AnExpressionBodyOfOnlyBlankLinesIsRejectedBeforeAnythingIsWritten() Assert.AreEqual("int a;\n", codeBlocker.ToString()); } + [TestMethod] + public void AnExpressionBodyWithADefaultValueIsRejectedBeforeAnythingIsWritten() + { + // It used to render "int P => x;" and drop the "= 5" without a word. + PropertyTemplate property = new() + { + Type = "int", + Name = "P", + DefaultValue = "5", + ExpressionBodyFactory = codeBlocker => codeBlocker.Write("x"), + }; + + using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf); + codeBlocker.WriteLine("int a;"); + + InvalidOperationException exception = + Assert.ThrowsExactly(() => property.WriteTo(codeBlocker)); + Assert.Contains("'P'", exception.Message); + Assert.AreEqual("int a;\n", codeBlocker.ToString()); + } + + [TestMethod] + public void AClassWithAnExpressionBodiedPropertyThatHasADefaultValueWritesNothing() + { + ClassTemplate type = new() { Name = "C" }; + type.Members.Add(new FieldTemplate { Name = "a", Type = "int" }); + type.Members.Add(new PropertyTemplate + { + Type = "int", + Name = "P", + DefaultValue = "5", + ExpressionBodyFactory = codeBlocker => codeBlocker.Write("x"), + }); + + using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf); + + InvalidOperationException exception = + Assert.ThrowsExactly(() => codeBlocker.AddClass(type)); + Assert.Contains("'P'", exception.Message); + Assert.AreEqual(string.Empty, codeBlocker.ToString()); + } + private static string RenderExpressionBody(Action expressionFactory) => Render(new PropertyTemplate { Type = "int", Name = "X", Keywords = { "public" }, ExpressionBodyFactory = expressionFactory }); diff --git a/CodeBlocker/Templates/PropertyTemplate.cs b/CodeBlocker/Templates/PropertyTemplate.cs index 471ec0a..cca793d 100644 --- a/CodeBlocker/Templates/PropertyTemplate.cs +++ b/CodeBlocker/Templates/PropertyTemplate.cs @@ -48,7 +48,8 @@ public class PropertyTemplate : MemberTemplate /// is . /// /// , and are all - /// ; or, with no expression body, an accessor's kind is unknown, or is + /// ; or is set together with a + /// ; or, with no expression body, an accessor's kind is unknown, or is /// with no ; or /// the expression body, or an expression accessor's body, writes no expression. Nothing is /// written in any of these cases. @@ -98,6 +99,16 @@ internal override void Validate() "Setter, or ExpressionBodyFactory."); } + if (ExpressionBodyFactory is not null && HasDefaultValue) + { + // C# has no initialiser on an expression-bodied property, so writing either one would + // silently drop the other. + throw new InvalidOperationException( + $"Property '{Name}' has both an expression body and a default value. An " + + "expression-bodied property cannot have an initialiser; clear ExpressionBodyFactory " + + "or DefaultValue."); + } + if (ExpressionBodyFactory is null) { // Likewise checked up front: the accessor list is only reached after "Type Name {".