Skip to content

Commit 88b4949

Browse files
committed
Fix #17467: Limit isNullable widening to stable TermRefs.
The Scala language specification has a peculiar clause about the nullness of singleton types of the form `path.type`. It says that `Null <:< path.type` if the *underlying* type `U` of `path` is nullable itself. The previous implementation of that rule was overly broad, as it indiscrimately widened all types. This resulted in problematic subtyping relationships like `Null <:< "foo"`. We do not widen anymore. Instead, we specifically handle `TermRef`s of stable members, which are how dotc represents singleton types. We also have a rule for `Null <:< null`, which is necessary for pattern matching exhaustivity to keep working in the presence of nulls.
1 parent d103f8c commit 88b4949

File tree

3 files changed

+44
-2
lines changed

3 files changed

+44
-2
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -901,16 +901,24 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
901901
// However, `null` can always be a value of `T` for Java side.
902902
// So the best solution here is to let `Null` be a subtype of non-primitive
903903
// value types temporarily.
904-
def isNullable(tp: Type): Boolean = tp.widenDealias match
904+
def isNullable(tp: Type): Boolean = tp.dealias match
905905
case tp: TypeRef =>
906906
val tpSym = tp.symbol
907907
ctx.mode.is(Mode.RelaxedOverriding) && !tpSym.isPrimitiveValueClass ||
908908
tpSym.isNullableClass
909+
case tp: TermRef =>
910+
// https://scala-lang.org/files/archive/spec/2.13/03-types.html#singleton-types
911+
// A singleton type is of the form p.type. Where p is a path pointing to a value which conforms to
912+
// scala.AnyRef [Scala 3: which scala.Null conforms to], the type denotes the set of values consisting
913+
// of null and the value denoted by p (i.e., the value v for which v eq p). [Otherwise,] the type
914+
// denotes the set consisting of only the value denoted by p.
915+
isNullable(tp.underlying) && tp.isStable
909916
case tp: RefinedOrRecType => isNullable(tp.parent)
910917
case tp: AppliedType => isNullable(tp.tycon)
911918
case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2)
912919
case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2)
913920
case AnnotatedType(tp1, _) => isNullable(tp1)
921+
case ConstantType(c) => c.tag == Constants.NullTag
914922
case _ => false
915923
val sym1 = tp1.symbol
916924
(sym1 eq NothingClass) && tp2.isValueTypeOrLambda ||

tests/neg/i17467.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
object Test:
2+
def test(): Unit =
3+
val a1: String = "foo"
4+
val a2: a1.type = null // OK
5+
6+
val b1: "foo" = null // error
7+
8+
val c1: "foo" = "foo"
9+
val c2: c1.type = null // error
10+
11+
type MyNullable = String
12+
val d1: MyNullable = "foo"
13+
val d2: d1.type = null // OK
14+
15+
type MyNonNullable = Int
16+
val e1: MyNonNullable = 5
17+
val e2: e1.type = null // error
18+
19+
summon[Null <:< "foo"] // error
20+
21+
val f1: Mod.type = null // error
22+
23+
var g1: AnyRef = "foo"
24+
val g2: g1.type = null // error // error
25+
end test
26+
27+
object Mod
28+
29+
class Bar:
30+
def me: this.type = null // error
31+
end Test

tests/run/t6443b.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ trait A {
22
type D >: Null <: C
33
def foo(d: D)(d2: d.type): Unit
44
trait C {
5-
def bar: Unit = foo(null)(null)
5+
def bar: Unit = {
6+
val nul = null
7+
foo(nul)(nul)
8+
}
69
}
710
}
811
object B extends A {

0 commit comments

Comments
 (0)