From c5799f701c2fa5e96a7f46247f914370ce6010f3 Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Wed, 10 Jul 2024 13:02:38 -0400 Subject: [PATCH] Annotated and Literal traits --- .../org/openrewrite/trait/TraitMatcher.java | 13 ++ .../org/openrewrite/java/trait/Annotated.java | 98 ++++++++++++ .../org/openrewrite/java/trait/Literal.java | 139 ++++++++++++++++++ .../org/openrewrite/java/trait/Traits.java | 46 ++++++ .../openrewrite/java/trait/AnnotatedTest.java | 65 ++++++++ .../openrewrite/java/trait/LiteralTest.java | 91 ++++++++++++ .../java/trait/MethodAccessTest.java | 3 +- .../java/trait/VariableAccessTest.java | 3 +- 8 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 rewrite-java/src/main/java/org/openrewrite/java/trait/Annotated.java create mode 100644 rewrite-java/src/main/java/org/openrewrite/java/trait/Literal.java create mode 100644 rewrite-java/src/main/java/org/openrewrite/java/trait/Traits.java create mode 100644 rewrite-java/src/test/java/org/openrewrite/java/trait/AnnotatedTest.java create mode 100644 rewrite-java/src/test/java/org/openrewrite/java/trait/LiteralTest.java diff --git a/rewrite-core/src/main/java/org/openrewrite/trait/TraitMatcher.java b/rewrite-core/src/main/java/org/openrewrite/trait/TraitMatcher.java index 7629ee0a7b98..dd0957eb9743 100644 --- a/rewrite-core/src/main/java/org/openrewrite/trait/TraitMatcher.java +++ b/rewrite-core/src/main/java/org/openrewrite/trait/TraitMatcher.java @@ -32,6 +32,19 @@ @Incubating(since = "8.30.0") public interface TraitMatcher> { + default U require(Tree tree, Cursor parent) { + return require(new Cursor(parent, tree)); + } + + default U require(Cursor cursor) { + return get(cursor).orElseThrow(() -> + new IllegalStateException("Expected this cursor to match the trait")); + } + + default Optional get(Tree tree, Cursor parent) { + return get(new Cursor(parent, tree)); + } + /** * Tests whether a tree at the cursor matches the trait, and if so, returns * a trait instance containing the semantic information represented by the tree. diff --git a/rewrite-java/src/main/java/org/openrewrite/java/trait/Annotated.java b/rewrite-java/src/main/java/org/openrewrite/java/trait/Annotated.java new file mode 100644 index 000000000000..508cfc0ecbcd --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/trait/Annotated.java @@ -0,0 +1,98 @@ +/* + * Copyright 2024 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 org.openrewrite.java.trait; + +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.openrewrite.Cursor; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.trait.SimpleTraitMatcher; +import org.openrewrite.trait.Trait; + +import java.util.Optional; + +@Value +public class Annotated implements Trait { + Cursor cursor; + + /** + * @param defaultAlias The name of the annotation attribute that is aliased to + * "value", if any. + * @return The attribute value. + */ + public Optional getDefaultAttribute(@Nullable String defaultAlias) { + if (getTree().getArguments() == null) { + return Optional.empty(); + } + for (Expression argument : getTree().getArguments()) { + if (!(argument instanceof J.Assignment)) { + return new Literal.Matcher().get(argument, cursor); + } + } + Optional valueAttr = getAttribute("value"); + if (valueAttr.isPresent()) { + return valueAttr; + } + return defaultAlias != null ? + getAttribute(defaultAlias) : + Optional.empty(); + } + + public Optional getAttribute(String attribute) { + if (getTree().getArguments() == null) { + return Optional.empty(); + } + for (Expression argument : getTree().getArguments()) { + if (argument instanceof J.Assignment) { + J.Assignment assignment = (J.Assignment) argument; + if (assignment.getVariable() instanceof J.Identifier) { + J.Identifier identifier = (J.Identifier) assignment.getVariable(); + if (identifier.getSimpleName().equals(attribute)) { + return new Literal.Matcher().get( + assignment.getAssignment(), + new Cursor(cursor, argument) + ); + } + } + } + } + return Optional.empty(); + } + + @RequiredArgsConstructor + public static class Matcher extends SimpleTraitMatcher { + private final AnnotationMatcher matcher; + + public Matcher(String signature) { + this.matcher = new AnnotationMatcher(signature); + } + + @Override + protected @Nullable Annotated test(Cursor cursor) { + Object value = cursor.getValue(); + if (value instanceof J.Annotation) { + J.Annotation annotation = (J.Annotation) value; + if (matcher.matches(annotation)) { + return new Annotated(cursor); + } + } + return null; + } + } +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/trait/Literal.java b/rewrite-java/src/main/java/org/openrewrite/java/trait/Literal.java new file mode 100644 index 000000000000..8302740949bb --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/trait/Literal.java @@ -0,0 +1,139 @@ +/* + * Copyright 2024 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 org.openrewrite.java.trait; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.openrewrite.Cursor; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.trait.SimpleTraitMatcher; +import org.openrewrite.trait.Trait; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +/** + * A literal in Java is either a {@link J.Literal} or a {@link J.NewArray} + * with a non-null initializer that itself literals or new arrays that recursively + * contain these constraints. In other languages this trait is inclusive + * of constructs like list or map literals. + */ +@RequiredArgsConstructor +public class Literal implements Trait { + @Getter + private final Cursor cursor; + + private final ObjectMapper mapper; + + public boolean isNull() { + return getTree() instanceof J.Literal && ((J.Literal) getTree()).getValue() == null; + } + + public boolean isNotNull() { + return !isNull(); + } + + public <@Nullable T> T getValue(Class type) { + return getValue(mapper.constructType(type)); + } + + public <@Nullable T> T getValue(TypeReference type) { + return getValue(mapper.constructType(type)); + } + + public <@Nullable T> T getValue(JavaType type) { + Expression lit = getTree(); + if (lit instanceof J.Literal) { + J.Literal literal = (J.Literal) lit; + if (literal.getValue() == null) { + //noinspection DataFlowIssue + return null; + } else if (type.isCollectionLikeType()) { + List l = singletonList(literal.getValue()); + return mapper.convertValue(l, type); + } else { + return mapper.convertValue(literal.getValue(), type); + } + } else if (lit instanceof J.NewArray) { + List untyped = untypedInitializerLiterals((J.NewArray) lit); + return mapper.convertValue(untyped, type); + } + //noinspection DataFlowIssue + return null; + } + + private List untypedInitializerLiterals(J.NewArray newArray) { + List acc = new ArrayList<>(); + for (Expression init : requireNonNull(newArray.getInitializer())) { + if (init instanceof J.Literal) { + acc.add(((J.Literal) init).getValue()); + } else { + acc.add(untypedInitializerLiterals((J.NewArray) init)); + } + } + return acc; + } + + public static class Matcher extends SimpleTraitMatcher { + private static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper(); + + private ObjectMapper mapper = DEFAULT_MAPPER; + + /** + * @param mapper A customized mapper, which should be rare, + * but possibly when you want a custom type factory. + * @return This matcher with a customized mapper set. + */ + public Matcher mapper(ObjectMapper mapper) { + this.mapper = mapper; + return this; + } + + @Override + protected @Nullable Literal test(Cursor cursor) { + Object value = cursor.getValue(); + return value instanceof J.Literal || + isNewArrayWithLiteralInitializer(value) ? + new Literal(cursor, mapper) : + null; + } + + private boolean isNewArrayWithLiteralInitializer(Object value) { + if (value instanceof J.NewArray) { + List init = ((J.NewArray) value).getInitializer(); + if (init == null) { + return false; + } + for (Expression expr : init) { + if (!(expr instanceof J.Literal) && + !isNewArrayWithLiteralInitializer(expr)) { + return false; + } + } + return true; + } + return false; + } + } +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/trait/Traits.java b/rewrite-java/src/main/java/org/openrewrite/java/trait/Traits.java new file mode 100644 index 000000000000..ee52f1882a44 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/trait/Traits.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 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 org.openrewrite.java.trait; + +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.MethodMatcher; + +public class Traits { + + public static Literal.Matcher literal() { + return new Literal.Matcher(); + } + + public static VariableAccess.Matcher variableAccess() { + return new VariableAccess.Matcher(); + } + + public static MethodAccess.Matcher methodAccess(MethodMatcher matcher) { + return new MethodAccess.Matcher(matcher); + } + + public static MethodAccess.Matcher methodAccess(String signature) { + return new MethodAccess.Matcher(signature); + } + + public static Annotated.Matcher annotated(AnnotationMatcher matcher) { + return new Annotated.Matcher(matcher); + } + + public static Annotated.Matcher annotated(String signature) { + return new Annotated.Matcher(signature); + } +} diff --git a/rewrite-java/src/test/java/org/openrewrite/java/trait/AnnotatedTest.java b/rewrite-java/src/test/java/org/openrewrite/java/trait/AnnotatedTest.java new file mode 100644 index 000000000000..025d1aecf874 --- /dev/null +++ b/rewrite-java/src/test/java/org/openrewrite/java/trait/AnnotatedTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024 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 org.openrewrite.java.trait; + +import org.junit.jupiter.api.Test; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.trait.Traits.annotated; + +public class AnnotatedTest implements RewriteTest { + + @Test + void attributes() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(() -> + annotated("@Example").asVisitor(a -> SearchResult.found(a.getTree(), + a.getDefaultAttribute("name") + .map(lit -> lit.getValue(String.class)) + .orElse("unknown")) + ) + )), + java( + """ + import java.lang.annotation.Repeatable; + @Repeatable + @interface Example { + String value() default ""; + String name() default ""; + } + """ + ), + java( + """ + @Example("test") + @Example(value = "test") + @Example(name = "test") + class Test { + } + """, + """ + /*~~(test)~~>*/@Example("test") + /*~~(test)~~>*/@Example(value = "test") + /*~~(test)~~>*/@Example(name = "test") + class Test { + } + """ + ) + ); + } +} diff --git a/rewrite-java/src/test/java/org/openrewrite/java/trait/LiteralTest.java b/rewrite-java/src/test/java/org/openrewrite/java/trait/LiteralTest.java new file mode 100644 index 000000000000..6da1fbfa6e49 --- /dev/null +++ b/rewrite-java/src/test/java/org/openrewrite/java/trait/LiteralTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2024 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 org.openrewrite.java.trait; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.jupiter.api.Test; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.test.RewriteTest; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.trait.Traits.literal; + +public class LiteralTest implements RewriteTest { + + @Test + void numericLiteral() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(() -> + literal().asVisitor(lit -> { + assertThat(lit.isNotNull()).isTrue(); + // NOTE: Jackson's coercion config allows us to + // coerce various numeric literal types to an Integer + // if we like + return SearchResult.found(lit.getTree(), + lit.getValue(Integer.class).toString()); + }) + ) + ), + java( + """ + class Test { + int n = 0; + int d = 0.0; + } + """, + """ + class Test { + int n = /*~~(0)~~>*/0; + int d = /*~~(0)~~>*/0.0; + } + """ + ) + ); + } + + @Test + void arrayLiteral() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(() -> + literal().asVisitor(lit -> { + assertThat(lit.isNotNull()).isTrue(); + return SearchResult.found(lit.getTree(), + String.join(",", lit.getValue(new TypeReference>() { + }))); + }) + ) + ), + java( + """ + class Test { + String[] s = new String[] { "a", "b", "c" }; + int[] n = new int[] { 0, 1, 2 }; + } + """, + """ + class Test { + String[] s = /*~~(a,b,c)~~>*/new String[] { /*~~(a)~~>*/"a", /*~~(b)~~>*/"b", /*~~(c)~~>*/"c" }; + int[] n = /*~~(0,1,2)~~>*/new int[] { /*~~(0)~~>*/0, /*~~(1)~~>*/1, /*~~(2)~~>*/2 }; + } + """ + ) + ); + } +} diff --git a/rewrite-java/src/test/java/org/openrewrite/java/trait/MethodAccessTest.java b/rewrite-java/src/test/java/org/openrewrite/java/trait/MethodAccessTest.java index 9b55fa3d6b2c..459766b134dc 100644 --- a/rewrite-java/src/test/java/org/openrewrite/java/trait/MethodAccessTest.java +++ b/rewrite-java/src/test/java/org/openrewrite/java/trait/MethodAccessTest.java @@ -23,6 +23,7 @@ import org.openrewrite.test.RewriteTest; import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.trait.Traits.methodAccess; import static org.openrewrite.test.RewriteTest.toRecipe; @SuppressWarnings("ALL") @@ -30,7 +31,7 @@ class MethodAccessTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { - spec.recipe(markMethodAccesses(new MethodAccess.Matcher( + spec.recipe(markMethodAccesses(methodAccess( new MethodMatcher("java.util.List add(..)", true)))); } diff --git a/rewrite-java/src/test/java/org/openrewrite/java/trait/VariableAccessTest.java b/rewrite-java/src/test/java/org/openrewrite/java/trait/VariableAccessTest.java index 6981de507cba..599736faea1d 100644 --- a/rewrite-java/src/test/java/org/openrewrite/java/trait/VariableAccessTest.java +++ b/rewrite-java/src/test/java/org/openrewrite/java/trait/VariableAccessTest.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.trait.Traits.variableAccess; import static org.openrewrite.test.RewriteTest.toRecipe; @SuppressWarnings("ALL") @@ -30,7 +31,7 @@ class VariableAccessTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { - spec.recipe(markVariableAccesses(new VariableAccess.Matcher())); + spec.recipe(markVariableAccesses(variableAccess())); } @Test