Skip to content

Commit

Permalink
Recipe workshop (#41)
Browse files Browse the repository at this point in the history
* Workshop

* Delete unused file

* Delete unused file

* Apply best practices and add some more comments

---------

Co-authored-by: Sam Snyder <sam@moderne.io>
  • Loading branch information
timtebeek and sambsnyd authored Feb 25, 2024
1 parent 3fc7c41 commit 0c37204
Show file tree
Hide file tree
Showing 17 changed files with 823 additions and 74 deletions.
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
plugins {
id("org.openrewrite.build.recipe-library") version "latest.release"

// Only needed when you want to apply the OpenRewriteBestPractices recipe to your recipes
// Only needed when you want to apply the OpenRewriteBestPractices recipe to your recipes through
// ./gradlew rewriteRun -Drewrite.activeRecipe=org.openrewrite.recipes.OpenRewriteBestPractices
id("org.openrewrite.rewrite") version "latest.release"
}

Expand All @@ -15,6 +16,8 @@ dependencies {
implementation(platform("org.openrewrite.recipe:rewrite-recipe-bom:latest.release"))

implementation("org.openrewrite:rewrite-java")
implementation("org.openrewrite:rewrite-yaml")
implementation("org.assertj:assertj-core:3.24.2")
runtimeOnly("org.openrewrite:rewrite-java-17")

// Refaster style recipes need the rewrite-templating annotation processor and dependency for generated recipes
Expand Down
108 changes: 108 additions & 0 deletions src/main/java/com/yourorg/AppendToReleaseNotes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yourorg;

import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.text.PlainText;
import org.openrewrite.text.PlainTextParser;
import org.openrewrite.text.PlainTextVisitor;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;

@Value
@EqualsAndHashCode(callSuper = false)
public class AppendToReleaseNotes extends ScanningRecipe<AppendToReleaseNotes.Accumulator> {

@Override
public String getDisplayName() {
return "Append to release notes";
}

@Override
public String getDescription() {
return "Adds the specified line to RELEASE.md.";
}

@Option(displayName = "Message",
description = "Message to append to the bottom of RELEASE.md.",
example = "## 1.0.0\n\n- New feature")
String message;

// The shared state between the scanner and the visitor. The custom class ensures we can easily extend the recipe.
public static class Accumulator {
boolean found;
}

@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}

@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new TreeVisitor<Tree, ExecutionContext>() {
@Override
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
if (tree instanceof SourceFile) {
Path sourcePath = ((SourceFile) tree).getSourcePath();
acc.found |= "RELEASE.md".equals(sourcePath.toString());
}
return tree;
}
};
}

@Override
public Collection<? extends SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
if (acc.found) {
return Collections.emptyList();
}
// If the file was not found, create it
return PlainTextParser.builder().build()
// We start with an empty string that we then append to in the visitor
.parse("")
// Be sure to set the source path for any generated file, so that the visitor can find it
.map(it -> (SourceFile) it.withSourcePath(Paths.get("RELEASE.md")))
.collect(Collectors.toList());
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return new PlainTextVisitor<ExecutionContext>() {
@Override
public PlainText visitText(PlainText text, ExecutionContext ctx) {
PlainText t = super.visitText(text, ctx);
// If the file is not RELEASE.md, don't modify it
if (!"RELEASE.md".equals(t.getSourcePath().toString())) {
return t;
}
// If the file already contains the message, don't append it again
if (t.getText().contains(message)) {
return t;
}
// Append the message to the end of the file
return t.withText(t.getText() + "\n" + message);
}
};
}
}
89 changes: 89 additions & 0 deletions src/main/java/com/yourorg/AssertEqualsToAssertThat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yourorg;

import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesType;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;

import java.util.List;

@Value
@EqualsAndHashCode(callSuper = false)
public class AssertEqualsToAssertThat extends Recipe {
@Override
public String getDisplayName() {
// language=markdown
return "JUnit `assertEquals()` to Assertj `assertThat()`";
}

@Override
public String getDescription() {
return "Use AssertJ assertThat instead of JUnit assertEquals().";
}

private static MethodMatcher MATCHER = new MethodMatcher("org.junit.jupiter.api.Assertions assertEquals(..)");

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new UsesType<>("org.junit.jupiter.api.Assertions", null),
new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
if (!MATCHER.matches(m)) {
return m;
}
List<Expression> arguments = m.getArguments();
maybeAddImport("org.assertj.core.api.Assertions");
maybeRemoveImport("org.junit.jupiter.api.Assertions");
if (arguments.size() == 2) {
Expression expected = arguments.get(0);
Expression actual = arguments.get(1);

m = JavaTemplate.builder("Assertions.assertThat(#{any()}).isEqualTo(#{any()})")
.imports("org.assertj.core.api.Assertions")
.javaParser(JavaParser.fromJavaVersion()
.classpath("assertj-core"))
.build()
.apply(getCursor(), m.getCoordinates().replace(), actual, expected);
} else if (arguments.size() == 3) {
Expression expected = arguments.get(0);
Expression actual = arguments.get(1);
Expression description = arguments.get(2);

m = JavaTemplate.builder("Assertions.assertThat(#{any()}).as(#{any()}).isEqualTo(#{any()})")
.imports("org.assertj.core.api.Assertions")
.javaParser(JavaParser.fromJavaVersion()
.classpath("assertj-core"))
.build()
.apply(getCursor(), m.getCoordinates().replace(), actual, description, expected);
}
return m;
}
});
}
}
73 changes: 73 additions & 0 deletions src/main/java/com/yourorg/ClassHierarchy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yourorg;

import com.yourorg.table.ClassHierarchyReport;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;

@Value
@EqualsAndHashCode(callSuper = false)
public class ClassHierarchy extends Recipe {

transient ClassHierarchyReport report = new ClassHierarchyReport(this);

@Override
public String getDisplayName() {
return "Class hierarchy";
}

@Override
public String getDescription() {
return "Produces a data table showing inheritance relationships between classes.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {

@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
JavaType.FullyQualified type = classDecl.getType();
// Capture all classes, which all extend java.lang.Object
if (type instanceof JavaType.Class && type.getSupertype() != null) {
JavaType.FullyQualified supertype = type.getSupertype();
// Capture the direct superclass
report.insertRow(ctx, new ClassHierarchyReport.Row(
type.getFullyQualifiedName(),
ClassHierarchyReport.Relationship.EXTENDS,
supertype.getFullyQualifiedName()));

// Capture all interfaces
for (JavaType.FullyQualified anInterface : type.getInterfaces()) {
report.insertRow(ctx, new ClassHierarchyReport.Row(
type.getFullyQualifiedName(),
ClassHierarchyReport.Relationship.IMPLEMENTS,
anInterface.getFullyQualifiedName()
));
}
}
return super.visitClassDeclaration(classDecl, ctx);
}
};
}
}
Loading

0 comments on commit 0c37204

Please sign in to comment.