@@ -2,19 +2,25 @@ package dotty.tools.dotc
2
2
package core
3
3
4
4
import Contexts .*
5
- import Flags .JavaDefined
5
+ import Flags .*
6
6
import StdNames .nme
7
7
import Symbols .*
8
8
import Types .*
9
- import dotty . tools . dotc . reporting . *
10
- import dotty . tools . dotc . core . Decorators . i
9
+ import Decorators . i
10
+ import reporting . *
11
11
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`
14
20
*
15
21
* The transformation is (conceptually) a function `n` that adheres to the following rules:
16
22
* (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
18
24
* (3) n(C[T]) = C[T] | Null if C is Java-defined
19
25
* (4) n(C[T]) = C[n(T)] | Null if C is Scala-defined
20
26
* (5) n(A|B) = n(A) | n(B) | Null
@@ -29,148 +35,165 @@ import dotty.tools.dotc.core.Decorators.i
29
35
* e.g. calling `get` on a `java.util.List[String]` already returns `String|Null` and not `String`, so
30
36
* we don't need to write `java.util.List[String | Null]`.
31
37
* - 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.
33
56
*
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.
37
58
*/
38
- object ImplicitNullInterop {
59
+ object ImplicitNullInterop :
39
60
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.
55
63
*/
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}" ):
57
65
assert(ctx.explicitNulls)
58
66
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
73
70
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 )
76
77
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)
84
80
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)
88
83
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).
91
87
*
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.)
99
93
*/
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
+
101
100
def nullify (tp : Type ): Type = if ctx.flexibleTypes then FlexibleType (tp) else OrNull (tp)
102
101
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
+ */
104
108
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
107
111
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 )
108
121
// 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
119
124
120
- // We don't nullify Java varargs at the top level.
125
+ // We don't nullify varargs (repeated parameters) at the top level.
121
126
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`,
122
127
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`.
123
128
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
124
129
// and not a `null` array.
125
130
def tyconNeedsNull (tp : Type ): Boolean =
126
- if outermostLevelAlreadyNullable then false
131
+ if skipCurrentLevel then false
127
132
else tp match
128
133
case tp : TypeRef
129
134
if ! ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass ) => false
130
135
case _ => true
131
136
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)
134
140
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
142
148
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
143
149
if tyconNeedsNull(tycon) then nullify(appTp2) else appTp2
144
150
case ptp : PolyType =>
145
151
derivedLambdaType(ptp)(ptp.paramInfos, this (ptp.resType))
146
152
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)
161
176
case tp : AnnotatedType =>
162
177
// We don't nullify the annotation part.
163
178
derivedAnnotatedType(tp, this (tp.underlying), tp.annot)
164
- case tp : OrType =>
165
- outermostLevelAlreadyNullable = true
166
- nullify(derivedOrType(tp, this (tp.tp1), this (tp.tp2)))
167
179
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