Skip to content

C#: Scaffold strategies for class members and attributes in template engine#7063

Merged
knutwannheden merged 10 commits intomainfrom
csharptemplate-scaffold-strategies-for-class-members-and-attributes
Mar 19, 2026
Merged

C#: Scaffold strategies for class members and attributes in template engine#7063
knutwannheden merged 10 commits intomainfrom
csharptemplate-scaffold-strategies-for-class-members-and-attributes

Conversation

@knutwannheden
Copy link
Contributor

Summary

  • Add ScaffoldKind enum (Expression, Statement, ClassMember, Attribute) to control how template code is wrapped and extracted
  • Add factory methods Expression(), Statement(), ClassMember(), Attribute() on both CSharpTemplate and CSharpPattern
  • Keep Create() for backward compatibility (auto-unwraps ExpressionStatement)

Previously the template engine only supported expression/statement context (wrapping in a method body). This adds purpose-built scaffolds so class members, attributes, and standalone expressions can be templated with correct AST structure and type attribution.

Usage examples

// Expression — scaffolds as: class __T__ { object __v__ = <code>; }
var tmpl = CSharpTemplate.Expression("1 + 2");
var tree = tmpl.GetTree(); // Binary

// Statement — scaffolds in method body, does NOT auto-unwrap ExpressionStatement
var tmpl = CSharpTemplate.Statement("throw new Exception()");
var tree = tmpl.GetTree(); // Throw

// ClassMember — scaffolds as: class __T__ { <code> }
var tmpl = CSharpTemplate.ClassMember("public void Foo() { }");
var tree = tmpl.GetTree(); // MethodDeclaration

var tmpl = CSharpTemplate.ClassMember("public string Name { get; set; }");
var tree = tmpl.GetTree(); // PropertyDeclaration

// Attribute — scaffolds as: class __T__ { [<code>] void __M__() {} }
// Extracts the Annotation node with full type attribution
var tmpl = CSharpTemplate.Attribute("Obsolete(\"Use new API\")",
    usings: ["System"]);
var tree = tmpl.GetTree(); // Annotation

// Pattern matching works the same way
var pat = CSharpPattern.Attribute("Obsolete");
if (pat.Match(someAnnotation, cursor) is { } match) { ... }

// Captures work in all scaffold kinds
var name = Capture.Name("name");
var tmpl = CSharpTemplate.ClassMember($"public void {name}() {{ }}");

Key design decisions

  • ScaffoldKind is internal — consumers use the factory methods
  • Create() unchanged: uses null scaffold kind → legacy auto-unwrap path
  • Statement() does NOT auto-unwrap ExpressionStatement (unlike Create())
  • Expression scaffold uses object __v__ (not var) to handle null/default
  • ClassMember auto-appends ; when code doesn't end with ; or }
  • Cache key includes scaffold kind to prevent collisions

Test plan

  • 24 new tests in ScaffoldStrategyTests.cs covering all scaffold kinds
  • Tests for both CSharpTemplate and CSharpPattern factory methods
  • Cache isolation test (same code, different scaffold kinds → different AST)
  • Statement vs Create unwrap contract documented in tests
  • Expression with typed capture (regression test for preamble field bug)
  • All 164 existing template tests still pass

Captures now carry an internal `CaptureKind` (Expression, Type, Name)
that tells the template engine what syntactic position the placeholder
occupies. This allows `BuildTypePreamble` to dispatch on kind and
generate appropriate scaffold code for each position.

New factory methods `Capture.Type()` and `Capture.Name()` create
captures with the correct kind pre-set while preserving compile-time
type safety via the generic parameter.
- Thread `dependencies` (IReadOnlyDictionary<string, string>) through
  CSharpTemplate.Create, CSharpPattern.Create, and TemplateEngine.Parse
  for future NuGet-based type attribution during scaffold parsing
- Include dependencies in the template cache key
- Add Capture.Expression() factory with explicit CaptureKind.Expression
- Update Of<T> docs to steer toward position-specific factories
…nd Attribute templates

The template engine previously only supported a single scaffold strategy
(wrapping code in a method body). This adds a ScaffoldKind enum and
context-specific factory methods to CSharpTemplate and CSharpPattern so
that class members, attributes, and expressions can be scaffolded and
extracted with correct AST structure and type attribution.
…type, and multi-member support

- Factor out class framing and preamble from BuildScaffold switch cases
- Use `object __v__` instead of `var __e__` for expression scaffold (avoids
  invalid C# when templating null/default)
- ExtractClassMember now handles multiple members by returning filtered block
- ExtractAttribute provides actionable error messages at each stage
- Add cache isolation test verifying different scaffold kinds don't collide
Statement() returns ExpressionStatement as-is; Create() auto-unwraps to
the inner expression. This is the key semantic difference that motivated
the explicit Statement scaffold kind.
- Fix critical bug: ExtractExpression now searches for __v__ by name
  instead of using FindFirst, which would find preamble fields first
  when typed captures are present
- Fix stale doc comments referencing var __e__ (now object __v__)
- ClassMember scaffold auto-appends semicolon for field declarations
- Add ExpressionWithTypedCapture test covering the preamble bug
Allows recipe authors to use CSharpTemplate.Apply() with substitution
values from imperative extraction, not just from CSharpPattern.Match().
This unlocks Attribute and ClassMember templates for recipes that
manually extract values from the AST.
AutoFormat now restores the prefix set by ApplyCoordinates after
formatting. The formatter handles internal whitespace (argument spacing)
while the prefix (indentation/newlines before the node) comes from the
original tree being replaced. This matches the JS/TS template engine
approach where prefix is set before formatting and preserved through it.

Fixes the issue where Attribute templates got unwanted newline+indent
prefix from the formatter seeing them as standalone class member
attributes.
@knutwannheden knutwannheden changed the base branch from main to csharp/capture-kind March 19, 2026 18:49
@knutwannheden knutwannheden force-pushed the csharptemplate-scaffold-strategies-for-class-members-and-attributes branch from e7d15f1 to b30cbf6 Compare March 19, 2026 18:53
@knutwannheden knutwannheden force-pushed the csharptemplate-scaffold-strategies-for-class-members-and-attributes branch from b30cbf6 to 29872cc Compare March 19, 2026 18:56
@knutwannheden knutwannheden changed the base branch from csharp/capture-kind to main March 19, 2026 18:57
@knutwannheden knutwannheden merged commit c92b5f1 into main Mar 19, 2026
1 check passed
@knutwannheden knutwannheden deleted the csharptemplate-scaffold-strategies-for-class-members-and-attributes branch March 19, 2026 18:59
@github-project-automation github-project-automation bot moved this from In Progress to Done in OpenRewrite Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant