diff --git a/rewrite/src/main/java/dev/morphia/rewrite/recipes/MorphiaConfigMigration.java b/rewrite/src/main/java/dev/morphia/rewrite/recipes/MorphiaConfigMigration.java new file mode 100644 index 00000000000..13c6b3a30be --- /dev/null +++ b/rewrite/src/main/java/dev/morphia/rewrite/recipes/MorphiaConfigMigration.java @@ -0,0 +1,51 @@ +package dev.morphia.rewrite.recipes; + +import org.jetbrains.annotations.NotNull; +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.MethodMatcher; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J.Identifier; +import org.openrewrite.java.tree.J.MethodInvocation; +import org.openrewrite.java.tree.JavaType; + +public class MorphiaConfigMigration extends Recipe { + private static final String OLD_TYPE = "dev.morphia.mapping.MapperOptions"; + + private static final String NEW_TYPE = "dev.morphia.config.MorphiaConfig"; + + @Override + public String getDisplayName() { + return "Migrate Morphia MapperOptions to MorphiaConfig"; + } + + @Override + public String getDescription() { + return "Converts uses of dev.morphia.mapping.MapperOptions to dev.morphia.config.MorphiaConfig."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesType<>(OLD_TYPE, true), + new MorphiaConfigMigrationVisitor()); + } + + private static class MorphiaConfigMigrationVisitor extends JavaIsoVisitor { + private static final MethodMatcher BUILDER_MATCHER = new MethodMatcher("dev.morphia.mapping.MapperOptions builder()"); + + @Override + public MethodInvocation visitMethodInvocation(@NotNull MethodInvocation methodInvocation, @NotNull ExecutionContext context) { + if (BUILDER_MATCHER.matches(methodInvocation)) { + return methodInvocation.withName(methodInvocation.getName().withSimpleName("load")) + .withSelect(((Identifier) methodInvocation.getSelect()) + .withSimpleName("MorphiaConfig") + .withType(JavaType.buildType(NEW_TYPE))); + } else { + return super.visitMethodInvocation(methodInvocation, context); + } + } + } +} \ No newline at end of file diff --git a/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/RemoveMethodInvocations.java b/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/RemoveMethodInvocations.java new file mode 100644 index 00000000000..09c23aabafd --- /dev/null +++ b/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/RemoveMethodInvocations.java @@ -0,0 +1,72 @@ +/* + * 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 dev.morphia.rewrite.recipes.openrewrite; + +import java.util.Objects; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.search.UsesMethod; + +import static java.util.Collections.singletonList; + +public class RemoveMethodInvocations extends Recipe { + @Option(displayName = "Method pattern", description = "A pattern to match method invocations for removal.", example = "java.lang.StringBuilder append(java.lang.String)") + String methodPattern; + + @Override + public String getDisplayName() { + return "Remove method invocations"; + } + + @Override + public String getDescription() { + return "Remove method invocations if syntactically safe."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesMethod<>(methodPattern), + new RemoveMethodInvocationsVisitor(singletonList(methodPattern))); + } + + public String getMethodPattern() { + return methodPattern; + } + + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RemoveMethodInvocations that)) { + return false; + } + if (!super.equals(o)) { + return false; + } + return Objects.equals(methodPattern, that.methodPattern); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), methodPattern); + } +} \ No newline at end of file diff --git a/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/RemoveMethodInvocationsVisitor.java b/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/RemoveMethodInvocationsVisitor.java new file mode 100644 index 00000000000..b5de6c4d8ff --- /dev/null +++ b/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/RemoveMethodInvocationsVisitor.java @@ -0,0 +1,284 @@ +/* + * 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 dev.morphia.rewrite.recipes.openrewrite; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JContainer; +import org.openrewrite.java.tree.JRightPadded; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.marker.Marker; + +import static org.openrewrite.Tree.randomId; + +/** + * This visitor removes method calls matching some criteria. + * Tries to intelligently remove within chains without breaking other methods in the chain. + */ +public class RemoveMethodInvocationsVisitor extends JavaVisitor { + private final Map>> matchers; + + public RemoveMethodInvocationsVisitor(Map>> matchers) { + this.matchers = matchers; + } + + public RemoveMethodInvocationsVisitor(List methodSignatures) { + this(methodSignatures.stream().collect(Collectors.toMap( + MethodMatcher::new, + signature -> args -> true))); + } + + @Override + public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx); + + if (inMethodCallChain()) { + List newArgs = ListUtils.map(m.getArguments(), arg -> (Expression) this.visit(arg, ctx)); + return m.withArguments(newArgs); + } + + J j = removeMethods(m, 0, isLambdaBody(), new Stack<>()); + if (j != null) { + j = j.withPrefix(m.getPrefix()); + // There should always be + if (!m.getArguments().isEmpty() && m.getArguments().stream().allMatch(ToBeRemoved::hasMarker)) { + return ToBeRemoved.withMarker(j); + } + } + + //noinspection DataFlowIssue allow returning null to remove the element + return j; + } + + private @Nullable J removeMethods(Expression expression, int depth, boolean isLambdaBody, Stack selectAfter) { + if (!(expression instanceof J.MethodInvocation)) { + return expression; + } + + boolean isStatement = isStatement(); + J.MethodInvocation m = (J.MethodInvocation) expression; + + if (m.getMethodType() == null || m.getSelect() == null) { + return expression; + } + + if (matchers.entrySet().stream().anyMatch(entry -> matches(m, entry.getKey(), entry.getValue()))) { + if (m.getSelect() instanceof J.Identifier || m.getSelect() instanceof J.NewClass) { + boolean keepSelect = depth != 0; + if (keepSelect) { + selectAfter.add(getSelectAfter(m)); + return m.getSelect(); + } else { + if (isStatement) { + return null; + } else if (isLambdaBody) { + return ToBeRemoved.withMarker(J.Block.createEmptyBlock()); + } else { + return m.getSelect(); + } + } + } else if (m.getSelect() instanceof J.MethodInvocation) { + return removeMethods(m.getSelect(), depth, isLambdaBody, selectAfter); + } + } + + J.MethodInvocation method = m.withSelect((Expression) removeMethods(m.getSelect(), depth + 1, isLambdaBody, selectAfter)); + + // inherit prefix + if (!selectAfter.isEmpty()) { + method = inheritSelectAfter(method, selectAfter); + } + + return method; + } + + private boolean matches(J.MethodInvocation m, MethodMatcher matcher, Predicate> argsMatches) { + return matcher.matches(m) && argsMatches.test(m.getArguments()); + } + + private boolean isStatement() { + return getCursor().dropParentUntil(p -> p instanceof J.Block || + p instanceof J.Assignment || + p instanceof J.VariableDeclarations.NamedVariable || + p instanceof J.Return || + p instanceof JContainer || + p == Cursor.ROOT_VALUE).getValue() instanceof J.Block; + } + + private boolean isLambdaBody() { + if (getCursor().getParent() == null) { + return false; + } + Object parent = getCursor().getParent().getValue(); + return parent instanceof J.Lambda && ((J.Lambda) parent).getBody() == getCursor().getValue(); + } + + private boolean inMethodCallChain() { + return getCursor().dropParentUntil(p -> !(p instanceof JRightPadded)).getValue() instanceof J.MethodInvocation; + } + + private J.MethodInvocation inheritSelectAfter(J.MethodInvocation method, Stack prefix) { + return (J.MethodInvocation) new JavaIsoVisitor() { + @Override + public @Nullable JRightPadded visitRightPadded(@Nullable JRightPadded right, + JRightPadded.Location loc, + ExecutionContext executionContext) { + if (right == null) + return null; + return prefix.isEmpty() ? right : right.withAfter(prefix.pop()); + } + }.visitNonNull(method, new InMemoryExecutionContext()); + } + + private Space getSelectAfter(J.MethodInvocation method) { + return new JavaIsoVisitor>() { + @Override + public @Nullable JRightPadded visitRightPadded(@Nullable JRightPadded right, + JRightPadded.Location loc, + List selectAfter) { + if (selectAfter.isEmpty()) { + selectAfter.add(right == null ? Space.EMPTY : right.getAfter()); + } + return right; + } + }.reduce(method, new ArrayList<>()).get(0); + } + + @SuppressWarnings("unused") // used in rewrite-spring / convenient for consumers + public static Predicate> isTrueArgument() { + return args -> args.size() == 1 && isTrue(args.get(0)); + } + + @SuppressWarnings("unused") // used in rewrite-spring / convenient for consumers + public static Predicate> isFalseArgument() { + return args -> args.size() == 1 && isFalse(args.get(0)); + } + + public static boolean isTrue(Expression expression) { + return isBoolean(expression, Boolean.TRUE); + } + + public static boolean isFalse(Expression expression) { + return isBoolean(expression, Boolean.FALSE); + } + + private static boolean isBoolean(Expression expression, Boolean b) { + if (expression instanceof J.Literal) { + return expression.getType() == JavaType.Primitive.Boolean && b.equals(((J.Literal) expression).getValue()); + } + return false; + } + + @Override + public J.Lambda visitLambda(J.Lambda lambda, ExecutionContext ctx) { + lambda = (J.Lambda) super.visitLambda(lambda, ctx); + J body = lambda.getBody(); + if (body instanceof J.MethodInvocation && ToBeRemoved.hasMarker(body)) { + Expression select = ((J.MethodInvocation) body).getSelect(); + List parameters = lambda.getParameters().getParameters(); + if (select instanceof J.Identifier && !parameters.isEmpty() && parameters.get(0) instanceof J.VariableDeclarations) { + J.VariableDeclarations declarations = (J.VariableDeclarations) parameters.get(0); + if (((J.Identifier) select).getSimpleName().equals(declarations.getVariables().get(0).getSimpleName())) { + return ToBeRemoved.withMarker(lambda); + } + } else if (select instanceof J.MethodInvocation) { + return lambda.withBody(select.withPrefix(body.getPrefix())); + } + } else if (body instanceof J.Block && ToBeRemoved.hasMarker(body)) { + return ToBeRemoved.withMarker(lambda.withBody(ToBeRemoved.removeMarker(body))); + } + return lambda; + } + + @Override + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + int statementsCount = block.getStatements().size(); + + block = (J.Block) super.visitBlock(block, ctx); + List statements = block.getStatements(); + if (!statements.isEmpty() && statements.stream().allMatch(ToBeRemoved::hasMarker)) { + return ToBeRemoved.withMarker(block.withStatements(Collections.emptyList())); + } + + if (statementsCount > 0 && statements.isEmpty()) { + return ToBeRemoved.withMarker(block.withStatements(Collections.emptyList())); + } + + if (statements.stream().anyMatch(ToBeRemoved::hasMarker)) { + //noinspection DataFlowIssue + return block.withStatements(statements.stream() + .filter(s -> !ToBeRemoved.hasMarker(s) + || s instanceof J.MethodInvocation && ((J.MethodInvocation) s).getSelect() instanceof J.MethodInvocation) + .map(s -> s instanceof J.MethodInvocation && ToBeRemoved.hasMarker(s) + ? ((J.MethodInvocation) s).getSelect().withPrefix(s.getPrefix()) + : s) + .collect(Collectors.toList())); + } + return block; + } + + static class ToBeRemoved implements Marker { + UUID id; + + static J2 withMarker(J2 j) { + return j.withMarkers(j.getMarkers().addIfAbsent(new ToBeRemoved(randomId()))); + } + + static J2 removeMarker(J2 j) { + return j.withMarkers(j.getMarkers().removeByType(ToBeRemoved.class)); + } + + static boolean hasMarker(J j) { + return j.getMarkers().findFirst(ToBeRemoved.class).isPresent(); + } + + public ToBeRemoved(UUID id) { + this.id = id; + } + + @Override + public UUID getId() { + return id; + } + + @Override + @SuppressWarnings("unchecked") + public M withId(@NotNull UUID id) { + return (M) (this.id == id ? this : new ToBeRemoved(id)); + } + + } +} \ No newline at end of file diff --git a/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/package-info.java b/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/package-info.java new file mode 100644 index 00000000000..a77f347f6c5 --- /dev/null +++ b/rewrite/src/main/java/dev/morphia/rewrite/recipes/openrewrite/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains sources of classes borrowed from openrewrite and modified for morphia's specific use. + */ +package dev.morphia.rewrite.recipes.openrewrite; \ No newline at end of file diff --git a/rewrite/src/main/resources/META-INF/rewrite/morphia3.yml b/rewrite/src/main/resources/META-INF/rewrite/morphia3.yml deleted file mode 100644 index 5eb0c253a39..00000000000 --- a/rewrite/src/main/resources/META-INF/rewrite/morphia3.yml +++ /dev/null @@ -1,14 +0,0 @@ -type: specs.openrewrite.org/v1beta/recipe -name: dev.morphia.UpgradeToMorphia30 -displayName: Upgrade to Morphia 3.0 -description: Adopt new dependencies and breaking changes in moving to Morphia 3. -recipeList: - - dev.morphia.rewrite.recipes.PipelineRewriteStage1: - matchOverrides: "true" - ignoreDefinition: "false" - - dev.morphia.rewrite.recipes.PipelineRewriteStage2: - matchOverrides: "true" - ignoreDefinition: "false" - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: dev.morphia.EntityInterceptor - newFullyQualifiedTypeName: dev.morphia.EntityListener diff --git a/rewrite/src/main/resources/META-INF/rewrite/rewrite.yml b/rewrite/src/main/resources/META-INF/rewrite/rewrite.yml new file mode 100644 index 00000000000..20a80deb760 --- /dev/null +++ b/rewrite/src/main/resources/META-INF/rewrite/rewrite.yml @@ -0,0 +1,19 @@ +type: specs.openrewrite.org/v1beta/recipe +name: dev.morphia.UpgradeToMorphia30 +displayName: Upgrade to Morphia 3.0 +description: Adopt new dependencies and breaking changes in moving to Morphia 3. +recipeList: + - dev.morphia.rewrite.recipes.openrewrite.RemoveMethodInvocations: + methodPattern: dev.morphia.mapping.MapperOptions.Builder cacheClassLookups(boolean) + - dev.morphia.rewrite.recipes.openrewrite.RemoveMethodInvocations: + methodPattern: dev.morphia.mapping.MapperOptions.Builder disableEmbeddedIndexes(boolean) + - dev.morphia.rewrite.recipes.openrewrite.RemoveMethodInvocations: + methodPattern: dev.morphia.mapping.MapperOptions.Builder build() + - dev.morphia.rewrite.recipes.PipelineRewrite + - dev.morphia.rewrite.recipes.MorphiaConfigMigration + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: dev.morphia.mapping.MapperOptions + newFullyQualifiedTypeName: dev.morphia.config.MorphiaConfig + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: dev.morphia.EntityInterceptor + newFullyQualifiedTypeName: dev.morphia.EntityListener diff --git a/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/Morphia2RewriteTest.java b/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/Morphia2RewriteTest.java deleted file mode 100644 index 7bc2f16a981..00000000000 --- a/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/Morphia2RewriteTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package dev.morphia.rewrite.recipes.test; - -public abstract class Morphia2RewriteTest extends MorphiaRewriteTest { -} diff --git a/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/MorphiaConfigMigrationTest.java b/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/MorphiaConfigMigrationTest.java new file mode 100644 index 00000000000..0f8ea152bfa --- /dev/null +++ b/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/MorphiaConfigMigrationTest.java @@ -0,0 +1,48 @@ +package dev.morphia.rewrite.recipes.test; + +import dev.morphia.rewrite.recipes.MorphiaConfigMigration; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.openrewrite.Recipe; + +import static org.openrewrite.java.Assertions.java; + +@SuppressWarnings("removal") +public class MorphiaConfigMigrationTest extends MorphiaRewriteTest { + @Override + protected @NotNull Recipe getRecipe() { + return new MorphiaConfigMigration(); + } + + @Test + void update() { + rewriteRun( + //language=java + java( + """ + import dev.morphia.mapping.MapperOptions; + import dev.morphia.mapping.NamingStrategy; + + public class UnwrapTest { + public void update() { + MapperOptions options = MapperOptions.builder() + .collectionNaming(NamingStrategy.camelCase()) + .build(); + } + } + """, + """ + import dev.morphia.mapping.MapperOptions; + import dev.morphia.mapping.NamingStrategy; + + public class UnwrapTest { + public void update() { + MapperOptions options = MorphiaConfig.load() + .collectionNaming(NamingStrategy.camelCase()); + } + } + """)); + } + +} diff --git a/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/RemoveMethodInvocationsTest.java b/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/RemoveMethodInvocationsTest.java new file mode 100644 index 00000000000..ce24d217c7d --- /dev/null +++ b/rewrite/src/test/java/dev/morphia/rewrite/recipes/test/RemoveMethodInvocationsTest.java @@ -0,0 +1,46 @@ +package dev.morphia.rewrite.recipes.test; + +import dev.morphia.rewrite.recipes.openrewrite.RemoveMethodInvocations; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.openrewrite.Recipe; + +import static org.openrewrite.java.Assertions.java; + +@SuppressWarnings("removal") +public class RemoveMethodInvocationsTest extends MorphiaRewriteTest { + @Override + protected @NotNull Recipe getRecipe() { + RemoveMethodInvocations invocations = new RemoveMethodInvocations(); + invocations.setMethodPattern("dev.morphia.mapping.MapperOptions.Builder build()"); + return invocations; + } + + @Test + void update() { + rewriteRun( + //language=java + java( + """ + import dev.morphia.mapping.MapperOptions; + + public class UnwrapTest { + public void update() { + MapperOptions options = MapperOptions.builder() + .build(); + } + } + """, + """ + import dev.morphia.mapping.MapperOptions; + + public class UnwrapTest { + public void update() { + MapperOptions options = MapperOptions.builder(); + } + } + """)); + } + +}