Skip to content

Commit 95b37a7

Browse files
authored
Add stableNull annotation to force tracking mutable fields (#23528)
```scala class A: @stableNull var s: String | Null = null def getS: String = if s == null then s = "" s ``` The annotation will be added as `private` in `scala` for now. We plan to make it public in the next major release.
2 parents 9f20aa5 + 269ef39 commit 95b37a7

File tree

6 files changed

+42
-5
lines changed

6 files changed

+42
-5
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,7 @@ class Definitions {
11011101
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
11021102
@tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary")
11031103
@tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames")
1104+
@tu lazy val StableNullAnnot: ClassSymbol = requiredClass("scala.annotation.stableNull")
11041105

11051106
@tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable")
11061107

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,21 @@ object Nullables:
186186
* Check `usedOutOfOrder` to see the explaination and example of "out of order".
187187
* See more examples in `tests/explicit-nulls/neg/var-ref-in-closure.scala`.
188188
*/
189-
def isTracked(ref: TermRef)(using Context) =
189+
def isTracked(ref: TermRef)(using Context) = // true
190+
val sym = ref.symbol
191+
192+
def isNullStableField: Boolean =
193+
ref.prefix.isStable
194+
&& sym.isField
195+
&& sym.hasAnnotation(defn.StableNullAnnot)
196+
190197
ref.isStable
191-
|| { val sym = ref.symbol
192-
val unit = ctx.compilationUnit
198+
|| isNullStableField
199+
|| { val unit = ctx.compilationUnit
193200
!ref.usedOutOfOrder
194201
&& sym.span.exists
195202
&& (unit ne NoCompilationUnit) // could be null under -Ytest-pickler
196-
&& unit.assignmentSpans.contains(sym.span.start)
197-
}
203+
&& unit.assignmentSpans.contains(sym.span.start) }
198204

199205
/** The nullability context to be used after a case that matches pattern `pat`.
200206
* If `pat` is `null`, this will assert that the selector `sel` is not null afterwards.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package scala.annotation
2+
3+
/** An annotation that can be used to mark a mutable field as trackable for nullability.
4+
* With explicit nulls, a normal mutable field cannot be tracked for nullability by flow typing,
5+
* since it can be updated to a null value at the same time.
6+
* This annotation will force the compiler to track the field for nullability, as long as the
7+
* prefix is a stable path.
8+
* See `tests/explicit-nulls/pos/force-track-var-fields.scala` for an example.
9+
*/
10+
private[scala] final class stableNull extends StaticAnnotation

project/Build.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,7 @@ object Build {
11411141
file(s"${baseDirectory.value}/src/scala/annotation/init.scala"),
11421142
file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"),
11431143
file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"),
1144+
file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"),
11441145
file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"),
11451146
file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"),
11461147
file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"),
@@ -1278,6 +1279,7 @@ object Build {
12781279
file(s"${baseDirectory.value}/src/scala/annotation/init.scala"),
12791280
file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"),
12801281
file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"),
1282+
file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"),
12811283
file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"),
12821284
file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"),
12831285
file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"),

project/MiMaFilters.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ object MiMaFilters {
2222

2323
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Conversion.underlying"),
2424
ProblemFilters.exclude[MissingClassProblem]("scala.Conversion$"),
25+
26+
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.stableNull"),
2527
),
2628

2729
// Additions since last LTS
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package scala
2+
3+
import scala.annotation.stableNull
4+
5+
class A:
6+
@stableNull var s: String | Null = null
7+
def getS: String =
8+
if s == null then s = ""
9+
s
10+
11+
def test(a: A): String =
12+
if a.s == null then
13+
a.s = ""
14+
a.s
15+
else
16+
a.s

0 commit comments

Comments
 (0)