From 88b494971707f4b2d6b0a249ed43976dbb0afabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 11 May 2023 16:19:10 +0200 Subject: [PATCH] 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. --- .../dotty/tools/dotc/core/TypeComparer.scala | 10 +++++- tests/neg/i17467.scala | 31 +++++++++++++++++++ tests/run/t6443b.scala | 5 ++- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/neg/i17467.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 6857e3da38ed..3b2cb0db78c3 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -901,16 +901,24 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // However, `null` can always be a value of `T` for Java side. // So the best solution here is to let `Null` be a subtype of non-primitive // value types temporarily. - def isNullable(tp: Type): Boolean = tp.widenDealias match + def isNullable(tp: Type): Boolean = tp.dealias match case tp: TypeRef => val tpSym = tp.symbol ctx.mode.is(Mode.RelaxedOverriding) && !tpSym.isPrimitiveValueClass || tpSym.isNullableClass + case tp: TermRef => + // https://scala-lang.org/files/archive/spec/2.13/03-types.html#singleton-types + // A singleton type is of the form p.type. Where p is a path pointing to a value which conforms to + // scala.AnyRef [Scala 3: which scala.Null conforms to], the type denotes the set of values consisting + // of null and the value denoted by p (i.e., the value v for which v eq p). [Otherwise,] the type + // denotes the set consisting of only the value denoted by p. + isNullable(tp.underlying) && tp.isStable case tp: RefinedOrRecType => isNullable(tp.parent) case tp: AppliedType => isNullable(tp.tycon) case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2) case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2) case AnnotatedType(tp1, _) => isNullable(tp1) + case ConstantType(c) => c.tag == Constants.NullTag case _ => false val sym1 = tp1.symbol (sym1 eq NothingClass) && tp2.isValueTypeOrLambda || diff --git a/tests/neg/i17467.scala b/tests/neg/i17467.scala new file mode 100644 index 000000000000..cf2cb7701c55 --- /dev/null +++ b/tests/neg/i17467.scala @@ -0,0 +1,31 @@ +object Test: + def test(): Unit = + val a1: String = "foo" + val a2: a1.type = null // OK + + val b1: "foo" = null // error + + val c1: "foo" = "foo" + val c2: c1.type = null // error + + type MyNullable = String + val d1: MyNullable = "foo" + val d2: d1.type = null // OK + + type MyNonNullable = Int + val e1: MyNonNullable = 5 + val e2: e1.type = null // error + + summon[Null <:< "foo"] // error + + val f1: Mod.type = null // error + + var g1: AnyRef = "foo" + val g2: g1.type = null // error // error + end test + + object Mod + + class Bar: + def me: this.type = null // error +end Test diff --git a/tests/run/t6443b.scala b/tests/run/t6443b.scala index 9320b1dcfe2f..796fd9d95df4 100644 --- a/tests/run/t6443b.scala +++ b/tests/run/t6443b.scala @@ -2,7 +2,10 @@ trait A { type D >: Null <: C def foo(d: D)(d2: d.type): Unit trait C { - def bar: Unit = foo(null)(null) + def bar: Unit = { + val nul = null + foo(nul)(nul) + } } } object B extends A {