Repository navigation
Description
What's wrong
PropertyTemplate.WriteTo takes the expression-body branch and returns without looking at DefaultValue, and Validate doesn't reject the combination:
CodeBlocker/CodeBlocker/Templates/PropertyTemplate.cs
Lines 58 to 106 in aa7ca53
| Ensure.NotNull(codeBlocker); | |
| Validate(); | |
| if (ExpressionBodyFactory is not null) | |
| { | |
| // Rendered before the declaration is written: an empty expression is only found by | |
| // running the callback, and "Type Name =>" would otherwise be left behind. | |
| string[] expression = TemplateRendering.RenderExpression(codeBlocker, ExpressionBodyFactory, $"Property '{Name}'"); | |
| base.WriteTo(codeBlocker); | |
| TemplateRendering.WriteExpressionBody(codeBlocker, expression); | |
| return; | |
| } | |
| // Likewise rendered up front, for an expression accessor's body. | |
| string[]? getterExpression = Getter?.RenderExpression(codeBlocker, "get"); | |
| string[]? setterExpression = Setter?.RenderExpression(codeBlocker, SetterKeyword); | |
| base.WriteTo(codeBlocker); | |
| if (CanUseShorthand) | |
| { | |
| WriteShorthand(codeBlocker); | |
| return; | |
| } | |
| WriteAccessorList(codeBlocker, getterExpression, setterExpression); | |
| } | |
| /// <inheritdoc/> | |
| internal override void Validate() | |
| { | |
| if (ExpressionBodyFactory is null && Getter is null && Setter is null) | |
| { | |
| // Writing "Type Name;" here would emit a field, not a property — which is what an | |
| // accessorless property used to render as. An abstract or interface property is | |
| // spelled with accessors that have no body: Getter = AccessorTemplate.Auto(). | |
| // Checked before the declaration is written, which would otherwise be left unfinished. | |
| throw new InvalidOperationException( | |
| $"Property '{Name}' declares no accessors and no expression body. Set Getter, " + | |
| "Setter, or ExpressionBodyFactory."); | |
| } | |
| if (ExpressionBodyFactory is null) | |
| { | |
| // Likewise checked up front: the accessor list is only reached after "Type Name {". | |
| Getter?.Validate(); | |
| Setter?.Validate(); | |
| } |
An expression-bodied property can't have an initialiser in C#. The template accepts both, writes only one of them, and says nothing about the other.
Reproduction
A PropertyTemplate with Name = "P", Type = "int", DefaultValue = "5" and ExpressionBodyFactory = cb => cb.Write("x") produces:
int P => x;
No exception is thrown, and the = 5 the caller asked for is gone. Elsewhere the library refuses invalid shapes in Validate (for example, no body at all, the empty expression body in #178, and the class pre-check in #169), so here the caller silently gets different code from what they specified.
Suggested fix / acceptance criteria
- In
PropertyTemplate.Validate, throwInvalidOperationExceptionwhenExpressionBodyFactoryis set andHasDefaultValueis true, naming the property. - Add a test: that template throws, and inside a class,
AddClassthrows before anything is written.
Activity
matt-edmondson commentedon Nov 5, 2026
Triage
Category: Bug
Priority: Medium. The caller silently gets different code from what they specified, and no error is raised. That breaks the library's "refuse invalid shapes in
Validate" contract. It only happens when a caller sets both properties, and the fix is a single check.Area / suggested assignment:
CodeBlocker/Templates/PropertyTemplate.cs, inValidate. This is the template validation series (#169, #178, #183).Duplicates / in progress
int P => ;// TODO(CS1525): the #175 guard checks only for blank lines #184) adds expression-body checks to validation, likely in or nearPropertyTemplate.Validate. Add this check on top of Check an expression body's expression before writing its class, and refuse a comment-only one [patch] #186 to avoid a conflict.Next steps: Throw
InvalidOperationExceptionnaming the property whenExpressionBodyFactory != null && HasDefaultValue. Add the standalone test and the inside-AddClasstest, which asserts nothing is written.Generated by Claude Code