diff --git a/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/KotlinSuspendingLambdaTest.java b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/KotlinSuspendingLambdaTest.java new file mode 100644 index 0000000000..4d7795a5be --- /dev/null +++ b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/KotlinSuspendingLambdaTest.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2009, 2023 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: + * Lukas Rössler - initial implementation + * + *******************************************************************************/ +package org.jacoco.core.test.validation.kotlin; + +import org.jacoco.core.test.validation.ValidationTestBase; +import org.jacoco.core.test.validation.kotlin.targets.KotlinSuspendingLambdaTarget; + +/** + * Test of suspending lambdas. + */ +public class KotlinSuspendingLambdaTest extends ValidationTestBase { + + public KotlinSuspendingLambdaTest() { + super(KotlinSuspendingLambdaTarget.class); + } + +} diff --git a/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinSuspendingLambdaTarget.kt b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinSuspendingLambdaTarget.kt new file mode 100644 index 0000000000..6c7a9344b1 --- /dev/null +++ b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinSuspendingLambdaTarget.kt @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2009, 2023 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: + * Lukas Rössler - initial implementation + * + *******************************************************************************/ +package org.jacoco.core.test.validation.kotlin.targets + +import kotlinx.coroutines.runBlocking +import org.jacoco.core.test.validation.targets.Stubs.nop + +/** + * This test targets suspending lambdas. + */ +object KotlinSuspendingLambdaTarget { + + private fun callLambda(suspendingLambda: suspend () -> Unit) = runBlocking { + suspendingLambda() + } + + @JvmStatic + fun main(args: Array) { + callLambda { // assertFullyCovered() + nop() // assertFullyCovered() + } // assertFullyCovered() + } +} diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinSuspendingLambdaFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinSuspendingLambdaFilterTest.java new file mode 100644 index 0000000000..12fadbc5f0 --- /dev/null +++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinSuspendingLambdaFilterTest.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2009, 2023 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: + * Lukas Rössler - initial implementation + * + *******************************************************************************/ +package org.jacoco.core.internal.analysis.filter; + +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.AbstractInsnNode; +import org.objectweb.asm.tree.MethodNode; + +/** + * Unit tests for {@link KotlinSuspendingLambdaFilter}. + */ +public class KotlinSuspendingLambdaFilterTest extends FilterTestBase { + + private final KotlinSuspendingLambdaFilter filter = new KotlinSuspendingLambdaFilter(); + + private final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "invokeSuspend", "(Ljava/lang/Object;)Ljava/lang/Object", null, + null); + + /** + * + * class SuspendingLambda { + * private fun foo(suspendingLambda: suspend () -> Unit) {} + * fun bar() { + * foo {} + * } + * } + * For this function, an inner class "SuspendingLambda$bar$1" with + * an "invokeSuspend" function is created, the byte code of this function is + * used for this test case. + */ + @Test + public void should_filter() { + context.classAnnotations + .add(KotlinGeneratedFilter.KOTLIN_METADATA_DESC); + Label l0 = new Label(); + Label l1 = new Label(); + Label l2 = new Label(); + Label l3 = new Label(); + m.visitLabel(l0); + m.visitVarInsn(Opcodes.ASTORE, 2); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "SuspendingLambda$bar$1", "label", + "I"); + m.visitTableSwitchInsn(0, 0, l2, l1); + AbstractInsnNode tableSwitchNode = m.instructions.getLast(); + m.visitLabel(l1); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitMethodInsn(Opcodes.INVOKESTATIC, "kotlin/ResultKt", + "throwOnFailure", "(Ljava/lang/Object;)V", false); + m.visitLabel(l3); + m.visitFieldInsn(Opcodes.GETSTATIC, "kotlin/Unit", "INSTANCE", + "Lkotlin/Unit;"); + m.visitInsn(Opcodes.ARETURN); + m.visitLabel(l2); + AbstractInsnNode throwBlockStart = m.instructions.getLast(); + m.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException"); + m.visitInsn(Opcodes.DUP); + m.visitLdcInsn("call to 'resume' before 'invoke' with coroutine"); + m.visitMethodInsn(Opcodes.INVOKESPECIAL, + "java/lang/IllegalStateException", "", + "(Ljava/lang/String;)V", false); + m.visitInsn(Opcodes.ATHROW); + AbstractInsnNode throwBlockEnd = m.instructions.getLast(); + + filter.filter(m, context, output); + + assertIgnored(new Range(tableSwitchNode, tableSwitchNode), + new Range(throwBlockStart, throwBlockEnd)); + } +} diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/AbstractMatcher.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/AbstractMatcher.java index fac15d3ac3..611b3c2cf1 100644 --- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/AbstractMatcher.java +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/AbstractMatcher.java @@ -18,6 +18,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; @@ -129,6 +130,39 @@ final void nextIsSwitch() { } } + /** + * Moves {@link #cursor} to next instruction if it is LDC with + * the given cst, otherwise sets it to null. + */ + final void nextIsLdc(final String cst) { + nextIs(Opcodes.LDC); + if (cursor == null) { + return; + } + if (((LdcInsnNode) cursor).cst.equals(cst)) { + return; + } + cursor = null; + } + + /** + * Moves {@link #cursor} to next instruction if it is LDC with + * cst that starts with cstPrefix, otherwise sets + * it to null. + */ + final void nextIsLdcStartingWith(final String cstPrefix) { + nextIs(Opcodes.LDC); + if (cursor == null) { + return; + } + LdcInsnNode ldcNode = (LdcInsnNode) cursor; + if (ldcNode.cst instanceof String + && ((String) ldcNode.cst).startsWith(cstPrefix)) { + return; + } + cursor = null; + } + /** * Moves {@link #cursor} to next instruction if it has given opcode, * otherwise sets it to null. 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 d8c17ceaaf..845e398ab0 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 KotlinSuspendingLambdaFilter()); } private Filters(final IFilter... filters) { diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinCoroutineFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinCoroutineFilter.java index 977649a4a2..4b4488f186 100644 --- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinCoroutineFilter.java +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinCoroutineFilter.java @@ -152,14 +152,7 @@ private void match(final MethodNode methodNode, cursor = s.dflt; nextIsType(Opcodes.NEW, "java/lang/IllegalStateException"); nextIs(Opcodes.DUP); - nextIs(Opcodes.LDC); - if (cursor == null) { - return; - } - if (!((LdcInsnNode) cursor).cst.equals( - "call to 'resume' before 'invoke' with coroutine")) { - return; - } + nextIsLdc("call to 'resume' before 'invoke' with coroutine"); nextIsInvoke(Opcodes.INVOKESPECIAL, "java/lang/IllegalStateException", "", "(Ljava/lang/String;)V"); diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinDefaultArgumentsFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinDefaultArgumentsFilter.java index f83167c3d7..56803fcb3c 100644 --- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinDefaultArgumentsFilter.java +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinDefaultArgumentsFilter.java @@ -87,13 +87,8 @@ public void match(final MethodNode methodNode, nextIs(Opcodes.IFNULL); nextIsType(Opcodes.NEW, "java/lang/UnsupportedOperationException"); nextIs(Opcodes.DUP); - nextIs(Opcodes.LDC); - if (cursor == null - || !(((LdcInsnNode) cursor).cst instanceof String) - || !(((String) ((LdcInsnNode) cursor).cst).startsWith( - "Super calls with default arguments not supported in this target"))) { - cursor = null; - } + nextIsLdcStartingWith( + "Super calls with default arguments not supported in this target"); nextIsInvoke(Opcodes.INVOKESPECIAL, "java/lang/UnsupportedOperationException", "", "(Ljava/lang/String;)V"); diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinSuspendingLambdaFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinSuspendingLambdaFilter.java new file mode 100644 index 0000000000..da51742a55 --- /dev/null +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinSuspendingLambdaFilter.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2009, 2023 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: + * Lukas Rössler - initial implementation + * + *******************************************************************************/ +package org.jacoco.core.internal.analysis.filter; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TableSwitchInsnNode; + +public class KotlinSuspendingLambdaFilter implements IFilter { + @Override + public void filter(MethodNode methodNode, IFilterContext context, + IFilterOutput output) { + final Matcher matcher = new Matcher(); + for (final AbstractInsnNode i : methodNode.instructions) { + matcher.match(i, output); + } + } + + private static class Matcher extends AbstractMatcher { + public void match(final AbstractInsnNode start, + final IFilterOutput output) { + if (Opcodes.TABLESWITCH != start.getOpcode()) { + return; + } + TableSwitchInsnNode switchInsnNode = (TableSwitchInsnNode) start; + + // follow the default jump to check whether this is our "call to + // 'resume' before 'invoke' with coroutine" IllegalStateException + cursor = switchInsnNode.dflt; + AbstractInsnNode startOfThrowBlock = cursor; + + nextIsType(Opcodes.NEW, "java/lang/IllegalStateException"); + nextIs(Opcodes.DUP); + nextIsLdc("call to 'resume' before 'invoke' with coroutine"); + nextIsInvoke(Opcodes.INVOKESPECIAL, + "java/lang/IllegalStateException", "", + "(Ljava/lang/String;)V"); + nextIs(Opcodes.ATHROW); + + if (cursor == null) { + return; + } + output.ignore(switchInsnNode, switchInsnNode); + output.ignore(startOfThrowBlock, cursor); + } + } +} diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinUnsafeCastOperatorFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinUnsafeCastOperatorFilter.java index 5236110db0..a1764b69bc 100644 --- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinUnsafeCastOperatorFilter.java +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinUnsafeCastOperatorFilter.java @@ -54,15 +54,7 @@ public void match(final String exceptionType, } nextIsType(Opcodes.NEW, exceptionType); nextIs(Opcodes.DUP); - nextIs(Opcodes.LDC); - if (cursor == null) { - return; - } - final LdcInsnNode ldc = (LdcInsnNode) cursor; - if (!(ldc.cst instanceof String && ((String) ldc.cst) - .startsWith("null cannot be cast to non-null type"))) { - return; - } + nextIsLdcStartingWith("null cannot be cast to non-null type"); nextIsInvoke(Opcodes.INVOKESPECIAL, exceptionType, "", "(Ljava/lang/String;)V"); nextIs(Opcodes.ATHROW);