diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinComposeFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinComposeFilterTest.java
new file mode 100644
index 000000000..a6564f15c
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinComposeFilterTest.java
@@ -0,0 +1,255 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2024 Mountainminds GmbH & Co. KG and Contributors
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.internal.analysis.filter;
+
+import static org.objectweb.asm.Opcodes.*;
+
+import org.jacoco.core.internal.instr.InstrSupport;
+import org.junit.Test;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.MethodNode;
+
+/**
+ * Unit tests for {@link KotlinComposeFilter}.
+ */
+public class KotlinComposeFilterTest extends FilterTestBase {
+
+ private final IFilter filter = new KotlinComposeFilter();
+
+ /**
+ *
+ * @androidx.compose.runtime.Composable
+ * fun example(x: Int) {
+ * if (x < 0)
+ * return
+ * example(x - 1)
+ * }
+ *
+ *
+ * transformed by Compose
+ * Kotlin compiler plugin version 2.0.0 into
+ *
+ *
+ * fun example(x: Int, $composer: Composer?, $changed: Int) {
+ * $composer = $composer.startRestartGroup(...)
+ * sourceInformation($composer, ...)
+ * val $dirty = $changed
+ * if ($changed and 0b0110 == 0) {
+ * $dirty = $dirty or if ($composer.changed(x)) 0b0100 else 0b0010
+ * }
+ * if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) {
+ * if (isTraceInProgress()) {
+ * traceEventStart(...)
+ * }
+ *
+ * if (x < 0) {
+ * if (isTraceInProgress()) {
+ * traceEventEnd()
+ * }
+ * $composer.endRestartGroup()?.updateScope { ... }
+ * return
+ * }
+ *
+ * example(x, $composer, 0)
+ *
+ * if (isTraceInProgress()) {
+ * traceEventEnd()
+ * }
+ * } else {
+ * $composer.skipToGroupEnd()
+ * }
+ * $composer.endRestartGroup()?.updateScope { ... }
+ * }
+ *
+ */
+ @Test
+ public void should_filter() {
+ context.classAnnotations
+ .add(KotlinGeneratedFilter.KOTLIN_METADATA_DESC);
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "f", "(Landroidx/compose/runtime/Composer;I)V", null, null);
+ m.visitAnnotation("Landroidx/compose/runtime/Composable;", false);
+
+ final Range range1 = new Range();
+ final Range range2 = new Range();
+ final Range range3 = new Range();
+ final Range range4 = new Range();
+ final Range range5 = new Range();
+ final Range range6 = new Range();
+ final Range range7 = new Range();
+
+ m.visitVarInsn(ALOAD, 1);
+ range1.fromInclusive = m.instructions.getLast();
+ m.visitLdcInsn(Integer.valueOf(-974630231));
+ m.visitMethodInsn(INVOKEINTERFACE, "androidx/compose/runtime/Composer",
+ "startRestartGroup", "(I)Landroidx/compose/runtime/Composer;",
+ true);
+ m.visitVarInsn(ASTORE, 1);
+ m.visitVarInsn(ILOAD, 2);
+ m.visitVarInsn(ISTORE, 3);
+ Label label1 = new Label();
+ m.visitLabel(label1);
+ m.visitVarInsn(ILOAD, 2);
+ m.visitIntInsn(BIPUSH, 14);
+ m.visitInsn(IAND);
+ Label label2 = new Label();
+ m.visitJumpInsn(IFNE, label2);
+ m.visitVarInsn(ILOAD, 3);
+ m.visitVarInsn(ALOAD, 1);
+ m.visitVarInsn(ILOAD, 0);
+ m.visitMethodInsn(INVOKEINTERFACE, "androidx/compose/runtime/Composer",
+ "changed", "(I)Z", true);
+ Label label3 = new Label();
+ m.visitJumpInsn(IFEQ, label3);
+ m.visitInsn(ICONST_4);
+ Label label4 = new Label();
+ m.visitJumpInsn(GOTO, label4);
+ m.visitLabel(label3);
+ m.visitInsn(ICONST_2);
+ m.visitLabel(label4);
+ m.visitInsn(IOR);
+ m.visitVarInsn(ISTORE, 3);
+ m.visitLabel(label2);
+ m.visitVarInsn(ILOAD, 3);
+ m.visitIntInsn(BIPUSH, 11);
+ m.visitInsn(IAND);
+ m.visitInsn(ICONST_2);
+ Label label5 = new Label();
+ m.visitJumpInsn(IF_ICMPNE, label5);
+ m.visitVarInsn(ALOAD, 1);
+ m.visitMethodInsn(INVOKEINTERFACE, "androidx/compose/runtime/Composer",
+ "getSkipping", "()Z", true);
+ Label label6 = new Label();
+ m.visitJumpInsn(IFNE, label6);
+ range1.toInclusive = m.instructions.getLast();
+
+ m.visitLabel(label5);
+ m.visitMethodInsn(INVOKESTATIC, "androidx/compose/runtime/ComposerKt",
+ "isTraceInProgress", "()Z", false);
+ Label label7 = new Label();
+ m.visitJumpInsn(IFEQ, label7);
+ range3.fromInclusive = m.instructions.getLast();
+ m.visitLdcInsn(Integer.valueOf(-974630231));
+ m.visitVarInsn(ILOAD, 3);
+ m.visitInsn(ICONST_M1);
+ m.visitLdcInsn("org.example.example (Example.kt:19)");
+ m.visitMethodInsn(INVOKESTATIC, "androidx/compose/runtime/ComposerKt",
+ "traceEventStart", "(IIILjava/lang/String;)V", false);
+ m.visitLabel(label7);
+ range3.toInclusive = m.instructions.getLast();
+
+ m.visitVarInsn(ILOAD, 0);
+ Label label8 = new Label();
+ m.visitJumpInsn(IFGE, label8);
+ m.visitMethodInsn(INVOKESTATIC, "androidx/compose/runtime/ComposerKt",
+ "isTraceInProgress", "()Z", false);
+ Label label9 = new Label();
+ m.visitJumpInsn(IFEQ, label9);
+ range4.fromInclusive = m.instructions.getLast();
+ m.visitMethodInsn(INVOKESTATIC, "androidx/compose/runtime/ComposerKt",
+ "traceEventEnd", "()V", false);
+ m.visitLabel(label9);
+ range4.toInclusive = m.instructions.getLast();
+
+ m.visitVarInsn(ALOAD, 1);
+ m.visitMethodInsn(INVOKEINTERFACE, "androidx/compose/runtime/Composer",
+ "endRestartGroup",
+ "()Landroidx/compose/runtime/ScopeUpdateScope;", true);
+ m.visitInsn(DUP);
+ Label label10 = new Label();
+ m.visitJumpInsn(IFNULL, label10);
+ range5.fromInclusive = m.instructions.getLast();
+ m.visitTypeInsn(NEW, "org/example/ExampleKt$example$4");
+ m.visitInsn(DUP);
+ m.visitVarInsn(ILOAD, 0);
+ m.visitVarInsn(ILOAD, 2);
+ m.visitMethodInsn(INVOKESPECIAL, "org/example/ExampleKt$example$4",
+ "", "(II)V", false);
+ m.visitTypeInsn(CHECKCAST, "kotlin/jvm/functions/Function2");
+ m.visitMethodInsn(INVOKEINTERFACE,
+ "androidx/compose/runtime/ScopeUpdateScope", "updateScope",
+ "(Lkotlin/jvm/functions/Function2;)V", true);
+ Label label11 = new Label();
+ m.visitJumpInsn(GOTO, label11);
+ m.visitLabel(label10);
+ m.visitFrame(Opcodes.F_SAME1, 0, null, 1,
+ new Object[] { "androidx/compose/runtime/ScopeUpdateScope" });
+ m.visitInsn(POP);
+ range5.toInclusive = m.instructions.getLast();
+
+ m.visitLabel(label11);
+ m.visitInsn(Opcodes.RETURN);
+
+ m.visitLabel(label8);
+ m.visitVarInsn(ILOAD, 0);
+ m.visitInsn(ICONST_1);
+ m.visitInsn(ISUB);
+ m.visitVarInsn(ALOAD, 1);
+ m.visitInsn(ICONST_0);
+ m.visitMethodInsn(INVOKESTATIC, "org/example/ExampleKt", "example",
+ "(ILandroidx/compose/runtime/Composer;I)V", false);
+
+ m.visitMethodInsn(INVOKESTATIC, "androidx/compose/runtime/ComposerKt",
+ "isTraceInProgress", "()Z", false);
+ Label label12 = new Label();
+ m.visitJumpInsn(IFEQ, label12);
+ range6.fromInclusive = m.instructions.getLast();
+ m.visitMethodInsn(INVOKESTATIC, "androidx/compose/runtime/ComposerKt",
+ "traceEventEnd", "()V", false);
+ m.visitJumpInsn(GOTO, label12);
+ m.visitLabel(label6);
+ range2.fromInclusive = m.instructions.getLast();
+ m.visitVarInsn(ALOAD, 1);
+ m.visitMethodInsn(INVOKEINTERFACE, "androidx/compose/runtime/Composer",
+ "skipToGroupEnd", "()V", true);
+ m.visitLabel(label12);
+ range6.toInclusive = m.instructions.getLast();
+
+ m.visitVarInsn(ALOAD, 1);
+ m.visitMethodInsn(INVOKEINTERFACE, "androidx/compose/runtime/Composer",
+ "endRestartGroup",
+ "()Landroidx/compose/runtime/ScopeUpdateScope;", true);
+ m.visitInsn(DUP);
+ Label label13 = new Label();
+ m.visitJumpInsn(IFNULL, label13);
+ range7.fromInclusive = m.instructions.getLast();
+ m.visitTypeInsn(NEW, "org/example/ExampleKt$example$5");
+ m.visitInsn(DUP);
+ m.visitVarInsn(ILOAD, 0);
+ m.visitVarInsn(ILOAD, 2);
+ m.visitMethodInsn(INVOKESPECIAL, "org/example/ExampleKt$example$5",
+ "", "(II)V", false);
+ m.visitTypeInsn(CHECKCAST, "kotlin/jvm/functions/Function2");
+ m.visitMethodInsn(INVOKEINTERFACE,
+ "androidx/compose/runtime/ScopeUpdateScope", "updateScope",
+ "(Lkotlin/jvm/functions/Function2;)V", true);
+ Label label14 = new Label();
+ m.visitJumpInsn(GOTO, label14);
+ m.visitLabel(label13);
+ m.visitFrame(Opcodes.F_SAME1, 0, null, 1,
+ new Object[] { "androidx/compose/runtime/ScopeUpdateScope" });
+ m.visitInsn(POP);
+ range7.toInclusive = m.instructions.getLast();
+
+ m.visitLabel(label14);
+ m.visitInsn(RETURN);
+ range2.toInclusive = m.instructions.getLast();
+
+ filter.filter(m, context, output);
+
+ assertIgnored(range1, range2, range3, range4, range5, range6, range7);
+ }
+
+}
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java
index 70bb751ec..07268c412 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java
@@ -48,7 +48,8 @@ public static IFilter all() {
new KotlinUnsafeCastOperatorFilter(),
new KotlinNotNullOperatorFilter(),
new KotlinDefaultArgumentsFilter(), new KotlinInlineFilter(),
- new KotlinCoroutineFilter(), new KotlinDefaultMethodsFilter());
+ new KotlinCoroutineFilter(), new KotlinDefaultMethodsFilter(),
+ new KotlinComposeFilter());
}
private Filters(final IFilter... filters) {
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinComposeFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinComposeFilter.java
new file mode 100644
index 000000000..48a1fbd56
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinComposeFilter.java
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2024 Mountainminds GmbH & Co. KG and Contributors
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.internal.analysis.filter;
+
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.AnnotationNode;
+import org.objectweb.asm.tree.JumpInsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+/**
+ * Filters bytecode generated by Compose Kotlin compiler plugin.
+ */
+final class KotlinComposeFilter implements IFilter {
+
+ public void filter(final MethodNode methodNode,
+ final IFilterContext context, final IFilterOutput output) {
+ if (!KotlinGeneratedFilter.isKotlinClass(context)) {
+ return;
+ }
+ if (!isComposable(methodNode)) {
+ return;
+ }
+ for (final AbstractInsnNode i : methodNode.instructions) {
+ if (i.getType() != AbstractInsnNode.METHOD_INSN) {
+ continue;
+ }
+ final MethodInsnNode mi = (MethodInsnNode) i;
+ if ("androidx/compose/runtime/Composer".equals(mi.owner)
+ && "getSkipping".equals(mi.name) && "()Z".equals(mi.desc)
+ && mi.getNext().getOpcode() == Opcodes.IFNE) {
+ // https://github.com/JetBrains/kotlin/blob/v2.0.0-RC2/plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt#L361-L384
+ final JumpInsnNode ji = (JumpInsnNode) mi.getNext();
+ output.ignore(methodNode.instructions.getFirst(), ji);
+ output.ignore(ji.label, methodNode.instructions.getLast());
+ } else if ("androidx/compose/runtime/Composer".equals(mi.owner)
+ && "endRestartGroup".equals(mi.name)
+ && "()Landroidx/compose/runtime/ScopeUpdateScope;"
+ .equals(mi.desc)
+ && mi.getNext().getOpcode() == Opcodes.DUP
+ && mi.getNext().getNext().getOpcode() == Opcodes.IFNULL) {
+ // https://github.com/JetBrains/kotlin/blob/v2.0.0-RC2/plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt#L430-L450
+ final JumpInsnNode ji = (JumpInsnNode) mi.getNext().getNext();
+ final AbstractInsnNode jumpTarget = AbstractMatcher
+ .skipNonOpcodes(ji.label);
+ if (jumpTarget.getOpcode() == Opcodes.POP) {
+ output.ignore(ji, jumpTarget);
+ }
+ } else if ("androidx/compose/runtime/ComposerKt".equals(mi.owner)
+ && "isTraceInProgress".equals(mi.name)
+ && "()Z".equals(mi.desc)
+ && mi.getNext().getOpcode() == Opcodes.IFEQ) {
+ // https://github.com/JetBrains/kotlin/blob/v2.0.0-RC2/plugins/compose/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt#L2123-L2163
+ final JumpInsnNode ji = (JumpInsnNode) mi.getNext();
+ output.ignore(ji, ji.label);
+ }
+ }
+ }
+
+ private static boolean isComposable(final MethodNode methodNode) {
+ if (methodNode.invisibleAnnotations == null) {
+ return false;
+ }
+ for (final AnnotationNode a : methodNode.invisibleAnnotations) {
+ if ("Landroidx/compose/runtime/Composable;".equals(a.desc)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index 48b2ac196..e5a53ba9a 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -20,6 +20,13 @@ Change History
Snapshot Build @qualified.bundle.version@ (@build.date@)
+New Features
+
+ - Part of bytecode generated by the Kotlin Compose compiler plugin is
+ filtered out during generation of report
+ (GitHub #1616).
+
+
Release 0.8.12 (2024/03/31)
New Features