Skip to content

Commit 90faddf

Browse files
committed
Merge pull request #46766 from scordio
* pr/46766: Polish "Decorate all Assert implementations with @CheckReturnValue" Decorate all Assert implementations with @CheckReturnValue Closes gh-46766
2 parents 458f7f6 + dfb05ee commit 90faddf

File tree

14 files changed

+164
-10
lines changed

14 files changed

+164
-10
lines changed

build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import org.assertj.core.api.ListAssert;
3939
import org.jspecify.annotations.Nullable;
4040

41+
import org.springframework.lang.CheckReturnValue;
42+
4143
import static org.assertj.core.api.Assertions.assertThat;
4244
import static org.assertj.core.api.Assertions.contentOf;
4345

@@ -172,6 +174,7 @@ JarAssert doesNotHaveEntryWithNameStartingWith(String prefix) {
172174
return this;
173175
}
174176

177+
@CheckReturnValue
175178
ListAssert<String> entryNamesInPath(String path) {
176179
List<String> matches = new ArrayList<>();
177180
withJarFile((jarFile) -> withEntries(jarFile,

buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
* @author Phillip Webb
7171
* @author Dmytro Nosan
7272
* @author Moritz Halbritter
73+
* @author Stefano Cordio
7374
*/
7475
public abstract class ArchitectureCheck extends DefaultTask {
7576

@@ -93,6 +94,8 @@ public ArchitectureCheck() {
9394
getRules().addAll(whenMainSources(
9495
() -> ArchitectureRules.configurationPropertiesDeprecation(ArchitectureCheckAnnotation.classFor(
9596
getAnnotationClasses().get(), ArchitectureCheckAnnotation.DEPRECATED_CONFIGURATION_PROPERTY))));
97+
getRules().addAll(whenMainSources(() -> Collections.singletonList(
98+
ArchitectureRules.allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue())));
9699
getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList(
97100
ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get())))));
98101
getRuleDescriptions().set(getRules().map(this::asDescriptions));

buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363

6464
import org.springframework.beans.factory.config.BeanDefinition;
6565
import org.springframework.context.annotation.Role;
66+
import org.springframework.lang.CheckReturnValue;
6667
import org.springframework.util.ResourceUtils;
6768

6869
/**
@@ -75,6 +76,7 @@
7576
* @author Phillip Webb
7677
* @author Ngoc Nhan
7778
* @author Moritz Halbritter
79+
* @author Stefano Cordio
7880
*/
7981
final class ArchitectureRules {
8082

@@ -158,6 +160,26 @@ private static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(
158160
.allowEmptyShould(true);
159161
}
160162

163+
static ArchRule allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue() {
164+
return ArchRuleDefinition.methods()
165+
.that()
166+
.areDeclaredInClassesThat()
167+
.implement("org.assertj.core.api.Assert")
168+
.and()
169+
.arePublic()
170+
.and()
171+
.doNotHaveModifier(JavaModifier.BRIDGE)
172+
.and(doNotReturnSelfType())
173+
.should()
174+
.beAnnotatedWith(CheckReturnValue.class)
175+
.allowEmptyShould(true);
176+
}
177+
178+
private static DescribedPredicate<JavaMethod> doNotReturnSelfType() {
179+
return DescribedPredicate.describe("do not return self type",
180+
(method) -> !method.getRawReturnType().equals(method.getOwner()));
181+
}
182+
161183
private static ArchRule allPackagesShouldBeFreeOfTangles() {
162184
return SlicesRuleDefinition.slices()
163185
.matching("(**)")

buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,18 @@
6262
* @author Scott Frederick
6363
* @author Ivan Malutin
6464
* @author Dmytro Nosan
65+
* @author Stefano Cordio
6566
*/
6667
class ArchitectureCheckTests {
6768

68-
private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9";
69+
private static final String ASSERTJ_CORE = "org.assertj:assertj-core:3.27.4";
6970

7071
private static final String JUNIT_JUPITER = "org.junit.jupiter:junit-jupiter:5.12.0";
7172

73+
private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.15";
74+
75+
private static final String SPRING_CORE = "org.springframework:spring-core:6.2.15";
76+
7277
private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1";
7378

7479
private GradleBuild gradleBuild;
@@ -452,6 +457,23 @@ void whenDeprecatedConfigurationPropertyIsMissingSinceShouldFailAndWriteReport()
452457
"DeprecatedConfigurationPropertySince.getProperty");
453458
}
454459

460+
@Test
461+
void whenCustomAssertionMethodNotReturningSelfIsAnnotatedWithCheckReturnValueShouldSucceedAndWriteEmptyReport()
462+
throws IOException {
463+
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/checkReturnValue");
464+
build(this.gradleBuild.withDependencies(ASSERTJ_CORE, SPRING_CORE), Task.CHECK_ARCHITECTURE_MAIN);
465+
}
466+
467+
@Test
468+
void whenCustomAssertionMethodNotReturningSelfIsNotAnnotatedWithCheckReturnValueShouldFailAndWriteReport()
469+
throws IOException {
470+
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/noCheckReturnValue");
471+
buildAndFail(this.gradleBuild.withDependencies(ASSERTJ_CORE), Task.CHECK_ARCHITECTURE_MAIN,
472+
"methods that are declared in classes that implement org.assertj.core.api.Assert and "
473+
+ "are public and do not have modifier BRIDGE and do not return self type should be annotated "
474+
+ "with @CheckReturnValue");
475+
}
476+
455477
private void prepareTask(Task task, String... sourceDirectories) throws IOException {
456478
for (String sourceDirectory : sourceDirectories) {
457479
FileSystemUtils.copyRecursively(
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
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+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
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+
17+
package org.springframework.boot.build.architecture.assertj.checkReturnValue;
18+
19+
import org.assertj.core.api.AbstractAssert;
20+
21+
import org.springframework.lang.CheckReturnValue;
22+
23+
public class WithCheckReturnValue extends AbstractAssert<WithCheckReturnValue, Object> {
24+
25+
WithCheckReturnValue() {
26+
super(null, WithCheckReturnValue.class);
27+
}
28+
29+
@CheckReturnValue
30+
public Object notReturningSelf() {
31+
return new Object();
32+
}
33+
34+
@Override
35+
public WithCheckReturnValue isEqualTo(Object expected) {
36+
return super.isEqualTo(expected);
37+
}
38+
39+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
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+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
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+
17+
package org.springframework.boot.build.architecture.assertj.noCheckReturnValue;
18+
19+
import org.assertj.core.api.AbstractAssert;
20+
21+
public class NoCheckReturnValue extends AbstractAssert<NoCheckReturnValue, Object> {
22+
23+
NoCheckReturnValue() {
24+
super(null, NoCheckReturnValue.class);
25+
}
26+
27+
public Object notReturningSelf() {
28+
return new Object();
29+
}
30+
31+
}

config/checkstyle/import-control.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
<allow pkg="io.micrometer.observation" />
77
<disallow pkg="io.micrometer" />
88

9+
<!-- Improve DevEx with fluent APIs -->
10+
<allow class="org.springframework.lang.CheckReturnValue" />
11+
912
<!-- Use JSpecify for nullability (not Spring) -->
1013
<allow class="org.springframework.lang.Contract" />
1114
<disallow pkg="org.springframework.lang" />

core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import org.assertj.core.api.AbstractObjectArrayAssert;
2727
import org.assertj.core.api.AbstractObjectAssert;
2828
import org.assertj.core.api.AbstractThrowableAssert;
29-
import org.assertj.core.api.Assertions;
3029
import org.assertj.core.api.MapAssert;
3130
import org.assertj.core.error.BasicErrorMessageFactory;
3231
import org.jspecify.annotations.Nullable;
@@ -37,6 +36,7 @@
3736
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3837
import org.springframework.context.ApplicationContext;
3938
import org.springframework.context.ConfigurableApplicationContext;
39+
import org.springframework.lang.CheckReturnValue;
4040
import org.springframework.util.Assert;
4141

4242
import static org.assertj.core.api.Assertions.assertThat;
@@ -224,13 +224,14 @@ public ApplicationContextAssert<C> doesNotHaveBean(String name) {
224224
* @return array assertions for the bean names
225225
* @throws AssertionError if the application context did not start
226226
*/
227+
@CheckReturnValue
227228
public <T> AbstractObjectArrayAssert<?, String> getBeanNames(Class<T> type) {
228229
if (this.startupFailure != null) {
229230
throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure,
230231
"to get beans names with type:%n <%s>", type));
231232
}
232-
return Assertions.assertThat(getApplicationContext().getBeanNamesForType(type))
233-
.as("Bean names of type <%s> from <%s>", type, getApplicationContext());
233+
return assertThat(getApplicationContext().getBeanNamesForType(type)).as("Bean names of type <%s> from <%s>",
234+
type, getApplicationContext());
234235
}
235236

236237
/**
@@ -249,6 +250,7 @@ public <T> AbstractObjectArrayAssert<?, String> getBeanNames(Class<T> type) {
249250
* @throws AssertionError if the application context contains multiple beans of the
250251
* given type
251252
*/
253+
@CheckReturnValue
252254
public <T> AbstractObjectAssert<?, T> getBean(Class<T> type) {
253255
return getBean(type, Scope.INCLUDE_ANCESTORS);
254256
}
@@ -270,6 +272,7 @@ public <T> AbstractObjectAssert<?, T> getBean(Class<T> type) {
270272
* @throws AssertionError if the application context contains multiple beans of the
271273
* given type
272274
*/
275+
@CheckReturnValue
273276
public <T> AbstractObjectAssert<?, T> getBean(Class<T> type, Scope scope) {
274277
Assert.notNull(scope, "'scope' must not be null");
275278
if (this.startupFailure != null) {
@@ -284,7 +287,7 @@ public <T> AbstractObjectAssert<?, T> getBean(Class<T> type, Scope scope) {
284287
getApplicationContext(), type, names));
285288
}
286289
T bean = (name != null) ? getApplicationContext().getBean(name, type) : null;
287-
return Assertions.assertThat(bean).as("Bean of type <%s> from <%s>", type, getApplicationContext());
290+
return assertThat(bean).as("Bean of type <%s> from <%s>", type, getApplicationContext());
288291
}
289292

290293
private @Nullable String getPrimary(String[] names, Scope scope) {
@@ -330,13 +333,14 @@ private boolean isPrimary(String name, Scope scope) {
330333
* is found
331334
* @throws AssertionError if the application context did not start
332335
*/
336+
@CheckReturnValue
333337
public AbstractObjectAssert<?, Object> getBean(String name) {
334338
if (this.startupFailure != null) {
335339
throwAssertionError(
336340
contextFailedToStartWhenExpecting(this.startupFailure, "to contain a bean of name:%n <%s>", name));
337341
}
338342
Object bean = findBean(name);
339-
return Assertions.assertThat(bean).as("Bean of name <%s> from <%s>", name, getApplicationContext());
343+
return assertThat(bean).as("Bean of name <%s> from <%s>", name, getApplicationContext());
340344
}
341345

342346
/**
@@ -357,6 +361,7 @@ public AbstractObjectAssert<?, Object> getBean(String name) {
357361
* name but a different type
358362
*/
359363
@SuppressWarnings("unchecked")
364+
@CheckReturnValue
360365
public <T> AbstractObjectAssert<?, T> getBean(String name, Class<T> type) {
361366
if (this.startupFailure != null) {
362367
throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure,
@@ -368,8 +373,8 @@ public <T> AbstractObjectAssert<?, T> getBean(String name, Class<T> type) {
368373
"%nExpecting:%n <%s>%nto contain a bean of name:%n <%s> (%s)%nbut found:%n <%s> of type <%s>",
369374
getApplicationContext(), name, type, bean, bean.getClass()));
370375
}
371-
return Assertions.assertThat((T) bean)
372-
.as("Bean of name <%s> and type <%s> from <%s>", name, type, getApplicationContext());
376+
return assertThat((T) bean).as("Bean of name <%s> and type <%s> from <%s>", name, type,
377+
getApplicationContext());
373378
}
374379

375380
private @Nullable Object findBean(String name) {
@@ -395,6 +400,7 @@ public <T> AbstractObjectAssert<?, T> getBean(String name, Class<T> type) {
395400
* no beans are found
396401
* @throws AssertionError if the application context did not start
397402
*/
403+
@CheckReturnValue
398404
public <T> MapAssert<String, T> getBeans(Class<T> type) {
399405
return getBeans(type, Scope.INCLUDE_ANCESTORS);
400406
}
@@ -414,14 +420,15 @@ public <T> MapAssert<String, T> getBeans(Class<T> type) {
414420
* no beans are found
415421
* @throws AssertionError if the application context did not start
416422
*/
423+
@CheckReturnValue
417424
public <T> MapAssert<String, T> getBeans(Class<T> type, Scope scope) {
418425
Assert.notNull(scope, "'scope' must not be null");
419426
if (this.startupFailure != null) {
420427
throwAssertionError(
421428
contextFailedToStartWhenExpecting(this.startupFailure, "to get beans of type:%n <%s>", type));
422429
}
423-
return Assertions.assertThat(scope.getBeansOfType(getApplicationContext(), type))
424-
.as("Beans of type <%s> from <%s>", type, getApplicationContext());
430+
return assertThat(scope.getBeansOfType(getApplicationContext(), type)).as("Beans of type <%s> from <%s>", type,
431+
getApplicationContext());
425432
}
426433

427434
/**
@@ -434,6 +441,7 @@ public <T> MapAssert<String, T> getBeans(Class<T> type, Scope scope) {
434441
* @return assertions on the cause of the failure
435442
* @throws AssertionError if the application context started without a failure
436443
*/
444+
@CheckReturnValue
437445
public AbstractThrowableAssert<?, ? extends Throwable> getFailure() {
438446
hasFailed();
439447
return assertThat(this.startupFailure);

core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.skyscreamer.jsonassert.comparator.JSONComparator;
4141

4242
import org.springframework.core.io.Resource;
43+
import org.springframework.lang.CheckReturnValue;
4344
import org.springframework.util.ObjectUtils;
4445
import org.springframework.util.StringUtils;
4546
import org.springframework.util.function.ThrowingFunction;
@@ -917,6 +918,7 @@ public JsonContentAssert doesNotHaveEmptyJsonPathValue(CharSequence expression,
917918
* @return a new assertion object whose object under test is the extracted item
918919
* @throws AssertionError if the path is not valid
919920
*/
921+
@CheckReturnValue
920922
public AbstractObjectAssert<?, Object> extractingJsonPathValue(CharSequence expression, Object... args) {
921923
return Assertions.assertThat(new JsonPathValue(expression, args).getValue(false));
922924
}
@@ -929,6 +931,7 @@ public AbstractObjectAssert<?, Object> extractingJsonPathValue(CharSequence expr
929931
* @return a new assertion object whose object under test is the extracted item
930932
* @throws AssertionError if the path is not valid or does not result in a string
931933
*/
934+
@CheckReturnValue
932935
public AbstractCharSequenceAssert<?, String> extractingJsonPathStringValue(CharSequence expression,
933936
Object... args) {
934937
return Assertions.assertThat(extractingJsonPathValue(expression, args, String.class, "a string"));
@@ -942,6 +945,7 @@ public AbstractCharSequenceAssert<?, String> extractingJsonPathStringValue(CharS
942945
* @return a new assertion object whose object under test is the extracted item
943946
* @throws AssertionError if the path is not valid or does not result in a number
944947
*/
948+
@CheckReturnValue
945949
public AbstractObjectAssert<?, Number> extractingJsonPathNumberValue(CharSequence expression, Object... args) {
946950
return Assertions.assertThat(extractingJsonPathValue(expression, args, Number.class, "a number"));
947951
}
@@ -954,6 +958,7 @@ public AbstractObjectAssert<?, Number> extractingJsonPathNumberValue(CharSequenc
954958
* @return a new assertion object whose object under test is the extracted item
955959
* @throws AssertionError if the path is not valid or does not result in a boolean
956960
*/
961+
@CheckReturnValue
957962
public AbstractBooleanAssert<?> extractingJsonPathBooleanValue(CharSequence expression, Object... args) {
958963
return Assertions.assertThat(extractingJsonPathValue(expression, args, Boolean.class, "a boolean"));
959964
}
@@ -968,6 +973,7 @@ public AbstractBooleanAssert<?> extractingJsonPathBooleanValue(CharSequence expr
968973
* @throws AssertionError if the path is not valid or does not result in an array
969974
*/
970975
@SuppressWarnings("unchecked")
976+
@CheckReturnValue
971977
public <E> ListAssert<E> extractingJsonPathArrayValue(CharSequence expression, Object... args) {
972978
return Assertions.assertThat(extractingJsonPathValue(expression, args, List.class, "an array"));
973979
}
@@ -983,6 +989,7 @@ public <E> ListAssert<E> extractingJsonPathArrayValue(CharSequence expression, O
983989
* @throws AssertionError if the path is not valid or does not result in a map
984990
*/
985991
@SuppressWarnings("unchecked")
992+
@CheckReturnValue
986993
public <K, V> MapAssert<K, V> extractingJsonPathMapValue(CharSequence expression, Object... args) {
987994
return Assertions.assertThat(extractingJsonPathValue(expression, args, Map.class, "a map"));
988995
}

0 commit comments

Comments
 (0)