@@ -2,19 +2,25 @@ package dotty.tools.dotc
22package core
33
44import Contexts .*
5- import Flags .JavaDefined
5+ import Flags .*
66import StdNames .nme
77import Symbols .*
88import 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