Skip to content

Commit

Permalink
Add recipe to consolidate @ExtendWith and @ContextConfiguration (#297)
Browse files Browse the repository at this point in the history
* Add recipe to replace @ExtendWith and @ContextConfiguration with @SpringJUnitConfig (#296)

* Override getSingleSourceApplicableTest for UsesType checks

Also apply default formatter for consistency

* Adopt String.formatted instead of String concatenation

* Consistently use replace with instead of into

* Adopt Applicability.and(..) to require both annotations

* Move and temporarily disable inclusion in update chain

---------

Co-authored-by: Tim te Beek <[email protected]>
  • Loading branch information
nbruno and timtebeek authored Feb 27, 2023
1 parent 92c9e9c commit 20ef4cc
Show file tree
Hide file tree
Showing 5 changed files with 477 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.spring.boot2;

import org.openrewrite.Applicability;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.AnnotationMatcher;
import org.openrewrite.java.ChangeType;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.search.UsesType;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;

import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;

public class ReplaceExtendWithAndContextConfiguration extends Recipe {
private static final String FQN_EXTEND_WITH = "org.junit.jupiter.api.extension.ExtendWith";
private static final String FQN_CONTEXT_CONFIGURATION = "org.springframework.test.context.ContextConfiguration";
private static final String FQN_SPRING_JUNIT_CONFIG = "org.springframework.test.context.junit.jupiter.SpringJUnitConfig";

public ReplaceExtendWithAndContextConfiguration() {
doNext(new UnnecessarySpringExtension());
}

@Override
public String getDisplayName() {
return "Replace `@ExtendWith` and `@ContextConfiguration` with `@SpringJunitConfig`";
}

@Override
public String getDescription() {
return "Replaces `@ExtendWith(SpringRunner.class)` and `@ContextConfiguration` with `@SpringJunitConfig`, " +
"preserving attributes on `@ContextConfiguration`, unless `@ContextConfiguration(loader = ...)` is used.";
}

@Override
public Duration getEstimatedEffortPerOccurrence() {
return Duration.ofMinutes(2);
}

@Override
protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
return Applicability.and(new UsesType<>(FQN_EXTEND_WITH), new UsesType<>(FQN_CONTEXT_CONFIGURATION));
}

@Override
protected TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
private final AnnotationMatcher CONTEXT_CONFIGURATION_ANNOTATION_MATCHER = new AnnotationMatcher("@" + FQN_CONTEXT_CONFIGURATION, true);

@Override
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext context) {
J.Annotation a = super.visitAnnotation(annotation, context);

if (CONTEXT_CONFIGURATION_ANNOTATION_MATCHER.matches(a) && getCursor().getParentOrThrow().getValue() instanceof J.ClassDeclaration) {
// @SpringJUnitConfig supports every attribute on @ContextConfiguration except loader()
// If it's present, skip the transformation since removing @ContextConfiguration will do harm
Optional<J.Assignment> loaderArg = findLoaderArgument(a);
if (loaderArg.isPresent()) {
return a;
}

// @ContextConfiguration value() is an alias for locations()
// @SpringJUnitConfig value() is an alias for classes()
// Since these are incompatible, we need to map value() to locations()
// If value() is present (either explicitly or implicitly), replace it with locations()
// Note that it's invalid to specify both value() and locations() on @ContextConfiguration
if (a.getArguments() != null) {
List<Expression> newArgs = new CopyOnWriteArrayList<>(a.getArguments());
replaceValueArgumentWithLocations(a, newArgs);
a = a.withArguments(newArgs);
}

// Change the @ContextConfiguration annotation to @SpringJUnitConfig
maybeRemoveImport(FQN_CONTEXT_CONFIGURATION);
maybeAddImport(FQN_SPRING_JUNIT_CONFIG);
a = (J.Annotation) new ChangeType(FQN_CONTEXT_CONFIGURATION, FQN_SPRING_JUNIT_CONFIG, false)
.getVisitor().visit(a, context, getCursor());
}

return a != null ? autoFormat(a, context) : annotation;
}

private void replaceValueArgumentWithLocations(J.Annotation a, List<Expression> newArgs) {
for (int i = 0; i < newArgs.size(); i++) {
Expression expression = newArgs.get(i);
if (expression instanceof J.Assignment) {
J.Assignment assignment = (J.Assignment) expression;
String name = ((J.Identifier) assignment.getVariable()).getSimpleName();
if (name.equals("value")) {
J.Assignment as = createLocationsAssignment(a, assignment.getAssignment())
.withPrefix(expression.getPrefix());
newArgs.set(i, as);
break;
}
} else {
// The implicit assignment to "value"
J.Assignment as = createLocationsAssignment(a, expression).withPrefix(expression.getPrefix());
newArgs.set(i, as);
break;
}
}
}

private J.Assignment createLocationsAssignment(J.Annotation annotation, Expression value) {
return (J.Assignment) ((J.Annotation) annotation.withTemplate(
JavaTemplate.builder(this::getCursor, "locations = #{any(String)}").build(),
annotation.getCoordinates().replaceArguments(),
value
)).getArguments().get(0);
}
};
}

