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

+ +

Release 0.8.12 (2024/03/31)

New Features