Skip to content

Commit fd52d7e

Browse files
authored
Rollout intermediate Steps (#449)
* rolling out new recipe to the intermediate steps * use only inline visitor instances
1 parent ae3448a commit fd52d7e

File tree

1 file changed

+98
-130
lines changed

1 file changed

+98
-130
lines changed

docs/authoring-recipes/writing-a-java-refactoring-recipe.md

Lines changed: 98 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ import com.fasterxml.jackson.annotation.JsonCreator;
6464
import com.fasterxml.jackson.annotation.JsonProperty;
6565
import lombok.EqualsAndHashCode;
6666
import lombok.Value;
67-
import org.jspecify.annotations.NonNull;
6867
import org.openrewrite.*;
6968

7069
// Making your recipe immutable helps make them idempotent and eliminates a variety of possible bugs.
@@ -76,27 +75,16 @@ public class SayHelloRecipe extends Recipe {
7675
@Option(displayName = "Fully Qualified Class Name",
7776
description = "A fully qualified class name indicating which class to add a hello() method to.",
7877
example = "com.yourorg.FooBar")
79-
@NonNull
8078
String fullyQualifiedClassName;
8179

82-
public SayHelloRecipe() {
83-
fullyQualifiedClassName = "";
84-
}
85-
86-
// All recipes must be serializable. This is verified by RewriteTest.rewriteRun() in your tests.
87-
@JsonCreator
88-
public SayHelloRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedClassName) {
89-
this.fullyQualifiedClassName = fullyQualifiedClassName;
90-
}
91-
9280
@Override
9381
public String getDisplayName() {
94-
return "Say Hello";
82+
return "Say 'Hello'";
9583
}
9684

9785
@Override
9886
public String getDescription() {
99-
return "Adds a \"hello\" method to the specified class.";
87+
return "Adds a `hello` method to the specified class.";
10088
}
10189

10290
// TODO: Override getVisitor() to return a JavaIsoVisitor to perform the refactoring
@@ -111,7 +99,6 @@ package com.yourorg;
11199

112100
import com.fasterxml.jackson.annotation.JsonCreator;
113101
import com.fasterxml.jackson.annotation.JsonProperty;
114-
import org.jspecify.annotations.NonNull;
115102
import org.openrewrite.*;
116103

117104
import java.util.Objects;
@@ -123,49 +110,20 @@ public class SayHelloRecipe extends Recipe {
123110
@Option(displayName = "Fully Qualified Class Name",
124111
description = "A fully qualified class name indicating which class to add a hello() method to.",
125112
example = "com.yourorg.FooBar")
126-
@NonNull
127113
String fullyQualifiedClassName;
128-
129-
@Override
130-
public boolean equals(Object o) {
131-
if (this == o) return true;
132-
if (o == null || getClass() != o.getClass()) return false;
133-
if (!super.equals(o)) return false;
134-
SayHelloRecipe that = (SayHelloRecipe) o;
135-
return Objects.equals(fullyQualifiedClassName, that.fullyQualifiedClassName);
136-
}
137-
138-
@Override
139-
public int hashCode() {
140-
return Objects.hash(super.hashCode(), fullyQualifiedClassName);
141-
}
142-
143-
public void setFullyQualifiedClassName(String fullyQualifiedClassName) {
144-
this.fullyQualifiedClassName = fullyQualifiedClassName;
145-
}
146-
147-
public String getFullyQualifiedClassName() {
148-
return fullyQualifiedClassName;
149-
}
150-
151-
public SayHelloRecipe() {
152-
fullyQualifiedClassName = "";
153-
}
154-
155-
// All recipes must be serializable. This is verified by RewriteTest.rewriteRun() in your tests.
156-
@JsonCreator
157-
public SayHelloRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedClassName) {
114+
115+
public SayHelloRecipe(String fullyQualifiedClassName) {
158116
this.fullyQualifiedClassName = fullyQualifiedClassName;
159117
}
160118

161119
@Override
162120
public String getDisplayName() {
163-
return "Say Hello";
121+
return "Say 'Hello'";
164122
}
165123

166124
@Override
167125
public String getDescription() {
168-
return "Adds a \"hello\" method to the specified class.";
126+
return "Adds a `hello` method to the specified class.";
169127
}
170128

171129
@Override
@@ -175,6 +133,20 @@ public class SayHelloRecipe extends Recipe {
175133
'}';
176134
}
177135

136+
@Override
137+
public boolean equals(Object o) {
138+
if (this == o) return true;
139+
if (o == null || getClass() != o.getClass()) return false;
140+
if (!super.equals(o)) return false;
141+
SayHelloRecipe that = (SayHelloRecipe) o;
142+
return Objects.equals(fullyQualifiedClassName, that.fullyQualifiedClassName);
143+
}
144+
145+
@Override
146+
public int hashCode() {
147+
return Objects.hash(super.hashCode(), fullyQualifiedClassName);
148+
}
149+
178150
// TODO: Override getVisitor() to return a JavaIsoVisitor to perform the refactoring
179151
}
180152
```
@@ -316,21 +288,19 @@ public class SayHelloRecipe extends Recipe {
316288
@Override
317289
public TreeVisitor<?, ExecutionContext> getVisitor() {
318290
// getVisitor() should always return a new instance of the visitor to avoid any state leaking between cycles
319-
return new SayHelloVisitor();
320-
}
321-
322-
public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
323-
@Override
324-
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
325-
// TODO: Filter out classes that don't match the fully qualified name
291+
return new JavaIsoVisitor<ExecutionContext>() {
292+
@Override
293+
public J.ClassDeclaration visitClassDeclaration (J.ClassDeclaration classDecl, ExecutionContext ctx){
294+
// TODO: Filter out classes that don't match the fully qualified name
326295

327-
// TODO: Filter out classes that already have a `hello()` method
296+
// TODO: Filter out classes that already have a `hello()` method
328297

329-
// TODO: Add a `hello()` method to classes that need it
330-
return classDecl;
331-
}
298+
// TODO: Add a `hello()` method to classes that need it
299+
return classDecl;
300+
}
301+
} ;
332302
}
333-
}
303+
}
334304
```
335305

336306
Now, let's work through each of those TODOs.
@@ -344,19 +314,22 @@ All of our logic lives inside of the `visitClassDeclaration` method. To filter o
344314
public class SayHelloRecipe extends Recipe {
345315
// ...
346316

347-
public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
348-
@Override
349-
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
350-
// Don't make changes to classes that don't match the fully qualified name
351-
if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
352-
return classDecl;
353-
}
317+
@Override
318+
public TreeVisitor<?, ExecutionContext> getVisitor() {
319+
return new JavaIsoVisitor<ExecutionContext>() {
320+
@Override
321+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
322+
// Don't make changes to classes that don't match the fully qualified name
323+
if (!TypeUtils.isOfClassType(classDecl.getType(), fullyQualifiedClassName)) {
324+
return classDecl;
325+
}
354326

355-
// TODO: Filter out classes that already have a `hello()` method
327+
// TODO: Filter out classes that already have a `hello()` method
356328

357-
// TODO: Add a `hello()` method to classes that need it
358-
return classDecl;
359-
}
329+
// TODO: Add a `hello()` method to classes that need it
330+
return classDecl;
331+
}
332+
};
360333
}
361334
}
362335
```
@@ -369,29 +342,32 @@ To filter out classes that already have a `hello()` method, we need to first fig
369342
// ...
370343
public class SayHelloRecipe extends Recipe {
371344
// ...
345+
@Override
346+
public TreeVisitor<?, ExecutionContext> getVisitor() {
347+
return new JavaIsoVisitor<ExecutionContext>() {
348+
@Override
349+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
350+
// Don't make changes to classes that don't match the fully qualified name
351+
if (!TypeUtils.isOfClassType(classDecl.getType(), fullyQualifiedClassName)) {
352+
return classDecl;
353+
}
372354

373-
public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
374-
@Override
375-
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
376-
// Don't make changes to classes that don't match the fully qualified name
377-
if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
378-
return classDecl;
379-
}
355+
// Check if the class already has a method named "hello".
356+
boolean helloMethodExists = classDecl.getBody().getStatements().stream()
357+
.filter(J.MethodDeclaration.class::isInstance)
358+
.map(J.MethodDeclaration.class::cast)
359+
.map(J.MethodDeclaration::getSimpleName)
360+
.anyMatch("hello"::equals);
380361

381-
// Check if the class already has a method named "hello"
382-
boolean helloMethodExists = classDecl.getBody().getStatements().stream()
383-
.filter(statement -> statement instanceof J.MethodDeclaration)
384-
.map(J.MethodDeclaration.class::cast)
385-
.anyMatch(methodDeclaration -> methodDeclaration.getName().getSimpleName().equals("hello"));
362+
// If the class already has a `hello()` method, don't make any changes to it.
363+
if (helloMethodExists) {
364+
return classDecl;
365+
}
386366

387-
// If the class already has a `hello()` method, don't make any changes to it.
388-
if (helloMethodExists) {
367+
// TODO: Add a `hello()` method to classes that need it
389368
return classDecl;
390369
}
391-
392-
// TODO: Add a `hello()` method to classes that need it
393-
return classDecl;
394-
}
370+
};
395371
}
396372
}
397373
```
@@ -405,21 +381,10 @@ Templates are created using the `JavaTemplate.builder()` method. Within a templa
405381
Here is what a template like that might look like for our recipe:
406382

