Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SuspendingLambda #1258

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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<String>) {
callLambda { // assertFullyCovered()
nop() // assertFullyCovered()
} // assertFullyCovered()
}
}
Original file line number Diff line number Diff line change
@@ -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);

/**
* <code>
* class SuspendingLambda {
* private fun foo(suspendingLambda: suspend () -> Unit) {}
* fun bar() {
* foo {}
* }
* }
* </code> 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", "<init>",
"(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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -129,6 +130,39 @@ final void nextIsSwitch() {
}
}

/**
* Moves {@link #cursor} to next instruction if it is <code>LDC</code> with
* the given <code>cst</code>, otherwise sets it to <code>null</code>.
*/
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 <code>LDC</code> with
* <code>cst</code> that starts with <code>cstPrefix</code>, otherwise sets
* it to <code>null</code>.
*/
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 <code>null</code>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", "<init>",
"(Ljava/lang/String;)V");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", "<init>",
"(Ljava/lang/String;)V");
Expand Down
Original file line number Diff line number Diff line change
@@ -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", "<init>",
"(Ljava/lang/String;)V");
nextIs(Opcodes.ATHROW);

if (cursor == null) {
return;
}
output.ignore(switchInsnNode, switchInsnNode);
output.ignore(startOfThrowBlock, cursor);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<init>",
"(Ljava/lang/String;)V");
nextIs(Opcodes.ATHROW);
Expand Down