private static Optional<J.Assignment> findLoaderArgument(J.Annotation annotation) {
if (annotation.getArguments() == null) {
return Optional.empty();
}
return annotation.getArguments().stream()
.filter(arg -> arg instanceof J.Assignment
&& ((J.Assignment) arg).getVariable() instanceof J.Identifier
&& "loader".equals(((J.Identifier) ((J.Assignment) arg).getVariable()).getSimpleName()))
.map(J.Assignment.class::cast)
.findFirst();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public class UnnecessarySpringExtension extends Recipe {
"org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest",
"org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest",
"org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest",
"org.springframework.batch.test.context.SpringBatchTest"
"org.springframework.batch.test.context.SpringBatchTest",
"org.springframework.test.context.junit.jupiter.SpringJUnitConfig"
);
private static final String EXTEND_WITH_SPRING_EXTENSION_ANNOTATION_PATTERN = "@org.junit.jupiter.api.extension.ExtendWith(org.springframework.test.context.junit.jupiter.SpringExtension.class)";

Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/META-INF/rewrite/spring-boot-24.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ recipeList:
- org.openrewrite.java.spring.boot2.OutputCaptureExtension
- org.openrewrite.java.spring.boot2.UnnecessarySpringRunWith
- org.openrewrite.java.spring.boot2.UnnecessarySpringExtension
# TODO Enable once tested on more projects through public.moderne.io
# - org.openrewrite.java.spring.boot2.ReplaceExtendWithAndContextConfiguration
- org.openrewrite.java.spring.boot2.RemoveObsoleteSpringRunners
- org.openrewrite.maven.AddDependency:
groupId: org.springframework.boot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.openrewrite.java.spring.boot2;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.openrewrite.Issue;
import org.openrewrite.config.Environment;
Expand All @@ -32,14 +33,10 @@ public void defaults(RecipeSpec spec) {
.recipe(Environment.builder()
.scanRuntimeClasspath()
.build()
.activateRecipes(
"org.openrewrite.java.spring.boot2.UnnecessarySpringRunWith",
"org.openrewrite.java.spring.boot2.UnnecessarySpringExtension",
"org.openrewrite.java.testing.junit5.JUnit4to5Migration"
)
.activateRecipes("org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration")
)
.parser(JavaParser.fromJavaVersion()
.classpath("spring-boot-test", "junit", "spring-test"));
.classpath("spring-boot-test", "junit", "spring-test", "spring-context"));
}

@Issue("https://github.com/openrewrite/rewrite-spring/issues/43")
Expand Down Expand Up @@ -72,7 +69,7 @@ public void testFindAll() {
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ProductionConfigurationTests {
class ProductionConfigurationTests {
@Test
void testFindAll() {
Expand All @@ -82,4 +79,56 @@ void testFindAll() {
)
);
}

@Issue("https://github.com/openrewrite/rewrite-spring/issues/296")
@Test
@Disabled("Requires inclusion of ReplaceExtendWithAndContextConfiguration after that's verified on more projects")
void springBootRunWithContextConfigurationReplacedWithSpringJUnitConfig() {
//language=java
rewriteRun(
java(
"""
package org.springframework.samples.petclinic.system;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProductionConfigurationTests.CustomConfiguration.class)
public class ProductionConfigurationTests {
@Test
public void testFindAll() {
}
@Configuration
static class CustomConfiguration {
}
}
""",
"""
package org.springframework.samples.petclinic.system;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = ProductionConfigurationTests.CustomConfiguration.class)
class ProductionConfigurationTests {
@Test
void testFindAll() {
}
@Configuration
static class CustomConfiguration {
}
}
"""
)
);
}
}
Loading

0 comments on commit 20ef4cc

Please sign in to comment.