:wave:, I came across an unexpected case where an ...
# compiler
t
👋, I came across an unexpected case where an annotation disappeared in the FIR. This is a simplified example:
Copy code
annotation class SomeAnn
fun foo(example: MutableList<String>) {
    @SomeAnn
    example += "x"
}
The annotation appears to be fine if it is in line with the statement:
Copy code
annotation class SomeAnn
fun foo(example: MutableList<String>) {
    @SomeAnn example += "x"
}
Is anyone familiar with why this might occur?
m
I get the following diagnostic:
Copy code
annotation class SomeAnn

fun foo(example: MutableList<String>) {
    <!WRONG_ANNOTATION_TARGET!>@SomeAnn<!> example += "x"
}
To make it work, you have to write:
Copy code
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.EXPRESSION)
annotation class SomeAnn

fun foo(example: MutableList<String>) {
    @SomeAnn example += "x"
}
But the FIR is actually pretty different. If the annotation is above your expression, it has nothing to do with it.
Copy code
@R|kotlin/annotation/Retention|(value = Q|kotlin/annotation/AnnotationRetention|.R|kotlin/annotation/AnnotationRetention.SOURCE|) @R|kotlin/annotation/Target|(allowedTargets = vararg(Q|kotlin/annotation/AnnotationTarget|.R|kotlin/annotation/AnnotationTarget.LOCAL_VARIABLE|, Q|kotlin/annotation/AnnotationTarget|.R|kotlin/annotation/AnnotationTarget.FIELD|, Q|kotlin/annotation/AnnotationTarget|.R|kotlin/annotation/AnnotationTarget.EXPRESSION|)) public final annotation class SomeAnn : R|kotlin/Annotation| {
    public constructor(): R|SomeAnn| {
        super<R|kotlin/Any|>()
    }

}
public final fun foo(example: R|kotlin/collections/MutableList<kotlin/String>|): R|kotlin/Unit| {
    R|<local>/example|.R|kotlin/collections/plusAssign|<R|kotlin/String|>(String(x))
}
Whereas when it is in the same line:
Copy code
@R|kotlin/annotation/Retention|(value = Q|kotlin/annotation/AnnotationRetention|.R|kotlin/annotation/AnnotationRetention.SOURCE|) @R|kotlin/annotation/Target|(allowedTargets = vararg(Q|kotlin/annotation/AnnotationTarget|.R|kotlin/annotation/AnnotationTarget.EXPRESSION|)) public final annotation class SomeAnn : R|kotlin/Annotation| {
    public constructor(): R|SomeAnn| {
        super<R|kotlin/Any|>()
    }

}
public final fun foo(example: R|kotlin/collections/MutableList<kotlin/String>|): R|kotlin/Unit| {
    @R|SomeAnn|() R|<local>/example|.R|kotlin/collections/plusAssign|<R|kotlin/String|>(String(x))
}
The corresponding grammar definition is:
Copy code
statement
    : (label | annotation)* ( declaration | assignment | loopStatement | expression)
    ;
https://github.com/Kotlin/kotlin-spec/blob/release/grammar/src/main/antlr/KotlinParser.g4 But since a newline is allowed, it looks like a bug.
Copy code
annotation
    : (singleAnnotation | multiAnnotation) NL*
    ;
Maybe it's because of the non-matching annotation target? But a warning like "useless annotation" would be great.
t
Ah, my apologies. The missing target is a typo — in practice, the annotation does have
EXPRESSION
as a target. The annotation will exist on the FIR on other expressions where a new line exists between the annotation and expression. But seems to disappear in this case.
m
Hey there, I'm a colleague of Merlin. We're currently experimenting with different variations of this, and so far this looks like a bug in the resolution of += assignments to plusAssign. In the naive case where += is interpreted as an assignment with a RHS expression it works, but if there is a custom plusAssign method on the target (like in MutableCollection) the rewritten expression will miss the annotation. Same bug also with -= and minusAssign and their cousins, but not with overloaded minus where the -= is resolved to a = b.minus(c). Likely the rewriting of +=/-=/etc into plusAssign etc doesn't have enough context to look into previous lines.
t
Ah, I see — thank you so much!
m
The compiler interprets
@Ann example += "test"
as
(@Ann example) += "test"
where the explicit receiver (example) is the annotation target, whereas
@Ann\n example += "test"
is interpreted as
@Ann() (example += "test")
and targets the statement. Both variants that annotate the statement will miss the annotation in the FIR. You can find the operator call transformations in
FirExpressionResolveTransformer.transformAssignmentOperatorStatement
. Expressions using
+=
with primitives like
Copy code
var i = 0
@SomeAnn
i += 6
include the annotations of the original
assignmentOperatorStatement
. They are transformed by the following snippet (in the local function
chooseOperator
):
Copy code
buildVariableAssignment {
	source = assignmentOperatorStatement.source
	lValue = assignmentLeftArgument
	rValue = resolvedOperatorCall
	annotations += assignmentOperatorStatement.annotations
}
Calls to
plusAssign
miss the annotations in the transformation process.
Copy code
val assignOperatorCall = generator.createAssignOperatorCall()
val resolvedAssignCall = resolveCandidateForAssignmentOperatorCall {
	assignOperatorCall.transformSingle(this, ResolutionMode.ContextDependent)
}

val assignCallReference = resolvedAssignCall.calleeReference as? FirNamedReferenceWithCandidate
val assignIsSuccessful = assignCallReference?.isError == false

...

fun chooseAssign(): FirStatement {
	callCompleter.completeCall(resolvedAssignCall, ResolutionMode.ContextIndependent)
	dataFlowAnalyzer.exitFunctionCall(resolvedAssignCall, callCompleted = true)
	return resolvedAssignCall
}
I would expected the last code snipped to also include the annotations from the original statements, like it's done in
chooseOperator
.
It's fixed since version 1.9.20-RC (https://youtrack.jetbrains.com/issue/KT-62473)
t
Ah, interesting, thank you for the analysis and info! I’ll make a note to update to
1.9.20
!