@@ -65,11 +65,15 @@ object ImplicitNullInterop:
6565 assert(ctx.explicitNulls)
6666
6767 // Skip `TYPE`, enum values, and modules
68- if isEnumValueDef || sym.name == nme.TYPE_ || sym.is(Flags .ModuleVal ) then
68+ if isEnumValueDef
69+ || sym.name == nme.TYPE_
70+ || sym.name == nme.getClass_
71+ || sym.name == nme.toString_
72+ || sym.is(Flags .ModuleVal ) then
6973 return tp
7074
7175 // Don't nullify result type for `toString`, constructors, and @NotNull methods
72- val skipResultType = sym.name == nme.toString_ || sym. isConstructor || hasNotNullAnnot(sym)
76+ val skipResultType = sym.isConstructor || hasNotNullAnnot(sym)
7377 // Don't nullify Given/implicit parameters
7478 val skipCurrentLevel = sym.isOneOf(GivenOrImplicitVal )
7579
@@ -103,7 +107,7 @@ object ImplicitNullInterop:
103107 * The symbols are still under construction, so we don't have precise information.
104108 * We purposely do not rely on precise subtyping checks here (e.g., asking whether `tp <:< AnyRef`),
105109 * 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 .
110+ * nullify only when we can recognize a concrete reference type or type parameters from Java .
107111 */
108112 def needsNull (tp : Type ): Boolean =
109113 if skipCurrentLevel || ! tp.hasSimpleKind then false
@@ -149,7 +153,7 @@ object ImplicitNullInterop:
149153
150154 skipCurrentLevel = savedSkipCurrentLevel
151155 val appTp2 = derivedAppliedType(appTp, tycon, targs2)
152- if tyconNeedsNull(tycon) then nullify(appTp2) else appTp2
156+ if tyconNeedsNull(tycon) && tp.hasSimpleKind then nullify(appTp2) else appTp2
153157 case ptp : PolyType =>
154158 derivedLambdaType(ptp)(ptp.paramInfos, this (ptp.resType))
155159 case mtp : MethodType =>
@@ -169,27 +173,43 @@ object ImplicitNullInterop:
169173 case tp : TypeBounds =>
170174 mapOver(tp)
171175 case tp : AndOrType =>
172- // For unions/intersections we recurse into constituents but do not force an outer `| Null` here;
173- // outer nullability is handled by the surrounding context. This keeps the result minimal and avoids
174- // duplicating `| Null` on both sides and at the outer level.
175- mapOver(tp)
176+ // For unions/intersections we recurse into both sides.
177+ // If both sides are nullalble, we only add `| Null` once.
178+ // This keeps the result minimal and avoids duplicating `| Null`
179+ // on both sides and at the outer level.
180+ (this (tp.tp1), this (tp.tp2)) match
181+ case (FlexibleType (_, t1), FlexibleType (_, t2)) if ctx.flexibleTypes =>
182+ FlexibleType (derivedAndOrType(tp, t1, t2))
183+ case (OrNull (t1), OrNull (t2)) =>
184+ OrNull (derivedAndOrType(tp, t1, t2))
185+ case (t1, t2) =>
186+ derivedAndOrType(tp, t1, t2)
176187 case tp : ExprType =>
177188 mapOver(tp)
178189 case tp : AnnotatedType =>
179190 // We don't nullify the annotation part.
180191 derivedAnnotatedType(tp, this (tp.underlying), tp.annot)
181192 case tp : RefinedType =>
182193 val savedSkipCurrentLevel = skipCurrentLevel
194+ val savedSkipResultType = skipResultType
183195
184- // Nullify parent at outer level; not refined members
185196 skipCurrentLevel = true
186197 val parent2 = this (tp.parent)
187198
188199 skipCurrentLevel = false
200+ skipResultType = false
189201 val refinedInfo2 = this (tp.refinedInfo)
190202
191203 skipCurrentLevel = savedSkipCurrentLevel
192- derivedRefinedType(tp, parent2, refinedInfo2)
204+ skipResultType = savedSkipResultType
205+
206+ parent2 match
207+ case FlexibleType (_, parent2a) if ctx.flexibleTypes =>
208+ FlexibleType (derivedRefinedType(tp, parent2a, refinedInfo2))
209+ case OrNull (parent2a) =>
210+ OrNull (derivedRefinedType(tp, parent2a, refinedInfo2))
211+ case _ =>
212+ derivedRefinedType(tp, parent2, refinedInfo2)
193213 case _ =>
194214 // In all other cases, return the type unchanged.
195215 // In particular, if the type is a ConstantType, then we don't nullify it because it is the
0 commit comments