Skip to content

Commit 49286a9

Browse files
authored
Relax comparison between Null and reference types in explicit nulls (#23308)
An up-to-date version of #19258 with merge conflicts resolved.
2 parents 0be2091 + 297bed4 commit 49286a9

File tree

16 files changed

+207
-140
lines changed

16 files changed

+207
-140
lines changed

compiler/src/dotty/tools/dotc/typer/Synthesizer.scala

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,19 +176,6 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
176176
cmpWithBoxed(cls1, cls2)
177177
else if cls2.isPrimitiveValueClass then
178178
cmpWithBoxed(cls2, cls1)
179-
else if ctx.mode.is(Mode.SafeNulls) then
180-
// If explicit nulls is enabled, and unsafeNulls is not enabled,
181-
// we want to disallow comparison between Object and Null.
182-
// If we have to check whether a variable with a non-nullable type has null value
183-
// (for example, a NotNull java method returns null for some reasons),
184-
// we can still cast it to a nullable type then compare its value.
185-
//
186-
// Example:
187-
// val x: String = null.asInstanceOf[String]
188-
// if (x == null) {} // error: x is non-nullable
189-
// if (x.asInstanceOf[String|Null] == null) {} // ok
190-
if cls1 == defn.NullClass || cls2 == defn.NullClass then cls1 == cls2
191-
else cls1 == defn.NothingClass || cls2 == defn.NothingClass
192179
else if cls1 == defn.NullClass then
193180
cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass)
194181
else if cls2 == defn.NullClass then

