Skip to content

Commit 7604809

Browse files
authored
Improvements to annotation use-site targets on properties (#401)
1 parent 327ddca commit 7604809

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed

Diff for: proposals/annotation-target-in-properties.md

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Improvements to annotation use-site targets on properties
2+
3+
* **Type**: Design proposal
4+
* **Author**: Alejandro Serrano
5+
* **Contributors**: Alexander Udalov, Ilmir Usmanov, Mikhail Zarechenskii
6+
* **Discussion**: [KEEP-402](https://github.com/Kotlin/KEEP/issues/402)
7+
* **Status**: Experimental in Kotlin 2.2.0
8+
* **Related YouTrack issue**: [KT-19289](https://youtrack.jetbrains.com/issue/KT-19289/Hibernate-Validator-Annotations-Dropped), [KTIJ-31300](https://youtrack.jetbrains.com/issue/KTIJ-31300/Draft-Validation-Annotation-Email-Doesnt-Work-in-Kotlin-Constructor)
9+
10+
## Abstract
11+
12+
Several kinds of property declarations in Kotlin define more than one use-site target for annotations. If an annotation is applied, one of those targets is chosen using the [defaulting rule](https://kotlinlang.org/docs/annotations.html#java-annotations). This KEEP proposes to change that behavior to choose several targets instead, and introduce a new `all` meta-target.
13+
14+
## Table of contents
15+
16+
* [Abstract](#abstract)
17+
* [Table of contents](#table-of-contents)
18+
* [Motivation](#motivation)
19+
* [Potential misunderstandings](#potential-misunderstandings)
20+
* [Alignment with Java](#alignment-with-java)
21+
* [Technical details](#technical-details)
22+
* [Compiler flags](#compiler-flags)
23+
* [Migration](#migration)
24+
* [Exceptions](#exceptions)
25+
* [Examples](#examples)
26+
* [Impact](#impact)
27+
* [Other design choices](#other-design-choices)
28+
29+
## Motivation
30+
31+
Kotlin offers succint syntax to define a constructor parameter, a property, and its underlying backing field, all in one go.
32+
33+
```kotlin
34+
class Person(val name: String, val age: Int)
35+
```
36+
37+
The other side of the coin is that whenever an annotation is given in that position, there is some ambiguity to which of those three elements the annotation should be applied. Kotlin provides a way to be explicit, namely [use-site targets](https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets).
38+
39+
> [!NOTE]
40+
> Throughout this proposal we shall use annotations from [Jakarta Bean Validation]( https://beanvalidation.org/) in the examples.
41+
42+
```kotlin
43+
class Person(@param:NotBlank val name: String, @field:PositiveOrZero val age: Int)
44+
```
45+
46+
Declaring a use-site target is not mandatory, though. In case none is given, the **defaulting rule** applies:
47+
48+
> If you don't specify a use-site target, the target is chosen according to the `@Target` annotation of the annotation being used. If there are multiple applicable targets, the first applicable target from the following list is used: `param`, `property`, `field`.
49+
50+
We argue below that this defaulting rule should be changed. Instead of choosing a single target, the annotation should be applied to _both_ the constructor parameter and the property or field. Furthermore, sometimes it is also important to apply the same annotation to getters and setters, a scenario that currently requires duplication.
51+
52+
### Potential misunderstandings
53+
54+
The main issue with the current defaulting rule is that developers are often surprised when an annotation is _not_ applied to the target they intended. Consider the following example, a variation of the previous one in which the properties are mutable.
55+
56+
```kotlin
57+
class Person(@NotBlank var name: String, @PositiveOrZero var age: Int)
58+
```
59+
60+
Following the defaulting rule, the validation annotations `@NotBlank` and `@PositiveOrZero` are applied _solely_ to the constructor parameter. In practical terms, this means that their values are validated only when first creating the instance, but not on later modifications of the properties. This does not seem like the intended behavior, and may lead to broken invariants.
61+
62+
### Alignment with Java
63+
64+
Java provides [records](https://openjdk.org/jeps/395) since version 16 (experimental since 14). Records are syntactically very close to definition of properties in primary constructors, and they also expand to several declarations in the underlying JVM platform.
65+
66+
```java
67+
record Person(String name, int age) { }
68+
```
69+
70+
The rules for [annotations on record components](https://openjdk.org/jeps/395#Annotations-on-record-components) go even further than the tryad of `param`, `property`, and `field`; they also apply to the property getter and to the Java-only `RECORD_COMPONENT` target.
71+
72+
Since JVM is one of the main targets for Kotlin, we think alignment with the rest of the players is very important. One reason is to make it easier for developers to work on multi-language projects, without having to remember small quirks per language. On top of that, libraries developed with Java in mind may assume the behavior of records, and they would then fail in a very similar scenario in Kotlin.
73+
74+
For full comparison, Scala also applies a [defaulting rule](https://www.scala-lang.org/api/current/scala/annotation/meta.html) giving preference to parameters:
75+
76+
> By default, annotations on (`val`-, `var`- or plain) constructor parameters end up on the parameter, not on any other entity.
77+
78+
However, they provide a way to create a version of an annotation with a specific target. That way the correct defaulting can be chosen per annotation.
79+
80+
## Technical details
81+
82+
**Param-and-property defaulting rule**: the defaulting rule should read as follows.
83+
84+
> If you don't specify a use-site target, the target is chosen according to the `@Target` annotation of the annotation being used. If there are multiple targets, choose one or more as follows:
85+
>
86+
> - If the constructor parameter target `param` is applicable, use it.
87+
> - If any of the property target `property` or field target `field` is applicable, use the first of those.
88+
>
89+
> It is an error if there are multiple targets and none of `param`, `property` and `field` is applicable.
90+
91+
**New `all` annotation use-site target**: in addition to the existing use-site targets, we define a new meta-target for _properties_. Such an annotation should be propagated, whenever applicable:
92+
93+
- To the parameter constructor (`param`), if the property is defined in the primary constructor,
94+
- To the property itself (`property`),
95+
- To the backing field (`field`), if the property has one,
96+
- To the getter (`get`),
97+
- To the setter parameter (`set_param`), if the property is defined as `var`.
98+
- If the class is annotated with `@JvmRecord`, to the Java-only target `RECORD_COMPONENT`.
99+
100+
The last rule ensures that way the behavior of a `@JvmRecord` with annotations using `all` as use-site target aligns with Java records.
101+
102+
Note that the annotation is **not** propagated to types, and potential extension receivers or context receivers/parameters.
103+
104+
The `all` target may **not** be used with [multiple annotations](https://kotlinlang.org/spec/syntax-and-grammar.html#grammar-rule-annotation). It is unclear what the behavior should be when the multiple annotations have different targets.
105+
106+
```kotlin
107+
@all:[A B] // forbidden, use `@all:A @all:B`
108+
val x: Int = 5
109+
```
110+
111+
The `all` target may **not** be used with [delegated properties](https://kotlinlang.org/spec/declarations.html#delegated-property-declaration). It is unclear whether the annotation should or should not be propagated to the underlying delegate; in other words, whether the `delegate` target should be part of the propagation.
112+
113+
### Compiler flags
114+
115+
The Kotlin compiler shall provide a flag to change the defaulting behavior.
116+
117+
- `-Xannotation-defaulting=first-only` corresponds to the defaulting rule in version 1.9 of the Kotlin specification.
118+
- `-Xannotation-defaulting=param-property` corresponds to the new proposed param-and-property defaulting rule.
119+
120+
#### Migration
121+
122+
The param-and-property defaulting rule should become the new defaulting rule in the language. For an orderly transition between the two worlds, we define an additional compiler flag.
123+
124+
- `-Xannotation-defaulting=first-only-warn` behaves as `first-only`; in addition, it raises a warning whenever the following are true:
125+
- The annotation does _not_ have a explicit use-site target,
126+
- Both the `param` and one of `property` or `field` targets are allowed for the specific element.
127+
128+
If the user wants to keep the `first-only` behavior but _not_ receive any warnings, the workaround is to explicitly write the use-site target. This is also a future-proof way to keep the current behavior.
129+
130+
> [!TIP]
131+
> _Tooling support_: in response to this warning, editors supporting Kotlin are suggested to include actions to make them go away. That may include enabling the proposed flag project-wise, or making the use-site target for an annotation explicit.
132+
133+
In the next version of the Kotlin compiler after this KEEP is approved, the default value of the flag should be `first-only-warn`. After this transitional period, the default value should change to `param-property`.
134+
135+
#### Exceptions
136+
137+
There are two exceptions to the above rule for migration. In these two cases _no_ warning should be issued, even though the target may change between versions.
138+
139+
- Deprecation and suppression annotations, including `@Deprecated` and `@Suppress`.
140+
- Annotations on properties of annotation classes; in this case instanced are created through special reflection support.
141+
142+
### Examples
143+
144+
Consider [`Email` from Jakarta Bean Validation](https://jakarta.ee/specifications/bean-validation/3.0/apidocs/jakarta/validation/constraints/email), whose targets are defined as follows.
145+
146+
```java
147+
@Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE})
148+
public @interface Email { }
149+
```
150+
151+
Those Java targets are mapped to the [corresponding ones in Kotlin](https://kotlinlang.org/spec/annotations.html#annotation-targets). In particular, note that `PROPERTY` is _not_ a targe.
152+
153+
Consider now the following code which uses the annotation in two different places.
154+
155+
```kotlin
156+
data class User(val username: String, /* 1️⃣ */ @Email val email: String) {
157+
/* 2️⃣ */ @Email val secondaryEmail: String? = null
158+
}
159+
```
160+
161+
Before this proposal, in position 1️⃣ the annotation is applied to the constructor parameter only (target `param`). With this proposal, now it is applied to targets `param` and `field`. There is no change in position 2️⃣, the annotation is still applied only to use-site target `field`.
162+
163+
```kotlin
164+
// equivalent to
165+
data class User(val username: String, @param:Email @field:Email val email: String) {
166+
@field:Email val secondaryEmail: String? = null
167+
}
168+
```
169+
170+
If the `Email` annotation is used with the `all` target instead,
171+
172+
```kotlin
173+
data class User(val username: String, /* 1️⃣ */ @all:Email val email: String) {
174+
/* 2️⃣ */ @all:Email val secondaryEmail: String? = null
175+
}
176+
```
177+
178+
Then the annotation is additionally applied as `@get:Email` in the two marked positions. In this case the `get` target comes from "translating" Java's `METHOD` target. If the property was defined as `var`, the additional `set_param` target would also be selected.
179+
180+
This behavior does not only apply to Java annotations. For example, [`IntRange` from `androidx.annotations`](https://developer.android.com/reference/androidx/annotation/IntRange) is defined ["natively" in Kotlin](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:annotation/annotation/src/commonMain/kotlin/androidx/annotation/IntRange.kt?q=file:androidx%2Fannotation%2FIntRange.kt%20class:androidx.annotation.IntRange).
181+
182+
An example in which the `property` target is involved is given by [`JSONName` from `kjson`](https://github.com/pwall567/kjson-annotations/blob/main/src/main/kotlin/io/kjson/annotation/JSONName.kt).
183+
184+
```kotlin
185+
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
186+
annotation class JSONName(val name: String)
187+
```
188+
189+
If we consider again the two positions in which the annotation may appear,
190+
191+
```kotlin
192+
data class User(val username: String, /* 1️⃣ */ @JSONName("mail1") val email: String) {
193+
/* 2️⃣ */ @JSONName("mail2") val secondaryEmail: String? = null
194+
}
195+
```
196+
197+
there is a change in behavior in 1️⃣ -- with this proposal `@JSONName` is applied to both the parameter and the property --, and no change in 2️⃣ -- `property` is still chosen as the use-site target.
198+
199+
```kotlin
200+
data class User(val username: String, @param:JSONName("mail1") @property:JSONName("mail1") val email: String) {
201+
@property:JSONName("mail2") val secondaryEmail: String? = null
202+
}
203+
```
204+
205+
The developer may select the three potential targets by using `@all:JSONName("mail1")` in the definition.
206+
207+
### Impact
208+
209+
To understand the impact of this change, we need to consider whether the annotation was defined in Java or in Kotlin. The reason is that annotations defined in Java may _not_ define `property` as one of their targets. As a consequence, the proposed defaulting rule effectively works as "apply to parameter and field". This is exactly the behavior we want, as described in the _Motivation_ section.
210+
211+
To understand whether the choice between `property` and `field` is required in the rule above, we have consulted open source repositories (for example, [query in GitHub Search](https://github.com/search?q=%40Target%28AnnotationTarget.PROPERTY%2C+AnnotationTarget.FIELD%29+lang%3AKotlin&type=code)). The conclusion is there is an important amount of annotations with both potential targets in the wild, which makes is dangerous to scrape the defaulting between `property` and `field` altogether.
212+
213+
## Other design choices
214+
215+
**Make `all` the default for `@JvmRecord`**: we have considered the possibility of fully aligning Kotlin's behavior with Java's in that case. Although this might be interesting for JVM-only projects, it is very unclear what the behavior should be for Multiplatform projects. Both potential options have strong drawbacks:
216+
217+
- If we make the behavior JVM-only, a different set of targets is chosen depending on the platform. This breaks some expectations around common code, and lacks uniformity.
218+
- If we make the behavior apply to all platforms, it seems quite un-intuitive that an annotation (`@JvmRecord`) affects how all other annotations are applied.
219+
220+
Furthermore, it is still very early to know how popular frameworks will handle Java records. If at a future time Java interoperability begins to suffer, we shall revisit this choice.

0 commit comments

Comments
 (0)