Skip to content

Commit e445207

Browse files
committed
Refactor ImplicitNullInterop
1 parent a542758 commit e445207

File tree

1 file changed

+136
-113
lines changed

1 file changed

+136
-113
lines changed

compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala

Lines changed: 136 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@ package dotty.tools.dotc
22
package core
33

44
import Contexts.*
5-
import Flags.JavaDefined
5+
import Flags.*
66
import StdNames.nme
77
import Symbols.*
88
import Types.*
9-
import dotty.tools.dotc.reporting.*
10-
import dotty.tools.dotc.core.Decorators.i
9+
import Decorators.i
10+
import reporting.*
1111

12-
/** This module defines methods to interpret types of Java symbols, which are implicitly nullable in Java,
13-
* as Scala types, which are explicitly nullable.
12+
/** This module defines methods to interpret types originating from sources without explicit nulls
13+
* (Java, and Scala code compiled without `-Yexplicit-nulls`) as Scala types with explicit nulls.
14+
* In those sources, reference types are implicitly nullable; here we make that nullability explicit.
15+
*
16+
* e.g. given a Java method: `String foo(String arg) { return arg; }`
17+
*
18+
* After calling `nullifyMember`, Scala will see the method as:
19+
* `def foo(arg: String | Null): String | Null`
1420
*
1521
* The transformation is (conceptually) a function `n` that adheres to the following rules:
1622
* (1) n(T) = T | Null if T is a reference type
17-
* (2) n(T) = T if T is a value type
23+
* (2) n(T) = T if T is a value type
1824
* (3) n(C[T]) = C[T] | Null if C is Java-defined
1925
* (4) n(C[T]) = C[n(T)] | Null if C is Scala-defined
2026
* (5) n(A|B) = n(A) | n(B) | Null
@@ -29,148 +35,165 @@ import dotty.tools.dotc.core.Decorators.i
2935
* e.g. calling `get` on a `java.util.List[String]` already returns `String|Null` and not `String`, so
3036
* we don't need to write `java.util.List[String | Null]`.
3137
* - if `C` is Scala-defined, however, then we want `n(C[T]) = C[n(T)] | Null`. This is because
32-
* `C` won't be nullified, so we need to indicate that its type argument is nullable.
38+
* Scala-defined classes are not implicitly nullified inside their bodies, so we need to indicate that
39+
* their type arguments are nullable when the defining source did not use explicit nulls.
40+
*
41+
* Why not use subtyping to nullify “exactly”?
42+
* -------------------------------------------------
43+
* The symbols we nullify here are often still under construction (e.g. during classfile loading or unpickling),
44+
* so we don't always have precise or stable type information available. Using full subtyping checks to determine
45+
* which parts are reference types would either force types prematurely or risk cyclic initializations. Therefore,
46+
* we use a conservative approach that targets concrete reference types without depending on precise subtype
47+
* information.
48+
*
49+
* Scope and limitations
50+
* -------------------------------------------------
51+
* The transformation is applied to types attached to members coming from Java and from Scala code compiled without
52+
* explicit nulls. The implementation is intentionally conservative and does not attempt to cover the full spectrum
53+
* of Scala types. In particular, we do not nullify type parameters or some complex type forms (e.g., match types,
54+
* or refined types) beyond straightforward mapping; in such cases we typically recurse only into obviously safe
55+
* positions or leave the type unchanged.
3356
*
34-
* Notice that since the transformation is only applied to types attached to Java symbols, it doesn't need
35-
* to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and
36-
* enum instances get special treatment.
57+
* Additionally, some kinds of symbols like constructors and enum instances get special treatment.
3758
*/
38-
object ImplicitNullInterop {
59+
object ImplicitNullInterop:
3960

40-
/** Transforms the type `tp` of Java member `sym` to be explicitly nullable.
41-
* `tp` is needed because the type inside `sym` might not be set when this method is called.
42-
*
43-
* e.g. given a Java method
44-
* String foo(String arg) { return arg; }
45-
*
46-
* After calling `nullifyMember`, Scala will see the method as
47-
*
48-
* def foo(arg: String | Null): String | Null
49-
*
50-
* If unsafeNulls is enabled, we can select on the return of `foo`:
51-
*
52-
* val len = foo("hello").length
53-
*
54-
* But the selection can throw an NPE if the returned value is `null`.
61+
/** Transforms the type `tp` of a member `sym` that originates from a source without explicit nulls.
62+
* `tp` is passed explicitly because the type stored in `sym` might not yet be set when this is called.
5563
*/
56-
def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(using Context): Type = trace(i"nullifyMember ${sym}, ${tp}"){
64+
def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(using Context): Type = trace(i"nullifyMember ${sym}, ${tp}"):
5765
assert(ctx.explicitNulls)
5866

59-
// Some special cases when nullifying the type
60-
if isEnumValueDef || sym.name == nme.TYPE_ // Don't nullify the `TYPE` field in every class and Java enum instances
61-
|| sym.is(Flags.ModuleVal) // Don't nullify Modules
62-
then
63-
tp
64-
else if sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) then
65-
// Don't nullify the return type of the `toString` method.
66-
// Don't nullify the return type of constructors.
67-
// Don't nullify the return type of methods with a not-null annotation.
68-
nullifyExceptReturnType(tp)
69-
else
70-
// Otherwise, nullify everything
71-
nullifyType(tp)
72-
}
67+
// Skip `TYPE`, enum values, and modules
68+
if isEnumValueDef || sym.name == nme.TYPE_ || sym.is(Flags.ModuleVal) then
69+
return tp
7370

