diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index cca01dd32a48..038ec31786c6 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -890,6 +890,8 @@ class Definitions { // Annotation classes @tu lazy val AnnotationDefaultAnnot: ClassSymbol = requiredClass("scala.annotation.internal.AnnotationDefault") + @tu lazy val BeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BeanProperty") + @tu lazy val BooleanBeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BooleanBeanProperty") @tu lazy val BodyAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Body") @tu lazy val ChildAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Child") @tu lazy val ContextResultCountAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ContextResultCount") diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 2bbdc68f9cc2..3a5764596af3 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -582,12 +582,15 @@ class TreeUnpickler(reader: TastyReader, else newSymbol(ctx.owner, name, flags, completer, privateWithin, coord) } - sym.annotations = annotFns.map(_(sym.owner)) + val annots = annotFns.map(_(sym.owner)) + sym.annotations = annots if sym.isOpaqueAlias then sym.setFlag(Deferred) + val isSyntheticBeanAccessor = flags.isAllOf(Method | Synthetic) && + annots.exists(a => a.matches(defn.BeanPropertyAnnot) || a.matches(defn.BooleanBeanPropertyAnnot)) val isScala2MacroDefinedInScala3 = flags.is(Macro, butNot = Inline) && flags.is(Erased) ctx.owner match { - case cls: ClassSymbol if !isScala2MacroDefinedInScala3 || cls == defn.StringContextClass => - // Enter all members of classes that are not Scala 2 macros. + case cls: ClassSymbol if (!isScala2MacroDefinedInScala3 || cls == defn.StringContextClass) && !isSyntheticBeanAccessor => + // Enter all members of classes that are not Scala 2 macros or synthetic bean accessors. // // For `StringContext`, enter `s`, `f` and `raw` // These definitions will be entered when defined in Scala 2. It is fine to enter them diff --git a/compiler/src/dotty/tools/dotc/transform/BeanProperties.scala b/compiler/src/dotty/tools/dotc/transform/BeanProperties.scala new file mode 100644 index 000000000000..baeb9cdbea98 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/BeanProperties.scala @@ -0,0 +1,66 @@ +package dotty.tools.dotc +package transform + +import core._ +import ast.tpd._ +import Annotations._ +import Contexts._ +import SymDenotations._ +import Symbols.newSymbol +import Decorators._ +import Flags._ +import Names._ +import Types._ +import util.Spans._ + +import DenotTransformers._ + +class BeanProperties(thisPhase: DenotTransformer): + def addBeanMethods(impl: Template)(using Context): Template = + val origBody = impl.body + cpy.Template(impl)(body = impl.body.flatMap { + case v: ValDef => generateAccessors(v) + case _ => Nil + } ::: origBody) + + def generateAccessors(valDef: ValDef)(using Context): List[Tree] = + import Symbols.defn + + def generateGetter(valDef: ValDef, annot: Annotation)(using Context) : Tree = + val prefix = if annot matches defn.BooleanBeanPropertyAnnot then "is" else "get" + val meth = newSymbol( + owner = ctx.owner, + name = prefixedName(prefix, valDef.name), + flags = Method | Synthetic, + info = MethodType(Nil, valDef.denot.info), + coord = annot.tree.span + ).enteredAfter(thisPhase).asTerm + meth.addAnnotations(valDef.symbol.annotations) + val body: Tree = ref(valDef.symbol) + DefDef(meth, body) + + def maybeGenerateSetter(valDef: ValDef, annot: Annotation)(using Context): Option[Tree] = + Option.when(valDef.denot.asSymDenotation.flags.is(Mutable)) { + val owner = ctx.owner + val meth = newSymbol( + owner, + name = prefixedName("set", valDef.name), + flags = Method | Synthetic, + info = MethodType(valDef.name :: Nil, valDef.denot.info :: Nil, defn.UnitType), + coord = annot.tree.span + ).enteredAfter(thisPhase).asTerm + meth.addAnnotations(valDef.symbol.annotations) + def body(params: List[List[Tree]]): Tree = Assign(ref(valDef.symbol), params.head.head) + DefDef(meth, body) + } + + def prefixedName(prefix: String, valName: Name) = + (prefix + valName.lastPart.toString.capitalize).toTermName + + val symbol = valDef.denot.symbol + symbol.getAnnotation(defn.BeanPropertyAnnot) + .orElse(symbol.getAnnotation(defn.BooleanBeanPropertyAnnot)) + .toList.flatMap { annot => + generateGetter(valDef, annot) +: maybeGenerateSetter(valDef, annot) ++: Nil + } + end generateAccessors diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index ba0337a797f3..09806b2b1679 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -75,6 +75,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase val superAcc: SuperAccessors = new SuperAccessors(thisPhase) val synthMbr: SyntheticMembers = new SyntheticMembers(thisPhase) + val beanProps: BeanProperties = new BeanProperties(thisPhase) private def newPart(tree: Tree): Option[New] = methPart(tree) match { case Select(nu: New, _) => Some(nu) @@ -322,8 +323,10 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase withNoCheckNews(templ.parents.flatMap(newPart)) { forwardParamAccessors(templ) synthMbr.addSyntheticMembers( + beanProps.addBeanMethods( superAcc.wrapTemplate(templ)( super.transform(_).asInstanceOf[Template])) + ) } case tree: ValDef => val tree1 = cpy.ValDef(tree)(rhs = normalizeErasedRhs(tree.rhs, tree.symbol)) diff --git a/tests/neg/beanAccessorsLeaking/A_1.scala b/tests/neg/beanAccessorsLeaking/A_1.scala new file mode 100644 index 000000000000..720c113d5289 --- /dev/null +++ b/tests/neg/beanAccessorsLeaking/A_1.scala @@ -0,0 +1,2 @@ +class A: + @scala.beans.BeanProperty val x = 6 \ No newline at end of file diff --git a/tests/neg/beanAccessorsLeaking/B_2.scala b/tests/neg/beanAccessorsLeaking/B_2.scala new file mode 100644 index 000000000000..bb551770a49f --- /dev/null +++ b/tests/neg/beanAccessorsLeaking/B_2.scala @@ -0,0 +1,2 @@ +class B(val a: A): + def x = a.getX() // error \ No newline at end of file diff --git a/tests/run/beans.check b/tests/run/beans.check new file mode 100644 index 000000000000..eb70eefb720c --- /dev/null +++ b/tests/run/beans.check @@ -0,0 +1,6 @@ +4 +true +10 +[@beans.LibraryAnnotation_1()] +some text +other text diff --git a/tests/run/beans/A_2.scala b/tests/run/beans/A_2.scala new file mode 100644 index 000000000000..309abc5b4e64 --- /dev/null +++ b/tests/run/beans/A_2.scala @@ -0,0 +1,17 @@ +class A: + @scala.beans.BeanProperty val x = 4 + @scala.beans.BooleanBeanProperty val y = true + @scala.beans.BeanProperty var mutableOneWithLongName = "some text" + + @scala.beans.BeanProperty + @beans.LibraryAnnotation_1 + val retainingAnnotation = 5 + +trait T: + @scala.beans.BeanProperty val x: Int + +class T1 extends T: + override val x = 5 + +class T2 extends T1: + override val x = 10 \ No newline at end of file diff --git a/tests/run/beans/LibraryAnnotation_1.java b/tests/run/beans/LibraryAnnotation_1.java new file mode 100644 index 000000000000..16b213bbfb40 --- /dev/null +++ b/tests/run/beans/LibraryAnnotation_1.java @@ -0,0 +1,7 @@ +package beans; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface LibraryAnnotation_1 {} \ No newline at end of file diff --git a/tests/run/beans/Test_3.java b/tests/run/beans/Test_3.java new file mode 100644 index 000000000000..4ac967419e50 --- /dev/null +++ b/tests/run/beans/Test_3.java @@ -0,0 +1,16 @@ +import java.util.Arrays; + +class JavaTest { + public A run() throws ReflectiveOperationException{ + A a = new A(); + System.out.println(a.getX()); + System.out.println(a.isY()); + System.out.println(new T2().getX()); + + System.out.println(Arrays.asList(a.getClass().getMethod("getRetainingAnnotation").getAnnotations())); + + System.out.println(a.getMutableOneWithLongName()); + a.setMutableOneWithLongName("other text"); + return a; + } +} \ No newline at end of file diff --git a/tests/run/beans/Test_4.scala b/tests/run/beans/Test_4.scala new file mode 100644 index 000000000000..807d91312b12 --- /dev/null +++ b/tests/run/beans/Test_4.scala @@ -0,0 +1,4 @@ +object Test: + def main(args: Array[String]) = + val a = JavaTest().run() + println(a.mutableOneWithLongName) \ No newline at end of file