Skip to content

Commit 4c77350

Browse files
committed
Refine null-safety with NullAway build-time checks
This commit introduces null-safety checks for spring-core at build-time in order to validate the consistency of Spring null-safety annotations and generate errors when inconsistencies are detected during a build (similar to what is done with Checkstyle). In order to make that possible, this commit also introduces a new org.springframework.lang.Contract annotation inspired from org.jetbrains.annotations.Contract, which allows to specify semantics of methods like Assert#notNull in order to prevent artificial additional null checks in Spring Framework code base. This commit only checks org.springframework.core package, follow-up commits will also extend the analysis to other modules, after related null-safety refinements. See gh-32475
1 parent 0a715bc commit 4c77350

14 files changed

+117
-1
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ plugins {
99
id 'de.undercouch.download' version '5.4.0'
1010
id 'me.champeau.jmh' version '0.7.2' apply false
1111
id 'me.champeau.mrjar' version '0.1.1'
12+
id "net.ltgt.errorprone" version "3.1.0" apply false
1213
}
1314

1415
ext {

gradle/spring-module.gradle

+18
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ apply plugin: 'org.springframework.build.optional-dependencies'
66
// apply plugin: 'com.github.johnrengelman.shadow'
77
apply plugin: 'me.champeau.jmh'
88
apply from: "$rootDir/gradle/publications.gradle"
9+
apply plugin: 'net.ltgt.errorprone'
910

1011
dependencies {
1112
jmh 'org.openjdk.jmh:jmh-core:1.37'
1213
jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
1314
jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37'
1415
jmh 'net.sf.jopt-simple:jopt-simple'
16+
errorprone 'com.uber.nullaway:nullaway:0.10.24'
17+
errorprone 'com.google.errorprone:error_prone_core:2.9.0'
1518
}
1619

1720
pluginManager.withPlugin("kotlin") {
@@ -109,3 +112,18 @@ publishing {
109112
// Disable publication of test fixture artifacts.
110113
components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
111114
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }
115+
116+
tasks.withType(JavaCompile).configureEach {
117+
options.errorprone {
118+
disableAllChecks = true
119+
option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract")
120+
option("NullAway:AnnotatedPackages", "org.springframework.core")
121+
option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," +
122+
"org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," +
123+
"org.springframework.javapoet,org.springframework.aot.nativex.substitution")
124+
}
125+
}
126+
tasks.compileJava {
127+
// The check defaults to a warning, bump it up to an error for the main sources
128+
options.errorprone.error("NullAway")
129+
}

spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Map;
2323
import java.util.Objects;
2424

25+
import org.springframework.lang.Contract;
2526
import org.springframework.lang.Nullable;
2627
import org.springframework.util.Assert;
2728
import org.springframework.util.ConcurrentReferenceHashMap;
@@ -80,6 +81,7 @@ Annotation[] findRepeatedAnnotations(Annotation annotation) {
8081

8182

8283
@Override
84+
@Contract("null -> false")
8385
public boolean equals(@Nullable Object other) {
8486
if (other == this) {
8587
return true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2024 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.lang;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Inspired from {@code org.jetbrains.annotations.Contract}, this variant has been introduce in the
27+
* {@code org.springframework.lang} package to avoid requiring an extra dependency, while still following the same semantics.
28+
*
29+
* <p>Specifies some aspects of the method behavior depending on the arguments. Can be used by tools for advanced data flow analysis.
30+
* Note that this annotation just describes how the code works and doesn't add any functionality by means of code generation.
31+
*
32+
* <p>Method contract has the following syntax:<br/>
33+
* contract ::= (clause ';')* clause<br/>
34+
* clause ::= args '->' effect<br/>
35+
* args ::= ((arg ',')* arg )?<br/>
36+
* arg ::= value-constraint<br/>
37+
* value-constraint ::= 'any' | 'null' | '!null' | 'false' | 'true'<br/>
38+
* effect ::= value-constraint | 'fail'
39+
*
40+
* The constraints denote the following:<br/>
41+
* <ul>
42+
* <li> _ - any value
43+
* <li> null - null value
44+
* <li> !null - a value statically proved to be not-null
45+
* <li> true - true boolean value
46+
* <li> false - false boolean value
47+
* <li> fail - the method throws an exception, if the arguments satisfy argument constraints
48+
* </ul>
49+
* <p>Examples:
50+
* <code>@Contract("_, null -> null")</code> - method returns null if its second argument is null<br/>
51+
* <code>@Contract("_, null -> null; _, !null -> !null")</code> - method returns null if its second argument is null and not-null otherwise<br/>
52+
* <code>@Contract("true -> fail")</code> - a typical assertFalse method which throws an exception if <code>true</code> is passed to it<br/>
53+
*
54+
* @author Sebastien Deleuze
55+
* @since 6.2
56+
* @see <a href="https://github.com/uber/NullAway/wiki/Configuration#custom-contract-annotations">NullAway custom contract annotations</a>
57+
*/
58+
@Documented
59+
@Retention(RetentionPolicy.CLASS)
60+
@Target(ElementType.METHOD)
61+
public @interface Contract {
62+
63+
/**
64+
* Contains the contract clauses describing causal relations between call arguments and the returned value.
65+
*/
66+
String value() default "";
67+
68+
/**
69+
* Specifies if this method is pure, i.e. has no visible side effects. This may be used for more precise data flow analysis, and
70+
* to check that the method's return value is actually used in the call place.
71+
*/
72+
boolean pure() default false;
73+
}

spring-core/src/main/java/org/springframework/util/Assert.java

+9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Map;
2121
import java.util.function.Supplier;
2222

23+
import org.springframework.lang.Contract;
2324
import org.springframework.lang.Nullable;
2425

2526
/**
@@ -71,6 +72,7 @@ public abstract class Assert {
7172
* @param message the exception message to use if the assertion fails
7273
* @throws IllegalStateException if {@code expression} is {@code false}
7374
*/
75+
@Contract("false, _ -> fail")
7476
public static void state(boolean expression, String message) {
7577
if (!expression) {
7678
throw new IllegalStateException(message);
@@ -92,6 +94,7 @@ public static void state(boolean expression, String message) {
9294
* @throws IllegalStateException if {@code expression} is {@code false}
9395
* @since 5.0
9496
*/
97+
@Contract("false, _ -> fail")
9598
public static void state(boolean expression, Supplier<String> messageSupplier) {
9699
if (!expression) {
97100
throw new IllegalStateException(nullSafeGet(messageSupplier));
@@ -106,6 +109,7 @@ public static void state(boolean expression, Supplier<String> messageSupplier) {
106109
* @param message the exception message to use if the assertion fails
107110
* @throws IllegalArgumentException if {@code expression} is {@code false}
108111
*/
112+
@Contract("false, _ -> fail")
109113
public static void isTrue(boolean expression, String message) {
110114
if (!expression) {
111115
throw new IllegalArgumentException(message);
@@ -124,6 +128,7 @@ public static void isTrue(boolean expression, String message) {
124128
* @throws IllegalArgumentException if {@code expression} is {@code false}
125129
* @since 5.0
126130
*/
131+
@Contract("false, _ -> fail")
127132
public static void isTrue(boolean expression, Supplier<String> messageSupplier) {
128133
if (!expression) {
129134
throw new IllegalArgumentException(nullSafeGet(messageSupplier));
@@ -137,6 +142,7 @@ public static void isTrue(boolean expression, Supplier<String> messageSupplier)
137142
* @param message the exception message to use if the assertion fails
138143
* @throws IllegalArgumentException if the object is not {@code null}
139144
*/
145+
@Contract("!null, _ -> fail")
140146
public static void isNull(@Nullable Object object, String message) {
141147
if (object != null) {
142148
throw new IllegalArgumentException(message);
@@ -154,6 +160,7 @@ public static void isNull(@Nullable Object object, String message) {
154160
* @throws IllegalArgumentException if the object is not {@code null}
155161
* @since 5.0
156162
*/
163+
@Contract("!null, _ -> fail")
157164
public static void isNull(@Nullable Object object, Supplier<String> messageSupplier) {
158165
if (object != null) {
159166
throw new IllegalArgumentException(nullSafeGet(messageSupplier));
@@ -167,6 +174,7 @@ public static void isNull(@Nullable Object object, Supplier<String> messageSuppl
167174
* @param message the exception message to use if the assertion fails
168175
* @throws IllegalArgumentException if the object is {@code null}
169176
*/
177+
@Contract("null, _ -> fail")
170178
public static void notNull(@Nullable Object object, String message) {
171179
if (object == null) {
172180
throw new IllegalArgumentException(message);
@@ -185,6 +193,7 @@ public static void notNull(@Nullable Object object, String message) {
185193
* @throws IllegalArgumentException if the object is {@code null}
186194
* @since 5.0
187195
*/
196+
@Contract("null, _ -> fail")
188197
public static void notNull(@Nullable Object object, Supplier<String> messageSupplier) {
189198
if (object == null) {
190199
throw new IllegalArgumentException(nullSafeGet(messageSupplier));

spring-core/src/main/java/org/springframework/util/ClassUtils.java

+1
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ public static boolean isPrimitiveWrapperArray(Class<?> clazz) {
544544
* @param clazz the class to check
545545
* @return the original class, or a primitive wrapper for the original primitive type
546546
*/
547+
@SuppressWarnings("NullAway")
547548
public static Class<?> resolvePrimitiveIfNecessary(Class<?> clazz) {
548549
Assert.notNull(clazz, "Class must not be null");
549550
return (clazz.isPrimitive() && clazz != void.class ? primitiveTypeToWrapperMap.get(clazz) : clazz);

spring-core/src/main/java/org/springframework/util/CollectionUtils.java

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.function.BiFunction;
3535
import java.util.function.Consumer;
3636

37+
import org.springframework.lang.Contract;
3738
import org.springframework.lang.Nullable;
3839

3940
/**
@@ -71,6 +72,7 @@ public static boolean isEmpty(@Nullable Collection<?> collection) {
7172
* @param map the Map to check
7273
* @return whether the given Map is empty
7374
*/
75+
@Contract("null -> true")
7476
public static boolean isEmpty(@Nullable Map<?, ?> map) {
7577
return (map == null || map.isEmpty());
7678
}

spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
* @param <V> the type of the cached values, does not allow null values
4646
* @see #get(Object)
4747
*/
48-
@SuppressWarnings({"unchecked"})
48+
@SuppressWarnings({"unchecked", "NullAway"})
4949
public final class ConcurrentLruCache<K, V> {
5050

5151
private final int capacity;

spring-core/src/main/java/org/springframework/util/MimeType.java

+1
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ public int compareTo(MimeType other) {
573573
else {
574574
String thisValue = getParameters().get(thisAttribute);
575575
String otherValue = other.getParameters().get(otherAttribute);
576+
Assert.notNull(thisValue, "Parameter for " + thisAttribute + " must not be null");
576577
if (otherValue == null) {
577578
otherValue = "";
578579
}

spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java

+1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ public static MimeType parseMimeType(String mimeType) {
213213
return cachedMimeTypes.get(mimeType);
214214
}
215215

216+
@SuppressWarnings("NullAway")
216217
private static MimeType parseMimeTypeInternal(String mimeType) {
217218
int index = mimeType.indexOf(';');
218219
String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim();

spring-core/src/main/java/org/springframework/util/NumberUtils.java

+1
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ else if (BigDecimal.class == targetClass || Number.class == targetClass) {
238238
* @see #convertNumberToTargetClass
239239
* @see #parseNumber(String, Class)
240240
*/
241+
@SuppressWarnings("NullAway")
241242
public static <T extends Number> T parseNumber(
242243
String text, Class<T> targetClass, @Nullable NumberFormat numberFormat) {
243244

spring-core/src/main/java/org/springframework/util/StringUtils.java

+5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.TimeZone;
3737
import java.util.stream.Collectors;
3838

39+
import org.springframework.lang.Contract;
3940
import org.springframework.lang.Nullable;
4041

4142
/**
@@ -122,6 +123,7 @@ public static boolean isEmpty(@Nullable Object str) {
122123
* @see #hasLength(String)
123124
* @see #hasText(CharSequence)
124125
*/
126+
@Contract("null -> false")
125127
public static boolean hasLength(@Nullable CharSequence str) {
126128
return (str != null && str.length() > 0);
127129
}
@@ -135,6 +137,7 @@ public static boolean hasLength(@Nullable CharSequence str) {
135137
* @see #hasLength(CharSequence)
136138
* @see #hasText(String)
137139
*/
140+
@Contract("null -> false")
138141
public static boolean hasLength(@Nullable String str) {
139142
return (str != null && !str.isEmpty());
140143
}
@@ -158,6 +161,7 @@ public static boolean hasLength(@Nullable String str) {
158161
* @see #hasLength(CharSequence)
159162
* @see Character#isWhitespace
160163
*/
164+
@Contract("null -> false")
161165
public static boolean hasText(@Nullable CharSequence str) {
162166
if (str == null) {
163167
return false;
@@ -188,6 +192,7 @@ public static boolean hasText(@Nullable CharSequence str) {
188192
* @see #hasLength(String)
189193
* @see Character#isWhitespace
190194
*/
195+
@Contract("null -> false")
191196
public static boolean hasText(@Nullable String str) {
192197
return (str != null && !str.isBlank());
193198
}

spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public interface ListenableFuture<T> extends Future<T> {
6262
* Expose this {@link ListenableFuture} as a JDK {@link CompletableFuture}.
6363
* @since 5.0
6464
*/
65+
@SuppressWarnings("NullAway")
6566
default CompletableFuture<T> completable() {
6667
CompletableFuture<T> completable = new DelegatingCompletableFuture<>(this);
6768
addCallback(completable::complete, completable::completeExceptionally);

spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public void addCallback(SuccessCallback<? super T> successCallback, FailureCallb
7070
}
7171

7272
@Override
73+
@SuppressWarnings("NullAway")
7374
public CompletableFuture<T> completable() {
7475
CompletableFuture<T> completable = new DelegatingCompletableFuture<>(this);
7576
this.callbacks.addSuccessCallback(completable::complete);

0 commit comments

Comments
 (0)