74-
private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean =
75-
ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined)
71+
// Skip result type for `toString`, constructors, and @NotNull methods
72+
val skipResultType = sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym)
73+
// Don't nullify parameter types for Scala-defined methods since those are already nullified
74+
val skipParamTypes = sym.is(Method) && !sym.is(Flags.JavaDefined)
75+
// Skip Given/implicit parameters
76+
val skipCurrentLevel = sym.isOneOf(GivenOrImplicitVal)
7677

77-
/** If tp is a MethodType, the parameters and the inside of return type are nullified,
78-
* but the result return type is not nullable.
79-
* If tp is a type of a field, the inside of the type is nullified,
80-
* but the result type is not nullable.
81-
*/
82-
private def nullifyExceptReturnType(tp: Type)(using Context): Type =
83-
new ImplicitNullMap(outermostLevelAlreadyNullable = true)(tp)
78+
val map = new ImplicitNullMap(skipResultType = skipResultType, skipParamTypes = skipParamTypes, skipCurrentLevel = skipCurrentLevel)
79+
map(tp)
8480

85-
/** Nullifies a type by adding `| Null` in the relevant places. */
86-
private def nullifyType(tp: Type)(using Context): Type =
87-
new ImplicitNullMap(outermostLevelAlreadyNullable = false)(tp)
81+
private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean =
82+
ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined)
8883

89-
/** A type map that implements the nullification function on types. Given a Java-sourced type or an
90-
* implicitly null type, this adds `| Null` in the right places to make the nulls explicit.
84+
/** A type map that implements the nullification function on types. Given a Java-sourced type or a type
85+
* coming from Scala code compiled without explicit nulls, this adds `| Null` or `FlexibleType` in the
86+
* right places to make nullability explicit in a conservative way (without forcing incomplete symbols).
9187
*
92-
* @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level.
93-
* For example, `Array[String] | Null` is already nullable at the
94-
* outermost level, but `Array[String | Null]` isn't.
95-
* If this parameter is set to true, then the types of fields, and the return
96-
* types of methods will not be nullified.
97-
* This is useful for e.g. constructors, and also so that `A & B` is nullified
98-
* to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`.
88+
* @param skipResultType do not nullify the method result type at the outermost level (e.g. for `toString`,
89+
* constructors, or methods annotated as not-null)
90+
* @param skipParamTypes do not nullify parameter types for the current method (used for Scala-defined methods
91+
* or specific parameter sections)
92+
* @param skipCurrentLevel do not nullify at the current level (used for implicit/Given parameters, varargs, etc.)
9993
*/
100-
private class ImplicitNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap {
94+
private class ImplicitNullMap(
95+
var skipResultType: Boolean = false ,
96+
var skipParamTypes: Boolean = false ,
97+
var skipCurrentLevel: Boolean = false
98+
)(using Context) extends TypeMap:
99+
101100
def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp)
102101

