Skip to content

Commit 5148950

Browse files
authored
C# Template API for pattern matching and code generation (#6916)
* C# Template API for structural pattern matching and code generation Add an idiomatic C# template API using InterpolatedStringHandler that lets recipe authors write concise pattern matching and code transformations: var expr = Capture.Of<Expression>("expr"); var pat = CSharpPattern.Create($"Console.Write({expr})"); var tmpl = CSharpTemplate.Create($"Console.WriteLine({expr})"); Core types: - Capture<T> / Capture.Of<T>() — typed capture placeholders - TemplateStringHandler — InterpolatedStringHandler intercepting {holes} - CSharpPattern — structural matching with Find() for search markers - CSharpTemplate — code generation via Apply() - MatchResult — typed dictionary of captured subtrees - PatternMatchingComparator — parallel tree walk for structural comparison - TemplateEngine — parse/cache template code snippets Covers expressions (method invocation, binary, unary, ternary, assignment, new, cast, array access, lambda, literals), statements (return, throw, if, while, do-while, foreach, switch, try-catch), and C#-specific syntax (default, sizeof, switch expressions, tuples, null-forgiving, is-pattern, as-cast, compound assignment). 85 tests passing across 5 test classes. * Convert TemplateApplyTests to RewriteRun with before/after assertions Replace low-level FindFirst + Assert pattern with RewriteRun tests that validate template replacements via before/after source text. Add tests for multiple occurrences, binary replacement, throw replacement, new class replacement, and Raw.Code splice.
1 parent 01ae722 commit 5148950

16 files changed

