Skip to content

Commit 41f78a2

Browse files
authored
C#: Add Preconditions.Not, And, and recipe-based Check support (#7348)
1 parent bfe436e commit 41f78a2

4 files changed

Lines changed: 225 additions & 1 deletion

File tree

rewrite-csharp/csharp/OpenRewrite/Core/Check.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,69 @@ internal Check(ITreeVisitor<ExecutionContext> precondition, ITreeVisitor<Executi
4545
return tree;
4646
}
4747
}
48+
49+
/// <summary>
50+
/// A Check that holds a reference to the Recipe used as the precondition,
51+
/// matching the Java RecipeCheck pattern.
52+
/// </summary>
53+
public class RecipeCheck : Check
54+
{
55+
public Recipe Recipe { get; }
56+
57+
internal RecipeCheck(Recipe check, ITreeVisitor<ExecutionContext> visitor)
58+
: base(check.GetVisitor(), visitor)
59+
{
60+
Recipe = check;
61+
}
62+
}
63+
64+
/// <summary>
65+
/// Negates a precondition: matches when the inner visitor does NOT modify the tree.
66+
/// </summary>
67+
internal class NotVisitor : TreeVisitor<Tree, ExecutionContext>
68+
{
69+
private readonly ITreeVisitor<ExecutionContext> _visitor;
70+
71+
public NotVisitor(ITreeVisitor<ExecutionContext> visitor)
72+
{
73+
_visitor = visitor;
74+
}
75+
76+
public override Tree? Visit(Tree? tree, ExecutionContext ctx)
77+
{
78+
var t2 = _visitor.Visit(tree, ctx);
79+
if (tree == t2 && tree != null)
80+
{
81+
return SearchResult.Found(tree);
82+
}
83+
return tree;
84+
}
85+
}
86+
87+
/// <summary>
88+
/// Combines multiple precondition visitors with AND semantics.
89+
/// All must match (modify the tree) for the result to be modified.
90+
/// </summary>
91+
internal class AndVisitor : TreeVisitor<Tree, ExecutionContext>
92+
{
93+
private readonly ITreeVisitor<ExecutionContext>[] _visitors;
94+
95+
public AndVisitor(ITreeVisitor<ExecutionContext>[] visitors)
96+
{
97+
_visitors = visitors;
98+
}
99+
100+
public override Tree? Visit(Tree? tree, ExecutionContext ctx)
101+
{
102+
Tree? t2 = tree;
103+
foreach (var v in _visitors)
104+
{
105+
t2 = v.Visit(tree, ctx);
106+
if (tree == t2)
107+
{
108+
return tree;
109+
}
110+
}
111+
return t2;
112+
}
113+
}

rewrite-csharp/csharp/OpenRewrite/Core/Preconditions.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,39 @@ public static ITreeVisitor<ExecutionContext> Check(
3232
{
3333
return new Check(precondition, visitor);
3434
}
35+
36+
/// <summary>
37+
/// Wraps a visitor with a recipe-based precondition check. The recipe's visitor
38+
/// is used as the precondition gate.
39+
/// </summary>
40+
public static ITreeVisitor<ExecutionContext> Check(
41+
Recipe check,
42+
ITreeVisitor<ExecutionContext> visitor)
43+
{
44+
if (check is IScanningRecipe)
45+
{
46+
throw new ArgumentException("ScanningRecipe is not supported as a check");
47+
}
48+
return new RecipeCheck(check, visitor);
49+
}
50+
51+
/// <summary>
52+
/// Negates a precondition visitor. Returns a search result when the inner visitor
53+
/// does NOT match (i.e., does not modify the tree).
54+
/// </summary>
55+
public static ITreeVisitor<ExecutionContext> Not(
56+
ITreeVisitor<ExecutionContext> visitor)
57+
{
58+
return new NotVisitor(visitor);
59+
}
60+
61+
/// <summary>
62+
/// Combines multiple precondition visitors with AND semantics. All visitors must
63+
/// match (modify the tree) for the combined precondition to match.
64+
/// </summary>
65+
public static ITreeVisitor<ExecutionContext> And(
66+
params ITreeVisitor<ExecutionContext>[] visitors)
67+
{
68+
return new AndVisitor(visitors);
69+
}
3570
}

