Skip to content

Commit bbb9c1c

Browse files
committed
Decorate all Assert implementations with @CheckReturnValue
Signed-off-by: Stefano Cordio <stefano.cordio@gmail.com>
1 parent bc25b58 commit bbb9c1c

File tree

14 files changed

+166
-12
lines changed

14 files changed

+166
-12
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
@@ -71,6 +71,7 @@
7171
* @author Phillip Webb
7272
* @author Dmytro Nosan
7373
* @author Moritz Halbritter
74+
* @author Stefano Cordio
7475
*/
7576
public abstract class ArchitectureCheck extends DefaultTask {
7677

@@ -95,6 +96,8 @@ public ArchitectureCheck() {
9596
.beanMethods(annotationClassFor(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION))));
9697
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(
9798
annotationClassFor(DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION))));
99+
getRules().addAll(whenMainSources(() -> Collections.singletonList(
100+
ArchitectureRules.allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue())));
98101
getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList(
99102
ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get())))));
100103
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

@@ -148,6 +150,26 @@ private static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(
148150
.allowEmptyShould(true);
149151
}
150152

153+
static ArchRule allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue() {
154+
return ArchRuleDefinition.methods()
155+
.that()
156+
.areDeclaredInClassesThat()
157+
.implement("org.assertj.core.api.Assert")
158+
.and()
159+
.arePublic()
160+
.and()
161+
.doNotHaveModifier(JavaModifier.BRIDGE)
162+
.and(doNotReturnSelfType())
163+
.should()
164+
.beAnnotatedWith(CheckReturnValue.class)
165+
.allowEmptyShould(true);
166+
}
167+
168+
private static DescribedPredicate<JavaMethod> doNotReturnSelfType() {
169+
return DescribedPredicate.describe("do not return self type",
170+
(method) -> !method.getRawReturnType().equals(method.getOwner()));
171+
}
172+
151173
private static ArchRule allPackagesShouldBeFreeOfTangles() {
152174
return SlicesRuleDefinition.slices()
153175
.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
@@ -59,13 +59,18 @@
5959
* @author Scott Frederick
6060
* @author Ivan Malutin
6161
* @author Dmytro Nosan
62+
* @author Stefano Cordio
6263
*/
6364
class ArchitectureCheckTests {
6465

65-
private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9";
66+
private static final String ASSERTJ_CORE = "org.assertj:assertj-core:3.27.4";
6667

6768
private static final String JUNIT_JUPITER = "org.junit.jupiter:junit-jupiter:5.12.0";
6869

70+
private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9";
71+
72+
private static final String SPRING_CORE = "org.springframework:spring-core:6.2.9";
73+
6974
private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1";
7075

7176
private GradleBuild gradleBuild;
@@ -353,6 +358,23 @@ void whenDeprecatedConfigurationPropertyIsMissingSinceShouldFailAndWriteReport()
353358
"DeprecatedConfigurationPropertySince.getProperty");
354359
}
355360

361+
@Test
362+
void whenCustomAssertionMethodNotReturningSelfIsAnnotatedWithCheckReturnValueShouldSucceedAndWriteEmptyReport()
363+
throws IOException {
364+
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/checkReturnValue");
365+
build(this.gradleBuild.withDependencies(ASSERTJ_CORE, SPRING_CORE), Task.CHECK_ARCHITECTURE_MAIN);
366+
}
367+
368+
@Test
369+
void whenCustomAssertionMethodNotReturningSelfIsNotAnnotatedWithCheckReturnValueShouldFailAndWriteReport()
370+
throws IOException {
371+
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/noCheckReturnValue");
372+
buildAndFail(this.gradleBuild.withDependencies(ASSERTJ_CORE), Task.CHECK_ARCHITECTURE_MAIN,
373+
"methods that are declared in classes that implement org.assertj.core.api.Assert and "
374+
+ "are public and do not have modifier BRIDGE and do not return self type should be annotated "
375+
+ "with @CheckReturnValue");
376+
}
377+
356378
private void prepareTask(Task task, String... sourceDirectories) throws IOException {
357379
for (String sourceDirectory : sourceDirectories) {
358380
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)