Lines changed: 2911 additions & 0 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
using OpenRewrite.Java;
17+
18+
namespace OpenRewrite.CSharp.Template;
19+
20+
/// <summary>
21+
/// Specifies where and how to apply a template in the tree.
22+
/// </summary>
23+
public sealed class CSharpCoordinates
24+
{
25+
public J Tree { get; }
26+
public CoordinateMode Mode { get; }
27+
28+
private CSharpCoordinates(J tree, CoordinateMode mode)
29+
{
30+
Tree = tree;
31+
Mode = mode;
32+
}
33+
34+
/// <summary>
35+
/// Replace the target tree element with the template result.
36+
/// </summary>
37+
public static CSharpCoordinates Replace(J tree) => new(tree, CoordinateMode.Replace);
38+
39+
/// <summary>
40+
/// Insert the template result before the target tree element.
41+
/// </summary>
42+
public static CSharpCoordinates Before(J tree) => new(tree, CoordinateMode.Before);
43+
44+
/// <summary>
45+
/// Insert the template result after the target tree element.
46+
/// </summary>
47+
public static CSharpCoordinates After(J tree) => new(tree, CoordinateMode.After);
48+
}
49+
50+
public enum CoordinateMode
51+
{
52+
Replace,
53+
Before,
54+
After
55+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
using OpenRewrite.Core;
17+
using OpenRewrite.Java;
18+
19+
namespace OpenRewrite.CSharp.Template;
20+
21+
/// <summary>
22+
/// A structural pattern for matching against C# AST nodes.
23+
/// Patterns are created using C# string interpolation with <see cref="Capture{T}"/> placeholders.
24+
/// </summary>
25+
/// <example>
26+
/// <code>
27+
/// var expr = Capture.Of&lt;Expression&gt;("expr");
28+
/// var pat = CSharpPattern.Create($"Console.Write({expr})");
29+
///
30+
/// if (pat.Match(methodInvocation, cursor) is { } match)
31+
/// {
32+
/// var capturedExpr = match.Get(expr);
33+
/// }
34+
/// </code>
35+
/// </example>
36+
public sealed class CSharpPattern
37+
{
38+
private readonly string _code;
39+
private readonly IReadOnlyDictionary<string, object> _captures;
40+
private readonly IReadOnlyList<string> _usings;
41+
private readonly IReadOnlyList<string> _context;
42+
private J? _cachedTree;
43+
44+
private CSharpPattern(string code, Dictionary<string, object> captures,
45+
IReadOnlyList<string>? usings, IReadOnlyList<string>? context)
46+
{
47+
_code = code;
48+
_captures = captures;
49+
_usings = usings ?? [];
50+
_context = context ?? [];
51+
}
52+
53+
/// <summary>
54+
/// Create a pattern from an interpolated string containing <see cref="Capture{T}"/> placeholders.
55+
/// </summary>
56+
public static CSharpPattern Create(TemplateStringHandler handler,
57+
IReadOnlyList<string>? usings = null, IReadOnlyList<string>? context = null)
58+
{
59+
return new CSharpPattern(handler.GetCode(), handler.GetCaptures(), usings, context);
60+
}
61+
62+
/// <summary>
63+
/// Create a pattern from a plain string (no captures — useful for exact matching).
64+
/// </summary>
65+
public static CSharpPattern Create(string code,
66+
IReadOnlyList<string>? usings = null, IReadOnlyList<string>? context = null)
67+
{
68+
return new CSharpPattern(code, new Dictionary<string, object>(), usings, context);
69+
}
70+
71+
/// <summary>
72+
/// Get the parsed pattern tree (cached after first parse).
73+
/// </summary>
74+
public J GetTree()
75+
{
76+
return _cachedTree ??= TemplateEngine.Parse(_code, _captures, _usings, _context);
77+
}
78+
79+
/// <summary>
80+
/// Match this pattern against an AST node.
81+
/// Returns a <see cref="MatchResult"/> if matched, null otherwise.
82+
/// </summary>
83+
public MatchResult? Match(J tree, Cursor cursor)
84+
{
85+
var patternTree = GetTree();
86+
var comparator = new PatternMatchingComparator(_captures);
87+
var captured = comparator.Match(patternTree, tree, cursor);
88+
return captured != null ? new MatchResult(captured) : null;
89+
}
90+
91+
/// <summary>
92+
/// Check if this pattern matches (without capturing).
93+
/// </summary>
94+
public bool Matches(J tree, Cursor cursor) => Match(tree, cursor) != null;
95+
96+
/// <summary>
97+
/// If this pattern matches the given tree node, return the node with a
98+
/// <see cref="SearchResult"/> marker added. Otherwise return the node unchanged.
99+
/// This is the search-only equivalent of template apply — it marks found
100+
/// syntax so that <c>/*~~&gt;*/</c> appears in printed output.
101+
/// </summary>
102+
public T Find<T>(T tree, Cursor cursor, string? description = null) where T : J
103+
{
104+
return Match(tree, cursor) != null ? SearchResult.Found(tree, description) : tree;
105+
}
106+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
using OpenRewrite.Core;
17+
using OpenRewrite.Java;
18+
19+
namespace OpenRewrite.CSharp.Template;
20+
21+
/// <summary>
22+
/// A parsed and cacheable template for generating C# AST nodes.
23+
/// Templates are created using C# string interpolation with <see cref="Capture{T}"/> placeholders,
24+
/// providing the C# equivalent of JavaScript's tagged template literals.
25+
/// </summary>
26+
/// <example>
27+
/// <code>
28+
/// // Simple template (no captures)
29+
/// var tmpl = CSharpTemplate.Create($"Console.WriteLine(\"hello\")");
30+
/// var result = tmpl.Apply(cursor);
31+
///
32+
/// // Template with captures from pattern match
33+
/// var expr = Capture.Of&lt;Expression&gt;("expr");
34+
/// var tmpl = CSharpTemplate.Create($"Console.WriteLine({expr})");
35+
/// var result = tmpl.Apply(cursor, values: match);
36+
///
37+
/// // Template with usings
38+
/// var tmpl = CSharpTemplate.Create(
39+
/// $"JsonSerializer.Serialize({expr})",
40+
/// usings: ["System.Text.Json"]);
41+
/// </code>
42+
/// </example>
43+
public sealed class CSharpTemplate
44+
{
45+
private readonly string _code;
46+
private readonly IReadOnlyDictionary<string, object> _captures;
47+
private readonly IReadOnlyList<string> _usings;
48+
private readonly IReadOnlyList<string> _context;
49+
private J? _cachedTree;
50+
51+
private CSharpTemplate(string code, Dictionary<string, object> captures,
52+
IReadOnlyList<string>? usings, IReadOnlyList<string>? context)
53+
{
54+
_code = code;
55+
_captures = captures;
56+
_usings = usings ?? [];
57+
_context = context ?? [];
58+
}
59+
60+
/// <summary>
61+
/// Create a template from an interpolated string containing <see cref="Capture{T}"/>
62+
/// and/or <see cref="Raw"/> placeholders.
63+
/// </summary>
64+
public static CSharpTemplate Create(TemplateStringHandler handler,
65+
IReadOnlyList<string>? usings = null, IReadOnlyList<string>? context = null)
66+
{
67+
return new CSharpTemplate(handler.GetCode(), handler.GetCaptures(), usings, context);
68+
}
69+
70+
/// <summary>
71+
/// Create a template from a plain string (no captures).
72+
/// </summary>
73+
public static CSharpTemplate Create(string code,
74+
IReadOnlyList<string>? usings = null, IReadOnlyList<string>? context = null)
75+
{
76+
return new CSharpTemplate(code, new Dictionary<string, object>(), usings, context);
77+
}
78+
79+
/// <summary>
80+
/// Get the parsed template tree (cached after first parse).
81+
/// </summary>
82+
public J GetTree()
83+
{
84+
return _cachedTree ??= TemplateEngine.Parse(_code, _captures, _usings, _context);
85+
}
86+
87+
/// <summary>
88+
/// Apply this template, substituting captured values from a <see cref="MatchResult"/>
89+
/// and returning the generated AST node.
90+
/// </summary>
91+
/// <param name="cursor">The cursor pointing to the current location in the tree.</param>
92+
/// <param name="values">Optional match result providing values for captures.</param>
93+
/// <param name="coordinates">Optional coordinates specifying where to apply (defaults to Replace).</param>
94+
/// <returns>The generated AST node with substitutions applied.</returns>
95+
public J? Apply(Cursor cursor, MatchResult? values = null,
96+
CSharpCoordinates? coordinates = null)
97+
{
98+
var tree = GetTree();
99+
100+
// Phase 1: placeholder substitution
101+
if (values != null)
102+
{
103+
tree = TemplateEngine.ApplySubstitutions(tree, values);
104+
}
105+
106+
// Phase 2: coordinate application (prefix preservation)
107+
if (coordinates != null)
108+
{
109+
tree = TemplateEngine.ApplyCoordinates(tree, cursor, coordinates);
110+
}
111+
else if (cursor.Value is J cursorValue)
112+
{
113+
tree = TemplateEngine.ApplyCoordinates(tree, cursor,
114+
CSharpCoordinates.Replace(cursorValue));
115+
}
116+
117+
return tree;
118+
}
119+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
using OpenRewrite.Core;
17+
using OpenRewrite.Java;
18+
19+
namespace OpenRewrite.CSharp.Template;
20+
21+
/// <summary>
22+
/// A named placeholder for pattern matching and template substitution.
23+
/// When used in an interpolated string passed to <see cref="CSharpTemplate.Create"/>
24+
/// or <see cref="CSharpPattern.Create"/>, the <see cref="TemplateStringHandler"/>
25+
/// intercepts it and registers the capture automatically.
26+
/// </summary>
27+
/// <typeparam name="T">The type of AST node this capture matches.</typeparam>
28+
public sealed class Capture<T> where T : J
29+
{
30+
public string Name { get; }
31+
public bool IsVariadic { get; }
32+
public int? MinCount { get; }
33+
public int? MaxCount { get; }
34+
public Func<T, Cursor, bool>? Constraint { get; }
35+
36+
internal Capture(string name, bool variadic = false,
37+
int? minCount = null, int? maxCount = null,
38+
Func<T, Cursor, bool>? constraint = null)
39+
{
40+
Name = name;
41+
IsVariadic = variadic;
42+
MinCount = minCount;
43+
MaxCount = maxCount;
44+
Constraint = constraint;
45+
}
46+
47+
/// <summary>
48+
/// Returns the placeholder identifier for this capture.
49+
/// Used by <see cref="TemplateStringHandler"/> and as a defense-in-depth
50+
/// fallback when used in a plain string interpolation.
51+
/// </summary>
52+
public override string ToString() => Placeholder.ToPlaceholder(Name);
53+
}
54+
55+
/// <summary>
56+
/// Factory methods for creating captures.
57+
/// </summary>
58+
public static class Capture
59+
{
60+
private static int _counter;
61+
62+
/// <summary>
63+
/// Create a capture that matches a single AST node of type <typeparamref name="T"/>.
64+
/// </summary>
65+
public static Capture<T> Of<T>(string? name = null) where T : J
66+
=> new(name ?? $"_capture_{Interlocked.Increment(ref _counter)}");
67+
68+
/// <summary>
69+
/// Create a variadic capture that matches zero or more elements.
70+
/// Useful for matching argument lists, statement sequences, etc.
71+
/// </summary>
72+
public static Capture<T> Variadic<T>(string? name = null,
73+
int? min = null, int? max = null) where T : J
74+
=> new(name ?? $"_capture_{Interlocked.Increment(ref _counter)}",
75+
variadic: true, minCount: min, maxCount: max);
76+
77+
/// <summary>
78+
/// Create a capture with a constraint predicate that must be satisfied for matching.
79+
/// </summary>
80+
public static Capture<T> WithConstraint<T>(string name,
81+
Func<T, Cursor, bool> constraint) where T : J
82+
=> new(name, constraint: constraint);
83+
}

0 commit comments

Comments
 (0)