rewrite-csharp/csharp/OpenRewrite/Core/SearchResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public sealed class SearchResult(Guid id, string? description) : Marker, IRpcCod
3030
/// Adds a SearchResult marker to the given tree node.
3131
/// Uses reflection to call WithMarkers on the concrete type.
3232
/// </summary>
33-
public static T Found<T>(T tree, string? description = null) where T : J
33+
public static T Found<T>(T tree, string? description = null) where T : Tree
3434
{
3535
var newMarkers = tree.Markers.Add(new SearchResult(Guid.NewGuid(), description));
3636
var withMarkers = tree.GetType().GetMethod("WithMarkers", [typeof(Markers)]);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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.CSharp;
17+
using OpenRewrite.Java;
18+
using OpenRewrite.Test;
19+
using ExecutionContext = OpenRewrite.Core.ExecutionContext;
20+
using Recipe = OpenRewrite.Core.Recipe;
21+
22+
namespace OpenRewrite.Tests.Core;
23+
24+
public class PreconditionsTest : RewriteTest
25+
{
26+
/// <summary>
27+
/// When the precondition Recipe matches, the inner visitor should run.
28+
/// </summary>
29+
[Fact]
30+
public void RecipePreconditionMatches()
31+
{
32+
RewriteRun(
33+
spec => spec.SetRecipe(new PreconditionedRenameRecipe
34+
{
35+
PreconditionClassName = "Foo",
36+
From = "Foo",
37+
To = "Bar"
38+
}),
39+
CSharp(
40+
"class Foo { }",
41+
"class Bar { }"
42+
)
43+
);
44+
}
45+
46+
/// <summary>
47+
/// When the precondition Recipe does NOT match, the inner visitor should not run.
48+
/// </summary>
49+
[Fact]
50+
public void RecipePreconditionDoesNotMatch()
51+
{
52+
RewriteRun(
53+
spec => spec.SetRecipe(new PreconditionedRenameRecipe
54+
{
55+
PreconditionClassName = "Missing",
56+
From = "Foo",
57+
To = "Bar"
58+
}),
59+
CSharp("class Foo { }")
60+
);
61+
}
62+
}
63+
64+
/// <summary>
65+
/// A recipe that renames a class, but only if a precondition recipe (FindClass) matches.
66+
/// Uses Preconditions.Check(Recipe, visitor) overload.
67+
/// </summary>
68+
file class PreconditionedRenameRecipe : OpenRewrite.Core.Recipe
69+
{
70+
public required string PreconditionClassName { get; init; }
71+
public required string From { get; init; }
72+
public required string To { get; init; }
73+
74+
public override string DisplayName => "Preconditioned rename class";
75+
public override string Description => "Renames a class only if the precondition recipe matches.";
76+
77+
public override OpenRewrite.Core.ITreeVisitor<ExecutionContext> GetVisitor()
78+
{
79+
return OpenRewrite.Core.Preconditions.Check(
80+
new FindClassRecipe { ClassName = PreconditionClassName },
81+
new RenameClassVisitor(From, To));
82+
}
83+
}
84+
85+
/// <summary>
86+
/// A simple search recipe that marks files containing a specific class name.
87+
/// Used as a precondition.
88+
/// </summary>
89+
file class FindClassRecipe : OpenRewrite.Core.Recipe
90+
{
91+
public required string ClassName { get; init; }
92+
93+
public override string DisplayName => "Find class";
94+
public override string Description => "Finds files containing a specific class.";
95+
96+
public override OpenRewrite.Core.ITreeVisitor<ExecutionContext> GetVisitor() => new FindClassVisitor(ClassName);
97+
}
98+
99+
file class FindClassVisitor(string className) : CSharpVisitor<ExecutionContext>
100+
{
101+
public override J VisitClassDeclaration(ClassDeclaration cd, ExecutionContext ctx)
102+
{
103+
cd = (ClassDeclaration)base.VisitClassDeclaration(cd, ctx);
104+
if (cd.Name.SimpleName == className)
105+
{
106+
return OpenRewrite.Core.SearchResult.Found(cd);
107+
}
108+
return cd;
109+
}
110+
}
111+
112+
file class RenameClassVisitor(string from, string to) : CSharpVisitor<ExecutionContext>
113+
{
114+
public override J VisitClassDeclaration(ClassDeclaration cd, ExecutionContext ctx)
115+
{
116+
cd = (ClassDeclaration)base.VisitClassDeclaration(cd, ctx);
117+
if (cd.Name.SimpleName == from)
118+
{
119+
return cd.WithName(cd.Name.WithSimpleName(to));
120+
}
121+
return cd;
122+
}
123+
}

0 commit comments

Comments
 (0)