diff --git a/src/main/java/org/openrewrite/java/template/RecipeDescriptor.java b/src/main/java/org/openrewrite/java/template/RecipeDescriptor.java new file mode 100644 index 00000000..682f0156 --- /dev/null +++ b/src/main/java/org/openrewrite/java/template/RecipeDescriptor.java @@ -0,0 +1,17 @@ +package org.openrewrite.java.template; + +import org.intellij.lang.annotations.Language; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +public @interface RecipeDescriptor { + @Language("markdown") + String name(); + + @Language("markdown") + String description(); + + String[] tags() default {}; +} diff --git a/src/main/java/org/openrewrite/java/template/processor/RefasterTemplateProcessor.java b/src/main/java/org/openrewrite/java/template/processor/RefasterTemplateProcessor.java index f606badf..31351b1a 100644 --- a/src/main/java/org/openrewrite/java/template/processor/RefasterTemplateProcessor.java +++ b/src/main/java/org/openrewrite/java/template/processor/RefasterTemplateProcessor.java @@ -128,13 +128,8 @@ public void visitClassDef(JCTree.JCClassDecl classDecl) { processingEnv.getMessager().printMessage(Kind.NOTE, "Generating template for " + descriptor.classDecl.getSimpleName()); - String templateName = classDecl.sym.fullname.toString().substring(classDecl.sym.packge().fullname.length() + 1); String templateFqn = classDecl.sym.fullname.toString() + "Recipe"; String templateCode = copy.toString().trim(); - String displayName = cu.docComments.getComment(classDecl) != null ? cu.docComments.getComment(classDecl).getText().trim() : "Refaster template `" + templateName + '`'; - if (displayName.endsWith(".")) { - displayName = displayName.substring(0, displayName.length() - 1); - } for (JCTree.JCMethodDecl template : descriptor.beforeTemplates) { for (Symbol anImport : ImportDetector.imports(template)) { @@ -192,16 +187,10 @@ public void visitClassDef(JCTree.JCClassDecl classDecl) { recipe.append("@NonNullApi\n"); recipe.append(modifiers).append("class ").append(recipeName).append(" extends Recipe {\n"); recipe.append("\n"); - recipe.append(" @Override\n"); - recipe.append(" public String getDisplayName() {\n"); - recipe.append(" return \"").append(escape(displayName)).append("\";\n"); - recipe.append(" }\n"); - recipe.append("\n"); - recipe.append(" @Override\n"); - recipe.append(" public String getDescription() {\n"); - recipe.append(" return \"Recipe created for the following Refaster template:\\n```java\\n").append(escape(templateCode)).append("\\n```\\n.\";\n"); - recipe.append(" }\n"); - recipe.append("\n"); + recipe.append(recipeDescriptor(classDecl, + "Refaster template `" + classDecl.sym.fullname.toString().substring(classDecl.sym.packge().fullname.length() + 1) + '`', + "Recipe created for the following Refaster template:\\n```java\\n" + escape(templateCode) + "\\n```\\n." + )); recipe.append(" @Override\n"); recipe.append(" public TreeVisitor getVisitor() {\n"); recipe.append(" JavaVisitor javaVisitor = new AbstractRefasterJavaVisitor() {\n"); @@ -362,19 +351,9 @@ public void visitClassDef(JCTree.JCClassDecl classDecl) { if (outerClassRequired) { String outerClassName = className.substring(className.lastIndexOf('.') + 1); out.write("public final class " + outerClassName + " extends Recipe {\n"); - - String simpleInputOuterFQN = inputOuterFQN.substring(inputOuterFQN.lastIndexOf('.') + 1); - out.write("\n" + - " @Override\n" + - " public String getDisplayName() {\n" + - " return \"`" + simpleInputOuterFQN + "` Refaster recipes\";\n" + - " }\n" + - "\n" + - " @Override\n" + - " public String getDescription() {\n" + - " return \"Refaster template recipes for `" + inputOuterFQN + "`.\";\n" + - " }\n" + - "\n"); + out.write(recipeDescriptor(classDecl, + String.format("`%s` Refaster recipes", inputOuterFQN.substring(inputOuterFQN.lastIndexOf('.') + 1)), + String.format("Refaster template recipes for `%s`.", inputOuterFQN))); String recipesAsList = recipes.keySet().stream() .map(r -> " new " + r.substring(r.lastIndexOf('.') + 1) + "()") .collect(Collectors.joining(",\n")); @@ -404,6 +383,39 @@ public void visitClassDef(JCTree.JCClassDecl classDecl) { } } + private String recipeDescriptor(JCTree.JCClassDecl classDecl, String defaultDisplayName, String defaultDescription) { + String displayName = defaultDisplayName; + String description = defaultDescription; + for (JCTree.JCAnnotation annotation : classDecl.getModifiers().getAnnotations()) { + if (annotation.type.toString().equals("org.openrewrite.java.template.RecipeDescriptor")) { + for (JCTree.JCExpression argExpr : annotation.getArguments()) { + JCTree.JCAssign arg = (JCTree.JCAssign) argExpr; + switch (arg.lhs.toString()) { + case "name": + displayName = ((JCTree.JCLiteral) arg.rhs).getValue().toString(); + break; + case "description": + description = ((JCTree.JCLiteral) arg.rhs).getValue().toString(); + break; + } + } + break; + } + } + + return "\n" + + " @Override\n" + + " public String getDisplayName() {\n" + + " return \"" + displayName + "\";\n" + + " }\n" + + "\n" + + " @Override\n" + + " public String getDescription() {\n" + + " return \"" + description + "\";\n" + + " }\n" + + "\n"; + } + private void maybeRemoveImport(Map> imports, Set beforeImports, Set afterImports, StringBuilder recipe) { for (String anImport : imports.values().stream().flatMap(Set::stream).collect(Collectors.toSet())) { if (anImport.startsWith("java.lang.")) { diff --git a/src/test/java/org/openrewrite/java/template/RefasterTemplateProcessorTest.java b/src/test/java/org/openrewrite/java/template/RefasterTemplateProcessorTest.java index a4aa02e7..dd9aa9d9 100644 --- a/src/test/java/org/openrewrite/java/template/RefasterTemplateProcessorTest.java +++ b/src/test/java/org/openrewrite/java/template/RefasterTemplateProcessorTest.java @@ -19,7 +19,6 @@ import com.google.errorprone.refaster.annotation.BeforeTemplate; import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.openrewrite.java.template.processor.RefasterTemplateProcessor; @@ -72,7 +71,6 @@ void nestedRecipes(String recipeName) { .hasSourceEquivalentTo(JavaFileObjects.forResource("refaster/" + recipeName + "Recipes.java")); } - @NotNull static Collection classpath() { return Arrays.asList( fileForClass(BeforeTemplate.class), diff --git a/src/test/resources/refaster/Matching.java b/src/test/resources/refaster/Matching.java index fbb0c39d..3db964f3 100644 --- a/src/test/resources/refaster/Matching.java +++ b/src/test/resources/refaster/Matching.java @@ -20,9 +20,20 @@ import org.openrewrite.java.template.Matches; import org.openrewrite.java.template.MethodInvocationMatcher; import org.openrewrite.java.template.NotMatches; +import org.openrewrite.java.template.RecipeDescriptor; +@RecipeDescriptor( + name = "Static analysis", + description = "A set of static analysis recipes.", + tags = "sast" +) public class Matching { + @RecipeDescriptor( + name = "Use String length comparison", + description = "Use String#length() == 0 instead of String#isEmpty().", + tags = "sast" + ) public static class StringIsEmpty { @BeforeTemplate boolean before(int i, @NotMatches(MethodInvocationMatcher.class) String s) { diff --git a/src/test/resources/refaster/MatchingRecipes.java b/src/test/resources/refaster/MatchingRecipes.java index 66b167c9..81b0b336 100644 --- a/src/test/resources/refaster/MatchingRecipes.java +++ b/src/test/resources/refaster/MatchingRecipes.java @@ -1,18 +1,3 @@ -/* - * Copyright 2023 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. - */ package foo; import org.openrewrite.ExecutionContext; @@ -39,12 +24,12 @@ public final class MatchingRecipes extends Recipe { @Override public String getDisplayName() { - return "`Matching` Refaster recipes"; + return "Static analysis"; } @Override public String getDescription() { - return "Refaster template recipes for `foo.Matching`."; + return "A set of static analysis recipes."; } @Override @@ -57,14 +42,15 @@ public List getRecipeList() { @NonNullApi public static class StringIsEmptyRecipe extends Recipe { + @Override public String getDisplayName() { - return "Refaster template `Matching.StringIsEmpty`"; + return "Use String length comparison"; } @Override public String getDescription() { - return "Recipe created for the following Refaster template:\n```java\npublic static class StringIsEmpty {\n \n @BeforeTemplate()\n boolean before(int i, @NotMatches(value = MethodInvocationMatcher.class)\n String s) {\n return s.substring(i).isEmpty();\n }\n \n @BeforeTemplate()\n boolean before2(int i, @Matches(value = MethodInvocationMatcher.class)\n String s) {\n return s.substring(i).isEmpty();\n }\n \n @AfterTemplate()\n boolean after(String s) {\n return s != null && s.length() == 0;\n }\n}\n```\n."; + return "Use String#length() == 0 instead of String#isEmpty()."; } @Override