diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 9c1e4951a798..4f596776d497 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -176,19 +176,6 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): cmpWithBoxed(cls1, cls2) else if cls2.isPrimitiveValueClass then cmpWithBoxed(cls2, cls1) - else if ctx.mode.is(Mode.SafeNulls) then - // If explicit nulls is enabled, and unsafeNulls is not enabled, - // we want to disallow comparison between Object and Null. - // If we have to check whether a variable with a non-nullable type has null value - // (for example, a NotNull java method returns null for some reasons), - // we can still cast it to a nullable type then compare its value. - // - // Example: - // val x: String = null.asInstanceOf[String] - // if (x == null) {} // error: x is non-nullable - // if (x.asInstanceOf[String|Null] == null) {} // ok - if cls1 == defn.NullClass || cls2 == defn.NullClass then cls1 == cls2 - else cls1 == defn.NothingClass || cls2 == defn.NothingClass else if cls1 == defn.NullClass then cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass) else if cls2 == defn.NullClass then diff --git a/docs/_docs/reference/experimental/explicit-nulls.md b/docs/_docs/reference/experimental/explicit-nulls.md index 50339c3fa1e4..1be4d7f4f450 100644 --- a/docs/_docs/reference/experimental/explicit-nulls.md +++ b/docs/_docs/reference/experimental/explicit-nulls.md @@ -90,26 +90,10 @@ More details can be found in [safe initialization](../other-new-features/safe-in ## Equality -We don't allow the double-equal (`==` and `!=`) and reference (`eq` and `ne`) comparison between -`AnyRef` and `Null` anymore, since a variable with a non-nullable type cannot have `null` as value. -`null` can only be compared with `Null`, nullable union (`T | Null`), or `Any` type. - -For some reason, if we really want to compare `null` with non-null values, we have to provide a type hint (e.g. `: Any`). - -```scala -val x: String = ??? -val y: String | Null = ??? - -x == null // error: Values of types String and Null cannot be compared with == or != -x eq null // error -"hello" == null // error - -y == null // ok -y == x // ok - -(x: String | Null) == null // ok -(x: Any) == null // ok -``` +We still allow the double-equal (`==` and `!=`), reference (`eq` and `ne`) comparison, +and pattern matching between `Null` and reference types. +Even if a type is non-nullable, we still need to consider the possibility of `null` value +caused by the Java methods or uninitialized values. ## Java Interoperability and Flexible Types diff --git a/tests/explicit-nulls/neg/equal1.scala b/tests/explicit-nulls/neg/equal1.scala index 3fab8ba4bf8a..45ec3084fd47 100644 --- a/tests/explicit-nulls/neg/equal1.scala +++ b/tests/explicit-nulls/neg/equal1.scala @@ -1,5 +1,8 @@ // Test what can be compared for equality against null. -class Foo { + +case class VC(x: Int) extends AnyVal + +def test = // Null itself val x0: Null = null x0 != x0 @@ -9,21 +12,21 @@ class Foo { null == null null != null - // Non-nullable types: error + // Non-nullable types: OK. val x1: String = "hello" - x1 != null // error - x1 == null // error - null == x1 // error - null != x1 // error - x1 == x0 // error - x0 != x1 // error - x1.asInstanceOf[String|Null] == null - x1.asInstanceOf[String|Null] == x0 + x1 != null + x1 == null + null == x1 + null != x1 + x1 == x0 + x0 != x1 + x1.asInstanceOf[String | Null] == null + x1.asInstanceOf[String | Null] == x0 x1.asInstanceOf[Any] == null x1.asInstanceOf[Any] == x0 // Nullable types: OK - val x2: String|Null = null + val x2: String | Null = null x2 == null null == x2 x2 == x0 @@ -41,4 +44,10 @@ class Foo { null == false // error 'a' == null // error null == 'b' // error -} + + // Nullable value types: OK. + val x3: Int | Null = null + x3 == null + null == x3 + x3 == x0 + x3 != x0 diff --git a/tests/explicit-nulls/neg/equal2.scala b/tests/explicit-nulls/neg/equal2.scala index 32cf21d918ac..299d29c103ab 100644 --- a/tests/explicit-nulls/neg/equal2.scala +++ b/tests/explicit-nulls/neg/equal2.scala @@ -1,5 +1,5 @@ -// Test that we can't compare for equality `null` with classes. -// This rule is for both regular classes and value classes. +// Test that we can compare values of regular classes against null, +// but not values of value classes. class Foo(x: Int) class Bar(x: Int) extends AnyVal @@ -7,12 +7,11 @@ class Bar(x: Int) extends AnyVal class Test { locally { val foo: Foo = new Foo(15) - foo == null // error: Values of types Null and Foo cannot be compared - null == foo // error - foo != null // error - null != foo // error + foo == null + null == foo + foo != null + null != foo - // To test against null, make the type nullable. val foo2: Foo | Null = foo // ok foo2 == null diff --git a/tests/explicit-nulls/neg/flow-match.scala b/tests/explicit-nulls/neg/flow-match.scala index e385758261cd..cbf77bfd0196 100644 --- a/tests/explicit-nulls/neg/flow-match.scala +++ b/tests/explicit-nulls/neg/flow-match.scala @@ -3,13 +3,11 @@ object MatchTest { def f6(s: String | Null): String = s match { case s2 => s2 // error - case null => "other" // error - case s3 => s3 + case s3 => s3 // OK since not null } def f7(s: String | Null): String = s match { case null => "other" - case null => "other" // error - case s3 => s3 + case s3 => s3 // OK since not null } -} +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/flow-strip-null.scala b/tests/explicit-nulls/neg/flow-strip-null.scala index 997d272d008f..7c79e1310f16 100644 --- a/tests/explicit-nulls/neg/flow-strip-null.scala +++ b/tests/explicit-nulls/neg/flow-strip-null.scala @@ -1,25 +1,21 @@ // Test we are correctly striping nulls from nullable unions. -class Foo { +class Foo: class B1 class B2 - locally { + + locally: val x: (Null | String) | Null | (B1 | (Null | B2)) = ??? - if (x != null) { + if x != null then val _: String | B1 | B2 = x // ok: can remove all nullable unions - } - } - locally { + locally: val x: (Null | String) & (Null | B1) = ??? - if (x != null) { + if x != null then val _: String & B1 = x // ok: can remove null from embedded intersection - } - } - locally { + locally: val x: (Null | B1) & B2 = ??? - if (x != null) {} // error: the type of x is not a nullable union, so we cannot remove the Null - } -} + if x != null then + val _: B1 & B2 = x // error: the type of x is not a nullable union, so we cannot remove the Null diff --git a/tests/explicit-nulls/unsafe-common/unsafe-equal.scala b/tests/explicit-nulls/pos/anyref-equal-nulls.scala similarity index 56% rename from tests/explicit-nulls/unsafe-common/unsafe-equal.scala rename to tests/explicit-nulls/pos/anyref-equal-nulls.scala index 493aaebfbff2..f7609425e894 100644 --- a/tests/explicit-nulls/unsafe-common/unsafe-equal.scala +++ b/tests/explicit-nulls/pos/anyref-equal-nulls.scala @@ -11,10 +11,10 @@ class S { null == s1 null != s1 - s2 == null // error - s2 != null // error - null == s2 // error - null != s2 // error + s2 == null + s2 != null + null == s2 + null != s2 s1 == s2 s1 != s2 @@ -27,21 +27,21 @@ class S { null != n s1 == n - s2 == n // error + s2 == n n != s1 - n != s2 // error + n != s2 } locally { - ss1 == null // error - ss1 != null // error - null == ss1 // error - null != ss1 // error - - ss1 == n // error - ss1 != n // error - n == ss1 // error - n != ss1 // error + ss1 == null + ss1 != null + null == ss1 + null != ss1 + + ss1 == n + ss1 != n + n == ss1 + n != ss1 ss1 == ss2 ss2 != ss1 diff --git a/tests/explicit-nulls/pos/pattern-matching.scala b/tests/explicit-nulls/pos/pattern-matching.scala deleted file mode 100644 index 7e84fb8cd513..000000000000 --- a/tests/explicit-nulls/pos/pattern-matching.scala +++ /dev/null @@ -1,38 +0,0 @@ - -class Foo { - val s: String = ??? - s match { - case s: String => 100 // warning: type test will always succeed - case _ => 200 // warning: unreachable - } - - s match { - case s: String => 100 // warning: type test will always succeed - case _ => 200 // warning: unreachable - } - - sealed trait Animal - case class Dog(name: String) extends Animal - case object Cat extends Animal - - val a: Animal = ??? - a match { - case Dog(name) => 100 - case Cat => 200 - case _ => 300 // warning: unreachable - } - - val a2: Animal|Null = ??? - a2 match { - case Dog(_) => 100 - case Cat => 200 - case _ => 300 // warning: only matches null - } - - val a3: Animal|Null = ??? - a3 match { - case Dog(_) => 100 - case Cat => 200 - case null => 300 // ok - } -} diff --git a/tests/explicit-nulls/run/pattern-matching.scala b/tests/explicit-nulls/run/pattern-matching.scala new file mode 100644 index 000000000000..bc893f8f40e2 --- /dev/null +++ b/tests/explicit-nulls/run/pattern-matching.scala @@ -0,0 +1,37 @@ +object Test: + + def main(args: Array[String]): Unit = + + val s: String = null.asInstanceOf[String] + + val r1 = s match + case s: String => 100 + case _ => 200 + assert(r1 == 200) + + val r2 = s match + case s: String => 100 + case null => 200 + assert(r2 == 200) + + val r3 = s match + case null => 100 + case _ => 200 + assert(r3 == 100) + + val s2: String | Null = null + + val r4 = s2 match + case s2: String => 100 + case _ => 200 + assert(r4 == 200) + + val r5 = s2 match + case s2: String => 100 + case null => 200 + assert(r5 == 200) + + val r6 = s2 match + case null => 200 + case s2: String => 100 + assert(r6 == 200) diff --git a/tests/explicit-nulls/unsafe-common/unsafe-match-null.scala b/tests/explicit-nulls/unsafe-common/unsafe-match-null.scala deleted file mode 100644 index fc76de6ddd3a..000000000000 --- a/tests/explicit-nulls/unsafe-common/unsafe-match-null.scala +++ /dev/null @@ -1,11 +0,0 @@ -def test1 = - val s: String = ??? - s match - case _: String => - case null => // error: Values of types Null and String cannot be compared - -def test2 = - val s: String | Null = ??? - s match - case _: String => - case null => diff --git a/tests/explicit-nulls/warn/flow-match.check b/tests/explicit-nulls/warn/flow-match.check new file mode 100644 index 000000000000..dc8d03881ffb --- /dev/null +++ b/tests/explicit-nulls/warn/flow-match.check @@ -0,0 +1,16 @@ +-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:6:9 ------------------------------- +6 | case null => "other" // warn + | ^^^^ + | Unreachable case +-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:7:9 ------------------------------- +7 | case s3 => s3 // warn + | ^^ + | Unreachable case +-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:12:9 ------------------------------ +12 | case null => "other" // warn + | ^^^^ + | Unreachable case +-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:14:9 ------------------------------ +14 | case s4 => s4.nn // warn + | ^^ + | Unreachable case diff --git a/tests/explicit-nulls/warn/flow-match.scala b/tests/explicit-nulls/warn/flow-match.scala new file mode 100644 index 000000000000..e152da47bb41 --- /dev/null +++ b/tests/explicit-nulls/warn/flow-match.scala @@ -0,0 +1,16 @@ +// Test unreachable matches in presence of nulls + +object MatchTest2 { + def f6(s: String | Null): String = s match { + case s2 => s2.nn + case null => "other" // warn + case s3 => s3 // warn + } + + def f7(s: String | Null): String = s match { + case null => "other" + case null => "other" // warn + case s3: String => s3 + case s4 => s4.nn // warn + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/warn/i21577.check b/tests/explicit-nulls/warn/i21577.check index b548a5bedc30..97f4ef346a43 100644 --- a/tests/explicit-nulls/warn/i21577.check +++ b/tests/explicit-nulls/warn/i21577.check @@ -15,7 +15,7 @@ | ^ | Unreachable case -- [E029] Pattern Match Exhaustivity Warning: tests/explicit-nulls/warn/i21577.scala:29:27 ----------------------------- -29 |def f7(s: String | Null) = s match // warn: not exhuastive +29 |def f7(s: String | Null) = s match // warn: not exhaustive | ^ | match may not be exhaustive. | @@ -23,10 +23,10 @@ | | longer explanation available when compiling with `-explain` -- [E029] Pattern Match Exhaustivity Warning: tests/explicit-nulls/warn/i21577.scala:36:33 ----------------------------- -36 |def f9(s: String | Int | Null) = s match // warn: not exhuastive +36 |def f9(s: String | Int | Null) = s match // warn: not exhaustive | ^ | match may not be exhaustive. | | It would fail on pattern case: _: Int | - | longer explanation available when compiling with `-explain` + | longer explanation available when compiling with `-explain` \ No newline at end of file diff --git a/tests/explicit-nulls/warn/i21577.scala b/tests/explicit-nulls/warn/i21577.scala index 1bba8f4da01f..a4fc26f85f2b 100644 --- a/tests/explicit-nulls/warn/i21577.scala +++ b/tests/explicit-nulls/warn/i21577.scala @@ -26,13 +26,13 @@ def f6(s: String) = s.trim() match def f61(s: String) = s.trim() match case _: String => -def f7(s: String | Null) = s match // warn: not exhuastive +def f7(s: String | Null) = s match // warn: not exhaustive case _: String => def f8(s: String | Null) = s match case _: String => case null => -def f9(s: String | Int | Null) = s match // warn: not exhuastive +def f9(s: String | Int | Null) = s match // warn: not exhaustive case _: String => - case null => \ No newline at end of file + case null => diff --git a/tests/explicit-nulls/warn/pattern-matching.check b/tests/explicit-nulls/warn/pattern-matching.check new file mode 100644 index 000000000000..b7b12b2a9215 --- /dev/null +++ b/tests/explicit-nulls/warn/pattern-matching.check @@ -0,0 +1,24 @@ +-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/pattern-matching.scala:7:9 ------------------------- +7 | case _ => 200 // warn: unreachable + | ^ + | Unreachable case +-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/pattern-matching.scala:11:9 ------------------------ +11 | case null => 200 // warn: unreachable + | ^^^^ + | Unreachable case +-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/pattern-matching.scala:14:9 ------------------------ +14 | case null => 100 // warn: unreachable + | ^^^^ + | Unreachable case +-- [E121] Pattern Match Warning: tests/explicit-nulls/warn/pattern-matching.scala:21:9 --------------------------------- +21 | case _ => 200 // warn: unreachable case except for null + | ^ + | Unreachable case except for null (if this is intentional, consider writing case null => instead). +-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/pattern-matching.scala:39:9 ------------------------ +39 | case _ => 300 // warn: unreachable case except for null + | ^ + | Unreachable case +-- [E121] Pattern Match Warning: tests/explicit-nulls/warn/pattern-matching.scala:45:9 --------------------------------- +45 | case _ => 300 // warn: unreachable case except for null + | ^ + | Unreachable case except for null (if this is intentional, consider writing case null => instead). diff --git a/tests/explicit-nulls/warn/pattern-matching.scala b/tests/explicit-nulls/warn/pattern-matching.scala new file mode 100644 index 000000000000..f1cabc302c87 --- /dev/null +++ b/tests/explicit-nulls/warn/pattern-matching.scala @@ -0,0 +1,50 @@ +class Foo: + + val s: String = ??? + + s match + case s: String => 100 + case _ => 200 // warn: unreachable + + s match + case s: String => 100 + case null => 200 // warn: unreachable + + s match + case null => 100 // warn: unreachable + case _ => 200 + + val s2: String | Null = ??? + + s2 match + case s2: String => 100 + case _ => 200 // warn: unreachable case except for null + + s2 match + case s2: String => 100 + case null => 200 + + s2 match + case null => 200 + case s2: String => 100 + + sealed trait Animal + case class Dog(name: String) extends Animal + case object Cat extends Animal + + val a: Animal = ??? + a match + case Dog(name) => 100 + case Cat => 200 + case _ => 300 // warn: unreachable case except for null + + val a2: Animal | Null = ??? + a2 match + case Dog(_) => 100 + case Cat => 200 + case _ => 300 // warn: unreachable case except for null + + a2 match + case Dog(_) => 100 + case Cat => 200 + case null => 300 // ok