From db11cd967c2c6ae31f68376fafa3a8ca6c35c8dc Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Tue, 25 Nov 2025 16:56:15 +0100 Subject: [PATCH] Add `@uncheckedOverride` annotation for definitions that may override Forward-port of Scala 2 PR 11175. --- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../dotty/tools/dotc/typer/RefChecks.scala | 8 +++--- .../unchecked/uncheckedOverride.scala | 22 +++++++++++++++ library/src/scala/math/Ordering.scala | 6 ++--- project/MiMaFilters.scala | 5 +++- tests/pos/t13127.scala | 27 +++++++++++++++++++ 6 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 library/src/scala/annotation/unchecked/uncheckedOverride.scala create mode 100644 tests/pos/t13127.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 28a22091b219..be24d17e21e1 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1078,6 +1078,7 @@ class Definitions { @tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws") @tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient") @tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked") + @tu lazy val UncheckedOverrideAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedOverride") @tu lazy val UncheckedStableAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedStable") @tu lazy val UncheckedVarianceAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedVariance") @tu lazy val UncheckedCapturesAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedCaptures") diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 7037a9a986aa..47f355aa8aea 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -390,6 +390,8 @@ object RefChecks { if makeOverridingPairsChecker == null then OverridingPairsChecker(clazz, self) else makeOverridingPairsChecker(clazz, self) + def isMarkedOverride(sym: Symbol) = sym.isAnyOverride || sym.hasAnnotation(defn.UncheckedOverrideAnnot) + /* Check that all conditions for overriding `other` by `member` * of class `clazz` are met. */ @@ -539,7 +541,7 @@ object RefChecks { ) && !member.is(Deferred) && !other.name.is(DefaultGetterName) - && !member.isAnyOverride + && !isMarkedOverride(member) then // Exclusion for default getters, fixes SI-5178. We cannot assign the Override flag to // the default getter: one default getter might sometimes override, sometimes not. Example in comment on ticket. @@ -569,9 +571,9 @@ object RefChecks { overrideError("needs `override` modifier") else if (other.is(AbsOverride) && other.isIncompleteIn(clazz) && !member.is(AbsOverride)) overrideError("needs `abstract override` modifiers") - else if member.is(Override) && other.isMutableVarOrAccessor then + else if isMarkedOverride(member) && other.isMutableVarOrAccessor then overrideError("cannot override a mutable variable") - else if member.isAnyOverride + else if isMarkedOverride(member) && !(member.owner.thisType.baseClasses.exists(_.isSubClass(other.owner))) && !member.is(Deferred) && !other.is(Deferred) && intersectionIsEmpty(member.extendedOverriddenSymbols, other.extendedOverriddenSymbols) diff --git a/library/src/scala/annotation/unchecked/uncheckedOverride.scala b/library/src/scala/annotation/unchecked/uncheckedOverride.scala new file mode 100644 index 000000000000..40e8f3c58853 --- /dev/null +++ b/library/src/scala/annotation/unchecked/uncheckedOverride.scala @@ -0,0 +1,22 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. dba Akka + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.annotation.unchecked + +import scala.annotation.StaticAnnotation +import scala.language.`2.13` + +/** + * Marking a definition `@uncheckedOverride` is equivalent to the `override` keyword, except that overriding is not + * enforced. A definition marked `@uncheckedOverride` is allowed to override nothing. + */ +final class uncheckedOverride extends StaticAnnotation diff --git a/library/src/scala/math/Ordering.scala b/library/src/scala/math/Ordering.scala index df6a505235d3..9bed6ff6bd2e 100644 --- a/library/src/scala/math/Ordering.scala +++ b/library/src/scala/math/Ordering.scala @@ -15,9 +15,9 @@ package math import scala.language.`2.13` import java.util.Comparator - import scala.language.implicitConversions import scala.annotation.migration +import scala.annotation.unchecked.uncheckedOverride /** Ordering is a trait whose instances each represent a strategy for sorting * instances of a type. @@ -104,10 +104,10 @@ trait Ordering[T] extends Comparator[T] with PartialOrdering[T] with Serializabl override def equiv(x: T, y: T): Boolean = compare(x, y) == 0 /** Return `x` if `x` >= `y`, otherwise `y`. */ - def max[U <: T](x: U, y: U): U = if (gteq(x, y)) x else y + @uncheckedOverride def max[U <: T](x: U, y: U): U = if (gteq(x, y)) x else y /** Return `x` if `x` <= `y`, otherwise `y`. */ - def min[U <: T](x: U, y: U): U = if (lteq(x, y)) x else y + @uncheckedOverride def min[U <: T](x: U, y: U): U = if (lteq(x, y)) x else y /** Return the opposite ordering of this one. * diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index e45558f044b3..134300fc6620 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -23,7 +23,10 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.NamedTuple.namedTupleOrdering"), ProblemFilters.exclude[MissingClassProblem]("scala.NamedTuple$namedTupleOrdering"), - // cc related + // scala/scala3#24545 + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.unchecked.uncheckedOverride"), + + // cc related ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.readOnlyCapability"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.onlyCapability"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.separationChecking"), diff --git a/tests/pos/t13127.scala b/tests/pos/t13127.scala new file mode 100644 index 000000000000..3c35437359f3 --- /dev/null +++ b/tests/pos/t13127.scala @@ -0,0 +1,27 @@ +import scala.annotation.unchecked.uncheckedOverride + +trait Hordering[T] extends java.util.Comparator[T] { + // overriding on java 26 and later, not overriding before + @uncheckedOverride def max[U <: T](x: U, y: U): U = x + @uncheckedOverride def min[U <: T](x: U, y: U): U = x +} + +trait Base[T] { + def max[U <: T](x: U, y: U): U = x + + def mux(x: String = "ex") = x +} + +trait Sub[T] extends Base[T] { + @uncheckedOverride def max[U <: T](x: U, y: U): U = x // overriding ok + @uncheckedOverride def min[U <: T](x: U, y: U): U = x // not overriding ok + + // overriding default + @uncheckedOverride def mux(x: String = "nox") = x + + def test = mux() +} + +trait OverrideOrdering[T] extends scala.math.Ordering[T] { + override def max[U <: T](x: U, y: U): U = x +}