From 7caac8ec24ddbe06a30010c81dff4bf771f512af Mon Sep 17 00:00:00 2001 From: Hamza Remmal Date: Sun, 16 Nov 2025 01:35:11 +0100 Subject: [PATCH 1/3] feat: allow annotations to annotate themselves --- .../tools/dotc/core/tasty/TreeUnpickler.scala | 35 +++++++++++-------- .../src/dotty/tools/dotc/typer/Namer.scala | 16 ++++----- tests/neg/i1212.scala | 1 - tests/pos/i1212.scala | 1 + 4 files changed, 27 insertions(+), 26 deletions(-) delete mode 100644 tests/neg/i1212.scala create mode 100644 tests/pos/i1212.scala diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index aa1fbf371fec..54f5de520a6b 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -668,6 +668,7 @@ class TreeUnpickler(reader: TastyReader, else newSymbol(ctx.owner, name, flags, completer, privateWithin, coord) } + registerSym(start, sym) val annotOwner = if sym.owner.isClass then newLocalDummy(sym.owner) else sym.owner sym.annotations = annotFns.map(_(annotOwner)) @@ -685,7 +686,6 @@ class TreeUnpickler(reader: TastyReader, cls.enter(sym) case _ => } - registerSym(start, sym) if (isClass) { if sym.owner.is(Package) && withCaptureChecks then sym.setFlag(CaptureChecked) @@ -768,7 +768,10 @@ class TreeUnpickler(reader: TastyReader, addFlag(Protected) privateWithin = readWithin case ANNOTATION => - annotFns = readAnnot :: annotFns + val annotFn = + val annot = readAnnot + (sym: Symbol) => annot.complete(sym) + annotFns = annotFn :: annotFns case tag => assert(false, s"illegal modifier tag $tag at $currentAddr, end = $end") } @@ -778,21 +781,23 @@ class TreeUnpickler(reader: TastyReader, private def readWithin(using Context): Symbol = readType().typeSymbol - private def readAnnot(using Context): Symbol => Annotation = + private def readAnnot(using Context): Trees.Lazy[Symbol => Annotation] = readByte() val end = readEnd() - val tp = readType() - val lazyAnnotTree = readLaterWithOwner(end, _.readTree()) - owner => - new DeferredSymAndTree(tp.typeSymbol, lazyAnnotTree(owner).complete): - // Only force computation of symbol if it has the right name. This added - // amount of laziness is sometimes necessary to avid cycles. Test case pos/i15980. - override def hasSymbol(sym: Symbol)(using Context) = tp match - case tp: TypeRef => - tp.designator match - case name: Name => name == sym.name && tp.symbol == sym - case _ => tp.symbol == sym - case _ => this.symbol == sym + readLater(end, reader => + val tp = reader.readType() + val lazyAnnotTree = reader.readLaterWithOwner(end, _.readTree()) + owner => + new DeferredSymAndTree(tp.typeSymbol, lazyAnnotTree(owner).complete): + // Only force computation of symbol if it has the right name. This added + // amount of laziness is sometimes necessary to avoid cycles. Test case pos/i15980. + override def hasSymbol(sym: Symbol)(using Context) = tp match + case tp: TypeRef => + tp.designator match + case name: Name => name == sym.name && tp.symbol == sym + case _ => tp.symbol == sym + case _ => this.symbol == sym + ) /** Create symbols for the definitions in the statement sequence between * current address and `end`. diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 02a55be9ea5a..2f8182dd7da0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -896,16 +896,12 @@ class Namer { typer: Typer => original.mods.withAnnotations: original.mods.annotations.mapConserve: annotTree => val cls = typedAheadAnnotationClass(annotTree)(using annotCtx) - if (cls eq sym) - report.error(em"An annotation class cannot be annotated with iself", annotTree.srcPos) - annotTree - else - val ann = - if cls.is(JavaDefined) then Checking.checkNamedArgumentForJavaAnnotation(annotTree, cls.asClass) - else annotTree - val ann1 = Annotation.deferred(cls)(typedAheadExpr(ann)(using annotCtx)) - sym.addAnnotation(ann1) - ann + val ann = + if cls.is(JavaDefined) then Checking.checkNamedArgumentForJavaAnnotation(annotTree, cls.asClass) + else annotTree + val ann1 = Annotation.deferred(cls)(typedAheadExpr(ann)(using annotCtx)) + sym.addAnnotation(ann1) + ann case _ => } diff --git a/tests/neg/i1212.scala b/tests/neg/i1212.scala deleted file mode 100644 index e2c98219c390..000000000000 --- a/tests/neg/i1212.scala +++ /dev/null @@ -1 +0,0 @@ -@ann class ann extends scala.annotation.Annotation // error: An annotation class cannot be annotated with iself diff --git a/tests/pos/i1212.scala b/tests/pos/i1212.scala new file mode 100644 index 000000000000..b975afe37015 --- /dev/null +++ b/tests/pos/i1212.scala @@ -0,0 +1 @@ +@ann class ann extends scala.annotation.Annotation From 2cfb7ee90f710663731d28ee783109f9f3904807 Mon Sep 17 00:00:00 2001 From: Hamza Remmal Date: Sat, 16 Aug 2025 21:35:41 +0200 Subject: [PATCH 2/3] chore: introduce `scala.annotation.documented` --- .../scala/annotation/constructorOnly.scala | 4 ++- library/src/scala/annotation/documented.scala | 6 ++++ .../src/scala/annotation/experimental.scala | 1 + library/src/scala/annotation/static.scala | 1 + library/src/scala/annotation/targetName.scala | 1 + .../src/scala/annotation/threadUnsafe.scala | 1 + library/src/scala/annotation/varargs.scala | 1 + library/src/scala/caps/package.scala | 6 ++-- library/src/scala/specialized.scala | 3 ++ library/src/scala/throws.scala | 3 ++ library/src/scala/transient.scala | 4 ++- library/src/scala/volatile.scala | 4 ++- .../tools/scaladoc/tasty/BasicSupport.scala | 32 ++++++------------- 13 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 library/src/scala/annotation/documented.scala diff --git a/library/src/scala/annotation/constructorOnly.scala b/library/src/scala/annotation/constructorOnly.scala index 4959b7c503b6..4977de6c8c60 100644 --- a/library/src/scala/annotation/constructorOnly.scala +++ b/library/src/scala/annotation/constructorOnly.scala @@ -22,4 +22,6 @@ import scala.annotation.meta.* * class fields. But it is checked that the field is eliminated before code * is generated. */ -@param @field class constructorOnly extends scala.annotation.StaticAnnotation +@param @field +@documented +class constructorOnly extends scala.annotation.StaticAnnotation diff --git a/library/src/scala/annotation/documented.scala b/library/src/scala/annotation/documented.scala new file mode 100644 index 000000000000..8aac014043f4 --- /dev/null +++ b/library/src/scala/annotation/documented.scala @@ -0,0 +1,6 @@ +package scala.annotation + +// TODO: Write the actual scaladoc of this annotation + +@documented +final class documented extends StaticAnnotation diff --git a/library/src/scala/annotation/experimental.scala b/library/src/scala/annotation/experimental.scala index ee91a6408b4b..761c51fe0dd1 100644 --- a/library/src/scala/annotation/experimental.scala +++ b/library/src/scala/annotation/experimental.scala @@ -7,5 +7,6 @@ import language.experimental.captureChecking * @see [[https://dotty.epfl.ch/docs/reference/other-new-features/experimental-defs]] * @syntax markdown */ +@documented final class experimental(message: String) extends StaticAnnotation: def this() = this("") diff --git a/library/src/scala/annotation/static.scala b/library/src/scala/annotation/static.scala index dc40ab9f3ac2..1c1e7949722f 100644 --- a/library/src/scala/annotation/static.scala +++ b/library/src/scala/annotation/static.scala @@ -12,4 +12,5 @@ import scala.annotation.meta.* @beanSetter @param @setter +@documented final class static extends StaticAnnotation diff --git a/library/src/scala/annotation/targetName.scala b/library/src/scala/annotation/targetName.scala index 17945fa86fb7..f5886eace6c2 100644 --- a/library/src/scala/annotation/targetName.scala +++ b/library/src/scala/annotation/targetName.scala @@ -7,4 +7,5 @@ import language.experimental.captureChecking * definition, its implementation will use the name `extname` instead of * the regular name. */ +@documented final class targetName(name: String) extends StaticAnnotation diff --git a/library/src/scala/annotation/threadUnsafe.scala b/library/src/scala/annotation/threadUnsafe.scala index dd87e74f3048..93433e41cbd8 100644 --- a/library/src/scala/annotation/threadUnsafe.scala +++ b/library/src/scala/annotation/threadUnsafe.scala @@ -6,4 +6,5 @@ import language.experimental.captureChecking * When this annotation is used, the initialization of the lazy val will use a * faster mechanism which is not thread-safe. */ +@documented final class threadUnsafe extends StaticAnnotation diff --git a/library/src/scala/annotation/varargs.scala b/library/src/scala/annotation/varargs.scala index 967e2df00f6f..c38b018d516d 100644 --- a/library/src/scala/annotation/varargs.scala +++ b/library/src/scala/annotation/varargs.scala @@ -18,4 +18,5 @@ import scala.language.`2.13` * Java varargs-style forwarder method for interop. This annotation can * only be applied to methods with repeated parameters. */ +@documented final class varargs extends scala.annotation.StaticAnnotation diff --git a/library/src/scala/caps/package.scala b/library/src/scala/caps/package.scala index 0527c3e149d6..6360bfad4e2c 100644 --- a/library/src/scala/caps/package.scala +++ b/library/src/scala/caps/package.scala @@ -3,7 +3,7 @@ package caps import language.experimental.captureChecking -import annotation.{experimental, compileTimeOnly, retainsCap} +import annotation.{experimental, documented, compileTimeOnly, retainsCap} /** * Base trait for classes that represent capabilities in the @@ -119,10 +119,10 @@ final class reserve extends annotation.StaticAnnotation /** Allowed only for source versions up to 3.7: * An annotation on parameters `x` stating that the method's body makes * use of the reach capability `x*`. Consequently, when calling the method - * we need to charge the deep capture set of the actual argiment to the + * we need to charge the deep capture set of the actual argument to the * environment. */ -@experimental +@experimental @documented final class use extends annotation.StaticAnnotation /** A trait that used to allow expressing existential types. Replaced by diff --git a/library/src/scala/specialized.scala b/library/src/scala/specialized.scala index f673fb6a3bb9..394bc260efa8 100644 --- a/library/src/scala/specialized.scala +++ b/library/src/scala/specialized.scala @@ -14,6 +14,8 @@ package scala import scala.language.`2.13` +import scala.annotation.documented + import Specializable._ /** Annotate type parameters on which code should be automatically @@ -30,6 +32,7 @@ import Specializable._ */ // class tspecialized[T](group: Group[T]) extends scala.annotation.StaticAnnotation { +@documented final class specialized(group: SpecializedGroup) extends scala.annotation.StaticAnnotation { def this(types: Specializable*) = this(new Group(types.toList)) def this() = this(Primitives) diff --git a/library/src/scala/throws.scala b/library/src/scala/throws.scala index 898c745a4804..a1fec6c3f033 100644 --- a/library/src/scala/throws.scala +++ b/library/src/scala/throws.scala @@ -14,6 +14,8 @@ package scala import scala.language.`2.13` +import scala.annotation.documented + /** * Annotation for specifying the exceptions thrown by a method. * For example: @@ -25,6 +27,7 @@ import scala.language.`2.13` * } * }}} */ +@documented final class throws[T <: Throwable](cause: String = "") extends scala.annotation.StaticAnnotation { def this(clazz: Class[T]) = this("") } diff --git a/library/src/scala/transient.scala b/library/src/scala/transient.scala index 3ca34fba9f69..1094c043ac7a 100644 --- a/library/src/scala/transient.scala +++ b/library/src/scala/transient.scala @@ -13,7 +13,9 @@ package scala import scala.language.`2.13` + +import scala.annotation.documented import scala.annotation.meta._ -@field +@field @documented final class transient extends scala.annotation.StaticAnnotation diff --git a/library/src/scala/volatile.scala b/library/src/scala/volatile.scala index 75b615ee6c7d..4369d15601af 100644 --- a/library/src/scala/volatile.scala +++ b/library/src/scala/volatile.scala @@ -13,7 +13,9 @@ package scala import scala.language.`2.13` + +import scala.annotation.documented import scala.annotation.meta._ -@field +@field @documented final class volatile extends scala.annotation.StaticAnnotation diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala index c96b6899501d..d9b6c1e157e4 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala @@ -41,29 +41,15 @@ trait BasicSupport: def documentation = parseComment(sym.docstring.getOrElse(""), sym.tree) def getAnnotations(): List[Annotation] = - // Custom annotations should be documented only if annotated by @java.lang.annotation.Documented - // We allow also some special cases - val fqNameAllowlist0 = Set( - "scala.specialized", - "scala.throws", - "scala.transient", - "scala.volatile", - "scala.annotation.experimental", - "scala.annotation.constructorOnly", - "scala.annotation.static", - "scala.annotation.targetName", - "scala.annotation.threadUnsafe", - "scala.annotation.varargs", - ) - val fqNameAllowlist = - if ccEnabled then - fqNameAllowlist0 + CaptureDefs.useAnnotFullName - else fqNameAllowlist0 - val documentedSymbol = summon[Quotes].reflect.Symbol.requiredClass("java.lang.annotation.Documented") - val annotations = sym.annotations.filter { a => - a.tpe.typeSymbol.hasAnnotation(documentedSymbol) || fqNameAllowlist.contains(a.symbol.fullName) - } - annotations.map(parseAnnotation).reverse + sym.annotations + .filter(a => a.tpe.typeSymbol.isDocumented) + .map(parseAnnotation) + .reverse + + def isDocumented: Boolean = + val javaDocumentedSymbol = reflect.Symbol.requiredClass("java.lang.annotation.Documented") + val scalaDocumentedSymbol = reflect.Symbol.requiredClass("scala.annotation.documented") + sym.hasAnnotation(scalaDocumentedSymbol) || sym.hasAnnotation(javaDocumentedSymbol) def isDeprecated(): Option[Annotation] = sym.annotations.find { a => From 3320f339090d35c18926ed1ebec96695e560f21a Mon Sep 17 00:00:00 2001 From: Hamza Remmal Date: Sat, 23 Aug 2025 11:45:54 +0200 Subject: [PATCH 3/3] chore: fix the signature test to include annotations --- scaladoc-testcases/src/tests/namedTuples.scala | 1 - .../src/tests/refinedFunctionTypes.scala | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/scaladoc-testcases/src/tests/namedTuples.scala b/scaladoc-testcases/src/tests/namedTuples.scala index 30a83e7e01b0..47aca287c4ba 100644 --- a/scaladoc-testcases/src/tests/namedTuples.scala +++ b/scaladoc-testcases/src/tests/namedTuples.scala @@ -1,6 +1,5 @@ package tests.namedTuples -import language.experimental.namedTuples import NamedTuple.* type Person = (name: String, age: Int) diff --git a/scaladoc-testcases/src/tests/refinedFunctionTypes.scala b/scaladoc-testcases/src/tests/refinedFunctionTypes.scala index d978a0ea2264..c2bcf347e181 100644 --- a/scaladoc-testcases/src/tests/refinedFunctionTypes.scala +++ b/scaladoc-testcases/src/tests/refinedFunctionTypes.scala @@ -4,29 +4,29 @@ package refinedFunctionTypes import annotation.experimental @experimental -infix type $throws[R, +E <: Exception] = CanThrow[E] ?=> R +infix type $throws[R, +E <: Exception] = CanThrow[E] ?=> R //expected: @experimental infix type $throws[R, +E <: Exception] = CanThrow[E] ?=> R @experimental -infix type $throws2[+E <: Exception] = (c: CanThrow[E]) ?=> c.type +infix type $throws2[+E <: Exception] = (c: CanThrow[E]) ?=> c.type //expected: @experimental infix type $throws2[+E <: Exception] = (c: CanThrow[E]) ?=> c.type @experimental -infix type $throws3[+E <: Exception] = [T] => (c: CanThrow[E]) ?=> c.type +infix type $throws3[+E <: Exception] = [T] => (c: CanThrow[E]) ?=> c.type //expected: @experimental infix type $throws3[+E <: Exception] = [T] => (c: CanThrow[E]) ?=> c.type @experimental -infix type $throws4[+E <: Exception] = [T] => (c: CanThrow[E]) ?=> T //expected: infix type $throws4[+E <: Exception] = [T] => CanThrow[E] ?=> T +infix type $throws4[+E <: Exception] = [T] => (c: CanThrow[E]) ?=> T //expected: @experimental infix type $throws4[+E <: Exception] = [T] => CanThrow[E] ?=> T type TA1 = (a: Int, b: (Boolean, String)) => List[(a.type, b.type)] type TA2 = (a: Int, b: (Boolean, String)) ?=> List[Boolean] @experimental -type TB0 = [R, E <: Exception] =>> PolyFunction { def apply[T](c: CanThrow[E]): R; } //expected: type TB0[R, E <: Exception] = [T] => CanThrow[E] => R +type TB0 = [R, E <: Exception] =>> PolyFunction { def apply[T](c: CanThrow[E]): R; } //expected: @experimental type TB0[R, E <: Exception] = [T] => CanThrow[E] => R @experimental -type TB1 = [R, E <: Exception] =>> PolyFunction { def apply[T](c: CanThrow[E], y: c.type): R; } //expected: type TB1[R, E <: Exception] = [T] => (c: CanThrow[E], y: c.type) => R +type TB1 = [R, E <: Exception] =>> PolyFunction { def apply[T](c: CanThrow[E], y: c.type): R; } //expected: @experimental type TB1[R, E <: Exception] = [T] => (c: CanThrow[E], y: c.type) => R @experimental -type TB2 = [R, E <: Exception] =>> PolyFunction { def apply[T](using c: CanThrow[E]): c.type; } //expected: type TB2[R, E <: Exception] = [T] => (c: CanThrow[E]) ?=> c.type +type TB2 = [R, E <: Exception] =>> PolyFunction { def apply[T](using c: CanThrow[E]): c.type; } //expected: @experimental type TB2[R, E <: Exception] = [T] => (c: CanThrow[E]) ?=> c.type type TC1 = [T] => (a: T) => T //expected: type TC1 = [T] => T => T