Skip to content

Commit d7f131a

Browse files
committed
Recipe trait
1 parent 027fde5 commit d7f131a

File tree

4 files changed

+356
-0
lines changed

4 files changed

+356
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
root = true
2+
3+
[*]
4+
insert_final_newline = true
5+
trim_trailing_whitespace = true
6+
7+
indent_size = 2
8+
ij_continuation_indent_size = 2
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright 2025 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+
package org.openrewrite.rewrite;
17+
18+
import lombok.Getter;
19+
import org.jspecify.annotations.Nullable;
20+
import org.openrewrite.Cursor;
21+
import org.openrewrite.Tree;
22+
import org.openrewrite.java.JavaIsoVisitor;
23+
import org.openrewrite.java.MethodMatcher;
24+
import org.openrewrite.java.tree.J;
25+
import org.openrewrite.java.tree.TypeUtils;
26+
import org.openrewrite.trait.SimpleTraitMatcher;
27+
import org.openrewrite.trait.Trait;
28+
import org.openrewrite.yaml.JsonPathMatcher;
29+
import org.openrewrite.yaml.YamlVisitor;
30+
import org.openrewrite.yaml.tree.Yaml;
31+
32+
import java.util.concurrent.atomic.AtomicReference;
33+
34+
@Getter
35+
public class Recipe implements Trait<Tree> {
36+
private static final MethodMatcher getDisplayName = new MethodMatcher("org.openrewrite.Recipe getDisplayName()", true);
37+
private static final MethodMatcher getDescription = new MethodMatcher("org.openrewrite.Recipe getDescription()", true);
38+
39+
private Cursor cursor;
40+
41+
public Recipe(Cursor cursor) {
42+
this.cursor = cursor;
43+
}
44+
45+
public String getDisplayName() {
46+
if (getTree() instanceof J.ClassDeclaration) {
47+
return getLiteralReturnValue(getDisplayName);
48+
}
49+
return getYamlMappingValue("displayName");
50+
}
51+
52+
public Recipe withDisplayName(String displayName) {
53+
if (getTree() instanceof J.ClassDeclaration) {
54+
return withLiteralReturnValue(getDisplayName, displayName);
55+
}
56+
return withYamlMappingValue("displayName", displayName);
57+
}
58+
59+
public String getDescription() {
60+
if (getTree() instanceof J.ClassDeclaration) {
61+
return getLiteralReturnValue(getDescription);
62+
}
63+
return getYamlMappingValue("description");
64+
}
65+
66+
public Recipe withDescription(String description) {
67+
if (getTree() instanceof J.ClassDeclaration) {
68+
return withLiteralReturnValue(getDescription, description);
69+
}
70+
return withYamlMappingValue("description", description);
71+
}
72+
73+
private String getYamlMappingValue(String key) {
74+
AtomicReference<String> value = new AtomicReference<>();
75+
new YamlVisitor<Integer>() {
76+
@Override
77+
public Yaml visitMappingEntry(Yaml.Mapping.Entry entry, Integer p) {
78+
if (new JsonPathMatcher("$." + key).matches(getCursor()) &&
79+
entry.getValue() instanceof Yaml.Scalar) {
80+
value.set(((Yaml.Scalar) entry.getValue()).getValue());
81+
}
82+
return entry;
83+
}
84+
}.visit(getTree(), 0, cursor.getParentOrThrow());
85+
return value.get();
86+
}
87+
88+
private String getLiteralReturnValue(MethodMatcher method) {
89+
StringBuilder retValue = new StringBuilder();
90+
J.ClassDeclaration cd = (J.ClassDeclaration) getTree();
91+
new JavaIsoVisitor<StringBuilder>() {
92+
@Override
93+
public J.Return visitReturn(J.Return aReturn, StringBuilder ret) {
94+
J.MethodDeclaration md = getCursor().firstEnclosing(J.MethodDeclaration.class);
95+
if (md != null && method.matches(md, cd)) {
96+
if (aReturn.getExpression() instanceof J.Literal) {
97+
ret.append(((J.Literal) aReturn.getExpression()).getValue());
98+
}
99+
}
100+
return aReturn;
101+
}
102+
}.visit(cd, retValue, getCursor().getParentOrThrow());
103+
return retValue.toString();
104+
}
105+
106+
private Recipe withLiteralReturnValue(MethodMatcher method, String value) {
107+
J.ClassDeclaration cd = (J.ClassDeclaration) getTree();
108+
cursor = new Cursor(cursor.getParent(), new JavaIsoVisitor<Integer>() {
109+
@Override
110+
public J.Return visitReturn(J.Return aReturn, Integer p) {
111+
J.MethodDeclaration md = getCursor().firstEnclosing(J.MethodDeclaration.class);
112+
if (md != null && method.matches(md, cd)) {
113+
if (aReturn.getExpression() instanceof J.Literal) {
114+
J.Literal exp = (J.Literal) aReturn.getExpression();
115+
if (!value.equals(exp.getValue())) {
116+
return aReturn.withExpression(exp
117+
.withValue(value)
118+
.withValueSource("\"" + value + "\""));
119+
}
120+
}
121+
}
122+
return aReturn;
123+
}
124+
}.visitNonNull(cd, 0, getCursor().getParentOrThrow()));
125+
return this;
126+
}
127+
128+
private Recipe withYamlMappingValue(String key, String value) {
129+
cursor = new Cursor(cursor.getParent(), new YamlVisitor<Integer>() {
130+
@Override
131+
public Yaml visitMappingEntry(Yaml.Mapping.Entry entry, Integer p) {
132+
if (new JsonPathMatcher("$." + key).matches(getCursor()) &&
133+
entry.getValue() instanceof Yaml.Scalar) {
134+
return entry.withValue(((Yaml.Scalar) entry.getValue()).withValue(value));
135+
}
136+
return entry;
137+
}
138+
}.visitNonNull(getTree(), 0, cursor.getParentOrThrow()));
139+
return this;
140+
}
141+
142+
public static class Matcher extends SimpleTraitMatcher<Recipe> {
143+
@Override
144+
protected @Nullable Recipe test(Cursor cursor) {
145+
Object value = cursor.getValue();
146+
if (value instanceof J.ClassDeclaration) {
147+
J.ClassDeclaration classDecl = (J.ClassDeclaration) value;
148+
if (TypeUtils.isAssignableTo("org.openrewrite.Recipe", classDecl.getType())) {
149+
return new Recipe(cursor);
150+
}
151+
} else if (value instanceof Yaml.Document) {
152+
AtomicReference<Recipe> recipe = new AtomicReference<>();
153+
new YamlVisitor<Integer>() {
154+
@Override
155+
public Yaml visitScalar(Yaml.Scalar scalar, Integer p) {
156+
if (new JsonPathMatcher("$.type").matches(getCursor().getParentOrThrow()) &&
157+
"specs.openrewrite.org/v1beta/recipe".equals(scalar.getValue())) {
158+
recipe.set(new Recipe(cursor));
159+
}
160+
return scalar;
161+
}
162+
}.visit((Yaml.Document) value, 0, cursor.getParentOrThrow());
163+
return recipe.get();
164+
}
165+
return null;
166+
}
167+
}
168+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2025 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+
@NullMarked
17+
package org.openrewrite.rewrite;
18+
19+
import org.jspecify.annotations.NullMarked;
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2025 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+
package org.openrewrite.rewrite;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.openrewrite.marker.SearchResult;
20+
import org.openrewrite.test.RecipeSpec;
21+
import org.openrewrite.test.RewriteTest;
22+
23+
import static org.openrewrite.java.Assertions.java;
24+
import static org.openrewrite.yaml.Assertions.yaml;
25+
26+
public class RecipeTest implements RewriteTest {
27+
28+
@Override
29+
public void defaults(RecipeSpec spec) {
30+
spec.recipe(RewriteTest.toRecipe(() -> new Recipe.Matcher().asVisitor(recipe ->
31+
SearchResult.found(recipe.getTree(), recipe.getDisplayName() + "=" + recipe.getDescription()))));
32+
}
33+
34+
@Test
35+
void classDefinedRecipe() {
36+
rewriteRun(
37+
//language=java
38+
java(
39+
"""
40+
import org.jetbrains.annotations.NotNull;
41+
import org.openrewrite.Recipe;
42+
43+
class MyRecipe extends Recipe {
44+
public @NotNull String getDisplayName() {
45+
return "My recipe";
46+
}
47+
48+
public @NotNull String getDescription() {
49+
return "My recipe description";
50+
}
51+
}
52+
""",
53+
"""
54+
import org.jetbrains.annotations.NotNull;
55+
import org.openrewrite.Recipe;
56+
57+
/*~~(My recipe=My recipe description)~~>*/class MyRecipe extends Recipe {
58+
public @NotNull String getDisplayName() {
59+
return "My recipe";
60+
}
61+
62+
public @NotNull String getDescription() {
63+
return "My recipe description";
64+
}
65+
}
66+
"""
67+
)
68+
);
69+
}
70+
71+
@Test
72+
void yamlDefinedRecipe() {
73+
rewriteRun(
74+
yaml(
75+
//language=yaml
76+
"""
77+
type: specs.openrewrite.org/v1beta/recipe
78+
name: org.openrewrite.MyRecipe
79+
displayName: My recipe
80+
description: My recipe description
81+
""",
82+
//language=yaml
83+
"""
84+
~~(My recipe=My recipe description)~~>type: specs.openrewrite.org/v1beta/recipe
85+
name: org.openrewrite.MyRecipe
86+
displayName: My recipe
87+
description: My recipe description
88+
"""
89+
)
90+
);
91+
}
92+
93+
@Test
94+
void changesDisplayName() {
95+
rewriteRun(
96+
spec -> spec.recipe(changeRecipeNameAndDescription()),
97+
//language=java
98+
java(
99+
"""
100+
import org.jetbrains.annotations.NotNull;
101+
import org.openrewrite.Recipe;
102+
103+
class MyRecipe extends Recipe {
104+
public @NotNull String getDisplayName() {
105+
return "My recipe";
106+
}
107+
108+
public @NotNull String getDescription() {
109+
return "My recipe description";
110+
}
111+
}
112+
""",
113+
"""
114+
import org.jetbrains.annotations.NotNull;
115+
import org.openrewrite.Recipe;
116+
117+
class MyRecipe extends Recipe {
118+
public @NotNull String getDisplayName() {
119+
return "My new recipe";
120+
}
121+
122+
public @NotNull String getDescription() {
123+
return "My new recipe description";
124+
}
125+
}
126+
"""
127+
)
128+
);
129+
}
130+
131+
@Test
132+
void changeYamlDefinedRecipe() {
133+
rewriteRun(
134+
spec -> spec.recipe(changeRecipeNameAndDescription()),
135+
yaml(
136+
//language=yaml
137+
"""
138+
type: specs.openrewrite.org/v1beta/recipe
139+
name: org.openrewrite.MyRecipe
140+
displayName: My recipe
141+
description: My recipe description
142+
""",
143+
//language=yaml
144+
"""
145+
type: specs.openrewrite.org/v1beta/recipe
146+
name: org.openrewrite.MyRecipe
147+
displayName: My new recipe
148+
description: My new recipe description
149+
"""
150+
)
151+
);
152+
}
153+
154+
private static org.openrewrite.Recipe changeRecipeNameAndDescription() {
155+
return RewriteTest.toRecipe(() -> new Recipe.Matcher()
156+
.asVisitor(recipe -> recipe
157+
.withDisplayName("My new recipe")
158+
.withDescription("My new recipe description").getTree())
159+
);
160+
}
161+
}

0 commit comments

Comments
 (0)