docs/_docs/reference/experimental/explicit-nulls.md

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,10 @@ More details can be found in [safe initialization](../other-new-features/safe-in
9090

9191
## Equality
9292

93-
We don't allow the double-equal (`==` and `!=`) and reference (`eq` and `ne`) comparison between
94-
`AnyRef` and `Null` anymore, since a variable with a non-nullable type cannot have `null` as value.
95-
`null` can only be compared with `Null`, nullable union (`T | Null`), or `Any` type.
96-
97-
For some reason, if we really want to compare `null` with non-null values, we have to provide a type hint (e.g. `: Any`).
98-
99-
```scala
100-
val x: String = ???
101-
val y: String | Null = ???
102-
103-
x == null // error: Values of types String and Null cannot be compared with == or !=
104-
x eq null // error
105-
"hello" == null // error
106-
107-
y == null // ok
108-
y == x // ok
109-
110-
(x: String | Null) == null // ok
111-
(x: Any) == null // ok
112-
```
93+
We still allow the double-equal (`==` and `!=`), reference (`eq` and `ne`) comparison,
94+
and pattern matching between `Null` and reference types.
95+
Even if a type is non-nullable, we still need to consider the possibility of `null` value
96+
caused by the Java methods or uninitialized values.
11397

11498
## Java Interoperability and Flexible Types
11599

tests/explicit-nulls/neg/equal1.scala

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// Test what can be compared for equality against null.
2-
class Foo {
2+
3+
case class VC(x: Int) extends AnyVal
4+
5+
def test =
36
// Null itself
47
val x0: Null = null
58
x0 != x0
@@ -9,21 +12,21 @@ class Foo {
912
null == null
1013
null != null
1114

12-
// Non-nullable types: error
15+
// Non-nullable types: OK.
1316
val x1: String = "hello"
14-
x1 != null // error
15-
x1 == null // error
16-
null == x1 // error
17-
null != x1 // error
18-
x1 == x0 // error
19-
x0 != x1 // error
20-
x1.asInstanceOf[String|Null] == null
21-
x1.asInstanceOf[String|Null] == x0
17+
x1 != null
18+
x1 == null
19+
null == x1
20+
null != x1
21+
x1 == x0
22+
x0 != x1
23+
x1.asInstanceOf[String | Null] == null
24+
x1.asInstanceOf[String | Null] == x0
2225
x1.asInstanceOf[Any] == null
2326
x1.asInstanceOf[Any] == x0
2427

2528
// Nullable types: OK
26-
val x2: String|Null = null
29+
val x2: String | Null = null
2730
x2 == null
2831
null == x2
2932
x2 == x0
@@ -41,4 +44,10 @@ class Foo {
4144
null == false // error
4245
'a' == null // error
4346
null == 'b' // error
44-
}
47+
48+
// Nullable value types: OK.
49+
val x3: Int | Null = null
50+
x3 == null
51+
null == x3
52+
x3 == x0
53+
x3 != x0

tests/explicit-nulls/neg/equal2.scala

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
// Test that we can't compare for equality `null` with classes.
2-
// This rule is for both regular classes and value classes.
1+
// Test that we can compare values of regular classes against null,
2+
// but not values of value classes.
33

44
class Foo(x: Int)
55
class Bar(x: Int) extends AnyVal
66

77
class Test {
88
locally {
99
val foo: Foo = new Foo(15)
10-
foo == null // error: Values of types Null and Foo cannot be compared
11-
null == foo // error
12-
foo != null // error
13-
null != foo // error
10+
foo == null
11+
null == foo
12+
foo != null
13+
null != foo
1414

15-
// To test against null, make the type nullable.
1615
val foo2: Foo | Null = foo
1716
// ok
1817
foo2 == null

tests/explicit-nulls/neg/flow-match.scala

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
object MatchTest {
44
def f6(s: String | Null): String = s match {
55
case s2 => s2 // error
6-
case null => "other" // error
7-
case s3 => s3
6+
case s3 => s3 // OK since not null
87
}
98

109
def f7(s: String | Null): String = s match {
1110
case null => "other"
12-
case null => "other" // error
13-
case s3 => s3
11+
case s3 => s3 // OK since not null
1412
}
15-
}
13+
}
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
// Test we are correctly striping nulls from nullable unions.
22

3-
class Foo {
3+
class Foo:
44

55
class B1
66
class B2
7-
locally {
7+
8+
locally:
89
val x: (Null | String) | Null | (B1 | (Null | B2)) = ???
9-
if (x != null) {
10+
if x != null then
1011
val _: String | B1 | B2 = x // ok: can remove all nullable unions
11-
}
12-
}
1312

14-
locally {
13+
locally:
1514
val x: (Null | String) & (Null | B1) = ???
16-
if (x != null) {
15+
if x != null then
1716
val _: String & B1 = x // ok: can remove null from embedded intersection
18-
}
19-
}
2017

21-
locally {
18+
locally:
2219
val x: (Null | B1) & B2 = ???
23-
if (x != null) {} // error: the type of x is not a nullable union, so we cannot remove the Null
24-
}
25-
}
20+
if x != null then
21+
val _: B1 & B2 = x // error: the type of x is not a nullable union, so we cannot remove the Null

tests/explicit-nulls/unsafe-common/unsafe-equal.scala renamed to tests/explicit-nulls/pos/anyref-equal-nulls.scala

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ class S {
1111
null == s1
1212
null != s1
1313

14-
s2 == null // error
15-
s2 != null // error
16-
null == s2 // error
17-
null != s2 // error
14+
s2 == null
15+
s2 != null
16+
null == s2
17+
null != s2
1818

1919
s1 == s2
2020
s1 != s2
@@ -27,21 +27,21 @@ class S {
2727
null != n
2828

2929
s1 == n
30-
s2 == n // error
30+
s2 == n
3131
n != s1
32-
n != s2 // error
32+
n != s2
3333
}
3434

3535
locally {
36-
ss1 == null // error
37-
ss1 != null // error
38-
null == ss1 // error
39-
null != ss1 // error
40-
41-
ss1 == n // error
42-
ss1 != n // error
43-
n == ss1 // error
44-
n != ss1 // error
36+
ss1 == null
37+
ss1 != null
38+
null == ss1
39+
null != ss1
40+
41+
ss1 == n
42+
ss1 != n
43+
n == ss1
44+
n != ss1
4545

4646
ss1 == ss2
4747
ss2 != ss1

tests/explicit-nulls/pos/pattern-matching.scala

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
object Test:
2+
3+
def main(args: Array[String]): Unit =
4+
5+
val s: String = null.asInstanceOf[String]
6+
7+
val r1 = s match
8+
case s: String => 100
9+
case _ => 200
10+
assert(r1 == 200)
11+
12+
val r2 = s match
13+
case s: String => 100
14+
case null => 200
15+
assert(r2 == 200)
16+
17+
val r3 = s match
18+
case null => 100
19+
case _ => 200
20+
assert(r3 == 100)
21+
22+
val s2: String | Null = null
23+
24+
val r4 = s2 match
25+
case s2: String => 100
26+
case _ => 200
27+
assert(r4 == 200)
28+
29+
val r5 = s2 match
30+
case s2: String => 100
31+
case null => 200
32+
assert(r5 == 200)
33+
34+
val r6 = s2 match
35+
case null => 200
36+
case s2: String => 100
37+
assert(r6 == 200)

tests/explicit-nulls/unsafe-common/unsafe-match-null.scala

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:6:9 -------------------------------
2+
6 | case null => "other" // warn
3+
| ^^^^
4+
| Unreachable case
5+
-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:7:9 -------------------------------
6+
7 | case s3 => s3 // warn
7+
| ^^
8+
| Unreachable case
9+
-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:12:9 ------------------------------
10+
12 | case null => "other" // warn
11+
| ^^^^
12+
| Unreachable case
13+
-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:14:9 ------------------------------
14+
14 | case s4 => s4.nn // warn
15+
| ^^
16+
| Unreachable case
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Test unreachable matches in presence of nulls
2+
3+
object MatchTest2 {
4+
def f6(s: String | Null): String = s match {
5+
case s2 => s2.nn
6+
case null => "other" // warn
7+
case s3 => s3 // warn
8+
}
9+
10+
def f7(s: String | Null): String = s match {
11+
case null => "other"
12+
case null => "other" // warn
13+
case s3: String => s3
14+
case s4 => s4.nn // warn
15+
}
16+
}

tests/explicit-nulls/warn/i21577.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@
1515
| ^
1616
| Unreachable case
1717
-- [E029] Pattern Match Exhaustivity Warning: tests/explicit-nulls/warn/i21577.scala:29:27 -----------------------------
18-
29 |def f7(s: String | Null) = s match // warn: not exhuastive
18+
29 |def f7(s: String | Null) = s match // warn: not exhaustive
1919
| ^
2020
| match may not be exhaustive.
2121
|
2222
| It would fail on pattern case: _: Null
2323
|
2424
| longer explanation available when compiling with `-explain`
2525
-- [E029] Pattern Match Exhaustivity Warning: tests/explicit-nulls/warn/i21577.scala:36:33 -----------------------------
26-
36 |def f9(s: String | Int | Null) = s match // warn: not exhuastive
26+
36 |def f9(s: String | Int | Null) = s match // warn: not exhaustive
2727
| ^
2828
| match may not be exhaustive.
2929
|
3030
| It would fail on pattern case: _: Int
3131
|
32-
| longer explanation available when compiling with `-explain`
32+
| longer explanation available when compiling with `-explain`

tests/explicit-nulls/warn/i21577.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ def f6(s: String) = s.trim() match
2626
def f61(s: String) = s.trim() match
2727
case _: String =>
2828

29-
def f7(s: String | Null) = s match // warn: not exhuastive
29+
def f7(s: String | Null) = s match // warn: not exhaustive
3030
case _: String =>
3131

3232
def f8(s: String | Null) = s match
3333
case _: String =>
3434
case null =>
3535

36-
def f9(s: String | Int | Null) = s match // warn: not exhuastive
36+
def f9(s: String | Int | Null) = s match // warn: not exhaustive
3737
case _: String =>
38-
case null =>
38+
case null =>

0 commit comments

Comments
 (0)