103-
/** Should we nullify `tp` at the outermost level? */
102+
/** Should we nullify `tp` at the outermost level?
103+
* The symbols are still under construction, so we don't have precise information.
104+
* We purposely do not rely on precise subtyping checks here (e.g., asking whether `tp <:< AnyRef`),
105+
* because doing so could force incomplete symbols or trigger cycles. Instead, we conservatively
106+
* nullify only when we can recognize a concrete reference type shape.
107+
*/
104108
def needsNull(tp: Type): Boolean =
105-
if outermostLevelAlreadyNullable || !tp.hasSimpleKind then false
106-
else tp match
109+
if skipCurrentLevel || !tp.hasSimpleKind then false
110+
else tp.dealias match
107111
case tp: TypeRef =>
112+
val isValueOrSpecialClass =
113+
tp.symbol.isValueClass
114+
|| tp.isRef(defn.NullClass)
115+
|| tp.isRef(defn.NullClass)
116+
|| tp.isRef(defn.NothingClass)
117+
|| tp.isRef(defn.UnitClass)
118+
|| tp.isRef(defn.SingletonClass)
119+
|| tp.isRef(defn.AnyKindClass)
120+
|| tp.isRef(defn.AnyClass)
108121
// We don't modify value types because they're non-nullable even in Java.
109-
!(tp.symbol.isValueClass
110-
// We don't modify some special types.
111-
|| tp.isRef(defn.NullClass)
112-
|| tp.isRef(defn.NothingClass)
113-
|| tp.isRef(defn.UnitClass)
114-
|| tp.isRef(defn.SingletonClass)
115-
|| tp.isRef(defn.AnyValClass)
116-
|| tp.isRef(defn.AnyKindClass)
117-
|| tp.isRef(defn.AnyClass))
118-
case _ => true
122+
tp.symbol.isNullableClassAfterErasure && !isValueOrSpecialClass
123+
case _ => false
119124

120-
// We don't nullify Java varargs at the top level.
125+
// We don't nullify varargs (repeated parameters) at the top level.
121126
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`,
122127
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`.
123128
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
124129
// and not a `null` array.
125130
def tyconNeedsNull(tp: Type): Boolean =
126-
if outermostLevelAlreadyNullable then false
131+
if skipCurrentLevel then false
127132
else tp match
128133
case tp: TypeRef
129134
if !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false
130135
case _ => true
131136

