ImplicitNullInterop
This module defines methods to interpret types originating from sources without explicit nulls (Java, and Scala code compiled without -Yexplicit-nulls) as Scala types with explicit nulls. In those sources, reference types are implicitly nullable; here we make that nullability explicit.
e.g. given a Java method: String foo(String arg) { return arg; }
After calling nullifyMember, Scala will see the method as: def foo(arg: String | Null): String | Null
The transformation is (conceptually) a function n that adheres to the following rules: (1) n(T) = T | Null if T is a reference type (2) n(T) = T if T is a value type (3) n(C[T]) = C[T] | Null if C is Java-defined (4) n(C[T]) = C[n(T)] | Null if C is Scala-defined (5) n(A|B) = n(A) | n(B) | Null (6) n(A&B) = n(A) & n(B) (7) n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R (8) n(T) = T otherwise
Treatment of generics (rules 3 and 4):
- if
Cis Java-defined, thenn(C[T]) = C[T] | Null. That is, we don't recurse on the type argument, and only add Null on the outside. This is becauseCitself will be nullified, and in particular so will be usages ofC's type argument within C's body. e.g. callinggeton ajava.util.List[String]already returnsString|Nulland notString, so we don't need to writejava.util.List[String | Null]. - if
Cis Scala-defined, however, then we wantn(C[T]) = C[n(T)] | Null. This is because Scala-defined classes are not implicitly nullified inside their bodies, so we need to indicate that their type arguments are nullable when the defining source did not use explicit nulls.
Why not use subtyping to nullify “exactly”?
The symbols we nullify here are often still under construction (e.g. during classfile loading or unpickling), so we don't always have precise or stable type information available. Using full subtyping checks to determine which parts are reference types would either force types prematurely or risk cyclic initializations. Therefore, we use a conservative approach that targets concrete reference types without depending on precise subtype information.
Scope and limitations
The transformation is applied to types attached to members coming from Java and from Scala code compiled without explicit nulls. The implementation is intentionally conservative and does not attempt to cover the full spectrum of Scala types. In particular, we do not nullify type parameters or some complex type forms (e.g., match types, or refined types) beyond straightforward mapping; in such cases we typically recurse only into obviously safe positions or leave the type unchanged.
Additionally, some kinds of symbols like constructors and enum instances get special treatment.
Attributes
- Graph
-
- Supertypes
- Self type
-
ImplicitNullInterop.type