From dbdfcffe40e9175868cfc3691133ef812a4bca8c Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Wed, 13 Mar 2024 11:25:49 +0100 Subject: [PATCH] Add message parameter to `@experimental` annotation --- .../src/dotty/tools/dotc/config/Feature.scala | 30 ++++++++++++------- .../dotty/tools/dotc/core/Annotations.scala | 26 ++++++++++++++++ .../tools/dotc/transform/AccessProxies.scala | 4 +-- .../tools/dotc/transform/PostTyper.scala | 9 +++--- .../scala/annotation/experimental.scala | 3 +- .../scala/annotation/experimental.scala | 4 +++ .../runtime/stdLibPatches/language.scala | 2 +- project/MiMaFilters.scala | 1 + tests/neg-macros/i18677-a.check | 4 +-- tests/neg-macros/i18677-b.check | 4 +-- ...perimental-message-experimental-flag.check | 10 +++++++ .../Lib_1.scala | 3 ++ .../Test_2.scala | 3 ++ tests/neg/experimental-message.check | 27 +++++++++++++++++ tests/neg/experimental-message.scala | 17 +++++++++++ tests/neg/use-experimental-def.check | 5 ++-- 16 files changed, 125 insertions(+), 27 deletions(-) rename library/{src => src-bootstrapped}/scala/annotation/experimental.scala (77%) create mode 100644 library/src-non-bootstrapped/scala/annotation/experimental.scala create mode 100644 tests/neg/experimental-message-experimental-flag.check create mode 100644 tests/neg/experimental-message-experimental-flag/Lib_1.scala create mode 100644 tests/neg/experimental-message-experimental-flag/Test_2.scala create mode 100644 tests/neg/experimental-message.check create mode 100644 tests/neg/experimental-message.scala diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 7eb95badd4d0..c90338302ce7 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -10,6 +10,7 @@ import util.{SrcPos, NoSourcePosition} import SourceVersion.* import reporting.Message import NameKinds.QualifiedName +import Annotations.ExperimentalAnnotation object Feature: @@ -131,12 +132,7 @@ object Feature: def checkExperimentalFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) = if !isExperimentalEnabled then - report.error( - em"""Experimental $which may only be used under experimental mode: - | 1. in a definition marked as @experimental, or - | 2. compiling with the -experimental compiler flag, or - | 3. with a nightly or snapshot version of the compiler.$note - """, srcPos) + report.error(experimentalUseSite(which) + note, srcPos) private def ccException(sym: Symbol)(using Context): Boolean = ccEnabled && defn.ccExperimental.contains(sym) @@ -146,12 +142,24 @@ object Feature: if sym.hasAnnotation(defn.ExperimentalAnnot) then sym else if sym.owner.hasAnnotation(defn.ExperimentalAnnot) then sym.owner else NoSymbol - if !ccException(experimentalSym) then - val note = + if !isExperimentalEnabled && !ccException(experimentalSym) then + val msg = + experimentalSym.getAnnotation(defn.ExperimentalAnnot).map { + case ExperimentalAnnotation(msg) if msg.nonEmpty => s": $msg" + case _ => "" + }.getOrElse("") + val markedExperimental = if experimentalSym.exists - then i"$experimentalSym is marked @experimental" - else i"$sym inherits @experimental" - checkExperimentalFeature("definition", srcPos, s"\n\n$note") + then i"$experimentalSym is marked @experimental$msg" + else i"$sym inherits @experimental$msg" + report.error(markedExperimental + "\n\n" + experimentalUseSite("definition"), srcPos) + + private def experimentalUseSite(which: String): String = + s"""Experimental $which may only be used under experimental mode: + | 1. in a definition marked as @experimental, or + | 2. compiling with the -experimental compiler flag, or + | 3. with a nightly or snapshot version of the compiler. + |""".stripMargin /** Check that experimental compiler options are only set for snapshot or nightly compiler versions. */ def checkExperimentalSettings(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 45dba97a79f7..a5ef4c26eed1 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -275,4 +275,30 @@ object Annotations { } } } + + object ExperimentalAnnotation { + + /** Create an instance of `@experimental()` */ + def apply(msg: String, span: Span)(using Context): Annotation = + Annotation(defn.ExperimentalAnnot, Literal(Constant(msg)), span) + + /** Matches and extracts the message from an instance of `@experimental(msg)` + * Returns `Some("")` for `@experimental` with no message. + */ + def unapply(a: Annotation)(using Context): Option[String] = + if a.symbol ne defn.ExperimentalAnnot then + None + else a.argumentConstant(0) match + case Some(Constant(msg: String)) => Some(msg) + case _ => Some("") + + /** Makes a copy of the `@experimental(msg)` annotation on `sym` + * None is returned if the symbol does not have an `@experimental` annotation. + */ + def copy(sym: Symbol)(using Context): Option[Annotation] = + sym.getAnnotation(defn.ExperimentalAnnot).map { + case annot @ ExperimentalAnnotation(msg) => ExperimentalAnnotation(msg, annot.tree.span) + } + } + } diff --git a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala index 6d445887e1d9..1f9334164496 100644 --- a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala +++ b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala @@ -12,6 +12,7 @@ import Decorators.* import Types.* import util.Spans.Span import config.Printers.transforms +import Annotations.ExperimentalAnnotation /** A utility class for generating access proxies. Currently used for * inline accessors and protected accessors. @@ -84,8 +85,7 @@ abstract class AccessProxies { val sym = newSymbol(owner, name, Synthetic | Method, info, coord = accessed.span).entered if accessed.is(Private) then sym.setFlag(Final) else if sym.allOverriddenSymbols.exists(!_.is(Deferred)) then sym.setFlag(Override) - if accessed.hasAnnotation(defn.ExperimentalAnnot) then - sym.addAnnotation(defn.ExperimentalAnnot) + ExperimentalAnnotation.copy(accessed).foreach(sym.addAnnotation) sym } diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 3bcec80b5b10..25fbae31bfe7 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -548,11 +548,10 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => def isTopLevelDefinitionInSource(sym: Symbol) = !sym.is(Package) && !sym.name.isPackageObjectName && (sym.owner.is(Package) || (sym.owner.isPackageObject && !sym.isConstructor)) - if !sym.hasAnnotation(defn.ExperimentalAnnot) - && (ctx.settings.experimental.value && isTopLevelDefinitionInSource(sym)) - || (sym.is(Module) && sym.companionClass.hasAnnotation(defn.ExperimentalAnnot)) - then - sym.addAnnotation(Annotation(defn.ExperimentalAnnot, sym.span)) + if sym.is(Module) then + ExperimentalAnnotation.copy(sym.companionClass).foreach(sym.addAnnotation) + if !sym.hasAnnotation(defn.ExperimentalAnnot) && ctx.settings.experimental.value && isTopLevelDefinitionInSource(sym) then + sym.addAnnotation(ExperimentalAnnotation("Added by -experimental", sym.span)) private def scala2LibPatch(tree: TypeDef)(using Context) = val sym = tree.symbol diff --git a/library/src/scala/annotation/experimental.scala b/library/src-bootstrapped/scala/annotation/experimental.scala similarity index 77% rename from library/src/scala/annotation/experimental.scala rename to library/src-bootstrapped/scala/annotation/experimental.scala index 69ab5b9c7221..185db51c07c1 100644 --- a/library/src/scala/annotation/experimental.scala +++ b/library/src-bootstrapped/scala/annotation/experimental.scala @@ -6,4 +6,5 @@ package scala.annotation * @syntax markdown */ @deprecatedInheritance("Scheduled for being final in the future", "3.4.0") -class experimental extends StaticAnnotation +class experimental(message: String) extends StaticAnnotation: + def this() = this("") diff --git a/library/src-non-bootstrapped/scala/annotation/experimental.scala b/library/src-non-bootstrapped/scala/annotation/experimental.scala new file mode 100644 index 000000000000..dbc3296aa1ab --- /dev/null +++ b/library/src-non-bootstrapped/scala/annotation/experimental.scala @@ -0,0 +1,4 @@ +package scala.annotation + +@deprecatedInheritance("Scheduled for being final in the future", "3.4.0") +class experimental extends StaticAnnotation diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 70d5f2d41907..3c9c172918d2 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -84,7 +84,7 @@ object language: object captureChecking /** Experimental support for automatic conversions of arguments, without requiring - * a langauge import `import scala.language.implicitConversions`. + * a language import `import scala.language.implicitConversions`. * * @see [[https://dotty.epfl.ch/docs/reference/experimental/into-modifier]] */ diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 7565d23b2c1b..e45e7a81904b 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -8,6 +8,7 @@ object MiMaFilters { val ForwardsBreakingChanges: Map[String, Seq[ProblemFilter]] = Map( // Additions that require a new minor version of the library Build.previousDottyVersion -> Seq( + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.annotation.experimental.this"), ), // Additions since last LTS diff --git a/tests/neg-macros/i18677-a.check b/tests/neg-macros/i18677-a.check index 963affc47181..d190ce36318a 100644 --- a/tests/neg-macros/i18677-a.check +++ b/tests/neg-macros/i18677-a.check @@ -7,10 +7,10 @@ |The tree does not conform to the compiler's tree invariants. | |Macro was: - |@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-a/Test_2.scala") @scala.annotation.experimental @extendFoo class AFoo() + |@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-a/Test_2.scala") @scala.annotation.experimental("Added by -experimental") @extendFoo class AFoo() | |The macro returned: - |@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-a/Test_2.scala") @scala.annotation.experimental @extendFoo class AFoo() extends Foo + |@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-a/Test_2.scala") @scala.annotation.experimental("Added by -experimental") @extendFoo class AFoo() extends Foo | |Error: |assertion failed: Parents of class symbol differs from the parents in the tree for class AFoo diff --git a/tests/neg-macros/i18677-b.check b/tests/neg-macros/i18677-b.check index b8fecde2e36d..eca2bdcde726 100644 --- a/tests/neg-macros/i18677-b.check +++ b/tests/neg-macros/i18677-b.check @@ -7,10 +7,10 @@ |The tree does not conform to the compiler's tree invariants. | |Macro was: - |@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-b/Test_2.scala") @scala.annotation.experimental @extendFoo class AFoo() + |@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-b/Test_2.scala") @scala.annotation.experimental("Added by -experimental") @extendFoo class AFoo() | |The macro returned: - |@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-b/Test_2.scala") @scala.annotation.experimental @extendFoo class AFoo() extends Foo + |@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-b/Test_2.scala") @scala.annotation.experimental("Added by -experimental") @extendFoo class AFoo() extends Foo | |Error: |assertion failed: Parents of class symbol differs from the parents in the tree for class AFoo diff --git a/tests/neg/experimental-message-experimental-flag.check b/tests/neg/experimental-message-experimental-flag.check new file mode 100644 index 000000000000..ce3a85a89916 --- /dev/null +++ b/tests/neg/experimental-message-experimental-flag.check @@ -0,0 +1,10 @@ + +-- Error: tests/neg/experimental-message-experimental-flag/Test_2.scala:3:10 ------------------------------------------- +3 |def g() = f() // error + | ^ + | method f is marked @experimental: Added by -experimental + | + | Experimental definition may only be used under experimental mode: + | 1. in a definition marked as @experimental, or + | 2. compiling with the -experimental compiler flag, or + | 3. with a nightly or snapshot version of the compiler. diff --git a/tests/neg/experimental-message-experimental-flag/Lib_1.scala b/tests/neg/experimental-message-experimental-flag/Lib_1.scala new file mode 100644 index 000000000000..dc0d774092a2 --- /dev/null +++ b/tests/neg/experimental-message-experimental-flag/Lib_1.scala @@ -0,0 +1,3 @@ +//> using options -Yno-experimental -experimental + +def f() = ??? diff --git a/tests/neg/experimental-message-experimental-flag/Test_2.scala b/tests/neg/experimental-message-experimental-flag/Test_2.scala new file mode 100644 index 000000000000..3c6309486778 --- /dev/null +++ b/tests/neg/experimental-message-experimental-flag/Test_2.scala @@ -0,0 +1,3 @@ +//> using options -Yno-experimental + +def g() = f() // error diff --git a/tests/neg/experimental-message.check b/tests/neg/experimental-message.check new file mode 100644 index 000000000000..d57fe58f27cf --- /dev/null +++ b/tests/neg/experimental-message.check @@ -0,0 +1,27 @@ +-- Error: tests/neg/experimental-message.scala:15:2 -------------------------------------------------------------------- +15 | f1() // error + | ^^ + | method f1 is marked @experimental + | + | Experimental definition may only be used under experimental mode: + | 1. in a definition marked as @experimental, or + | 2. compiling with the -experimental compiler flag, or + | 3. with a nightly or snapshot version of the compiler. +-- Error: tests/neg/experimental-message.scala:16:2 -------------------------------------------------------------------- +16 | f2() // error + | ^^ + | method f2 is marked @experimental + | + | Experimental definition may only be used under experimental mode: + | 1. in a definition marked as @experimental, or + | 2. compiling with the -experimental compiler flag, or + | 3. with a nightly or snapshot version of the compiler. +-- Error: tests/neg/experimental-message.scala:17:2 -------------------------------------------------------------------- +17 | f3() // error + | ^^ + | method f3 is marked @experimental: not yet stable + | + | Experimental definition may only be used under experimental mode: + | 1. in a definition marked as @experimental, or + | 2. compiling with the -experimental compiler flag, or + | 3. with a nightly or snapshot version of the compiler. diff --git a/tests/neg/experimental-message.scala b/tests/neg/experimental-message.scala new file mode 100644 index 000000000000..1fe9b7f0d793 --- /dev/null +++ b/tests/neg/experimental-message.scala @@ -0,0 +1,17 @@ +//> using options -Yno-experimental + +import scala.annotation.experimental + +@experimental +def f1() = ??? + +@experimental() +def f2() = ??? + +@experimental("not yet stable") +def f3() = ??? + +def g() = + f1() // error + f2() // error + f3() // error diff --git a/tests/neg/use-experimental-def.check b/tests/neg/use-experimental-def.check index 66c4a7a305b5..cb8fc1402b69 100644 --- a/tests/neg/use-experimental-def.check +++ b/tests/neg/use-experimental-def.check @@ -1,10 +1,9 @@ -- Error: tests/neg/use-experimental-def.scala:7:15 -------------------------------------------------------------------- 7 |def bar: Int = foo // error | ^^^ + | method foo is marked @experimental + | | Experimental definition may only be used under experimental mode: | 1. in a definition marked as @experimental, or | 2. compiling with the -experimental compiler flag, or | 3. with a nightly or snapshot version of the compiler. - | - | method foo is marked @experimental - |