diff --git a/build.gradle.kts b/build.gradle.kts index 05470a8..ab8d0bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,8 @@ dependencies { implementation(platform("org.openrewrite:rewrite-bom:${latest}")) implementation("org.openrewrite:rewrite-java") + implementation("org.openrewrite:rewrite-yaml") + implementation("org.assertj:assertj-core:3.24.2") runtimeOnly("org.openrewrite:rewrite-java-17") // Need to have a slf4j binding to see any output enabled from the parser. runtimeOnly("ch.qos.logback:logback-classic:1.2.+") diff --git a/src/main/java/com/yourorg/AppendToReleaseNotes.java b/src/main/java/com/yourorg/AppendToReleaseNotes.java new file mode 100644 index 0000000..472054d --- /dev/null +++ b/src/main/java/com/yourorg/AppendToReleaseNotes.java @@ -0,0 +1,90 @@ +package com.yourorg; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.text.PlainText; +import org.openrewrite.text.PlainTextParser; +import org.openrewrite.text.PlainTextVisitor; +import org.openrewrite.yaml.tree.Yaml; + +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 = true) +public class AppendToReleaseNotes extends ScanningRecipe { + + + @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.") + String message; + + + public static class Accumulator { + boolean found; + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } + + @Override + public TreeVisitor getScanner(Accumulator acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext executionContext) { + if(tree instanceof SourceFile) { + Path sourcePath = ((SourceFile) tree).getSourcePath(); + acc.found |= "RELEASE.md".equals(sourcePath.toString()); + } + return tree; + } + }; + } + + @Override + public Collection generate(Accumulator acc, ExecutionContext ctx) { + if(acc.found) { + return Collections.emptyList(); + } + return PlainTextParser.builder().build() + .parse("") + .map(it -> (SourceFile) it.withSourcePath(Paths.get("RELEASE.md"))) + .collect(Collectors.toList()); + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + return new PlainTextVisitor() { + @Override + public PlainText visitText(PlainText text, ExecutionContext executionContext) { + PlainText t = super.visitText(text, executionContext); + if(!"RELEASE.md".equals(t.getSourcePath().toString())) { + return t; + } + if(t.getText().contains(message)) { + return t; + } + return t.withText(t.getText() + "\n" + message); + } + }; + } +} diff --git a/src/main/java/com/yourorg/AssertEqualsToAssertThat.java b/src/main/java/com/yourorg/AssertEqualsToAssertThat.java new file mode 100644 index 0000000..1358d73 --- /dev/null +++ b/src/main/java/com/yourorg/AssertEqualsToAssertThat.java @@ -0,0 +1,71 @@ +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.*; +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 = true) +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 getVisitor() { + return Preconditions.check(new UsesType<>("org.junit.jupiter.api.Assertions", null), + new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) { + J.MethodInvocation m = super.visitMethodInvocation(method, executionContext); + if(!MATCHER.matches(m)) { + return m; + } + List 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; + } + }); + } +} diff --git a/src/main/java/com/yourorg/ClassHierarchy.java b/src/main/java/com/yourorg/ClassHierarchy.java new file mode 100644 index 0000000..fbf692d --- /dev/null +++ b/src/main/java/com/yourorg/ClassHierarchy.java @@ -0,0 +1,55 @@ +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 = true) +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 getVisitor() { + return new JavaIsoVisitor() { + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + JavaType.FullyQualified type = classDecl.getType(); + if(type instanceof JavaType.Class && type.getSupertype() != null) { + JavaType.FullyQualified supertype = type.getSupertype(); + report.insertRow(ctx, new ClassHierarchyReport.Row(type.getFullyQualifiedName(), + ClassHierarchyReport.Relationship.EXTENDS, + supertype.getFullyQualifiedName())); + + for (JavaType.FullyQualified anInterface : type.getInterfaces()) { + report.insertRow(ctx, new ClassHierarchyReport.Row( + type.getFullyQualifiedName(), + ClassHierarchyReport.Relationship.IMPLEMENTS, + anInterface.getFullyQualifiedName() + )); + } + } + return super.visitClassDeclaration(classDecl, ctx); + } + }; + } +} diff --git a/src/main/java/com/yourorg/UpdateConcoursePipeline.java b/src/main/java/com/yourorg/UpdateConcoursePipeline.java new file mode 100644 index 0000000..f947ca3 --- /dev/null +++ b/src/main/java/com/yourorg/UpdateConcoursePipeline.java @@ -0,0 +1,76 @@ +package com.yourorg; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.yaml.ChangePropertyValue; +import org.openrewrite.yaml.YamlIsoVisitor; +import org.openrewrite.yaml.tree.Yaml; + +@Value +@EqualsAndHashCode(callSuper = true) +public class UpdateConcoursePipeline extends Recipe { + @Override + public String getDisplayName() { + return "Update concourse pipeline"; + } + + @Override + public String getDescription() { + return "Update the tag filter on concourse pipelines."; + } + + @Option(displayName = "New tag filter version", + description = "tag filter version.") + String version; + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.or( + new FindSourceFiles("ci/pipeline*.yml").getVisitor(), + new FindSourceFiles("ci/pipeline*.yaml").getVisitor()), + new YamlIsoVisitor() { + + @Override + public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx); + if (e.getKey().getValue().equals("source")) { + Yaml.Block value = e.getValue(); + if(!(value instanceof Yaml.Mapping)) { + return e; + } + Yaml.Mapping mapping = (Yaml.Mapping) value; + Yaml.Mapping.Entry uriEntry = null; + Yaml.Mapping.Entry tagFilter = null; + for (Yaml.Mapping.Entry mappingEntry : mapping.getEntries()) { + if("uri".equals(mappingEntry.getKey().getValue())) { + uriEntry = mappingEntry; + } else if("tag_filter".equals(mappingEntry.getKey().getValue())) { + tagFilter = mappingEntry; + } + } + if(uriEntry == null || tagFilter == null) { + return e; + } + if(!(uriEntry.getValue() instanceof Yaml.Scalar) || !(tagFilter.getValue() instanceof Yaml.Scalar)) { + return e; + } + Yaml.Scalar uriValue = (Yaml.Scalar) uriEntry.getValue(); + if(!uriValue.getValue().contains(".git")) { + return e; + } + Yaml.Scalar tagFilterValue = (Yaml.Scalar) tagFilter.getValue(); + if(version.equals(tagFilterValue.getValue())) { + return e; + } + return (Yaml.Mapping.Entry) new ChangePropertyValue("source.tag_filter", version, null, null, null) + .getVisitor() + .visitNonNull(e, ctx); + } + return e; + } + } + ); + } +} diff --git a/src/main/java/com/yourorg/table/ClassHierarchyReport.java b/src/main/java/com/yourorg/table/ClassHierarchyReport.java new file mode 100644 index 0000000..1320b4c --- /dev/null +++ b/src/main/java/com/yourorg/table/ClassHierarchyReport.java @@ -0,0 +1,35 @@ +package com.yourorg.table; + +import lombok.Value; +import org.openrewrite.Column; +import org.openrewrite.DataTable; +import org.openrewrite.Recipe; + +public class ClassHierarchyReport extends DataTable { + + public ClassHierarchyReport(Recipe recipe) { + super(recipe, + "Class hierarchy report", + "Records inheritance relationships between classes."); + } + + @Value + public static class Row { + @Column(displayName = "Class name", + description = "Fully qualified name of the class.") + String className; + + @Column(displayName = "Relationship", + description = "Whether the class implements a super interface or extends a superclass.") + Relationship relationship; + + @Column(displayName = "Super class name", + description = "Fully qualified name of the superclass.") + String superClassName; + } + + public enum Relationship { + EXTENDS, + IMPLEMENTS + } +} diff --git a/src/main/resources/META-INF/rewrite/assertjmigration.yml b/src/main/resources/META-INF/rewrite/assertjmigration.yml new file mode 100644 index 0000000..42547b5 --- /dev/null +++ b/src/main/resources/META-INF/rewrite/assertjmigration.yml @@ -0,0 +1,13 @@ +--- +type: specs.openrewrite.org/v1beta/recipe +name: com.desjardins.UseApacheStringUtils +displayName: Use apache string utils +description: Replace Spring string utilities with apache string utilities +recipeList: + - org.openrewrite.java.dependencies.AddDependency: + groupId: org.assertj + artifactId: assertj-core + version: 3.x + onlyIfUsing: org.junit.jupiter.api.Assertions + configuration: implementation + - com.yourorg.AssertEqualsToAssertThat diff --git a/src/main/resources/META-INF/rewrite/rewrite.yml b/src/main/resources/META-INF/rewrite/rewrite.yml deleted file mode 100644 index 4adde50..0000000 --- a/src/main/resources/META-INF/rewrite/rewrite.yml +++ /dev/null @@ -1,28 +0,0 @@ -# -# Copyright 2021 the original author or authors. -#

-# 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 -#

-# https://www.apache.org/licenses/LICENSE-2.0 -#

-# 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. -# - -# Include any Declarative YAML format recipes here, as per: -# https://docs.openrewrite.org/reference/yaml-format-reference ---- -type: specs.openrewrite.org/v1beta/recipe -name: com.yourorg.RecipeA -displayName: Recipe A -description: Applies NoGuavaListsNewArrayList. -tags: - - tag1 - - tag2 -recipeList: - - com.yourorg.NoGuavaListsNewArrayList diff --git a/src/main/resources/META-INF/rewrite/stringuitls.yml b/src/main/resources/META-INF/rewrite/stringuitls.yml new file mode 100644 index 0000000..0f487b8 --- /dev/null +++ b/src/main/resources/META-INF/rewrite/stringuitls.yml @@ -0,0 +1,15 @@ +--- +type: specs.openrewrite.org/v1beta/recipe +name: com.desjardins.UseApacheStringUtils +displayName: Use apache string utils +description: Replace Spring string utilities with apache string utilities +recipeList: + - org.openrewrite.java.dependencies.AddDependency: + groupId: org.apache.commons + artifactId: commons-lang3 + version: latest.release + onlyIfUsing: org.springframework.util.StringUtils + configuration: implementation + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: org.springframework.util.StringUtils + newFullyQualifiedTypeName: org.apache.commons.lang3.StringUtils diff --git a/src/test/java/com/yourorg/AppendToReleaseNotesTest.java b/src/test/java/com/yourorg/AppendToReleaseNotesTest.java new file mode 100644 index 0000000..dd108fb --- /dev/null +++ b/src/test/java/com/yourorg/AppendToReleaseNotesTest.java @@ -0,0 +1,40 @@ +package com.yourorg; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import java.nio.file.Paths; + +import static org.openrewrite.test.SourceSpecs.text; + +public class AppendToReleaseNotesTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AppendToReleaseNotes("Hello world")); + } + + @Test + void createNewReleaseNotes() { + rewriteRun( + text(null, + """ + Hello world + """) + ); + } + + @Test + void editExistingReleaseNotes() { + rewriteRun( + text(""" + You say goodbye, I say + """, + """ + You say goodbye, I say + Hello world + """, + spec -> spec.path(Paths.get("RELEASE.md"))) + ); + } +} diff --git a/src/test/java/com/yourorg/AssertEqualsToAssertThatTest.java b/src/test/java/com/yourorg/AssertEqualsToAssertThatTest.java new file mode 100644 index 0000000..e09157a --- /dev/null +++ b/src/test/java/com/yourorg/AssertEqualsToAssertThatTest.java @@ -0,0 +1,69 @@ +package com.yourorg; + +import org.junit.jupiter.api.Test; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +public class AssertEqualsToAssertThatTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AssertEqualsToAssertThat()) + .parser(JavaParser.fromJavaVersion() + .classpath("junit-jupiter-api")); + } + + @Test + void twoArgument() { + rewriteRun( + //language=java + java(""" + import org.junit.jupiter.api.Assertions; + + class A { + void foo() { + Assertions.assertEquals(1, 2); + } + } + """, + """ + import org.assertj.core.api.Assertions; + + class A { + void foo() { + Assertions.assertThat(2).isEqualTo(1); + } + } + """) + ); + } + + + @Test + void withDescription() { + rewriteRun( + //language=java + java(""" + import org.junit.jupiter.api.Assertions; + + class A { + void foo() { + Assertions.assertEquals(1, 2, "one equals two, everyone knows that"); + } + } + """, + """ + import org.assertj.core.api.Assertions; + + class A { + void foo() { + Assertions.assertThat(2).as("one equals two, everyone knows that").isEqualTo(1); + } + } + """) + ); + } +} diff --git a/src/test/java/com/yourorg/ClassHierarchyTest.java b/src/test/java/com/yourorg/ClassHierarchyTest.java new file mode 100644 index 0000000..360aaeb --- /dev/null +++ b/src/test/java/com/yourorg/ClassHierarchyTest.java @@ -0,0 +1,65 @@ +package com.yourorg; + +import com.yourorg.table.ClassHierarchyReport; +import org.junit.jupiter.api.Test; +import org.openrewrite.java.Assertions; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.java.Assertions.java; + +public class ClassHierarchyTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ClassHierarchy()); + } + + @Test + void basic() { + rewriteRun( + spec -> spec.dataTable(ClassHierarchyReport.Row.class, rows -> { + assertThat(rows).containsExactly(new ClassHierarchyReport.Row("A", ClassHierarchyReport.Relationship.EXTENDS,"java.lang.Object")); + }), + //language=java + java(""" + class A {} + """) + ); + } + + @Test + void aExtendsB() { + rewriteRun( + spec -> spec.dataTable(ClassHierarchyReport.Row.class, rows -> { + assertThat(rows).containsExactly( + new ClassHierarchyReport.Row("B", ClassHierarchyReport.Relationship.EXTENDS,"java.lang.Object"), + new ClassHierarchyReport.Row("A", ClassHierarchyReport.Relationship.EXTENDS,"B")); + }), + //language=java + java(""" + class B {} + """), + java(""" + class A extends B {} + """) + ); + } + + @Test + void interfaceRelationship() { + rewriteRun( + spec -> spec.dataTable(ClassHierarchyReport.Row.class, rows -> { + assertThat(rows).containsExactly( + new ClassHierarchyReport.Row("A", ClassHierarchyReport.Relationship.EXTENDS,"java.lang.Object"), + new ClassHierarchyReport.Row("A", ClassHierarchyReport.Relationship.IMPLEMENTS,"java.io.Serializable")); + }), + // language=java + java(""" + import java.io.Serializable; + class A implements Serializable {} + """) + ); + } +} diff --git a/src/test/java/com/yourorg/UpdateConcoursePipelineTest.java b/src/test/java/com/yourorg/UpdateConcoursePipelineTest.java new file mode 100644 index 0000000..5470e92 --- /dev/null +++ b/src/test/java/com/yourorg/UpdateConcoursePipelineTest.java @@ -0,0 +1,39 @@ +package com.yourorg; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.yaml.Assertions; + +import java.nio.file.Paths; + +import static org.openrewrite.yaml.Assertions.yaml; + +public class UpdateConcoursePipelineTest implements RewriteTest { + + @Test + void updateTagFilter() { + rewriteRun( + spec -> spec.recipe(new UpdateConcoursePipeline("8.2.0")), + //language=yaml + yaml(""" + --- + resources: + - name: tasks + type: git + source: + uri: git@github.com:Example/concourse-tasks.git + tag_filter: 8.1.0 + """, + """ + --- + resources: + - name: tasks + type: git + source: + uri: git@github.com:Example/concourse-tasks.git + tag_filter: 8.2.0 + """, + spec -> spec.path(Paths.get("ci/pipeline.yml"))) + ); + } +}