Skip to content

Commit c78bbd3

Browse files
Simplify chained AssertJ assertions (#385)
* initial commit * working draft, need to update yaml and add new test file for full list of test cases * added rspec tag * Update src/main/java/org/openrewrite/java/testing/assertj/SimplifyChainedAssertJAssertions.java Co-authored-by: Tim te Beek <[email protected]> * stashing changes * Add missing license header * Add another missing license header * Update src/main/java/org/openrewrite/java/testing/assertj/SimplifyChainedAssertJAssertions.java Co-authored-by: Tim te Beek <[email protected]> * updates to paramterized tests * finished adding parameterized tests, all pass except for one * Update src/main/resources/META-INF/rewrite/assertj.yml Co-authored-by: Tim te Beek <[email protected]> * Update src/main/resources/META-INF/rewrite/assertj.yml Co-authored-by: Tim te Beek <[email protected]> * Update src/main/java/org/openrewrite/java/testing/assertj/SimplifyChainedAssertJAssertions.java Co-authored-by: Tim te Beek <[email protected]> * Update src/test/java/org/openrewrite/java/testing/assertj/MigrateChainedAssertToAssertJTest.java Co-authored-by: Tim te Beek <[email protected]> * Rename remaining yaml recipes * Drop redeclared recipes and fix compilation issues * Demonstrate issue with chained assertions * Fix chained assertion case * Demonstrate differing types with additional test * fixed some of the tests failing * working on function to filter out some recipes running on tests that they shouldn't run on * stashing changes * fixed issue where recipes run prematurely, added fourth required type parameter * fixed issue where Strings were being passed directly into method that was expecting a Path, String is now wrapped in Path.of() * stashing changes * most tests pass except, the ArrayList tests still do not pass for some reason * Move recipe to AssertJ best practices * Fix assertThat method matcher * Disable the array replacements, as array.length is not a method invocation * Update failing test case as the input does not compile * Fix yaml recipe for containsOnlyKeys * Add an example to include with our documentation * Formatting and early returns * Disable failing test * this recipe and replace the UseExplicitSize, IsEmpty and Contains * Drop UseExplicitContains; cover doesNotContain * Drop UseExplicitIsEmpty * Drop UseExplicitSize * Drop Array replacements * Adopt TypeUtils.isAssignableTo for simplicity * Document that .as(String) is not yet supported --------- Co-authored-by: Tim te Beek <[email protected]>
1 parent 847f3d8 commit c78bbd3

File tree

10 files changed

+1313
-687
lines changed

10 files changed

+1313
-687
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.testing.assertj;
17+
18+
import lombok.AllArgsConstructor;
19+
import lombok.NoArgsConstructor;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Option;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.internal.lang.Nullable;
25+
import org.openrewrite.java.JavaIsoVisitor;
26+
import org.openrewrite.java.JavaParser;
27+
import org.openrewrite.java.JavaTemplate;
28+
import org.openrewrite.java.MethodMatcher;
29+
import org.openrewrite.java.tree.Expression;
30+
import org.openrewrite.java.tree.J;
31+
import org.openrewrite.java.tree.TypeUtils;
32+
33+
import java.util.ArrayList;
34+
import java.util.Collections;
35+
import java.util.List;
36+
import java.util.Set;
37+
38+
@AllArgsConstructor
39+
@NoArgsConstructor
40+
public class SimplifyChainedAssertJAssertion extends Recipe {
41+
@Option(displayName = "AssertJ Assertion",
42+
description = "The chained AssertJ assertion to move to dedicated assertion.",
43+
example = "equals",
44+
required = false)
45+
@Nullable
46+
String chainedAssertion;
47+
48+
@Option(displayName = "AssertJ Assertion",
49+
description = "The AssertJ assert that should be replaced.",
50+
example = "isTrue",
51+
required = false)
52+
@Nullable
53+
String assertToReplace;
54+
55+
@Option(displayName = "AssertJ Assertion",
56+
description = "The AssertJ method to migrate to.",
57+
example = "isEqualTo",
58+
required = false)
59+
@Nullable
60+
String dedicatedAssertion;
61+
62+
@Option(displayName = "Required Type",
63+
description = "Specifies the type the recipe should run on.",
64+
example = "java.lang.String",
65+
required = false)
66+
@Nullable
67+
String requiredType;
68+
69+
@Override
70+
public String getDisplayName() {
71+
return "Simplify AssertJ chained assertions";
72+
}
73+
74+
@Override
75+
public Set<String> getTags() {
76+
return Collections.singleton("RSPEC-5838");
77+
}
78+
79+
@Override
80+
public String getDescription() {
81+
return "Many AssertJ chained assertions have dedicated assertions that function the same. " +
82+
"It is best to use the dedicated assertions.";
83+
}
84+
85+
@Override
86+
public TreeVisitor<?, ExecutionContext> getVisitor() {
87+
return new SimplifyChainedAssertJAssertionsVisitor();
88+
}
89+
90+
private class SimplifyChainedAssertJAssertionsVisitor extends JavaIsoVisitor<ExecutionContext> {
91+
private final MethodMatcher ASSERT_THAT_MATCHER = new MethodMatcher("org.assertj.core.api.Assertions assertThat(..)");
92+
private final MethodMatcher CHAINED_ASSERT_MATCHER = new MethodMatcher("java..* " + chainedAssertion + "(..)");
93+
private final MethodMatcher ASSERT_TO_REPLACE = new MethodMatcher("org.assertj.core.api.* " + assertToReplace + "(..)");
94+
95+
@Override
96+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation methodInvocation, ExecutionContext ctx) {
97+
J.MethodInvocation mi = super.visitMethodInvocation(methodInvocation, ctx);
98+
99+
// assert has correct assertion
100+
if (!ASSERT_TO_REPLACE.matches(mi)) {
101+
return mi;
102+
}
103+
104+
// assertThat has method call
105+
J.MethodInvocation assertThat = (J.MethodInvocation) mi.getSelect();
106+
if (!ASSERT_THAT_MATCHER.matches(assertThat) || !(assertThat.getArguments().get(0) instanceof J.MethodInvocation)) {
107+
return mi;
108+
}
109+
110+
J.MethodInvocation assertThatArg = (J.MethodInvocation) assertThat.getArguments().get(0);
111+
if (!CHAINED_ASSERT_MATCHER.matches(assertThatArg) && !hasZeroArgument(mi)) {
112+
return mi;
113+
}
114+
115+
// method call has select
116+
Expression select = assertThatArg.getSelect() != null ? assertThatArg.getSelect() : assertThatArg;
117+
if (!TypeUtils.isAssignableTo(requiredType, select.getType())) {
118+
return mi;
119+
}
120+
121+
List<Expression> arguments = new ArrayList<>(Collections.singletonList(select));
122+
String template = getTemplate(arguments, assertThatArg, mi);
123+
String formattedTemplate = String.format(template, dedicatedAssertion);
124+
return JavaTemplate.builder(formattedTemplate)
125+
.contextSensitive()
126+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "junit-jupiter-api-5.9", "assertj-core-3.24"))
127+
.build()
128+
.apply(getCursor(), mi.getCoordinates().replace(), arguments.toArray());
129+
}
130+
}
131+
132+
private String getTemplate(List<Expression> arguments, J.MethodInvocation assertThatArg, J.MethodInvocation methodToReplace) {
133+
if (hasZeroArgument(methodToReplace)) {
134+
return "assertThat(#{any()}).%s()";
135+
}
136+
137+
if (!(assertThatArg.getArguments().get(0) instanceof J.Empty)
138+
&& !(methodToReplace.getArguments().get(0) instanceof J.Empty)) {
139+
// Note: this should be the only case when more than one argument needs to be handled. When the assertions involve the map functions
140+
arguments.add(assertThatArg.getArguments().get(0));
141+
arguments.add(methodToReplace.getArguments().get(0));
142+
return "assertThat(#{any()}).%s(#{any()}, #{any()})";
143+
}
144+
145+
if (!(assertThatArg.getArguments().get(0) instanceof J.Empty)
146+
|| !(methodToReplace.getArguments().get(0) instanceof J.Empty)) {
147+
Expression argumentToAdd = assertThatArg.getArguments().get(0) instanceof J.Empty ? methodToReplace.getArguments().get(0) : assertThatArg.getArguments().get(0);
148+
argumentToAdd = argumentToAdd instanceof J.MethodInvocation ? ((J.MethodInvocation) argumentToAdd).getSelect() : argumentToAdd;
149+
arguments.add(argumentToAdd);
150+
151+
if (requiredType.equals("java.nio.file.Path") && dedicatedAssertion.contains("Raw")
152+
&& TypeUtils.isAssignableTo("java.lang.String", assertThatArg.getArguments().get(0).getType())) {
153+
return "assertThat(#{any()}).%s(Path.of(#{any()}))";
154+
}
155+
return "assertThat(#{any()}).%s(#{any()})";
156+
}
157+
158+
return "assertThat(#{any()}).%s()";
159+
}
160+
161+
private boolean hasZeroArgument(J.MethodInvocation method) {
162+
List<Expression> arguments = method.getArguments();
163+
if (arguments.size() == 1 && arguments.get(0) instanceof J.Literal) {
164+
J.Literal literalArg = (J.Literal) arguments.get(0);
165+
return literalArg.getValue() != null && literalArg.getValue().equals(0);
166+
}
167+
return false;
168+
}
169+
}

src/main/java/org/openrewrite/java/testing/assertj/UseExplicitContains.java

Lines changed: 0 additions & 107 deletions
This file was deleted.

src/main/java/org/openrewrite/java/testing/assertj/UseExplicitIsEmpty.java

Lines changed: 0 additions & 99 deletions
This file was deleted.

0 commit comments

Comments
 (0)