diff --git a/src/main/java/org/openrewrite/java/migrate/ReferenceCloneMethod.java b/src/main/java/org/openrewrite/java/migrate/ReferenceCloneMethod.java new file mode 100644 index 0000000000..5fa45dd207 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/ReferenceCloneMethod.java @@ -0,0 +1,89 @@ +/* + * 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.migrate; + +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.JavaTemplate; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.ShortenFullyQualifiedTypeReferences; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.TypeUtils; + + +@Value +@EqualsAndHashCode(callSuper = false) +class ReferenceCloneMethod extends Recipe { + private static final MethodMatcher REFERENCE_CLONE = new MethodMatcher("java.lang.ref.Reference clone()", true); + + @Override + public String getDisplayName() { + return "Replace `java.lang.ref.Reference.clone()` with constructor call"; + } + + @Override + public String getDescription() { + return "The recipe replaces any clone calls that may resolve to a `java.lang.ref.Reference.clone()` " + + "or any of its known subclasses: `java.lang.ref.PhantomReference`, `java.lang.ref.SoftReference`, and `java.lang.ref.WeakReference` " + + "with a constructor call passing in the referent and reference queue as parameters."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + new UsesMethod<>(REFERENCE_CLONE), + new JavaVisitor() { + private static final String REFERENCE_CLONE_REPLACED = "REFERENCE_CLONE_REPLACED"; + + @Override + public J visitTypeCast(J.TypeCast typeCast, ExecutionContext ctx) { + J j = super.visitTypeCast(typeCast, ctx); + if (Boolean.TRUE.equals(getCursor().pollNearestMessage(REFERENCE_CLONE_REPLACED)) + && j instanceof J.TypeCast) { + J.TypeCast tc = (J.TypeCast) j; + if (TypeUtils.isOfType(tc.getType(), tc.getExpression().getType())) { + return tc.getExpression(); + } + } + return j; + } + + @Override + public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + super.visitMethodInvocation(method, ctx); + if (REFERENCE_CLONE.matches(method) && method.getSelect() instanceof J.Identifier) { + J.Identifier methodRef = (J.Identifier) method.getSelect(); + String template = "new " + methodRef.getType().toString() + "(" + methodRef.getSimpleName() + ", new ReferenceQueue<>())"; + getCursor().putMessageOnFirstEnclosing(J.TypeCast.class, REFERENCE_CLONE_REPLACED, true); + J replacement = JavaTemplate.builder(template) + .contextSensitive() + .imports("java.lang.ref.ReferenceQueue") + .build().apply(getCursor(), method.getCoordinates().replace()); + doAfterVisit(ShortenFullyQualifiedTypeReferences.modifyOnly(replacement)); + return replacement; + } + return method; + } + } + ); + } +} diff --git a/src/main/resources/META-INF/rewrite/java-version-11.yml b/src/main/resources/META-INF/rewrite/java-version-11.yml index 0ee8ac7099..5fbd0db412 100644 --- a/src/main/resources/META-INF/rewrite/java-version-11.yml +++ b/src/main/resources/META-INF/rewrite/java-version-11.yml @@ -67,8 +67,8 @@ recipeList: - org.openrewrite.java.migrate.RemovedSecurityManagerMethods - org.openrewrite.java.migrate.UpgradePluginsForJava11 - org.openrewrite.java.migrate.RemovedPolicy + - org.openrewrite.java.migrate.ReferenceCloneMethod - org.openrewrite.java.migrate.ThreadStopDestroy - --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.migrate.UpgradeBuildToJava11 @@ -263,4 +263,4 @@ recipeList: - org.openrewrite.java.migrate.RemoveMethodInvocation: methodPattern: java.lang.Thread destroy() - org.openrewrite.java.migrate.RemoveMethodInvocation: - methodPattern: java.lang.Thread stop(java.lang.Throwable) \ No newline at end of file + methodPattern: java.lang.Thread stop(java.lang.Throwable) diff --git a/src/test/java/org/openrewrite/java/migrate/ReferenceCloneMethodTest.java b/src/test/java/org/openrewrite/java/migrate/ReferenceCloneMethodTest.java new file mode 100644 index 0000000000..0514c013f3 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/ReferenceCloneMethodTest.java @@ -0,0 +1,93 @@ +/* + * 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.migrate; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class ReferenceCloneMethodTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ReferenceCloneMethod()); + } + + @DocumentExample + @Test + void referenceCloneRemoval() { + rewriteRun( + //language=java + java( + """ + import java.lang.ref.WeakReference; + import java.lang.ref.SoftReference; + import java.lang.ref.PhantomReference; + + class Foo { + void foo() throws Exception{ + WeakReference ref = new WeakReference(null); + WeakReference ref1 = (WeakReference) ref.clone(); + SoftReference ref3 = new SoftReference(null); + SoftReference ref4 = (SoftReference) ref3.clone(); + PhantomReference ref5 = new PhantomReference(null,null); + PhantomReference ref6 = (PhantomReference) ref5.clone(); + } + } + """, + """ + import java.lang.ref.WeakReference; + import java.lang.ref.SoftReference; + import java.lang.ref.PhantomReference; + + class Foo { + void foo() throws Exception{ + WeakReference ref = new WeakReference(null); + WeakReference ref1 = new WeakReference(ref, new ReferenceQueue<>()); + SoftReference ref3 = new SoftReference(null); + SoftReference ref4 = new SoftReference(ref3, new ReferenceQueue<>()); + PhantomReference ref5 = new PhantomReference(null,null); + PhantomReference ref6 = new PhantomReference(ref5, new ReferenceQueue<>()); + } + } + """ + ) + ); + } + + @Test + void noCloneRemoval() { + rewriteRun( + //language=java + java( + """ + class ClonableClass implements Cloneable { + public ClonableClass(int id) { + } + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + } + """ + ) + ); + } +}