kotlinx.coroutines
kotlinx.coroutines copied to clipboard
JaCoCo reports coverage on invalid line="0" when using flatMapLatest() or combine()
Describe the bug
JaCoCo generates coverage reports for a function invokeSuspend
on (invalid) line="0"
in code that uses flatMapLatest()
or combine()
. This seems likely to be related to the convergence of inline
functions which accept crossinline
and suspend
lambdas. I'm not particularly well-versed in Kotlin bytecode, but at a glance I don't see any obvious issues to the generated invokeSuspend
function. It may also be that the invokeSuspend
function should be marked synthetic to start with, avoiding it appearing in coverage reports at all.
This is a somewhat bizarre issue which may be ultimately the responsibility of the Kotlin compiler or JaCoCo. I'm submitting it here since I can't reproduce it in non-suspend
inline
functions with crossinline
parameters, so my guess is that it's related to the generating of code by coroutines.
Provide a Reproducer
These simple snippets demonstrate the issue:
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
fun flatMapLatest() {
emptyFlow<Unit>().flatMapLatest { emptyFlow<Unit>() }
}
which produces this in the JaCoCo report:
<class name="FlatMapLatestKt$foo$$inlined$flatMapLatest$1" sourcefilename="Merge.kt">
<method desc="(Ljava/lang/Object;)Ljava/lang/Object;" line="0" name="invokeSuspend">
<counter covered="0" missed="28" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<counter covered="0" missed="28" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</class>
and
import kotlinx.coroutines.flow.combine
fun combine() {
combine<Unit, Unit>(emptyList()) { }
}
which produces:
<class name="CombineKt$bar$$inlined$combine$1$3" sourcefilename="Zip.kt">
<method desc="(Ljava/lang/Object;)Ljava/lang/Object;" line="0" name="invokeSuspend">
<counter covered="0" missed="29" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<counter covered="0" missed="29" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</class>
Both can be found in this sample project: https://github.com/dzirbel/compose-playground/tree/jacoco (repurposed for testing Compose, but without any Compose dependencies in this branch) and which can reproduce the issue by running ./gradlew jacocoTestReport
and inspecting build/reports/jacoco/test/jacocoTestReport.xml
. For reference, these are the contents (with some XML formatting):
Full JaCoCo report
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE report
PUBLIC '-//JACOCO//DTD Report 1.1//EN'
'report.dtd'>
<report name="compose-playground">
<sessioninfo dump="1696668887764" id="stella-11a0ee1d" start="1696668887356"/>
<package name="">
<class name="CombineKt" sourcefilename="Combine.kt">
<method desc="()V" line="5" name="bar">
<counter covered="0" missed="6" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<counter covered="0" missed="6" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</class>
<class name="CombineKt$bar$$inlined$combine$1$3" sourcefilename="Zip.kt">
<method desc="(Ljava/lang/Object;)Ljava/lang/Object;" line="0" name="invokeSuspend">
<counter covered="0" missed="29" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<counter covered="0" missed="29" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</class>
<class name="FlatMapLatestKt$foo$$inlined$flatMapLatest$1" sourcefilename="Merge.kt">
<method desc="(Ljava/lang/Object;)Ljava/lang/Object;" line="0" name="invokeSuspend">
<counter covered="0" missed="28" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<counter covered="0" missed="28" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</class>
<class name="CombineKt$bar$$inlined$combine$1$2" sourcefilename="Zip.kt">
<method desc="()[Ljava/lang/Object;" line="291" name="invoke">
<counter covered="0" missed="5" type="INSTRUCTION"/>
<counter covered="0" missed="1" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<counter covered="0" missed="5" type="INSTRUCTION"/>
<counter covered="0" missed="1" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</class>
<class name="CombineKt$bar$$inlined$combine$1" sourcefilename="SafeCollector.common.kt">
<method desc="([Lkotlinx/coroutines/flow/Flow;)V" line="107" name="<init>">
<counter covered="0" missed="6" type="INSTRUCTION"/>
<counter covered="0" missed="1" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<method desc="(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;" line="109" name="collect">
<counter covered="0" missed="8" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<counter covered="0" missed="14" type="INSTRUCTION"/>
<counter covered="0" missed="3" type="LINE"/>
<counter covered="0" missed="2" type="COMPLEXITY"/>
<counter covered="0" missed="2" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</class>
<class name="FlatMapLatestKt" sourcefilename="FlatMapLatest.kt">
<method desc="()V" line="5" name="foo">
<counter covered="0" missed="5" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
</method>
<counter covered="0" missed="5" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</class>
<sourcefile name="FlatMapLatest.kt">
<line cb="0" ci="0" mb="0" mi="4" nr="5"/>
<line cb="0" ci="0" mb="0" mi="1" nr="6"/>
<counter covered="0" missed="5" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</sourcefile>
<sourcefile name="Zip.kt">
<line cb="0" ci="0" mb="0" mi="8" nr="0"/>
<line cb="0" ci="0" mb="0" mi="5" nr="291"/>
<line cb="0" ci="0" mb="0" mi="21" nr="292"/>
<counter covered="0" missed="34" type="INSTRUCTION"/>
<counter covered="0" missed="3" type="LINE"/>
<counter covered="0" missed="2" type="COMPLEXITY"/>
<counter covered="0" missed="2" type="METHOD"/>
<counter covered="0" missed="2" type="CLASS"/>
</sourcefile>
<sourcefile name="SafeCollector.common.kt">
<line cb="0" ci="0" mb="0" mi="3" nr="107"/>
<line cb="0" ci="0" mb="0" mi="7" nr="109"/>
<line cb="0" ci="0" mb="0" mi="1" nr="110"/>
<counter covered="0" missed="14" type="INSTRUCTION"/>
<counter covered="0" missed="3" type="LINE"/>
<counter covered="0" missed="2" type="COMPLEXITY"/>
<counter covered="0" missed="2" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</sourcefile>
<sourcefile name="Combine.kt">
<line cb="0" ci="0" mb="0" mi="5" nr="5"/>
<line cb="0" ci="0" mb="0" mi="1" nr="6"/>
<counter covered="0" missed="6" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</sourcefile>
<sourcefile name="Merge.kt">
<line cb="0" ci="0" mb="0" mi="7" nr="0"/>
<line cb="0" ci="0" mb="0" mi="21" nr="193"/>
<counter covered="0" missed="28" type="INSTRUCTION"/>
<counter covered="0" missed="2" type="LINE"/>
<counter covered="0" missed="1" type="COMPLEXITY"/>
<counter covered="0" missed="1" type="METHOD"/>
<counter covered="0" missed="1" type="CLASS"/>
</sourcefile>
<counter covered="0" missed="87" type="INSTRUCTION"/>
<counter covered="0" missed="12" type="LINE"/>
<counter covered="0" missed="7" type="COMPLEXITY"/>
<counter covered="0" missed="7" type="METHOD"/>
<counter covered="0" missed="6" type="CLASS"/>
</package>
<counter covered="0" missed="87" type="INSTRUCTION"/>
<counter covered="0" missed="12" type="LINE"/>
<counter covered="0" missed="7" type="COMPLEXITY"/>
<counter covered="0" missed="7" type="METHOD"/>
<counter covered="0" missed="6" type="CLASS"/>
</report>
Context
This issue has caused errors in Codecov's processing of JaCoCo reports in my project; see https://github.com/codecov/feedback/issues/72. I can't say for sure whether line="0"
is a valid JaCoCo report entry, but if it breaks their report parsing it may cause issues with others as well.
I only began seeing this issue recently when upgrading my version of Compose Multiplatform (from 1.4.3 to 1.5.0), which may have bumped the version of Kotlin or Coroutines being pulled into my project. However, I've reproduced it now on versions dating back for a fair while; it appears to be present in every configuration I've tested:
- Kotlin 1.9.10, Coroutines 1.7.3, JaCoCo 0.8.10
- Kotlin 1.9.10, Coroutines 1.6.4, JaCoCo 0.8.10
- Kotlin 1.9.10, Coroutines 1.5.2, JaCoCo 0.8.10
- Kotlin 1.8.22, Coroutines 1.5.2, JaCoCo 0.8.10
- Kotlin 1.8.22, Coroutines 1.4.3, JaCoCo 0.8.10
- Kotlin 1.8.22, Coroutines 1.4.3, JaCoCo 0.8.9
Update: after further testing, I can no longer reproduce the issue when using JaCoCo 0.8.8 (but can with 0.8.9 and 0.8.10). I'll investigate more tomorrow, but if that finding holds then it's likely to be an issue specific to JaCoCo rather than coroutines; apologies for the noise if so.
Update: after further testing, I can no longer reproduce the issue when using JaCoCo 0.8.8 (but can with 0.8.9 and 0.8.10). I'll investigate more tomorrow, but if that finding holds then it's likely to be an issue specific to JaCoCo rather than coroutines; apologies for the noise if so.
The conclusion from this thread is that JaCoCo 0.8.9+ more accurately reflects incorrectly emitted line numbers of zero from the Kotlin compiler, so previous versions were masking this issue but it's ultimately the responsibility of the Kotlin compiler to avoid marking code at line 0. Given the issue is specifically for invokeSuspend
, I suspect it is the responsibility of Coroutines for this case.
@dzirbel Thanks for your effort! Do you know if there is already a ticket opened on Jetbrain's Kotlin bug tracker about this problem?
@dzirbel Thanks for your effort! Do you know if there is already a ticket opened on Jetbrain's Kotlin bug tracker about this problem?
No, not as far as I know.
incorrectly emitted line numbers of zero from the Kotlin compiler
Please explain why zero is an incorrect number. Kotlin can't emit the actual line number because, for synthetic functions, there isn't one, and looking at the spec for the corresponding part of the class file format (https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-4.html#jvms-4.7.12), I don't see any mentions of zero being prohibited somehow. All it says is,
The value of the line_number item gives the corresponding line number in the original source file.
I don't understand why Kotlin should change its behavior to work around JaCoCo not supporting the full range of possible values in the given fields.
to work around JaCoCo
I think the general problem affects other things too. For example, the line numbers of Exception stack traces are also off in certain scenarios.
Kotlin can't emit the actual line number because, for synthetic functions, there isn't one
I'd argue that you can identify the line number in the original source file. It would make sense of line_number to point to the line where the synthetic function originates from. Isn't this how it works for most other language features, too? Such as the synthetic accessors of val
s?
I understand it's not an easy thing to solve, but solving it instead of saying "not my problem" would actually be an improvement.
Let me rephrase this. There are three options:
- Kotlin emits informative line numbers in all cases.
- Kotlin continues emitting 0.
- Kotlin emits some other fictitious line numbers.
Option 1 would be the best, of course, and there are many discussions and requests asking for this, with a thorough analysis of how difficult this is and the inherent limitations. Just Google "Kotlin line numbers," and you'll see many results. No one is trying to sweep it under the rug as "not our problem," as you phrased it.
Yet even if option 1 eventually comes, it's still not going to be a fast process. If Kotlin really is non-compliant and must never emit 0 as a line number, we have to take option 3 today as a temporary measure. So, the question is, must we? Why? If we are fully compliant, but another tool isn't, why should we change the code to accommodate another tool's bug?
Pushing it towards the compiler: https://youtrack.jetbrains.com/issue/KT-69283/Incorrect-synthetic-line-numbers-when-inlining-suspend-funs