Skip to content

Commit 4ec72e1

Browse files
committed
Better error messages for selecting members on nullable type
1 parent 3b91292 commit 4ec72e1

File tree

4 files changed

+89
-72
lines changed

4 files changed

+89
-72
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,15 +145,20 @@ object ErrorReporting {
145145
else ""
146146

147147
def selectErrorAddendum
148-
(tree: untpd.RefTree, qual1: Tree, qualType: Type, suggestImports: Type => String)
148+
(tree: untpd.RefTree, qual1: Tree, qualType: Type, suggestImports: Type => String, foundWithoutNull: Boolean = false)
149149
(using Context): String =
150150
val attempts: List[Tree] = qual1.getAttachment(Typer.HiddenSearchFailure) match
151151
case Some(failures) =>
152152
for failure <- failures
153153
if !failure.reason.isInstanceOf[Implicits.NoMatchingImplicits]
154154
yield failure.tree
155155
case _ => Nil
156-
if qualType.derivesFrom(defn.DynamicClass) then
156+
if foundWithoutNull then
157+
i""".
158+
|Since explicit-nulls is enabled, ${qualType.widen} could have null value during runtime.
159+
|If you want to select ${tree.name} without checking the nullity,
160+
|insert a .nn before .${tree.name} or import scala.language.unsafeNulls."""
161+
else if qualType.derivesFrom(defn.DynamicClass) then
157162
"\npossible cause: maybe a wrong Dynamic method signature?"
158163
else if attempts.nonEmpty then
159164
val attemptStrings = attempts.map(_.showIndented(4)).distinct

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,12 @@ trait TypeAssigner {
163163
errorType(ex"$qualType does not have a constructor", tree.srcPos)
164164
else {
165165
val kind = if (name.isTypeName) "type" else "value"
166-
def addendum = err.selectErrorAddendum(tree, qual1, qualType, importSuggestionAddendum)
166+
val foundWithoutNull = qualType match {
167+
case OrNull(qualType1) =>
168+
reallyExists(qualType1.findMember(name, pre))
169+
case _ => false
170+
}
171+
def addendum = err.selectErrorAddendum(tree, qual1, qualType, importSuggestionAddendum, foundWithoutNull)
167172
errorType(NotAMember(qualType, name, kind, addendum), tree.srcPos)
168173
}
169174
}

docs/docs/internals/explicit-nulls.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ During adapting, if the type of the tree is not a subtype of the expected type,
9999
2. If the `tree.tpe` is not nullable or the last step fails, we search on the tree directly.
100100
3. If the last step fails, we try to cast tree to `pt` if the two types `isUnsafeConvertable`.
101101

102-
Since implicit search (find candidates and try to type the new tree) could run in some different contexts, we have to pass the `UnsafeNullConversion` mode to the search context.
102+
Since implicit search (finding candidates and trying to type the new tree) could run in some different contexts, we have to pass the `UnsafeNullConversion` mode to the search context.
103103

104104
The SAM type conversion also happens in `adaptToSubType`. We need to strip `Null` from `pt` in order to get class information.
105105

docs/docs/reference/other-new-features/explicit-nulls.md

Lines changed: 75 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ Explicit nulls is an opt-in feature that modifies the Scala type system, which m
77
(anything that extends `AnyRef`) _non-nullable_.
88

99
This means the following code will no longer typecheck:
10-
```
11-
val x: String = null // error: found `Null`, but required `String`
10+
```scala
11+
val x: String = null // error: found `Null`, but required `String`
1212
```
1313

1414
Instead, to mark a type as nullable we use a [union type](https://dotty.epfl.ch/docs/reference/new-types/union-types.html)
1515

16+
```scala
17+
val x: String | Null = null // ok
1618
```
17-
val x: String|Null = null // ok
19+
20+
A nullable type could have null value during runtime; hence, it is not safe to select a member without checking its nullity.
21+
```scala
22+
x.trim // error: trim is not member of String | Null
1823
```
1924

2025
Explicit nulls are enabled via a `-Yexplicit-nulls` flag.
@@ -24,13 +29,30 @@ Read on for details.
2429
## New Type Hierarchy
2530

2631
When explicit nulls are enabled, the type hierarchy changes so that `Null` is only a subtype of
27-
`Any`, as opposed to every reference type.
32+
`Any`, as opposed to every reference type, which means `null` is no longer a value of `AnyRef` and its subtypes.
2833

2934
This is the new type hierarchy:
3035
![](../../../images/explicit-nulls/explicit-nulls-type-hierarchy.png "Type Hierarchy for Explicit Nulls")
3136

3237
After erasure, `Null` remains a subtype of all reference types (as forced by the JVM).
3338

39+
## Working with Null
40+
41+
To make working with nullable values easier, we propose a utility function to the standard library:
42+
An extension method `.nn` to "cast away" nullability.
43+
44+
```scala
45+
extension [T](x: T | Null) def nn: x.type & T =
46+
if x == null then
47+
throw new NullPointerException("tried to cast away nullability, but value is null")
48+
else x.asInstanceOf[x.type & T]
49+
```
50+
51+
This means that given `x: String|Null`, `x.nn` has type `String`, so we can call all the
52+
usual methods on it. Of course, `x.nn` will throw a NPE if `x` is `null`.
53+
54+
Don't use `.nn` on mutable variables directly, because it may introduce an unknown type into the type of the variable.
55+
3456
## Unsoundness
3557

3658
The new type system is unsound with respect to `null`. This means there are still instances where an expression has a non-nullable type like `String`, but its value is actually `null`.
@@ -71,24 +93,6 @@ y == x // ok
7193
(x: Any) == null // ok
7294
```
7395

74-
## Working with Null
75-
76-
To make working with nullable values easier, we propose adding a few utilities to the standard library.
77-
So far, we have found the following useful:
78-
79-
- An extension method `.nn` to "cast away" nullability
80-
81-
```scala
82-
def[T] (x: T|Null) nn: x.type & T =
83-
if (x == null) throw new NullPointerException("tried to cast away nullability, but value is null")
84-
else x.asInstanceOf[x.type & T]
85-
```
86-
87-
This means that given `x: String|Null`, `x.nn` has type `String`, so we can call all the
88-
usual methods on it. Of course, `x.nn` will throw a NPE if `x` is `null`.
89-
90-
Don't use `.nn` on mutable variables directly, because it may introduce an unknown type into the type of the variable.
91-
9296
## Java Interop
9397

9498
The compiler can load Java classes in two ways: from source or from bytecode. In either case,
@@ -249,7 +253,7 @@ We illustrate the rules with following examples:
249253

250254
### Override check
251255

252-
When we check overriding between Scala classes and Java classes, the rules are relaxed for `Null` type with this feature.
256+
When we check overriding between Scala classes and Java classes, the rules are relaxed for `Null` type with this feature, in order to help users to working with Java libraries.
253257

254258
Suppose we have Java method `String f(String x)`, we can override this method in Scala in any of the following forms:
255259

@@ -263,51 +267,7 @@ def f(x: String | Null): String
263267
def f(x: String): String
264268
```
265269

266-
### UnsafeNulls
267-
268-
It is difficult to work with nullable values, we introduce a language feature `unsafeNulls`. Inside this "unsafe" scope, all `T | Null` values can be used as `T`, and `Null` type keeps being a subtype of `Any`.
269-
270-
User can import `scala.language.unsafeNulls` to create such scope, or use `-language:unsafeNulls` to enable this feature globally. The following unsafe null operations will apply to all nullable types:
271-
1. select member of `T` on `T | Null` object
272-
2. call extension methods of `T` on `T | Null`
273-
3. convert `T1` to `T2` if `T1.stripAllNulls <:< T2.stripAllNulls` or `T1` is `Null` and `T2` has null value after erasure
274-
275-
The intention of this `unsafeNulls` is to give users a better migration path for explicit nulls. Projects for Scala 2 or regular dotty can try this by adding `-Yexplicit-nulls -language:unsafeNulls` to the compile options. A small number of manual modifications are expected (for example, some code relies on the fact of `Null <:< AnyRef`). To migrate to full explicit nulls in the future, `-language:unsafeNulls` can be dropped and add `import scala.language.unsafeNulls` only when needed.
276-
277-
```scala
278-
def f(x: String): String = ???
279-
280-
import scala.language.unsafeNulls
281-
282-
val s: String | Null = ???
283-
val a: String = s // unsafely convert String | Null to String
284-
285-
val b1 = s.trim() // call .trim() on String | Null unsafely
286-
val b2 = b1.length()
287-
288-
f(s).trim() // pass String | Null as an argument of type String unsafely
289-
290-
val c: String = null // Null to String
291-
292-
val d1: Array[String] = ???
293-
val d2: Array[String | Null] = d1 // unsafely convert Array[String] to Array[String | Null]
294-
val d3: Array[String] = Array(null) // unsafe
295-
```
296-
297-
Without the `unsafeNulls`, all these unsafe operations will not be compiled.
298-
299-
`unsafeNulls` also works for extension methods and implicit search.
300-
301-
```scala
302-
import scala.language.unsafeNulls
303-
304-
val x = "hello, world!".split(" ").map(_.length)
305-
306-
given Conversion[String, Array[String]] = _ => ???
307-
308-
val y: String | Null = ???
309-
val z: Array[String | Null] = y
310-
```
270+
Note that some of the definitions could cause unsoundness. For example, the return type is not nullable, but a `null` value is actually returned.
311271

312272
## Flow Typing
313273

@@ -473,6 +433,53 @@ We don't support:
473433
}
474434
```
475435
436+
### UnsafeNulls
437+
438+
It is difficult to work with nullable values, we introduce a language feature `unsafeNulls`. Inside this "unsafe" scope, all `T | Null` values can be used as `T`, and `Null` type keeps being a subtype of `Any`.
439+
440+
Users can import `scala.language.unsafeNulls` to create such scopes, or use `-language:unsafeNulls` to enable this feature globally. The following unsafe null operations will apply to all nullable types:
441+
1. select member of `T` on `T | Null` object
442+
2. call extension methods of `T` on `T | Null`
443+
3. convert `T1` to `T2` if `T1.stripAllNulls <:< T2.stripAllNulls` or `T1` is `Null` and `T2` has null value after erasure
444+
4. allow equality check between `T` and `T | Null`
445+
446+
The intention of this `unsafeNulls` is to give users a better migration path for explicit nulls. Projects for Scala 2 or regular dotty can try this by adding `-Yexplicit-nulls -language:unsafeNulls` to the compile options. A small number of manual modifications are expected (for example, some code relies on the fact of `Null <:< AnyRef`). To migrate to full explicit nulls in the future, `-language:unsafeNulls` can be dropped and add `import scala.language.unsafeNulls` only when needed.
447+
448+
```scala
449+
def f(x: String): String = ???
450+
451+
import scala.language.unsafeNulls
452+
453+
val s: String | Null = ???
454+
val a: String = s // unsafely convert String | Null to String
455+
456+
val b1 = s.trim() // call .trim() on String | Null unsafely
457+
val b2 = b1.length()
458+
459+
f(s).trim() // pass String | Null as an argument of type String unsafely
460+
461+
val c: String = null // Null to String
462+
463+
val d1: Array[String] = ???
464+
val d2: Array[String | Null] = d1 // unsafely convert Array[String] to Array[String | Null]
465+
val d3: Array[String] = Array(null) // unsafe
466+
```
467+
468+
Without the `unsafeNulls`, all these unsafe operations will not be typechecked.
469+
470+
`unsafeNulls` also works for extension methods and implicit search.
471+
472+
```scala
473+
import scala.language.unsafeNulls
474+
475+
val x = "hello, world!".split(" ").map(_.length)
476+
477+
given Conversion[String, Array[String]] = _ => ???
478+
479+
val y: String | Null = ???
480+
val z: Array[String | Null] = y
481+
```
482+
476483
## Binary Compatibility
477484
478485
Our strategy for binary compatibility with Scala binaries that predate explicit nulls

0 commit comments

Comments
 (0)