From 541e4803a4da19ae315643e825dd824c574ce35f Mon Sep 17 00:00:00 2001 From: Shivani Sharma Date: Sat, 7 Sep 2024 05:42:03 +1000 Subject: [PATCH] Move Junit 5 assertThrows to last statement in lamdba (#590) * Initial stab at recipe to move assertThrows to last statement in lamdba * Silence the review bot by applying recommended changes * Handle the non-assignment case already * Add a note on another case to check * Handle case where variable is declared with assertThrows * Add test case for assertThrows with error message * Update tests to account for removal of unnecessary curly braces. Also add warning of possible compilation error in description. * Update warning in description --------- Co-authored-by: Tim te Beek --- .../junit5/AssertThrowsOnLastStatement.java | 146 ++++++++++ .../AssertThrowsOnLastStatementTest.java | 255 ++++++++++++++++++ 2 files changed, 401 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/testing/junit5/AssertThrowsOnLastStatement.java create mode 100644 src/test/java/org/openrewrite/java/testing/junit5/AssertThrowsOnLastStatementTest.java diff --git a/src/main/java/org/openrewrite/java/testing/junit5/AssertThrowsOnLastStatement.java b/src/main/java/org/openrewrite/java/testing/junit5/AssertThrowsOnLastStatement.java new file mode 100644 index 000000000..ffbb0f330 --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/junit5/AssertThrowsOnLastStatement.java @@ -0,0 +1,146 @@ +/* + * 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. + */ +package org.openrewrite.java.testing.junit5; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.staticanalysis.LambdaBlockToExpression; + +import java.util.Collections; +import java.util.List; + +import static java.util.Collections.singletonList; + + +public class AssertThrowsOnLastStatement extends Recipe { + + @Override + public String getDisplayName() { + return "Applies Junit 5 assertThrows on last statement in lamdba block only"; + } + + @Override + public String getDescription() { + return "Applies Junit 5 assertThrows on last statement in lambda block only, in extremely rare cases may cause " + + "compilation errors if lambda uses non final variables."; + } + + @Override + public TreeVisitor getVisitor() { + MethodMatcher assertThrowsMatcher = new MethodMatcher( + "org.junit.jupiter.api.Assertions assertThrows(java.lang.Class, org.junit.jupiter.api.function.Executable, ..)"); + return Preconditions.check(new UsesMethod<>(assertThrowsMatcher), new JavaIsoVisitor() { + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl, ExecutionContext ctx) { + J.MethodDeclaration m = super.visitMethodDeclaration(methodDecl, ctx); + + m = m.withBody(m.getBody().withStatements(ListUtils.flatMap(m.getBody().getStatements(), methodStatement -> { + J statementToCheck = methodStatement; + final J.VariableDeclarations assertThrowsWithVarDec; + final J.VariableDeclarations.NamedVariable assertThrowsVar; + + if (methodStatement instanceof J.VariableDeclarations) { + assertThrowsWithVarDec = (J.VariableDeclarations) methodStatement; + List assertThrowsNamedVars = assertThrowsWithVarDec.getVariables(); + if (assertThrowsNamedVars.size() != 1) { + return methodStatement; + } + + // has variable declaration for assertThrows eg Throwable ex = assertThrows(....) + assertThrowsVar = assertThrowsNamedVars.get(0); + statementToCheck = assertThrowsVar.getInitializer(); + } else { + assertThrowsWithVarDec = null; + assertThrowsVar = null; + } + + if (!(statementToCheck instanceof J.MethodInvocation)) { + return methodStatement; + } + + J.MethodInvocation methodInvocation = (J.MethodInvocation) statementToCheck; + if (!assertThrowsMatcher.matches(methodInvocation)) { + return methodStatement; + } + + List arguments = methodInvocation.getArguments(); + if (arguments.size() <= 1) { + return methodStatement; + } + + Expression arg = arguments.get(1); + if (!(arg instanceof J.Lambda)) { + return methodStatement; + } + + J.Lambda lambda = (J.Lambda) arg; + if (!(lambda.getBody() instanceof J.Block)) { + return methodStatement; + } + + J.Block body = (J.Block) lambda.getBody(); + if (body == null) { + return methodStatement; + } + + List lambdaStatements = body.getStatements(); + if (lambdaStatements.size() <= 1) { + return methodStatement; + } + + // TODO Check to see if last line in lambda does not use a non-final variable + + // move all the statements from the body into before the method invocation, except last one + return ListUtils.map(lambdaStatements, (idx, lambdaStatement) -> { + if (idx < lambdaStatements.size() - 1) { + return lambdaStatement.withPrefix(methodStatement.getPrefix().withComments(Collections.emptyList())); + } + + J.MethodInvocation newAssertThrows = methodInvocation.withArguments( + ListUtils.map(arguments, (argIdx, argument) -> { + if (argIdx == 1) { + // Only retain the last statement in the lambda block + return lambda.withBody(body.withStatements(singletonList(lambdaStatement))); + } + return argument; + }) + ); + + if (assertThrowsWithVarDec == null) { + return newAssertThrows; + } + + J.VariableDeclarations.NamedVariable newAssertThrowsVar = assertThrowsVar.withInitializer(newAssertThrows); + return assertThrowsWithVarDec.withVariables(singletonList(newAssertThrowsVar)); + }); + + }))); + + doAfterVisit(new LambdaBlockToExpression().getVisitor()); + return m; + } + }); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/junit5/AssertThrowsOnLastStatementTest.java b/src/test/java/org/openrewrite/java/testing/junit5/AssertThrowsOnLastStatementTest.java new file mode 100644 index 000000000..744fed420 --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/junit5/AssertThrowsOnLastStatementTest.java @@ -0,0 +1,255 @@ +/* + * 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. + */ +package org.openrewrite.java.testing.junit5; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class AssertThrowsOnLastStatementTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion() + //.logCompilationWarningsAndErrors(true) + .classpathFromResources(new InMemoryExecutionContext(), "junit-jupiter-api-5.9")) + .recipe(new AssertThrowsOnLastStatement()); + } + + @DocumentExample + @Test + void applyToLastStatementWithDeclaringVariableThreeLines() { + //language=java + rewriteRun( + java( + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> { + foo(); + System.out.println("foo"); + foo(); + }); + assertEquals("Error message", exception.getMessage()); + } + void foo() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + foo(); + System.out.println("foo"); + Throwable exception = assertThrows(IllegalArgumentException.class, () -> + foo()); + assertEquals("Error message", exception.getMessage()); + } + void foo() { + } + } + """ + ) + ); + } + + @Test + void applyToLastStatementWithDeclaringVariableThreeLinesHasLineBefore() { + //language=java + rewriteRun( + java( + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + System.out.println("bla"); + Throwable exception = assertThrows(IllegalArgumentException.class, () -> { + foo(); + System.out.println("foo"); + foo(); + }); + assertEquals("Error message", exception.getMessage()); + } + void foo() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + System.out.println("bla"); + foo(); + System.out.println("foo"); + Throwable exception = assertThrows(IllegalArgumentException.class, () -> + foo()); + assertEquals("Error message", exception.getMessage()); + } + void foo() { + } + } + """ + ) + ); + } + + @Test + void applyToLastStatementNoDeclaringVariableTwoLinesNoLinesAfter() { + //language=java + rewriteRun( + java( + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + assertThrows(IllegalArgumentException.class, () -> { + System.out.println("foo"); + foo(); + }); + } + void foo() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + System.out.println("foo"); + assertThrows(IllegalArgumentException.class, () -> + foo()); + } + void foo() { + } + } + """ + ) + ); + } + + @Test + void applyToLastStatementHasMessage() { + //language=java + rewriteRun( + java( + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + assertThrows(IllegalArgumentException.class, () -> { + System.out.println("foo"); + foo(); + }, "message"); + } + void foo() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + System.out.println("foo"); + assertThrows(IllegalArgumentException.class, () -> { + foo(); + }, "message"); + } + void foo() { + } + } + """ + ) + ); + } + + @Test + void makeNoChangesAsOneLine() { + //language=java + rewriteRun( + java( + """ + import org.junit.jupiter.api.Test; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MyTest { + + @Test + public void test() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> foo()); + assertEquals("Error message", exception.getMessage()); + } + void foo() { + } + } + """ + ) + ); + } +}