Skip to content

A property's ExpressionBodyFactory runs depth + 3 times (6 at depth 3), so a stateful factory writes different code depending on how deeply its class is nested #202

Open

Description

@matt-edmondson

What's wrong

The expression callback is rendered once to validate it and again to write it, and validation itself is repeated at every nesting level:

  • PropertyTemplate.Validate renders the expression to check it (CodeBlocker/Templates/PropertyTemplate.cs:101), and WriteTo renders it again (PropertyTemplate.cs:67).
  • ClassTemplate.WriteTo calls Validate (ClassTemplate.cs:78). That recursively validates every member and nested class (ClassTemplate.cs:122,127). Each nested ClassTemplate.WriteTo then validates its whole subtree again.
  • AddSourceFile validates all classes (SourceFileTemplate.cs:62). It then calls AddClasses, which validates them a second time (SourceFileTemplate.cs:105).

So a property's callback runs depth + 3 times, though the remarks at PropertyTemplate.cs:92 say "That runs the callback once more". AccessorKind.Expression getters and setters behave the same way.

Repro (net10.0, at c739886)

int nextId = 0;
PropertyTemplate IdProperty(string name) => new()
{
    Type = "int", Name = name, Keywords = { "public" },
    ExpressionBodyFactory = cb => cb.Write($"{nextId++}"),
};

var c1 = new ClassTemplate { Name = "Ids" };
c1.Members.Add(IdProperty("First")); c1.Members.Add(IdProperty("Second"));
using (var w = CodeBlocker.Create()) { w.AddClass(c1); Console.WriteLine(w); }

nextId = 0;
var c2 = new ClassTemplate { Name = "Ids" };
c2.Members.Add(IdProperty("First")); c2.Members.Add(IdProperty("Second"));
var outer = new ClassTemplate { Name = "Outer" }; outer.NestedClasses.Add(c2);
var file = new SourceFileTemplate(); file.Classes.Add(outer);
using (var w = CodeBlocker.Create()) { w.AddSourceFile(file); Console.WriteLine(w); }

Observed:

  • Standalone class: public int First => 2; and public int Second => 3;
  • Nested inside Outer and written through AddSourceFile: public int First => 8; and public int Second => 9;

With a counter at depth 3 (Outer > Mid > Inner), each callback ran 6 times.

Expected: First => 0 and Second => 1 in both cases, or at least the same output wherever the class sits.

Why it matters

Factories are user code, and source generators commonly give them side effects: counters for unique names, consuming an enumerator, reporting diagnostics. Those effects repeat a nesting-dependent number of times. Generated names then depend on where a class sits, and diagnostics are duplicated. It is also O(depth) rendering work per property.

Suggested fix

Render each expression callback once and reuse the result. Two ways to do that:

  • cache the rendered lines for the duration of the outermost WriteTo/AddSourceFile
  • validate only at the outermost entry point and pass an "already validated" flag down

This fits the pre-rendering #176 proposes for throwing BodyFactories.

Acceptance: a test counts callback invocations for a property in a class nested two or more levels deep and written through AddSourceFile. It expects 1 invocation, or 2 if the documented validation pass is kept.

Activity

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