diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java index 7701ba4099fb..a4a5ba1aa127 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java @@ -37,6 +37,8 @@ import org.assertj.core.api.AssertProvider; import org.assertj.core.api.ListAssert; +import org.springframework.lang.CheckReturnValue; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.contentOf; @@ -176,6 +178,7 @@ JarAssert doesNotHaveEntryWithNameStartingWith(String prefix) { return this; } + @CheckReturnValue ListAssert entryNamesInPath(String path) { List matches = new ArrayList<>(); withJarFile((jarFile) -> withEntries(jarFile, diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java index bd0cca192657..45eae88362cd 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java @@ -68,6 +68,7 @@ * @author Phillip Webb * @author Dmytro Nosan * @author Moritz Halbritter + * @author Stefano Cordio */ public abstract class ArchitectureCheck extends DefaultTask { @@ -80,6 +81,8 @@ public ArchitectureCheck() { getRules().addAll(ArchitectureRules.standard()); getRules().addAll(whenMainSources( () -> Collections.singletonList(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType()))); + getRules().addAll(whenMainSources(() -> Collections.singletonList( + ArchitectureRules.allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue()))); getRules().addAll(and(getNullMarked(), isMainSourceSet()).map(whenTrue( () -> Collections.singletonList(ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked())))); getRuleDescriptions().set(getRules().map(this::asDescriptions)); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java index e45c24ee6ba8..3f09be02d972 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java @@ -63,6 +63,7 @@ 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 @@ * @author Phillip Webb * @author Ngoc Nhan * @author Moritz Halbritter + * @author Stefano Cordio */ final class ArchitectureRules { @@ -129,6 +131,24 @@ static ArchRule allBeanMethodsShouldReturnNonPrivateType() { .allowEmptyShould(true); } + static ArchRule allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue() { + return ArchRuleDefinition.methods() + .that() + .areDeclaredInClassesThat() + .implement("org.assertj.core.api.Assert") + .and() + .arePublic() + .and(dontReturnSelfType()) + .should() + .beAnnotatedWith(CheckReturnValue.class) + .allowEmptyShould(true); + } + + private static DescribedPredicate dontReturnSelfType() { + return DescribedPredicate.describe("don't return self type", + (method) -> !method.getRawReturnType().equals(method.getOwner())); + } + private static ArchRule allPackagesShouldBeFreeOfTangles() { return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles(); } diff --git a/config/checkstyle/import-control.xml b/config/checkstyle/import-control.xml index 770737c57f59..5ace4688e01c 100644 --- a/config/checkstyle/import-control.xml +++ b/config/checkstyle/import-control.xml @@ -3,6 +3,7 @@ + diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java index 6f5be5b15ae1..f39319f6d757 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java @@ -26,7 +26,6 @@ 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.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 ApplicationContextAssert doesNotHaveBean(String name) { * @return array assertions for the bean names * @throws AssertionError if the application context did not start */ + @CheckReturnValue public AbstractObjectArrayAssert getBeanNames(Class 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 AbstractObjectArrayAssert getBeanNames(Class type) { * @throws AssertionError if the application context contains multiple beans of the * given type */ + @CheckReturnValue public AbstractObjectAssert getBean(Class type) { return getBean(type, Scope.INCLUDE_ANCESTORS); } @@ -270,6 +272,7 @@ public AbstractObjectAssert getBean(Class type) { * @throws AssertionError if the application context contains multiple beans of the * given type */ + @CheckReturnValue public AbstractObjectAssert getBean(Class type, Scope scope) { Assert.notNull(scope, "'scope' must not be null"); if (this.startupFailure != null) { @@ -284,7 +287,7 @@ public AbstractObjectAssert getBean(Class type, Scope scope) { 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 @@ private boolean isPrimary(String name, Scope scope) { * is found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public AbstractObjectAssert 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 AbstractObjectAssert getBean(String name) { * name but a different type */ @SuppressWarnings("unchecked") + @CheckReturnValue public AbstractObjectAssert getBean(String name, Class type) { if (this.startupFailure != null) { throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure, @@ -368,8 +373,8 @@ public AbstractObjectAssert getBean(String name, Class type) { "%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 AbstractObjectAssert getBean(String name, Class type) { * no beans are found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public MapAssert getBeans(Class type) { return getBeans(type, Scope.INCLUDE_ANCESTORS); } @@ -414,14 +420,15 @@ public MapAssert getBeans(Class type) { * no beans are found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public MapAssert getBeans(Class 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 MapAssert getBeans(Class type, Scope scope) { * @return assertions on the cause of the failure * @throws AssertionError if the application context started without a failure */ + @CheckReturnValue public AbstractThrowableAssert getFailure() { hasFailed(); return assertThat(this.startupFailure); diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java index 8fe013a19e0f..b0d2f7559f00 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java @@ -40,6 +40,7 @@ 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; @@ -916,6 +917,7 @@ public JsonContentAssert doesNotHaveEmptyJsonPathValue(CharSequence expression, * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid */ + @CheckReturnValue public AbstractObjectAssert extractingJsonPathValue(CharSequence expression, Object... args) { return Assertions.assertThat(new JsonPathValue(expression, args).getValue(false)); } @@ -928,6 +930,7 @@ public AbstractObjectAssert extractingJsonPathValue(CharSequence expr * @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 extractingJsonPathStringValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, String.class, "a string")); @@ -941,6 +944,7 @@ public AbstractCharSequenceAssert extractingJsonPathStringValue(CharS * @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 extractingJsonPathNumberValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, Number.class, "a number")); } @@ -953,6 +957,7 @@ public AbstractObjectAssert extractingJsonPathNumberValue(CharSequenc * @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")); } @@ -967,6 +972,7 @@ public AbstractBooleanAssert extractingJsonPathBooleanValue(CharSequence expr * @throws AssertionError if the path is not valid or does not result in an array */ @SuppressWarnings("unchecked") + @CheckReturnValue public ListAssert extractingJsonPathArrayValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, List.class, "an array")); } @@ -982,6 +988,7 @@ public ListAssert extractingJsonPathArrayValue(CharSequence expression, O * @throws AssertionError if the path is not valid or does not result in a map */ @SuppressWarnings("unchecked") + @CheckReturnValue public MapAssert extractingJsonPathMapValue(CharSequence expression, Object... args) { return Assertions.assertThat(extractingJsonPathValue(expression, args, Map.class, "a map")); } diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java index d58612f85959..d406ee93c21c 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java @@ -22,6 +22,8 @@ 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 @@ protected ObjectContentAssert(A actual) { * allow chaining of array-specific assertions from this call. * @return an array assertion object */ + @CheckReturnValue public AbstractObjectArrayAssert asArray() { return asInstanceOf(InstanceOfAssertFactories.ARRAY); } @@ -50,6 +53,7 @@ public AbstractObjectArrayAssert asArray() { * chaining of map-specific assertions from this call. * @return a map assertion object */ + @CheckReturnValue public AbstractMapAssert asMap() { return asInstanceOf(InstanceOfAssertFactories.MAP); } diff --git a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java index dbf5dcea437a..14ea8581df09 100644 --- a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java +++ b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java @@ -32,6 +32,7 @@ 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. @@ -89,7 +90,7 @@ protected LabelsAssert(Map labels) { /** * Asserts for the JSON content in the {@code io.buildpacks.build.metadata} label. * - * See the * spec */ @@ -99,10 +100,12 @@ public static class BuildMetadataAssert extends AbstractAssert buildpacks() { return this.actual.extractingJsonPathArrayValue("$.buildpacks[*].id"); } + @CheckReturnValue public AbstractListAssert, String, ObjectAssert> processOfType(String type) { return this.actual.extractingJsonPathArrayValue("$.processes[?(@.type=='%s')]", type) .singleElement() @@ -122,7 +125,7 @@ private Collection getArgs(Object obj) { /** * Asserts for the JSON content in the {@code io.buildpacks.lifecycle.metadata} label. * - * See the * spec */ @@ -132,14 +135,17 @@ public static class LifecycleMetadataAssert extends AbstractAssert buildpackLayers(String buildpackId) { return this.actual.extractingJsonPathArrayValue("$.buildpacks[?(@.key=='%s')].layers", buildpackId); } + @CheckReturnValue public AbstractListAssert, Object, ObjectAssert> appLayerShas() { return this.actual.extractingJsonPathArrayValue("$.app").extracting("sha"); } + @CheckReturnValue public AbstractObjectAssert sbomLayerSha() { return this.actual.extractingJsonPathValue("$.sbom.sha"); } diff --git a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java index fbc242a647d6..78afc1764b92 100644 --- a/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java +++ b/system-test/spring-boot-image-system-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java @@ -34,6 +34,7 @@ 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 LayerContentAssert(Layer layer) { super(layer, LayerContentAssert.class); } + @CheckReturnValue public ListAssert entries() { List entryNames = new ArrayList<>(); try { diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/ScheduledExecutorServiceAssert.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/ScheduledExecutorServiceAssert.java index b9e5b075ef6d..65155facd61f 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/ScheduledExecutorServiceAssert.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/ScheduledExecutorServiceAssert.java @@ -24,6 +24,7 @@ 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 @@ private boolean producesVirtualThreads() { * @param actual the {@link ScheduledExecutorService} * @return the assertion instance */ + @CheckReturnValue public static ScheduledExecutorServiceAssert assertThat(ScheduledExecutorService actual) { return new ScheduledExecutorServiceAssert(actual); } diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java index 14a5685aec72..5104d4388110 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java @@ -22,6 +22,7 @@ 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 @@ private boolean producesVirtualThreads() { * @param actual the {@link SimpleAsyncTaskExecutor} * @return the assertion instance */ + @CheckReturnValue public static SimpleAsyncTaskExecutorAssert assertThat(SimpleAsyncTaskExecutor actual) { return new SimpleAsyncTaskExecutorAssert(actual); }