Skip to content

A property with both ExpressionBodyFactory and DefaultValue silently drops the default: writes int P => x; with no error #190

Open

Description

@matt-edmondson

What's wrong

PropertyTemplate.WriteTo takes the expression-body branch and returns without looking at DefaultValue, and Validate doesn't reject the combination:

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, throw InvalidOperationException when ExpressionBodyFactory is set and HasDefaultValue is true, naming the property.
  • Add a test: that template throws, and inside a class, AddClass throws before anything is written.

Activity

  1. matt-edmondson commented on Nov 5, 2026

    @matt-edmondson
    ContributorAuthor

    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, in Validate. This is the template validation series (#169, #178, #183).

    Duplicates / in progress

    Next steps: Throw InvalidOperationException naming the property when ExpressionBodyFactory != null && HasDefaultValue. Add the standalone test and the inside-AddClass test, which asserts nothing is written.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions