Skip to content

Commit 64e4c1c

Browse files
authored
feat: Use type-parameter-level nullability for Signal APIs (#23630)
Follow Java's List<E> pattern where nullability flows from the type argument rather than being hardcoded on method signatures. This makes Signal<String>.get() return non-null String while Signal<@nullable String>.get() returns @nullable String. Key changes: - All signal type parameters use <T extends @nullable Object> bound - Remove explicit @nullable from public method signatures (get, peek, set, replace, map, computed, etc.)
1 parent c769cc1 commit 64e4c1c

File tree

64 files changed

+963
-236
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+963
-236
lines changed

flow-server/pom.xml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,40 @@
224224
<fork>true</fork>
225225
</configuration>
226226
</execution>
227+
<execution>
228+
<id>default-testCompile</id>
229+
<configuration>
230+
<compilerArgs>
231+
<arg>-XDcompilePolicy=simple</arg>
232+
<arg>-XDaddTypeAnnotationsToSymbol=true</arg>
233+
<arg>--should-stop=ifError=FLOW</arg>
234+
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
235+
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
236+
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
237+
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
238+
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
239+
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
240+
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
241+
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
242+
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
243+
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
244+
<arg>-Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:OnlyNullMarked=true</arg>
245+
</compilerArgs>
246+
<annotationProcessorPaths>
247+
<path>
248+
<groupId>com.google.errorprone</groupId>
249+
<artifactId>error_prone_core</artifactId>
250+
<version>2.47.0</version>
251+
</path>
252+
<path>
253+
<groupId>com.uber.nullaway</groupId>
254+
<artifactId>nullaway</artifactId>
255+
<version>0.13.1</version>
256+
</path>
257+
</annotationProcessorPaths>
258+
<fork>true</fork>
259+
</configuration>
260+
</execution>
227261
</executions>
228262
</plugin>
229263
<plugin>

flow-server/src/main/java/com/vaadin/flow/component/HasComponents.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Objects;
2323
import java.util.Optional;
2424

25+
import org.jspecify.annotations.Nullable;
2526
import org.slf4j.LoggerFactory;
2627

2728
import com.vaadin.flow.dom.Element;
@@ -245,7 +246,8 @@ default void addComponentAsFirst(Component component) {
245246
* @throws BindingActiveException
246247
* thrown if a binding for children already exists
247248
*/
248-
default <T, S extends Signal<T>> void bindChildren(Signal<List<S>> list,
249+
default <T extends @Nullable Object, S extends Signal<T>> void bindChildren(
250+
Signal<List<S>> list,
249251
SerializableFunction<S, Component> childFactory) {
250252
var self = (Component & HasComponents) this;
251253
var node = self.getElement().getNode();

flow-server/src/main/java/com/vaadin/flow/component/SignalPropertySupport.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import java.io.Serializable;
1919
import java.util.Objects;
2020

21+
import org.jspecify.annotations.Nullable;
22+
2123
import com.vaadin.flow.function.SerializableConsumer;
2224
import com.vaadin.flow.shared.Registration;
2325
import com.vaadin.flow.signals.BindingActiveException;
@@ -65,7 +67,8 @@
6567
* @param <T>
6668
* the type of the property
6769
*/
68-
public class SignalPropertySupport<T> implements Serializable {
70+
public class SignalPropertySupport<T extends @Nullable Object>
71+
implements Serializable {
6972

7073
private final SerializableConsumer<T> valueChangeConsumer;
7174

@@ -104,8 +107,8 @@ private SignalPropertySupport(Component owner,
104107
* @return a new instance of SignalPropertySupport
105108
* @see #bind(Signal)
106109
*/
107-
public static <T> SignalPropertySupport<T> create(Component owner,
108-
SerializableConsumer<T> valueChangeConsumer) {
110+
public static <T extends @Nullable Object> SignalPropertySupport<T> create(
111+
Component owner, SerializableConsumer<T> valueChangeConsumer) {
109112
return new SignalPropertySupport<>(owner, valueChangeConsumer);
110113
}
111114

flow-server/src/main/java/com/vaadin/flow/dom/Element.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.stream.Stream;
3131

3232
import org.jsoup.nodes.Document;
33+
import org.jspecify.annotations.Nullable;
3334
import tools.jackson.core.type.TypeReference;
3435
import tools.jackson.databind.JsonNode;
3536
import tools.jackson.databind.node.BaseJsonNode;
@@ -899,8 +900,8 @@ public Element setPropertyMap(String name, Map<String, ?> value) {
899900
* thrown when there is already an existing binding
900901
* @see #setProperty(String, String)
901902
*/
902-
public <T> void bindProperty(String name, Signal<T> signal,
903-
SerializableConsumer<T> writeCallback) {
903+
public <T extends @Nullable Object> void bindProperty(String name,
904+
Signal<T> signal, SerializableConsumer<T> writeCallback) {
904905
verifySetPropertyName(name);
905906

906907
getStateProvider().bindPropertySignal(this, name, signal,

flow-server/src/main/java/com/vaadin/flow/dom/ElementEffect.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.Objects;
2424
import java.util.stream.Collectors;
2525

26+
import org.jspecify.annotations.Nullable;
27+
2628
import com.vaadin.flow.component.Component;
2729
import com.vaadin.flow.component.ComponentUtil;
2830
import com.vaadin.flow.component.UI;
@@ -145,8 +147,8 @@ public static Registration effect(Element owner,
145147
* @param <T>
146148
* the type of the signal value
147149
*/
148-
public static <T> Registration bind(Element owner, Signal<T> signal,
149-
SerializableBiConsumer<Element, T> setter) {
150+
public static <T extends @Nullable Object> Registration bind(Element owner,
151+
Signal<T> signal, SerializableBiConsumer<Element, T> setter) {
150152
return effect(owner, () -> {
151153
setter.accept(owner, signal.get());
152154
});
@@ -239,7 +241,7 @@ public void close() {
239241
* @throws IllegalStateException
240242
* thrown if parent element isn't empty
241243
*/
242-
public static <T, S extends Signal<T>> Registration bindChildren(
244+
public static <T extends @Nullable Object, S extends Signal<T>> Registration bindChildren(
243245
Element parentElement, Signal<List<S>> list,
244246
SerializableFunction<S, Element> childFactory) {
245247
Objects.requireNonNull(parentElement, "Parent element cannot be null");
@@ -263,7 +265,7 @@ public static <T, S extends Signal<T>> Registration bindChildren(
263265
valueSignalToChildCache)))::close;
264266
}
265267

266-
private static <T, S extends Signal<T>> void runEffect(
268+
private static <T extends @Nullable Object, S extends Signal<T>> void runEffect(
267269
BindChildrenEffectContext<T, S> context) {
268270
// Cache the children to avoid multiple traversals
269271
LinkedList<Element> remainingChildren = context
@@ -291,7 +293,7 @@ private static <T, S extends Signal<T>> void runEffect(
291293
* Validate that parent element has no children not belonging to the list of
292294
* child signals.
293295
*/
294-
private static <T, S extends Signal<T>> void validate(
296+
private static <T extends @Nullable Object, S extends Signal<T>> void validate(
295297
BindChildrenEffectContext<T, S> context) {
296298
LinkedList<Element> children = context.parentChildrenToLinkedList();
297299
int index = 0;
@@ -321,7 +323,7 @@ private static <T, S extends Signal<T>> void validate(
321323
* Remove all existing children in valueSignalToChildCache map that are no
322324
* longer present in the list of child signals.
323325
*/
324-
private static <T, S extends Signal<T>> void removeNotPresentChildren(
326+
private static <T extends @Nullable Object, S extends Signal<T>> void removeNotPresentChildren(
325327
BindChildrenEffectContext<T, S> context,
326328
HashSet<Element> remainingChildrenSet) {
327329
var toRemove = new HashSet<>(context.valueSignalToChildCache.keySet());
@@ -339,7 +341,7 @@ private static <T, S extends Signal<T>> void removeNotPresentChildren(
339341
* removing any existing elements. Creates new elements with the element
340342
* factory if not found from the cache.
341343
*/
342-
private static <T, S extends Signal<T>> void updateByChildSignals(
344+
private static <T extends @Nullable Object, S extends Signal<T>> void updateByChildSignals(
343345
BindChildrenEffectContext<T, S> context,
344346
LinkedList<Element> remainingChildren,
345347
HashSet<Element> remainingChildrenSet) {
@@ -410,7 +412,7 @@ private static <T, S extends Signal<T>> void updateByChildSignals(
410412
* @param <S>
411413
* the type of the signal in the list
412414
*/
413-
private record BindChildrenEffectContext<T, S extends Signal<T>>(
415+
private record BindChildrenEffectContext<T extends @Nullable Object, S extends Signal<T>>(
414416
Element parentElement, List<S> childSignalsList,
415417
SerializableFunction<S, Element> childElementFactory,
416418
HashMap<S, Element> valueSignalToChildCache)

flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeMap.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.function.Consumer;
2727
import java.util.stream.Stream;
2828

29+
import org.jspecify.annotations.Nullable;
30+
2931
import com.vaadin.flow.dom.Element;
3032
import com.vaadin.flow.dom.ElementEffect;
3133
import com.vaadin.flow.function.SerializableBiConsumer;
@@ -565,7 +567,8 @@ boolean usesSingleMap() {
565567
* given key
566568
*
567569
*/
568-
protected <T> void bindSignal(Element owner, String key, Signal<T> signal,
570+
protected <T extends @Nullable Object> void bindSignal(Element owner,
571+
String key, Signal<T> signal,
569572
SerializableBiConsumer<Element, T> setter,
570573
SerializableConsumer<?> writeCallback) {
571574
Objects.requireNonNull(signal, "Signal cannot be null");

flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SignalBindingFeature.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.HashMap;
2020
import java.util.Map;
2121

22+
import org.jspecify.annotations.Nullable;
23+
2224
import com.vaadin.flow.function.SerializableBiPredicate;
2325
import com.vaadin.flow.function.SerializableConsumer;
2426
import com.vaadin.flow.internal.StateNode;
@@ -174,7 +176,8 @@ public <T> SerializableConsumer<T> getWriteCallback(String key) {
174176
* the type of the signal value
175177
* @return the signal bound to the given key, or null if no signal is bound
176178
*/
177-
public <T> Signal<T> getSignal(String key) {
179+
@SuppressWarnings("unchecked")
180+
public <T extends @Nullable Object> Signal<T> getSignal(String key) {
178181
if (values == null) {
179182
return null;
180183
}
@@ -208,8 +211,9 @@ public <T> Signal<T> getSignal(String key) {
208211
* @param <T>
209212
* the type of the signal value
210213
*/
211-
public <T> boolean updateSignalByWriteCallback(String key, T oldValue,
212-
T newValue, SerializableBiPredicate<T, T> valueEquals,
214+
public <T extends @Nullable Object> boolean updateSignalByWriteCallback(
215+
String key, T oldValue, T newValue,
216+
SerializableBiPredicate<T, T> valueEquals,
213217
SerializableConsumer<T> revertCallback) {
214218
SerializableConsumer<T> callback = getWriteCallback(key);
215219
Signal<T> signal = getSignal(key);

flow-server/src/main/java/com/vaadin/flow/signals/Signal.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
* the signal value type
5555
*/
5656
@FunctionalInterface
57-
public interface Signal<T> extends Serializable {
57+
public interface Signal<T extends @Nullable Object> extends Serializable {
5858
/**
5959
* Gets the current value of this signal. The value is read in a way that
6060
* takes the current transaction into account and in the case of clustering
@@ -81,7 +81,6 @@ public interface Signal<T> extends Serializable {
8181
* @throws IllegalStateException
8282
* if called outside a reactive context
8383
*/
84-
@Nullable
8584
T get();
8685

8786
/**
@@ -95,7 +94,7 @@ public interface Signal<T> extends Serializable {
9594
*
9695
* @return the signal value
9796
*/
98-
default @Nullable T peek() {
97+
default T peek() {
9998
/*
10099
* Subclasses are encouraged to use an approach with less overhead than
101100
* what this very generic implementation can do.
@@ -121,7 +120,8 @@ public interface Signal<T> extends Serializable {
121120
* the mapper function to use, not <code>null</code>
122121
* @return the computed signal, not <code>null</code>
123122
*/
124-
default <C> Signal<C> map(SignalMapper<T, C> mapper) {
123+
default <C extends @Nullable Object> Signal<C> map(
124+
SignalMapper<T, C> mapper) {
125125
return () -> mapper.map(get());
126126
}
127127

@@ -210,22 +210,21 @@ static Registration unboundEffect(EffectAction action) {
210210
* the computation callback, not <code>null</code>
211211
* @return the computed signal, not <code>null</code>
212212
*/
213-
static <T> Signal<T> computed(SignalComputation<T> computation) {
213+
static <T extends @Nullable Object> Signal<T> computed(
214+
SignalComputation<T> computation) {
214215
return new ComputedSignal<>(computation);
215216
}
216217

217218
/**
218-
* Crates a new computed signal containing the negation of the provided
219-
* boolean-valued signal. <code>null</code> values are preserved as
220-
* <code>null</code>.
221-
*
219+
* Creates a new computed signal containing the negation of the provided
220+
* boolean-valued signal.
221+
*
222222
* @param signal
223223
* the boolean-valued signal to negate, not <code>null</code>
224224
* @return the negated signal, not <code>null</code>
225225
*/
226226
static Signal<Boolean> not(Signal<Boolean> signal) {
227-
return Objects.requireNonNull(signal)
228-
.map(value -> value == null ? null : !value);
227+
return Objects.requireNonNull(signal).map(value -> !value);
229228
}
230229

231230
/**
@@ -250,7 +249,7 @@ static Signal<Boolean> not(Signal<Boolean> signal) {
250249
* @return a transaction operation containing the supplier return value and
251250
* the eventual result
252251
*/
253-
static <T> TransactionOperation<T> runInTransaction(
252+
static <T extends @Nullable Object> TransactionOperation<T> runInTransaction(
254253
ValueSupplier<T> transactionTask) {
255254
return Transaction.runInTransaction(transactionTask);
256255
}
@@ -289,7 +288,8 @@ static TransactionOperation<Void> runInTransaction(
289288
* the supplier to run, not <code>null</code>
290289
* @return the value returned from the supplier
291290
*/
292-
static <T> @Nullable T runWithoutTransaction(ValueSupplier<T> task) {
291+
static <T extends @Nullable Object> T runWithoutTransaction(
292+
ValueSupplier<T> task) {
293293
return Transaction.runWithoutTransaction(task);
294294
}
295295

@@ -316,7 +316,7 @@ static void runWithoutTransaction(TransactionTask task) {
316316
* the supplier task to run, not <code>null</code>
317317
* @return the value returned from the supplier
318318
*/
319-
static <T> @Nullable T untracked(ValueSupplier<T> task) {
319+
static <T extends @Nullable Object> T untracked(ValueSupplier<T> task) {
320320
/*
321321
* Note that there's no Runnable overload since the whole point of
322322
* untracked is to read values.

flow-server/src/main/java/com/vaadin/flow/signals/function/SignalComputation.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@
3535
* @see Signal#computed(SignalComputation)
3636
*/
3737
@FunctionalInterface
38-
public interface SignalComputation<T> extends Serializable {
38+
public interface SignalComputation<T extends @Nullable Object>
39+
extends Serializable {
3940
/**
4041
* Computes the signal value, automatically tracking dependencies on other
4142
* signals.
4243
*
43-
* @return the computed value, may be <code>null</code>
44+
* @return the computed value
4445
*/
45-
@Nullable
4646
T compute();
4747
}

flow-server/src/main/java/com/vaadin/flow/signals/function/SignalMapper.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@
3535
* @see Signal#map(SignalMapper)
3636
*/
3737
@FunctionalInterface
38-
public interface SignalMapper<T, R> extends Serializable {
38+
public interface SignalMapper<T extends @Nullable Object, R extends @Nullable Object>
39+
extends Serializable {
3940
/**
4041
* Applies this mapper to transform a signal value.
4142
*
4243
* @param value
43-
* the input value, may be <code>null</code>
44-
* @return the transformed value, may be <code>null</code>
44+
* the input value
45+
* @return the transformed value
4546
*/
46-
@Nullable
47-
R map(@Nullable T value);
47+
R map(T value);
4848
}

0 commit comments

Comments
 (0)