diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 2f1fe0e1c058..69b70e0a1720 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -176,7 +176,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID]: MatchableWarningID, CannotExtendFunctionID, LossyWideningConstantConversionID, - ImplicitSearchTooLargeID + ImplicitSearchTooLargeID, + TargetNameOnTopLevelClassID def errorNumber = ordinal - 2 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 797073115bb5..48135f9aa35d 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -2540,3 +2540,24 @@ import transform.SymUtils._ | |${openSearchPairs.reverse.map(showQuery)}%\n% """ + + class TargetNameOnTopLevelClass(symbol: Symbol)(using Context) + extends SyntaxMsg(TargetNameOnTopLevelClassID): + def msg = em"${hl("@targetName")} annotation not allowed on top-level $symbol" + def explain = + val annot = symbol.getAnnotation(defn.TargetNameAnnot).get + em"""The @targetName annotation may be applied to a top-level ${hl("val")} or ${hl("def")}, but not + |a top-level ${hl("class")}, ${hl("trait")}, or ${hl("object")}. + | + |This restriction is due to the naming convention of Java classfiles, whose filenames + |are based on the name of the class defined within. If @targetName were permitted + |here, the name of the classfile would be based on the target name, and the compiler + |could not associate that classfile with the Scala-visible defined name of the class. + | + |If your use case requires @targetName, consider wrapping $symbol in an ${hl("object")} + |(and possibly exporting it), as in the following example: + | + |${hl("object Wrapper:")} + | $annot $symbol { ... } + | + |${hl("export")} Wrapper.${symbol.name} ${hl("// optional")}""" diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 1e597e42d2a5..7b7bdd5945be 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -505,6 +505,8 @@ object Checking { fail(TailrecNotApplicable(sym)) else if sym.is(Inline) then fail("Inline methods cannot be @tailrec") + if sym.hasAnnotation(defn.TargetNameAnnot) && sym.isClass && sym.isTopLevelClass then + fail(TargetNameOnTopLevelClass(sym)) if (sym.hasAnnotation(defn.NativeAnnot)) { if (!sym.is(Deferred)) fail(NativeMembersMayNotHaveImplementation(sym)) diff --git a/docs/_docs/reference/other-new-features/targetName.md b/docs/_docs/reference/other-new-features/targetName.md index d2a654697d15..09886968a232 100644 --- a/docs/_docs/reference/other-new-features/targetName.md +++ b/docs/_docs/reference/other-new-features/targetName.md @@ -29,7 +29,7 @@ The [`@targetName`](https://scala-lang.org/api/3.x/scala/annotation/targetName.h of type `String`. That string is called the _external name_ of the definition that's annotated. - 2. A `@targetName` annotation can be given for all kinds of definitions. + 2. A `@targetName` annotation can be given for all kinds of definitions except a top-level `class`, `trait`, or `object`. 3. The name given in a [`@targetName`](https://scala-lang.org/api/3.x/scala/annotation/targetName.html) annotation must be a legal name for the defined entities on the host platform. diff --git a/tests/neg/targetName-toplevel.scala b/tests/neg/targetName-toplevel.scala new file mode 100644 index 000000000000..28e8b0e84ca3 --- /dev/null +++ b/tests/neg/targetName-toplevel.scala @@ -0,0 +1,18 @@ +import scala.annotation.targetName + +@targetName("B1") class A1 // error targetName on top-level class +@targetName("B2") trait A2 // error targetName on top-level trait +@targetName("B3") object A3 // error targetName on top-level object + +@targetName("bar") def foo = 42 // OK + +object Outer: + @targetName("B1") class A1 // OK + @targetName("B2") trait A2 // OK + @targetName("B3") object A3 // OK + @targetName("D1") class C1 // OK + @targetName("D2") trait C2 // OK + @targetName("D3") object C3 // OK + +export Outer.{A1, A2, A3} // error // error // error already defined +export Outer.{C1, C2, C3} // OK diff --git a/tests/run/targetName-interop/Test_2.java b/tests/run/targetName-interop/Test_2.java index 6e93f3cd3533..a1c710174c68 100644 --- a/tests/run/targetName-interop/Test_2.java +++ b/tests/run/targetName-interop/Test_2.java @@ -4,7 +4,7 @@ public class Test_2 { public static void main(String[] args) { - Alpha a = new Bar(); + Alpha a = new Outer$Bar(); assert a.foo() == 1; assert a.bar("a").equals("aa"); Alpha aa = a.append(a); diff --git a/tests/run/targetName-interop/alpha_1.scala b/tests/run/targetName-interop/alpha_1.scala index 41fb26c3f019..6408fccb48e5 100644 --- a/tests/run/targetName-interop/alpha_1.scala +++ b/tests/run/targetName-interop/alpha_1.scala @@ -11,10 +11,11 @@ abstract class Alpha[T] { } -@targetName("Bar") class | extends Alpha[String] { +object Outer { + @targetName("Bar") class | extends Alpha[String] { - @targetName("bar") override def foo(x: String) = x ++ x - - @targetName("append") override def ++ (xs: Alpha[String]) = this + @targetName("bar") override def foo(x: String) = x ++ x + @targetName("append") override def ++ (xs: Alpha[String]) = this + } } \ No newline at end of file diff --git a/tests/run/targetName-modules-1/7721_1.scala b/tests/run/targetName-modules-1/7721_1.scala index 5e59433c6beb..8420c14107ab 100644 --- a/tests/run/targetName-modules-1/7721_1.scala +++ b/tests/run/targetName-modules-1/7721_1.scala @@ -1,5 +1,7 @@ package alpha -@scala.annotation.targetName("A") object B { - def foo = 23 +object Outer { + @scala.annotation.targetName("A") object B { + def foo = 23 + } } diff --git a/tests/run/targetName-modules-1/Test_2.java b/tests/run/targetName-modules-1/Test_2.java index 2dbed46aa644..e92e602362c6 100644 --- a/tests/run/targetName-modules-1/Test_2.java +++ b/tests/run/targetName-modules-1/Test_2.java @@ -3,7 +3,7 @@ public class Test_2 { public static void main(String[] args) { - assert A.foo() == 23; - assert A$.MODULE$.foo() == 23; + assert Outer$.A.foo() == 23; + assert Outer$A$.MODULE$.foo() == 23; } } diff --git a/tests/run/targetName-modules-2/7723_1.scala b/tests/run/targetName-modules-2/7723_1.scala index 128dac508a14..8865c5ada577 100644 --- a/tests/run/targetName-modules-2/7723_1.scala +++ b/tests/run/targetName-modules-2/7723_1.scala @@ -1,3 +1,4 @@ package alpha -@scala.annotation.targetName("A") class B(val i: Int = 1) +object Outer: + @scala.annotation.targetName("A") class B(val i: Int = 1) diff --git a/tests/run/targetName-modules-2/Test_2.java b/tests/run/targetName-modules-2/Test_2.java index 39bdf269a98a..7d48407a6ded 100644 --- a/tests/run/targetName-modules-2/Test_2.java +++ b/tests/run/targetName-modules-2/Test_2.java @@ -3,8 +3,8 @@ public class Test_2 { public static void main(String[] args) { - assert new A(101).i() == 101; - assert new A(A.$lessinit$greater$default$1()).i() == 101; - assert new A(A$.MODULE$.$lessinit$greater$default$1()).i() == 101; + assert new Outer$A(101).i() == 101; + assert new Outer$A(Outer$A.$lessinit$greater$default$1()).i() == 101; + assert new Outer$A(Outer$A$.MODULE$.$lessinit$greater$default$1()).i() == 101; } } diff --git a/tests/run/targetName-separate/Foo_1.scala b/tests/run/targetName-separate/Foo_1.scala new file mode 100644 index 000000000000..dca3c888470c --- /dev/null +++ b/tests/run/targetName-separate/Foo_1.scala @@ -0,0 +1,5 @@ +object Outer: + @annotation.targetName("Bar") class Foo: + def it: Int = 42 + +export Outer.Foo diff --git a/tests/run/targetName-separate/Test_2.scala b/tests/run/targetName-separate/Test_2.scala new file mode 100644 index 000000000000..f051adde1531 --- /dev/null +++ b/tests/run/targetName-separate/Test_2.scala @@ -0,0 +1,3 @@ +@main def Test = + assert(new Foo().it == 42) + assert(Foo().it == 42)