Merge pull request #46766 from scordio

* pr/46766:
  Polish "Decorate all Assert implementations with @CheckReturnValue"
  Decorate all Assert implementations with @CheckReturnValue

Closes gh-46766
This commit is contained in:
Stéphane Nicoll
2025-12-19 10:49:58 +01:00
14 changed files with 164 additions and 10 deletions
@@ -38,6 +38,8 @@ import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.ListAssert;
import org.jspecify.annotations.Nullable;
import org.springframework.lang.CheckReturnValue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.contentOf;
@@ -172,6 +174,7 @@ abstract class AbstractArchiveIntegrationTests {
return this;
}
@CheckReturnValue
ListAssert<String> entryNamesInPath(String path) {
List<String> matches = new ArrayList<>();
withJarFile((jarFile) -> withEntries(jarFile,
@@ -70,6 +70,7 @@ import org.gradle.api.tasks.VerificationException;
* @author Phillip Webb
* @author Dmytro Nosan
* @author Moritz Halbritter
* @author Stefano Cordio
*/
public abstract class ArchitectureCheck extends DefaultTask {
@@ -93,6 +94,8 @@ public abstract class ArchitectureCheck extends DefaultTask {
getRules().addAll(whenMainSources(
() -> ArchitectureRules.configurationPropertiesDeprecation(ArchitectureCheckAnnotation.classFor(
getAnnotationClasses().get(), ArchitectureCheckAnnotation.DEPRECATED_CONFIGURATION_PROPERTY))));
getRules().addAll(whenMainSources(() -> Collections.singletonList(
ArchitectureRules.allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue())));
getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList(
ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get())))));
getRuleDescriptions().set(getRules().map(this::asDescriptions));
@@ -63,6 +63,7 @@ import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Role;
import org.springframework.lang.CheckReturnValue;
import org.springframework.util.ResourceUtils;
/**
@@ -75,6 +76,7 @@ import org.springframework.util.ResourceUtils;
* @author Phillip Webb
* @author Ngoc Nhan
* @author Moritz Halbritter
* @author Stefano Cordio
*/
final class ArchitectureRules {
@@ -158,6 +160,26 @@ final class ArchitectureRules {
.allowEmptyShould(true);
}
static ArchRule allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue() {
return ArchRuleDefinition.methods()
.that()
.areDeclaredInClassesThat()
.implement("org.assertj.core.api.Assert")
.and()
.arePublic()
.and()
.doNotHaveModifier(JavaModifier.BRIDGE)
.and(doNotReturnSelfType())
.should()
.beAnnotatedWith(CheckReturnValue.class)
.allowEmptyShould(true);
}
private static DescribedPredicate<JavaMethod> doNotReturnSelfType() {
return DescribedPredicate.describe("do not return self type",
(method) -> !method.getRawReturnType().equals(method.getOwner()));
}
private static ArchRule allPackagesShouldBeFreeOfTangles() {
return SlicesRuleDefinition.slices()
.matching("(**)")
@@ -62,13 +62,18 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Scott Frederick
* @author Ivan Malutin
* @author Dmytro Nosan
* @author Stefano Cordio
*/
class ArchitectureCheckTests {
private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9";
private static final String ASSERTJ_CORE = "org.assertj:assertj-core:3.27.4";
private static final String JUNIT_JUPITER = "org.junit.jupiter:junit-jupiter:5.12.0";
private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.15";
private static final String SPRING_CORE = "org.springframework:spring-core:6.2.15";
private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1";
private GradleBuild gradleBuild;
@@ -452,6 +457,23 @@ class ArchitectureCheckTests {
"DeprecatedConfigurationPropertySince.getProperty");
}
@Test
void whenCustomAssertionMethodNotReturningSelfIsAnnotatedWithCheckReturnValueShouldSucceedAndWriteEmptyReport()
throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/checkReturnValue");
build(this.gradleBuild.withDependencies(ASSERTJ_CORE, SPRING_CORE), Task.CHECK_ARCHITECTURE_MAIN);
}
@Test
void whenCustomAssertionMethodNotReturningSelfIsNotAnnotatedWithCheckReturnValueShouldFailAndWriteReport()
throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/noCheckReturnValue");
buildAndFail(this.gradleBuild.withDependencies(ASSERTJ_CORE), Task.CHECK_ARCHITECTURE_MAIN,
"methods that are declared in classes that implement org.assertj.core.api.Assert and "
+ "are public and do not have modifier BRIDGE and do not return self type should be annotated "
+ "with @CheckReturnValue");
}
private void prepareTask(Task task, String... sourceDirectories) throws IOException {
for (String sourceDirectory : sourceDirectories) {
FileSystemUtils.copyRecursively(
@@ -0,0 +1,39 @@
/*
* Copyright 2012-present 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.springframework.boot.build.architecture.assertj.checkReturnValue;
import org.assertj.core.api.AbstractAssert;
import org.springframework.lang.CheckReturnValue;
public class WithCheckReturnValue extends AbstractAssert<WithCheckReturnValue, Object> {
WithCheckReturnValue() {
super(null, WithCheckReturnValue.class);
}
@CheckReturnValue
public Object notReturningSelf() {
return new Object();
}
@Override
public WithCheckReturnValue isEqualTo(Object expected) {
return super.isEqualTo(expected);
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2012-present 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.springframework.boot.build.architecture.assertj.noCheckReturnValue;
import org.assertj.core.api.AbstractAssert;
public class NoCheckReturnValue extends AbstractAssert<NoCheckReturnValue, Object> {
NoCheckReturnValue() {
super(null, NoCheckReturnValue.class);
}
public Object notReturningSelf() {
return new Object();
}
}
+3
View File
@@ -6,6 +6,9 @@
<allow pkg="io.micrometer.observation" />
<disallow pkg="io.micrometer" />
<!-- Improve DevEx with fluent APIs -->
<allow class="org.springframework.lang.CheckReturnValue" />
<!-- Use JSpecify for nullability (not Spring) -->
<allow class="org.springframework.lang.Contract" />
<disallow pkg="org.springframework.lang" />
@@ -26,7 +26,6 @@ import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractObjectArrayAssert;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.AbstractThrowableAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.MapAssert;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.jspecify.annotations.Nullable;
@@ -37,6 +36,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.CheckReturnValue;
import org.springframework.util.Assert;
import static org.assertj.core.api.Assertions.assertThat;
@@ -224,13 +224,14 @@ public class ApplicationContextAssert<C extends ApplicationContext>
* @return array assertions for the bean names
* @throws AssertionError if the application context did not start
*/
@CheckReturnValue
public <T> AbstractObjectArrayAssert<?, String> getBeanNames(Class<T> type) {
if (this.startupFailure != null) {
throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure,
"to get beans names with type:%n <%s>", type));
}
return Assertions.assertThat(getApplicationContext().getBeanNamesForType(type))
.as("Bean names of type <%s> from <%s>", type, getApplicationContext());
return assertThat(getApplicationContext().getBeanNamesForType(type)).as("Bean names of type <%s> from <%s>",
type, getApplicationContext());
}
/**
@@ -249,6 +250,7 @@ public class ApplicationContextAssert<C extends ApplicationContext>
* @throws AssertionError if the application context contains multiple beans of the
* given type
*/
@CheckReturnValue
public <T> AbstractObjectAssert<?, T> getBean(Class<T> type) {
return getBean(type, Scope.INCLUDE_ANCESTORS);
}
@@ -270,6 +272,7 @@ public class ApplicationContextAssert<C extends ApplicationContext>
* @throws AssertionError if the application context contains multiple beans of the
* given type
*/
@CheckReturnValue
public <T> AbstractObjectAssert<?, T> getBean(Class<T> type, Scope scope) {
Assert.notNull(scope, "'scope' must not be null");
if (this.startupFailure != null) {
@@ -284,7 +287,7 @@ public class ApplicationContextAssert<C extends ApplicationContext>
getApplicationContext(), type, names));
}
T bean = (name != null) ? getApplicationContext().getBean(name, type) : null;
return Assertions.assertThat(bean).as("Bean of type <%s> from <%s>", type, getApplicationContext());
return assertThat(bean).as("Bean of type <%s> from <%s>", type, getApplicationContext());
}
private @Nullable String getPrimary(String[] names, Scope scope) {
@@ -330,13 +333,14 @@ public class ApplicationContextAssert<C extends ApplicationContext>
* is found
* @throws AssertionError if the application context did not start
*/
@CheckReturnValue
public AbstractObjectAssert<?, Object> getBean(String name) {
if (this.startupFailure != null) {
throwAssertionError(
contextFailedToStartWhenExpecting(this.startupFailure, "to contain a bean of name:%n <%s>", name));
}
Object bean = findBean(name);
return Assertions.assertThat(bean).as("Bean of name <%s> from <%s>", name, getApplicationContext());
return assertThat(bean).as("Bean of name <%s> from <%s>", name, getApplicationContext());
}
/**
@@ -357,6 +361,7 @@ public class ApplicationContextAssert<C extends ApplicationContext>
* name but a different type
*/
@SuppressWarnings("unchecked")
@CheckReturnValue
public <T> AbstractObjectAssert<?, T> getBean(String name, Class<T> type) {
if (this.startupFailure != null) {
throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure,
@@ -368,8 +373,8 @@ public class ApplicationContextAssert<C extends ApplicationContext>
"%nExpecting:%n <%s>%nto contain a bean of name:%n <%s> (%s)%nbut found:%n <%s> of type <%s>",
getApplicationContext(), name, type, bean, bean.getClass()));
}
return Assertions.assertThat((T) bean)
.as("Bean of name <%s> and type <%s> from <%s>", name, type, getApplicationContext());
return assertThat((T) bean).as("Bean of name <%s> and type <%s> from <%s>", name, type,
getApplicationContext());
}
private @Nullable Object findBean(String name) {
@@ -395,6 +400,7 @@ public class ApplicationContextAssert<C extends ApplicationContext>
* no beans are found
* @throws AssertionError if the application context did not start
*/
@CheckReturnValue
public <T> MapAssert<String, T> getBeans(Class<T> type) {
return getBeans(type, Scope.INCLUDE_ANCESTORS);
}
@@ -414,14 +420,15 @@ public class ApplicationContextAssert<C extends ApplicationContext>
* no beans are found
* @throws AssertionError if the application context did not start
*/
@CheckReturnValue
public <T> MapAssert<String, T> getBeans(Class<T> type, Scope scope) {
Assert.notNull(scope, "'scope' must not be null");
if (this.startupFailure != null) {
throwAssertionError(
contextFailedToStartWhenExpecting(this.startupFailure, "to get beans of type:%n <%s>", type));
}
return Assertions.assertThat(scope.getBeansOfType(getApplicationContext(), type))
.as("Beans of type <%s> from <%s>", type, getApplicationContext());
return assertThat(scope.getBeansOfType(getApplicationContext(), type)).as("Beans of type <%s> from <%s>", type,
getApplicationContext());
}
/**
@@ -434,6 +441,7 @@ public class ApplicationContextAssert<C extends ApplicationContext>
* @return assertions on the cause of the failure
* @throws AssertionError if the application context started without a failure
*/
@CheckReturnValue
public AbstractThrowableAssert<?, ? extends Throwable> getFailure() {
hasFailed();
return assertThat(this.startupFailure);
@@ -40,6 +40,7 @@ import org.skyscreamer.jsonassert.JSONCompareResult;
import org.skyscreamer.jsonassert.comparator.JSONComparator;
import org.springframework.core.io.Resource;
import org.springframework.lang.CheckReturnValue;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.function.ThrowingFunction;
@@ -917,6 +918,7 @@ public class JsonContentAssert extends AbstractAssert<JsonContentAssert, CharSeq
* @return a new assertion object whose object under test is the extracted item
* @throws AssertionError if the path is not valid
*/
@CheckReturnValue
public AbstractObjectAssert<?, Object> extractingJsonPathValue(CharSequence expression, Object... args) {
return Assertions.assertThat(new JsonPathValue(expression, args).getValue(false));
}
@@ -929,6 +931,7 @@ public class JsonContentAssert extends AbstractAssert<JsonContentAssert, CharSeq
* @return a new assertion object whose object under test is the extracted item
* @throws AssertionError if the path is not valid or does not result in a string
*/
@CheckReturnValue
public AbstractCharSequenceAssert<?, String> extractingJsonPathStringValue(CharSequence expression,
Object... args) {
return Assertions.assertThat(extractingJsonPathValue(expression, args, String.class, "a string"));
@@ -942,6 +945,7 @@ public class JsonContentAssert extends AbstractAssert<JsonContentAssert, CharSeq
* @return a new assertion object whose object under test is the extracted item
* @throws AssertionError if the path is not valid or does not result in a number
*/
@CheckReturnValue
public AbstractObjectAssert<?, Number> extractingJsonPathNumberValue(CharSequence expression, Object... args) {
return Assertions.assertThat(extractingJsonPathValue(expression, args, Number.class, "a number"));
}
@@ -954,6 +958,7 @@ public class JsonContentAssert extends AbstractAssert<JsonContentAssert, CharSeq
* @return a new assertion object whose object under test is the extracted item
* @throws AssertionError if the path is not valid or does not result in a boolean
*/
@CheckReturnValue
public AbstractBooleanAssert<?> extractingJsonPathBooleanValue(CharSequence expression, Object... args) {
return Assertions.assertThat(extractingJsonPathValue(expression, args, Boolean.class, "a boolean"));
}
@@ -968,6 +973,7 @@ public class JsonContentAssert extends AbstractAssert<JsonContentAssert, CharSeq
* @throws AssertionError if the path is not valid or does not result in an array
*/
@SuppressWarnings("unchecked")
@CheckReturnValue
public <E> ListAssert<E> extractingJsonPathArrayValue(CharSequence expression, Object... args) {
return Assertions.assertThat(extractingJsonPathValue(expression, args, List.class, "an array"));
}
@@ -983,6 +989,7 @@ public class JsonContentAssert extends AbstractAssert<JsonContentAssert, CharSeq
* @throws AssertionError if the path is not valid or does not result in a map
*/
@SuppressWarnings("unchecked")
@CheckReturnValue
public <K, V> MapAssert<K, V> extractingJsonPathMapValue(CharSequence expression, Object... args) {
return Assertions.assertThat(extractingJsonPathValue(expression, args, Map.class, "a map"));
}
@@ -22,6 +22,8 @@ import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.Assert;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.springframework.lang.CheckReturnValue;
/**
* AssertJ {@link Assert} for {@link ObjectContent}.
*
@@ -41,6 +43,7 @@ public class ObjectContentAssert<A> extends AbstractObjectAssert<ObjectContentAs
* allow chaining of array-specific assertions from this call.
* @return an array assertion object
*/
@CheckReturnValue
public AbstractObjectArrayAssert<?, Object> asArray() {
return asInstanceOf(InstanceOfAssertFactories.ARRAY);
}
@@ -50,6 +53,7 @@ public class ObjectContentAssert<A> extends AbstractObjectAssert<ObjectContentAs
* chaining of map-specific assertions from this call.
* @return a map assertion object
*/
@CheckReturnValue
public AbstractMapAssert<?, ?, Object, Object> asMap() {
return asInstanceOf(InstanceOfAssertFactories.MAP);
}
@@ -32,6 +32,7 @@ import org.assertj.core.api.ListAssert;
import org.assertj.core.api.ObjectAssert;
import org.springframework.boot.test.json.JsonContentAssert;
import org.springframework.lang.CheckReturnValue;
/**
* AssertJ {@link org.assertj.core.api.Assert} for Docker image container configuration.
@@ -99,10 +100,12 @@ public class ContainerConfigAssert extends AbstractAssert<ContainerConfigAssert,
super(jsonContentAssert, BuildMetadataAssert.class);
}
@CheckReturnValue
public ListAssert<Object> buildpacks() {
return this.actual.extractingJsonPathArrayValue("$.buildpacks[*].id");
}
@CheckReturnValue
public AbstractListAssert<?, List<? extends String>, String, ObjectAssert<String>> processOfType(String type) {
return this.actual.extractingJsonPathArrayValue("$.processes[?(@.type=='%s')]", type)
.singleElement()
@@ -132,14 +135,17 @@ public class ContainerConfigAssert extends AbstractAssert<ContainerConfigAssert,
super(jsonContentAssert, LifecycleMetadataAssert.class);
}
@CheckReturnValue
public ListAssert<Object> buildpackLayers(String buildpackId) {
return this.actual.extractingJsonPathArrayValue("$.buildpacks[?(@.key=='%s')].layers", buildpackId);
}
@CheckReturnValue
public AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> appLayerShas() {
return this.actual.extractingJsonPathArrayValue("$.app").extracting("sha");
}
@CheckReturnValue
public AbstractObjectAssert<?, Object> sbomLayerSha() {
return this.actual.extractingJsonPathValue("$.sbom.sha");
}
@@ -34,6 +34,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.test.json.JsonContentAssert;
import org.springframework.lang.CheckReturnValue;
import org.springframework.util.StreamUtils;
/**
@@ -73,6 +74,7 @@ public class ImageAssert extends AbstractAssert<ImageAssert, ImageReference> {
super(layer, LayerContentAssert.class);
}
@CheckReturnValue
public ListAssert<String> entries() {
List<String> entryNames = new ArrayList<>();
try {
@@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assert;
import org.springframework.lang.CheckReturnValue;
import org.springframework.util.ReflectionUtils;
/**
@@ -85,6 +86,7 @@ public final class ScheduledExecutorServiceAssert
* @param actual the {@link ScheduledExecutorService}
* @return the assertion instance
*/
@CheckReturnValue
public static ScheduledExecutorServiceAssert assertThat(ScheduledExecutorService actual) {
return new ScheduledExecutorServiceAssert(actual);
}
@@ -22,6 +22,7 @@ import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assert;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.lang.CheckReturnValue;
import org.springframework.util.ReflectionUtils;
/**
@@ -77,6 +78,7 @@ public final class SimpleAsyncTaskExecutorAssert
* @param actual the {@link SimpleAsyncTaskExecutor}
* @return the assertion instance
*/
@CheckReturnValue
public static SimpleAsyncTaskExecutorAssert assertThat(SimpleAsyncTaskExecutor actual) {
return new SimpleAsyncTaskExecutorAssert(actual);
}