diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 386bae0f68c2..d58c103904b0 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1101,6 +1101,7 @@ class Definitions { @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName") @tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary") @tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames") + @tu lazy val StableNullAnnot: ClassSymbol = requiredClass("scala.annotation.stableNull") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 86b9a337e69a..609dad894b6c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -186,15 +186,21 @@ object Nullables: * Check `usedOutOfOrder` to see the explaination and example of "out of order". * See more examples in `tests/explicit-nulls/neg/var-ref-in-closure.scala`. */ - def isTracked(ref: TermRef)(using Context) = + def isTracked(ref: TermRef)(using Context) = // true + val sym = ref.symbol + + def isNullStableField: Boolean = + ref.prefix.isStable + && sym.isField + && sym.hasAnnotation(defn.StableNullAnnot) + ref.isStable - || { val sym = ref.symbol - val unit = ctx.compilationUnit + || isNullStableField + || { val unit = ctx.compilationUnit !ref.usedOutOfOrder && sym.span.exists && (unit ne NoCompilationUnit) // could be null under -Ytest-pickler - && unit.assignmentSpans.contains(sym.span.start) - } + && unit.assignmentSpans.contains(sym.span.start) } /** The nullability context to be used after a case that matches pattern `pat`. * If `pat` is `null`, this will assert that the selector `sel` is not null afterwards. diff --git a/library/src/scala/annotation/stableNull.scala b/library/src/scala/annotation/stableNull.scala new file mode 100644 index 000000000000..e2ebac72fce5 --- /dev/null +++ b/library/src/scala/annotation/stableNull.scala @@ -0,0 +1,10 @@ +package scala.annotation + +/** An annotation that can be used to mark a mutable field as trackable for nullability. + * With explicit nulls, a normal mutable field cannot be tracked for nullability by flow typing, + * since it can be updated to a null value at the same time. + * This annotation will force the compiler to track the field for nullability, as long as the + * prefix is a stable path. + * See `tests/explicit-nulls/pos/force-track-var-fields.scala` for an example. + */ +private[scala] final class stableNull extends StaticAnnotation diff --git a/project/Build.scala b/project/Build.scala index 5f75d156f5a4..8eb6fd0a8aa3 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1141,6 +1141,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/init.scala"), file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"), file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"), file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"), file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"), file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"), @@ -1278,6 +1279,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/init.scala"), file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"), file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"), file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"), file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"), file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"), diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 864f6f6f272f..5a4be70987a5 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -22,6 +22,8 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Conversion.underlying"), ProblemFilters.exclude[MissingClassProblem]("scala.Conversion$"), + + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.stableNull"), ), // Additions since last LTS diff --git a/tests/explicit-nulls/pos/force-track-var-fields.scala b/tests/explicit-nulls/pos/force-track-var-fields.scala new file mode 100644 index 000000000000..27f0448b8023 --- /dev/null +++ b/tests/explicit-nulls/pos/force-track-var-fields.scala @@ -0,0 +1,16 @@ +package scala + +import scala.annotation.stableNull + +class A: + @stableNull var s: String | Null = null + def getS: String = + if s == null then s = "" + s + +def test(a: A): String = + if a.s == null then + a.s = "" + a.s + else + a.s \ No newline at end of file