Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gradle/spring-module.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ publishing {
}
}

nullability {
nullAwayVersion = "0.14.0"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternately, we could wait for a new nullability plugin release that defaults to 0.14.0. The other changes would remain valid.

}

// Disable publication of test fixture artifacts.
components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

import org.jspecify.annotations.Nullable;
import org.quartz.SchedulerConfigException;
import org.quartz.simpl.SimpleThreadPool;

Expand Down Expand Up @@ -83,7 +84,7 @@ public Future<?> submit(Runnable task) {
}

@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
FutureTask<T> future = new FutureTask<>(task);
execute(future);
return future;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public Future<?> submit(Runnable task) {
}

@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
return this.adaptedExecutor.submit(task);
}

Expand Down Expand Up @@ -208,7 +208,7 @@ public Future<?> submit(Runnable task) {
}

@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
return super.submit(ManagedTaskBuilder.buildManagedTask(task, task.toString()));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ public Future<?> submit(Runnable task) {
}

@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public Future<?> submit(Runnable task) {
}

@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ public Future<?> submit(Runnable task) {
}

@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
ExecutorService executor = getThreadPoolExecutor();
try {
return executor.submit(task);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ public Future<?> submit(Runnable task) {
}

@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
ExecutorService executor = getScheduledExecutor();
try {
return executor.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public ConvertingComparator(Comparator<T> comparator, Converter<S, T> converter)
* @param conversionService the conversion service
* @param targetType the target type
*/
@SuppressWarnings("NullAway") // Retain support for comparators that handle a null conversion result

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is Codex's explanation for this warning suppression. I read through it and it makes sense to me, but I'm not an expert on this code. I'm not sure what is the best fix (that is not a suppression).


The suppression covers a real mismatch between the legacy runtime behavior and the declared generic types.

The constructor adapts a ConversionService into a Converter:

public ConvertingComparator(
        Comparator<T> comparator,
        ConversionService conversionService,
        Class<? extends T> targetType) {

    this(comparator,
            new ConversionServiceConverter<>(conversionService, targetType));
}

The delegated constructor expects:

ConvertingComparator(Comparator<T>, Converter<S, T>)

But ConversionService.convert(...) is explicitly nullable:

<T> @Nullable T convert(@Nullable Object source, Class<T> targetType);

Consequently, the adapter is:

Converter<S, @Nullable T>

NullAway correctly observes that this cannot safely become Converter<S, T>. During comparison, the result is passed directly to the comparator:

T converted = this.converter.convert(source);
return this.comparator.compare(converted1, converted2);

If conversion returns null:

  • A null-aware comparator such as Comparator.nullsFirst(...) handles it correctly.
  • A comparator that does not support null will likely throw NullPointerException.

The existing API allows both possibilities and leaves responsibility with the supplied comparator. That relationship is not accurately represented by the constructor’s Comparator<T> type.

The comment therefore means:

Do not force the conversion result to be non-null, because existing callers may intentionally supply a comparator that supports null.

For example, adding this would satisfy NullAway:

return Objects.requireNonNull(
        this.conversionService.convert(source, this.targetType));

But it would change behavior by rejecting null before a null-aware comparator could process it.

Accurately modeling this would require a larger API redesign, likely requiring this constructor to produce a ConvertingComparator<S, @Nullable T> and accept a comparator whose input type is nullable. Constructors cannot independently change the enclosing class’s type argument, so a static factory or internal class redesign would probably be needed.

Thus, this suppression is:

  • Narrowly scoped to the affected constructor.
  • Preserving established runtime behavior.
  • Not claiming that NullAway is wrong.
  • Documenting that null handling is delegated to the caller-provided comparator.

public ConvertingComparator(
Comparator<T> comparator, ConversionService conversionService, Class<? extends T> targetType) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
final class CharacterToNumberFactory implements ConverterFactory<Character, Number> {

@Override
public <T extends Number> Converter<Character, @Nullable T> getConverter(Class<T> targetType) {
public <T extends Number> Converter<Character, ? extends @Nullable T> getConverter(Class<T> targetType) {
return new CharacterToNumber<>(targetType);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
final class NumberToNumberConverterFactory implements ConverterFactory<Number, Number>, ConditionalConverter {

@Override
public <T extends Number> Converter<Number, @Nullable T> getConverter(Class<T> targetType) {
public <T extends Number> Converter<Number, ? extends @Nullable T> getConverter(Class<T> targetType) {
return new NumberToNumber<>(targetType);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ public Future<?> submit(Runnable task) {

@SuppressWarnings("deprecation")
@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
FutureTask<T> future = new FutureTask<>(task);
execute(future, TIMEOUT_INDEFINITE);
return future;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public Future<?> submit(Runnable task) {
}

@Override
public <T> Future<T> submit(Callable<T> task) {
public <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
try {
if (this.taskDecorator == null &&
this.concurrentExecutor instanceof ExecutorService executorService) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
* @since 3.0
* @param <V> the value type
*/
public class LinkedCaseInsensitiveMap<V> implements Map<String, V>, Serializable, Cloneable {
public class LinkedCaseInsensitiveMap<V extends @Nullable Object> implements Map<String, V>, Serializable, Cloneable {

@Serial
private static final long serialVersionUID = -1797561627545787622L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
* @param <T> the type of objects that may be compared by this comparator
* @see Comparator#thenComparing(Comparator)
*/
public class InstanceComparator<T> implements Comparator<T> {
public class InstanceComparator<T extends @Nullable Object> implements Comparator<T> {

private final Class<?>[] instanceOrder;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ public void query(String sql, RowCallbackHandler rch) throws DataAccessException
}

@Override
public <T> Stream<T> queryForStream(String sql, RowMapper<T> rowMapper) throws DataAccessException {
public <T extends @Nullable Object> Stream<T> queryForStream(String sql, RowMapper<T> rowMapper) throws DataAccessException {
class StreamStatementCallback implements StatementCallback<Stream<T>>, SqlProvider {
@Override
public Stream<T> doInStatement(Statement stmt) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public void query(String sql, RowCallbackHandler rch) throws DataAccessException
}

@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
public <T extends @Nullable Object> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, EmptySqlParameterSource.INSTANCE, rowMapper);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,12 @@ public CookieAssertions expectCookie() {
}

@Override
public <E> ListBodySpec<E> expectBodyList(Class<E> elementType) {
public <E extends @Nullable Object> ListBodySpec<E> expectBodyList(Class<E> elementType) {
return getListBodySpec(this.response.bodyToFlux(elementType));
}

@Override
public <E> ListBodySpec<E> expectBodyList(ParameterizedTypeReference<E> elementType) {
public <E extends @Nullable Object> ListBodySpec<E> expectBodyList(ParameterizedTypeReference<E> elementType) {
Flux<E> flux = this.response.bodyToFlux(elementType);
return getListBodySpec(flux);
}
Expand Down