407383
```java
408-
// ...
409-
public class SayHelloRecipe extends Recipe {
410-
// ...
411-
412-
public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
413-
private final JavaTemplate helloTemplate =
414-
JavaTemplate.builder( "public String hello() { return \"Hello from #{}!\"; }")
415-
.build();
416-
417-
@Override
418-
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
419-
// ...
420-
}
421-
}
422-
}
384+
JavaTemplate.apply("public String hello() { return \"Hello from #{}!\"; }",
385+
updateCursor(classDecl),
386+
classDecl.getBody().getCoordinates().addMethodDeclaration(Comparator.comparing(J.MethodDeclaration::getSimpleName)),
387+
fullyQualifiedClassName);
423388
```
424389

425390
We then could use that template to add a `hello()` method as desired by:
@@ -429,36 +394,39 @@ We then could use that template to add a `hello()` method as desired by:
429394
public class SayHelloRecipe extends Recipe {
430395
// ...
431396

432-
public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
433-
private final JavaTemplate helloTemplate =
434-
JavaTemplate.builder( "public String hello() { return \"Hello from #{}!\"; }")
435-
.build();
436-
437-
@Override
438-
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
439-
// Don't make changes to classes that don't match the fully qualified name
440-
if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
441-
return classDecl;
442-
}
397+
@Override
398+
public TreeVisitor<?, ExecutionContext> getVisitor() {
399+
return new JavaIsoVisitor<ExecutionContext>() {
400+
private final JavaTemplate helloTemplate =
401+
JavaTemplate.builder("public String hello() { return \"Hello from #{}!\"; }")
402+
.build();
443403

444-
// Check if the class already has a method named "hello"
445-
boolean helloMethodExists = classDecl.getBody().getStatements().stream()
446-
.filter(statement -> statement instanceof J.MethodDeclaration)
447-
.map(J.MethodDeclaration.class::cast)
448-
.anyMatch(methodDeclaration -> methodDeclaration.getName().getSimpleName().equals("hello"));
404+
@Override
405+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
406+
// Don't make changes to classes that don't match the fully qualified name
407+
if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
408+
return classDecl;
409+
}
449410

450-
// If the class already has a `hello()` method, don't make any changes to it.
451-
if (helloMethodExists) {
452-
return classDecl;
453-
}
411+
// Check if the class already has a method named "hello"
412+
boolean helloMethodExists = classDecl.getBody().getStatements().stream()
413+
.filter(statement -> statement instanceof J.MethodDeclaration)
414+
.map(J.MethodDeclaration.class::cast)
415+
.anyMatch(methodDeclaration -> methodDeclaration.getName().getSimpleName().equals("hello"));
454416

455-
// Interpolate the fullyQualifiedClassName into the template and use the resulting LST to update the class body
456-
classDecl = classDecl.withBody( helloTemplate.apply(new Cursor(getCursor(), classDecl.getBody()),
457-
classDecl.getBody().getCoordinates().lastStatement(),
458-
fullyQualifiedClassName ));
417+
// If the class already has a `hello()` method, don't make any changes to it.
418+
if (helloMethodExists) {
419+
return classDecl;
420+
}
459421

460-
return classDecl;
461-
}
422+
// insert the defined method into the existing class declaration
423+
return JavaTemplate.apply(
424+
"public String hello() { return \"Hello from #{}!\"; }",
425+
updateCursor(classDecl),
426+
classDecl.getBody().getCoordinates().addMethodDeclaration(Comparator.comparing(J.MethodDeclaration::getSimpleName)),
427+
fullyQualifiedClassName);
428+
}
429+
};
462430
}
463431
}
464432
```

0 commit comments

Comments
 (0)