Skip to content

A BodyFactory that throws leaves a half-written member, and inside a class the closing brace lands on it: public void M()} #176

Open

Description

@matt-edmondson

What's wrong

Method, constructor, operator and property templates write their doc comment, attributes and signature first. Only then do they call the user's BodyFactory/ExpressionBodyFactory, through TemplateRendering.RenderFragment (WriteBody at TemplateRendering.cs#L281, WriteExpressionBody at #L328). If the callback throws, the header is already in the buffer.

Inside a class, the using Scope in ClassTemplate.cs#L129 still writes } while the exception unwinds, and that brace lands on the unfinished line.

Repro (at 21ac04c)

cb.AddClass(new ClassTemplate { Name = "C", Members = {
  new FieldTemplate { Name = "a", Type = "int" },
  new MethodTemplate { Name = "M", Type = "void", Keywords = { "public" },
    BodyFactory = _ => throw new InvalidOperationException("no body") },
  new FieldTemplate { Name = "b", Type = "int" } } });

The exception propagates, and the buffer then holds:

class C
{
	int a;

	int b;

	public void M()}

A standalone property with a throwing ExpressionBodyFactory and a summary leaves /// <summary>s</summary> followed by int P.

Why it matters

The callback is user code: a generator walking its model can throw on a malformed input. The templates promise elsewhere that when they throw, "nothing is written" and a caller that catches the exception can keep using the CodeBlocker. Here the caller is left with a buffer they can't recover. If they log the error and emit the rest of the file (common in source generators that report a diagnostic and continue), the file doesn't compile.

This is related to #164 and its open PR #169, but it's a different case. Those pre-check things the library itself rejects, before anything is written. Validate() never runs user callbacks, so a throwing callback is still unhandled after #169.

Suggested fix / acceptance criteria

  • The callbacks already render into a separate buffer (RenderFragment). Render the body, expression body and accessor bodies before writing the member header, then write the header and splice in the pre-rendered text.
  • For ClassTemplate, render the members into a fragment before writing the declaration and opening the Scope, so a member failure leaves nothing of the class behind.
  • Tests: in both repros above the exception propagates and cb.ToString() is unchanged from before the call.

Activity

  1. matt-edmondson commented on Nov 3, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    Category: Bug
    Priority: Medium. The output is corrupted, but only on the exception path. A caller only hits it if they catch an exception thrown by their own BodyFactory/ExpressionBodyFactory and then keep writing to the same CodeBlocker. That is a real pattern in source generators, and it breaks the library's "nothing is written when a template throws" promise. Normal successful generation is unaffected.
    Area / suggested assignment: Templates rendering pipeline (TemplateRendering.WriteBody / WriteExpressionBody, ClassTemplate.WriteTo). This is the maintainer of the recent "check before writing" series.

    Duplicates / in-progress check

    Next steps

    1. Merge Check a class's members before writing any of it [patch] #169 and Refuse an expression body that writes no expression, before writing [patch] #178.
    2. Extend the pre-render approach to block bodies and accessor bodies: render the body before writing the header, then splice it in.
    3. Render the class members into a fragment before opening the Scope.
    4. Add the two repros from the issue as tests that assert cb.ToString() is unchanged after the exception.

    Generated by Claude Code

  2. matt-edmondson commented on Nov 4, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    The prerequisites from the earlier triage have landed: #169 and #178 are both merged. This is still not actionable yet, because open PR #182 ("Drop the blank lines around a braced member body") edits TemplateRendering.cs, the same body-writing path this fix has to restructure. The two would conflict.

    • Depends on PR Drop the blank lines around a braced member body [patch] #182. Land it first, then render block, expression and accessor bodies before writing the header, and render class members into a fragment before opening the Scope.
    • Scope: larger than one function. The fix touches the method, constructor, operator, property and class templates, plus TemplateRendering.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    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