Skip to content

Commit 2787b47

Browse files
authored
Merge pull request scala#5570 from adriaanm/t10075
SI-10075 propagate annotations to lazy val's underlying field
2 parents ee1c02b + 7bf8ffa commit 2787b47

File tree

10 files changed

+199
-22
lines changed

10 files changed

+199
-22
lines changed

src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ abstract class BCodeSkelBuilder extends BCodeHelpers {
494494
genDefDef(statified)
495495
} else {
496496
val forwarderDefDef = {
497-
val dd1 = global.gen.mkStatic(deriveDefDef(dd)(_ => EmptyTree), traitSuperAccessorName(sym), _.cloneSymbol)
497+
val dd1 = global.gen.mkStatic(deriveDefDef(dd)(_ => EmptyTree), traitSuperAccessorName(sym), _.cloneSymbol.withoutAnnotations)
498498
dd1.symbol.setFlag(Flags.ARTIFACT).resetFlag(Flags.OVERRIDE)
499499
val selfParam :: realParams = dd1.vparamss.head.map(_.symbol)
500500
deriveDefDef(dd1)(_ =>

src/compiler/scala/tools/nsc/transform/Erasure.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,9 @@ abstract class Erasure extends InfoTransform
12051205
treeCopy.ArrayValue(
12061206
tree1, elemtpt setType specialScalaErasure.applyInArray(elemtpt.tpe), trees map transform).clearType()
12071207
case DefDef(_, _, _, _, tpt, _) =>
1208-
fields.dropFieldAnnotationsFromGetter(tree.symbol) // TODO: move this in some post-processing transform in the fields phase?
1208+
// TODO: move this in some post-processing transform in the fields phase?
1209+
if (fields.symbolAnnotationsTargetFieldAndGetter(tree.symbol))
1210+
fields.dropFieldAnnotationsFromGetter(tree.symbol)
12091211

12101212
try super.transform(tree1).clearType()
12111213
finally tpt setType specialErasure(tree1.symbol)(tree1.symbol.tpe).resultType

src/compiler/scala/tools/nsc/transform/Fields.scala

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,25 @@ abstract class Fields extends InfoTransform with ast.TreeDSL with TypingTransfor
176176
// NOTE: this only considers type, filter on flags first!
177177
def fieldMemoizationIn(accessorOrField: Symbol, site: Symbol) = new FieldMemoization(accessorOrField, site)
178178

179-
// drop field-targeting annotations from getters
179+
// drop field-targeting annotations from getters (done during erasure because we first need to create the field symbol)
180180
// (in traits, getters must also hold annotations that target the underlying field,
181181
// because the latter won't be created until the trait is mixed into a class)
182182
// TODO do bean getters need special treatment to suppress field-targeting annotations in traits?
183183
def dropFieldAnnotationsFromGetter(sym: Symbol) =
184-
if (sym.isGetter && sym.owner.isTrait) {
185-
sym setAnnotations (sym.annotations filter AnnotationInfo.mkFilter(GetterTargetClass, defaultRetention = false))
186-
}
184+
sym setAnnotations (sym.annotations filter AnnotationInfo.mkFilter(GetterTargetClass, defaultRetention = false))
185+
186+
def symbolAnnotationsTargetFieldAndGetter(sym: Symbol): Boolean = sym.isGetter && (sym.isLazy || sym.owner.isTrait)
187+
188+
// A trait val/var or a lazy val does not receive an underlying field symbol until this phase.
189+
// Since annotations need a carrier symbol from the beginning, both field- and getter-targeting annotations
190+
// are kept on the getter symbol for these until they are dropped by dropFieldAnnotationsFromGetter
191+
def getterTreeAnnotationsTargetFieldAndGetter(owner: Symbol, mods: Modifiers) = mods.isLazy || owner.isTrait
192+
193+
// Propagate field-targeting annotations from getter to field.
194+
// By the way, we must keep them around long enough to see them here (now that we have created the field),
195+
// which is why dropFieldAnnotationsFromGetter is not called until erasure.
196+
private def propagateFieldAnnotations(getter: Symbol, field: TermSymbol): Unit =
197+
field setAnnotations (getter.annotations filter AnnotationInfo.mkFilter(FieldTargetClass, defaultRetention = true))
187198

188199

189200
// can't use the referenced field since it already tracks the module's moduleClass
@@ -241,6 +252,7 @@ abstract class Fields extends InfoTransform with ast.TreeDSL with TypingTransfor
241252
sym
242253
}
243254

255+
244256
private object synthFieldsAndAccessors extends TypeMap {
245257
private def newTraitSetter(getter: Symbol, clazz: Symbol) = {
246258
// Add setter for an immutable, memoizing getter
@@ -388,10 +400,12 @@ abstract class Fields extends InfoTransform with ast.TreeDSL with TypingTransfor
388400
val accessorSymbolSynth = checkedAccessorSymbolSynth(tp.typeSymbol)
389401

390402
// expand module def in class/object (if they need it -- see modulesNeedingExpansion above)
391-
val expandedModulesAndLazyVals = (
403+
val expandedModulesAndLazyVals =
392404
modulesAndLazyValsNeedingExpansion flatMap { member =>
393405
if (member.isLazy) {
394-
List(newLazyVarMember(member), accessorSymbolSynth.newSlowPathSymbol(member))
406+
val lazyVar = newLazyVarMember(member)
407+
propagateFieldAnnotations(member, lazyVar)
408+
List(lazyVar, accessorSymbolSynth.newSlowPathSymbol(member))
395409
}
396410
// expanding module def (top-level or nested in static module)
397411
else List(if (member.isStatic) { // implies m.isOverridingSymbol as per above filter
@@ -404,7 +418,7 @@ abstract class Fields extends InfoTransform with ast.TreeDSL with TypingTransfor
404418
member setFlag NEEDS_TREES
405419
newModuleVarMember(member)
406420
})
407-
})
421+
}
408422

409423
// println(s"expanded modules for $clazz: $expandedModules")
410424

@@ -419,8 +433,9 @@ abstract class Fields extends InfoTransform with ast.TreeDSL with TypingTransfor
419433
val clonedAccessor = (member cloneSymbol clazz) setPos clazz.pos
420434
setMixedinAccessorFlags(member, clonedAccessor)
421435

422-
if (clonedAccessor.isGetter)
423-
clonedAccessor setAnnotations (clonedAccessor.annotations filter AnnotationInfo.mkFilter(GetterTargetClass, defaultRetention = false))
436+
// note: check original member when deciding how to triage annotations, then act on the cloned accessor
437+
if (symbolAnnotationsTargetFieldAndGetter(member)) // this simplifies to member.isGetter, but the full formulation really ties the triage together
438+
dropFieldAnnotationsFromGetter(clonedAccessor)
424439

425440
// if we don't cloneInfo, method argument symbols are shared between trait and subclasses --> lambalift proxy crash
426441
// TODO: use derive symbol variant?
@@ -450,7 +465,11 @@ abstract class Fields extends InfoTransform with ast.TreeDSL with TypingTransfor
450465
}
451466
else if (member hasFlag LAZY) {
452467
val mixedinLazy = cloneAccessor()
453-
val lazyVar = newLazyVarMember(mixedinLazy)
468+
val lazyVar = newLazyVarMember(mixedinLazy) // link lazy var member to the mixedin lazy accessor
469+
470+
// propagate from original member. since mixed in one has only retained the annotations targeting the getter
471+
propagateFieldAnnotations(member, lazyVar)
472+
454473
// println(s"mixing in lazy var: $lazyVar for $member")
455474
List(lazyVar, accessorSymbolSynth.newSlowPathSymbol(mixedinLazy), newSuperLazy(mixedinLazy, site, lazyVar))
456475
}
@@ -460,9 +479,7 @@ abstract class Fields extends InfoTransform with ast.TreeDSL with TypingTransfor
460479

461480
setFieldFlags(member, field)
462481

463-
// filter getter's annotations to exclude those only meant for the field
464-
// we must keep them around long enough to see them here, though, when we create the field
465-
field setAnnotations (member.annotations filter AnnotationInfo.mkFilter(FieldTargetClass, defaultRetention = true))
482+
propagateFieldAnnotations(member, field)
466483

467484
List(cloneAccessor(), field)
468485
} else List(cloneAccessor()) // no field needed (constant-typed getter has constant as its RHS)

src/compiler/scala/tools/nsc/typechecker/Namers.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -902,9 +902,10 @@ trait Namers extends MethodSynthesis {
902902
// Annotations on ValDefs can be targeted towards the following: field, getter, setter, beanGetter, beanSetter, param.
903903
// The defaults are:
904904
// - (`val`-, `var`- or plain) constructor parameter annotations end up on the parameter, not on any other entity.
905-
// - val/var member annotations solely end up on the underlying field, except in traits (@since 2.12),
905+
// - val/var member annotations solely end up on the underlying field, except in traits and for all lazy vals (@since 2.12),
906906
// where there is no field, and the getter thus holds annotations targeting both getter & field.
907-
// As soon as there is a field/getter (in subclasses mixing in the trait), we triage the annotations.
907+
// As soon as there is a field/getter (in subclasses mixing in the trait, or after expanding the lazy val during the fields phase),
908+
// we triage the annotations.
908909
//
909910
// TODO: these defaults can be surprising for annotations not meant for accessors/fields -- should we revisit?
910911
// (In order to have `@foo val X` result in the X getter being annotated with `@foo`, foo needs to be meta-annotated with @getter)
@@ -918,15 +919,17 @@ trait Namers extends MethodSynthesis {
918919
BeanPropertyAnnotationLimitationError(tree)
919920
}
920921

922+
val canTriageAnnotations = isSetter || !fields.getterTreeAnnotationsTargetFieldAndGetter(owner, mods)
923+
921924
def filterAccessorAnnotations: AnnotationInfo => Boolean =
922-
if (isSetter || !owner.isTrait)
925+
if (canTriageAnnotations)
923926
annotationFilter(if (isSetter) SetterTargetClass else GetterTargetClass, defaultRetention = false)
924927
else (ann =>
925928
annotationFilter(FieldTargetClass, defaultRetention = true)(ann) ||
926929
annotationFilter(GetterTargetClass, defaultRetention = true)(ann))
927930

928931
def filterBeanAccessorAnnotations: AnnotationInfo => Boolean =
929-
if (isSetter || !owner.isTrait)
932+
if (canTriageAnnotations)
930933
annotationFilter(if (isSetter) BeanSetterTargetClass else BeanGetterTargetClass, defaultRetention = false)
931934
else (ann =>
932935
annotationFilter(FieldTargetClass, defaultRetention = true)(ann) ||

src/compiler/scala/tools/nsc/typechecker/Typers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2027,7 +2027,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
20272027

20282028
// allow trait accessors: it's the only vehicle we have to hang on to annotations that must be passed down to
20292029
// the field that's mixed into a subclass
2030-
if (sym.hasAnnotation(definitions.VolatileAttr) && !((sym hasFlag MUTABLE) || (sym hasFlag ACCESSOR) && sym.owner.isTrait))
2030+
if (sym.hasAnnotation(definitions.VolatileAttr) && !((sym hasFlag MUTABLE | LAZY) || (sym hasFlag ACCESSOR) && sym.owner.isTrait))
20312031
VolatileValueError(vdef)
20322032

20332033
val rhs1 =

test/files/run/junitForwarders/C_1.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ object Test extends App {
1010
assert(s == e, s"found: $s\nexpected: $e")
1111
}
1212
check(classOf[C], "foo - @org.junit.Test()")
13-
// TODO scala-dev#213: should `foo$` really carry the @Test annotation?
14-
check(classOf[T], "$init$ - ;foo - @org.junit.Test();foo$ - @org.junit.Test()")
13+
// scala/scala-dev#213, scala/scala#5570: `foo$` should not have the @Test annotation
14+
check(classOf[T], "$init$ - ;foo - @org.junit.Test();foo$ - ")
1515
}

test/files/run/t10075.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class NotSerializable
2+
3+
trait SerializableActually {
4+
@transient
5+
lazy val notSerializedTLV: NotSerializable = new NotSerializable
6+
7+
@transient
8+
val notSerializedTL: NotSerializable = new NotSerializable
9+
10+
@transient
11+
var notSerializedTR: NotSerializable = new NotSerializable
12+
}
13+
14+
class SerializableBecauseTransient extends Serializable with SerializableActually {
15+
@transient
16+
lazy val notSerializedLV: NotSerializable = new NotSerializable
17+
18+
@transient
19+
val notSerializedL: NotSerializable = new NotSerializable
20+
21+
@transient
22+
var notSerializedR: NotSerializable = new NotSerializable
23+
}
24+
25+
// Indirectly check that the @transient annotation on `notSerialized` made it to the underyling field in bytecode.
26+
// If it doesn't, `writeObject` will fail to serialize the field `notSerialized`, because `NotSerializable` is not serializable
27+
object Test {
28+
def main(args: Array[String]): Unit = {
29+
val obj = new SerializableBecauseTransient
30+
// must force, since `null` valued field is serialized regardless of its type
31+
val forceTLV = obj.notSerializedTLV
32+
val forceLV = obj.notSerializedLV
33+
new java.io.ObjectOutputStream(new java.io.ByteArrayOutputStream) writeObject obj
34+
}
35+
}

test/files/run/t10075b.check

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
private volatile byte C.bitmap$0
2+
@RetainedAnnotation() private int C.lzyValFieldAnnotation
3+
public int C.lzyValFieldAnnotation()
4+
private int C.lzyValFieldAnnotation$lzycompute()
5+
private int C.lzyValGetterAnnotation
6+
@RetainedAnnotation() public int C.lzyValGetterAnnotation()
7+
private int C.lzyValGetterAnnotation$lzycompute()
8+
@RetainedAnnotation() private final int C.valFieldAnnotation
9+
public int C.valFieldAnnotation()
10+
private final int C.valGetterAnnotation
11+
@RetainedAnnotation() public int C.valGetterAnnotation()
12+
@RetainedAnnotation() private int C.varFieldAnnotation
13+
public int C.varFieldAnnotation()
14+
public void C.varFieldAnnotation_$eq(int)
15+
private int C.varGetterAnnotation
16+
@RetainedAnnotation() public int C.varGetterAnnotation()
17+
public void C.varGetterAnnotation_$eq(int)
18+
private int C.varSetterAnnotation
19+
public int C.varSetterAnnotation()
20+
@RetainedAnnotation() public void C.varSetterAnnotation_$eq(int)
21+
public static void T.$init$(T)
22+
public abstract void T.T$_setter_$valFieldAnnotation_$eq(int)
23+
public abstract void T.T$_setter_$valGetterAnnotation_$eq(int)
24+
public default int T.lzyValFieldAnnotation()
25+
public static int T.lzyValFieldAnnotation$(T)
26+
@RetainedAnnotation() public default int T.lzyValGetterAnnotation()
27+
public static int T.lzyValGetterAnnotation$(T)
28+
@RetainedAnnotation() public default int T.method()
29+
public static int T.method$(T)
30+
public abstract int T.valFieldAnnotation()
31+
@RetainedAnnotation() public abstract int T.valGetterAnnotation()
32+
public abstract int T.varFieldAnnotation()
33+
public abstract void T.varFieldAnnotation_$eq(int)
34+
@RetainedAnnotation() public abstract int T.varGetterAnnotation()
35+
public abstract void T.varGetterAnnotation_$eq(int)
36+
public abstract int T.varSetterAnnotation()
37+
@RetainedAnnotation() public abstract void T.varSetterAnnotation_$eq(int)
38+
public void TMix.T$_setter_$valFieldAnnotation_$eq(int)
39+
public void TMix.T$_setter_$valGetterAnnotation_$eq(int)
40+
private volatile byte TMix.bitmap$0
41+
@RetainedAnnotation() private int TMix.lzyValFieldAnnotation
42+
public int TMix.lzyValFieldAnnotation()
43+
private int TMix.lzyValFieldAnnotation$lzycompute()
44+
private int TMix.lzyValGetterAnnotation
45+
@RetainedAnnotation() public int TMix.lzyValGetterAnnotation()
46+
private int TMix.lzyValGetterAnnotation$lzycompute()
47+
@RetainedAnnotation() public int TMix.method()
48+
@RetainedAnnotation() private final int TMix.valFieldAnnotation
49+
public int TMix.valFieldAnnotation()
50+
private final int TMix.valGetterAnnotation
51+
@RetainedAnnotation() public int TMix.valGetterAnnotation()
52+
@RetainedAnnotation() private int TMix.varFieldAnnotation
53+
public int TMix.varFieldAnnotation()
54+
public void TMix.varFieldAnnotation_$eq(int)
55+
private int TMix.varGetterAnnotation
56+
@RetainedAnnotation() public int TMix.varGetterAnnotation()
57+
public void TMix.varGetterAnnotation_$eq(int)
58+
private int TMix.varSetterAnnotation
59+
public int TMix.varSetterAnnotation()
60+
@RetainedAnnotation() public void TMix.varSetterAnnotation_$eq(int)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import java.lang.annotation.*;
2+
3+
@Retention(RetentionPolicy.RUNTIME)
4+
@interface RetainedAnnotation { }
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
class C {
2+
@(RetainedAnnotation @annotation.meta.field)
3+
lazy val lzyValFieldAnnotation = 42
4+
5+
@(RetainedAnnotation @annotation.meta.getter)
6+
lazy val lzyValGetterAnnotation = 42
7+
8+
@(RetainedAnnotation @annotation.meta.field)
9+
val valFieldAnnotation = 42
10+
11+
@(RetainedAnnotation @annotation.meta.getter)
12+
val valGetterAnnotation = 42
13+
14+
@(RetainedAnnotation @annotation.meta.field)
15+
var varFieldAnnotation = 42
16+
17+
@(RetainedAnnotation @annotation.meta.getter)
18+
var varGetterAnnotation = 42
19+
20+
@(RetainedAnnotation @annotation.meta.setter)
21+
var varSetterAnnotation = 42
22+
}
23+
24+
trait T {
25+
@(RetainedAnnotation @annotation.meta.field)
26+
lazy val lzyValFieldAnnotation = 42
27+
28+
@(RetainedAnnotation @annotation.meta.getter)
29+
lazy val lzyValGetterAnnotation = 42
30+
31+
@(RetainedAnnotation @annotation.meta.field)
32+
val valFieldAnnotation = 42
33+
34+
@(RetainedAnnotation @annotation.meta.getter)
35+
val valGetterAnnotation = 42
36+
37+
@(RetainedAnnotation @annotation.meta.field)
38+
var varFieldAnnotation = 42
39+
40+
@(RetainedAnnotation @annotation.meta.getter)
41+
var varGetterAnnotation = 42
42+
43+
@(RetainedAnnotation @annotation.meta.setter)
44+
var varSetterAnnotation = 42
45+
46+
@RetainedAnnotation
47+
def method = 42
48+
}
49+
class TMix extends T
50+
51+
object Test extends App {
52+
(List(classOf[C], classOf[T], classOf[TMix]).
53+
flatMap(cls => cls.getDeclaredFields ++ cls.getDeclaredMethods)).
54+
sortBy(x => (x.getDeclaringClass.getName, x.getName, x.toString)).
55+
foreach(x => println(x.getAnnotations.toList.mkString(" ") + " " + x))
56+
}

0 commit comments

Comments
 (0)