132-
override def apply(tp: Type): Type = tp match {
133-
case tp: TypeRef if needsNull(tp) => nullify(tp)
137+
override def apply(tp: Type): Type = tp match
138+
case tp: TypeRef if needsNull(tp) =>
139+
nullify(tp)
134140
case appTp @ AppliedType(tycon, targs) =>
135-
val oldOutermostNullable = outermostLevelAlreadyNullable
136-
// We don't make the outmost levels of type arguments nullable if tycon is Java-defined.
137-
// This is because Java classes are _all_ nullified, so both `java.util.List[String]` and
138-
// `java.util.List[String|Null]` contain nullable elements.
139-
outermostLevelAlreadyNullable = tp.classSymbol.is(JavaDefined)
140-
val targs2 = targs map this
141-
outermostLevelAlreadyNullable = oldOutermostNullable
141+
val savedSkipCurrentLevel = skipCurrentLevel
142+
143+
// If Java-defined tycon, don't nullify outer level of type args (Java classes are fully nullified)
144+
skipCurrentLevel = tp.classSymbol.is(JavaDefined)
145+
val targs2 = targs.map(this)
146+
147+
skipCurrentLevel = savedSkipCurrentLevel
142148
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
143149
if tyconNeedsNull(tycon) then nullify(appTp2) else appTp2
144150
case ptp: PolyType =>
145151
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
146152
case mtp: MethodType =>
147-
val oldOutermostNullable = outermostLevelAlreadyNullable
148-
outermostLevelAlreadyNullable = false
149-
val paramInfos2 = mtp.paramInfos map this
150-
outermostLevelAlreadyNullable = oldOutermostNullable
151-
derivedLambdaType(mtp)(paramInfos2, this(mtp.resType))
152-
case tp: TypeAlias => mapOver(tp)
153-
case tp: TypeBounds => mapOver(tp)
154-
case tp: AndType =>
155-
// nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add
156-
// duplicate `Null`s at the outermost level inside `A` and `B`.
157-
outermostLevelAlreadyNullable = true
158-
nullify(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
159-
case tp: TypeParamRef if needsNull(tp) => nullify(tp)
160-
case tp: ExprType => mapOver(tp)
153+
val savedSkipCurrentLevel = skipCurrentLevel
154+
155+
// Skip param types for implicit/using sections
156+
val skipThisParamList = skipParamTypes || mtp.isImplicitMethod
157+
skipCurrentLevel = skipThisParamList
158+
val paramInfos2 = mtp.paramInfos.map(this)
159+
160+
skipCurrentLevel = skipResultType
161+
val resType2 = this(mtp.resType)
162+
163+
skipCurrentLevel = savedSkipCurrentLevel
164+
derivedLambdaType(mtp)(paramInfos2, resType2)
165+
case tp: TypeAlias =>
166+
mapOver(tp)
167+
case tp: TypeBounds =>
168+
mapOver(tp)
169+
case tp: AndOrType =>
170+
// For unions/intersections we recurse into constituents but do not force an outer `| Null` here;
171+
// outer nullability is handled by the surrounding context. This keeps the result minimal and avoids
172+
// duplicating `| Null` on both sides and at the outer level.
173+
mapOver(tp)
174+
case tp: ExprType =>
175+
mapOver(tp)
161176
case tp: AnnotatedType =>
162177
// We don't nullify the annotation part.
163178
derivedAnnotatedType(tp, this(tp.underlying), tp.annot)
164-
case tp: OrType =>
165-
outermostLevelAlreadyNullable = true
166-
nullify(derivedOrType(tp, this(tp.tp1), this(tp.tp2)))
167179
case tp: RefinedType =>
168-
outermostLevelAlreadyNullable = true
169-
nullify(mapOver(tp))
170-
// In all other cases, return the type unchanged.
171-
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
172-
// type of a final non-nullable field.
173-
case _ => tp
174-
}
175-
}
176-
}
180+
val savedSkipCurrentLevel = skipCurrentLevel
181+
182+
// Nullify parent at outer level; not refined members
183+
skipCurrentLevel = true
184+
val parent2 = this(tp.parent)
185+
186+
skipCurrentLevel = false
187+
val refinedInfo2 = this(tp.refinedInfo)
188+
189+
skipCurrentLevel = savedSkipCurrentLevel
190+
derivedRefinedType(tp, parent2, refinedInfo2)
191+
case _ =>
192+
// In all other cases, return the type unchanged.
193+
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
194+
// type of a final non-nullable field. We also deliberately do not attempt to nullify
195+
// complex computed types such as match types here; those remain as-is to avoid forcing
196+
// incomplete information during symbol construction.
197+
tp
198+
end apply
199+
end ImplicitNullMap

0 commit comments

Comments
 (0)