Skip to content

Commit a056143

Browse files
C#: Replace IRewriteRule/RewriteRule with CSharpTemplate.Rewrite() (#7134)
Consolidate the declarative pattern→template rewriting API into CSharpTemplate as static Rewrite() factory methods that directly return a CSharpVisitor<ExecutionContext>.
1 parent 519950d commit a056143

5 files changed

Lines changed: 261 additions & 309 deletions

File tree

rewrite-csharp/csharp/OpenRewrite/CSharp/Template/CSharpTemplate.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
using OpenRewrite.Core;
1717
using OpenRewrite.Java;
18+
using ExecutionContext = OpenRewrite.Core.ExecutionContext;
1819

1920
namespace OpenRewrite.CSharp.Template;
2021

@@ -245,4 +246,147 @@ public J GetTree()
245246

246247
return null;
247248
}
249+
250+
// ===============================================================
251+
// Rewrite — declarative pattern→template visitor factory
252+
// ===============================================================
253+
254+
/// <summary>
255+
/// Create a <see cref="CSharpVisitor{ExecutionContext}"/> that matches a single pattern
256+
/// and applies a template via <see cref="TreeVisitor{T,P}.PostVisit"/>. The pattern's
257+
/// fast-reject ensures only nodes whose type matches the pattern root are fully compared.
258+
/// </summary>
259+
/// <example>
260+
/// <code>
261+
/// var expr = Capture.Expression();
262+
/// return CSharpTemplate.Rewrite(
263+
/// CSharpPattern.Expression($"Console.Write({expr})"),
264+
/// CSharpTemplate.Expression($"Console.WriteLine({expr})"));
265+
/// </code>
266+
/// </example>
267+
public static CSharpVisitor<ExecutionContext> Rewrite(CSharpPattern before, CSharpTemplate after) =>
268+
new RewriteVisitor([(before, after)]);
269+
270+
/// <summary>
271+
/// Create a visitor that tries multiple patterns against a shared template. First match wins.
272+
/// </summary>
273+
/// <example>
274+
/// <code>
275+
/// var s = Capture.Expression("s", type: "string");
276+
/// return CSharpTemplate.Rewrite(
277+
/// [
278+
/// CSharpPattern.Expression($"{s} == null || {s} == \"\""),
279+
/// CSharpPattern.Expression($"{s} == null || {s}.Length == 0"),
280+
/// ],
281+
/// CSharpTemplate.Expression($"string.IsNullOrEmpty({s})"));
282+
/// </code>
283+
/// </example>
284+
public static CSharpVisitor<ExecutionContext> Rewrite(CSharpPattern[] befores, CSharpTemplate after) =>
285+
new RewriteVisitor(Array.ConvertAll(befores, b => (b, after)));
286+
287+
/// <summary>
288+
/// Create a visitor that tries multiple (pattern, template) pairs in order. First match wins.
289+
/// Use this when different patterns map to different templates.
290+
/// </summary>
291+
public static CSharpVisitor<ExecutionContext> Rewrite(
292+
params (CSharpPattern before, CSharpTemplate after)[] rules) =>
293+
new RewriteVisitor(rules);
294+
295+
/// <summary>
296+
/// Creates a visitor that splices statements from any <see cref="Block"/> marked with
297+
/// <see cref="SyntheticBlockContainer"/> into its parent block. Register once via
298+
/// <see cref="TreeVisitor{T,P}.DoAfterVisit"/> — a single instance handles all
299+
/// synthetic blocks produced during the visit.
300+
/// </summary>
301+
/// <example>
302+
/// <code>
303+
/// var match = pat.Match(ret, Cursor);
304+
/// if (match != null)
305+
/// {
306+
/// var result = tmpl.Apply(Cursor, values: match);
307+
/// if (result is Block block &amp;&amp; block.Markers.FindFirst&lt;SyntheticBlockContainer&gt;() != null)
308+
/// {
309+
/// MaybeDoAfterVisit(CSharpTemplate.CreateBlockFlattener&lt;ExecutionContext&gt;());
310+
/// return block;
311+
/// }
312+
/// return result ?? ret;
313+
/// }
314+
/// </code>
315+
/// </example>
316+
public static CSharpVisitor<P> CreateBlockFlattener<P>() => new BlockFlattener<P>();
317+
318+
// ===============================================================
319+
// Implementation
320+
// ===============================================================
321+
322+
private sealed class RewriteVisitor((CSharpPattern before, CSharpTemplate after)[] rules)
323+
: CSharpVisitor<ExecutionContext>
324+
{
325+
public override J? PostVisit(J tree, ExecutionContext ctx)
326+
{
327+
foreach (var (before, after) in rules)
328+
{
329+
var match = before.Match(tree, Cursor);
330+
if (match != null)
331+
return after.Apply(Cursor, values: match);
332+
}
333+
return tree;
334+
}
335+
}
336+
337+
private sealed class BlockFlattener<P> : CSharpVisitor<P>, IEquatable<BlockFlattener<P>>
338+
{
339+
public bool Equals(BlockFlattener<P>? other) => other is not null;
340+
public override bool Equals(object? obj) => obj is BlockFlattener<P>;
341+
public override int GetHashCode() => typeof(BlockFlattener<P>).GetHashCode();
342+
public override J VisitBlock(Block block, P ctx)
343+
{
344+
block = (Block)base.VisitBlock(block, ctx);
345+
346+
var statements = block.Statements;
347+
var newStatements = new List<JRightPadded<Statement>>(statements.Count);
348+
var changed = false;
349+
350+
foreach (var stmt in statements)
351+
{
352+
if (stmt.Element is Block inner &&
353+
inner.Markers.FindFirst<SyntheticBlockContainer>() != null)
354+
{
355+
// Splice inner block's statements into the parent.
356+
// An empty inner block is intentionally dropped — flattening a block
357+
// that produced no statements should remove the slot.
358+
var innerStmts = inner.Statements;
359+
for (var i = 0; i < innerStmts.Count; i++)
360+
{
361+
var s = innerStmts[i];
362+
if (i == 0)
363+
{
364+
// Transfer the original statement's prefix (comments, blank lines)
365+
// to the first spliced statement.
366+
s = s.WithElement(SetStatementPrefix(s.Element, stmt.Element.Prefix));
367+
}
368+
newStatements.Add(s);
369+
}
370+
changed = true;
371+
}
372+
else
373+
{
374+
newStatements.Add(stmt);
375+
}
376+
}
377+
378+
return changed ? block.WithStatements(newStatements) : block;
379+
}
380+
381+
/// <summary>
382+
/// Sets the prefix on a statement, handling <see cref="ExpressionStatement"/> which
383+
/// delegates its prefix to its inner expression and has no <c>WithPrefix</c> method.
384+
/// </summary>
385+
private static Statement SetStatementPrefix(Statement stmt, Space prefix)
386+
{
387+
if (stmt is ExpressionStatement es)
388+
return es.WithExpression(J.SetPrefix(es.Expression, prefix));
389+
return (Statement)J.SetPrefix(stmt, prefix);
390+
}
391+
}
248392
}

rewrite-csharp/csharp/OpenRewrite/CSharp/Template/RewriteRule.cs

Lines changed: 0 additions & 194 deletions
This file was deleted.

rewrite-csharp/csharp/OpenRewrite/CSharp/Template/TemplateEngine.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ namespace OpenRewrite.CSharp.Template;
2424
/// Marker placed on a <see cref="Block"/> that is a synthetic container for multiple statements
2525
/// produced by a multi-statement template, rather than a real block in the source code.
2626
/// Used by <see cref="TemplateEngine.AutoFormat"/> to format each statement at the parent level,
27-
/// and by <see cref="RewriteRule.CreateBlockFlattener{P}"/> to identify blocks to splice.
27+
/// and by <see cref="CSharpTemplate.CreateBlockFlattener{P}"/> to identify blocks to splice.
2828
/// </summary>
2929
public sealed class SyntheticBlockContainer : Marker
3030
{

0 commit comments

Comments
 (0)