diff --git a/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinControlStructuresTarget.kt b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinControlStructuresTarget.kt index 6e0780c999..0c49741223 100644 --- a/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinControlStructuresTarget.kt +++ b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinControlStructuresTarget.kt @@ -72,7 +72,7 @@ object KotlinControlStructuresTarget { private fun missedForBlock() { - for (j in i2()..i1()) { // assertPartlyCovered(3, 1) + for (j in i2()..i1()) { // assertPartlyCovered() nop() // assertNotCovered() } @@ -80,10 +80,30 @@ object KotlinControlStructuresTarget { private fun executedForBlock() { - for (j in i1()..i2()) { // assertFullyCovered(1, 3) + for (j in i1()..i2()) { // assertFullyCovered() nop() // assertFullyCovered() } + val limit = 10 // assertFullyCovered() + for (j in i1() until limit) { // assertFullyCovered() + nop() // assertFullyCovered() + } + + for (j in limit downTo i1()) { // assertFullyCovered() + nop() // assertFullyCovered() + } + + for (i in 0 until i1()) { // assertFullyCovered() + nop() // assertFullyCovered() + } + + for (i in 0 until i2()) { // assertFullyCovered() + nop() // assertFullyCovered() + } + + for (i in i1() downTo 0) { // assertFullyCovered() + nop() // assertFullyCovered() + } } private fun missedForEachBlock() { diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinForLoopFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinForLoopFilterTest.java new file mode 100644 index 0000000000..a0443d457c --- /dev/null +++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinForLoopFilterTest.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * 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: + * Fabian Mastenbroek - initial API and 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 KotlinForLoopFilter}. + */ +public class KotlinForLoopFilterTest extends FilterTestBase { + + private final KotlinForLoopFilter filter = new KotlinForLoopFilter(); + + /** + *
+	 * class Example {
+	 *   fun example() {
+	 *     for (j in 0 until i1()) {}
+	 *   }
+	 *   private fun i1() = 1
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_5_until() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "example", "()V", null, null); + final Label label1 = new Label(); + final Label label2 = new Label(); + + m.visitInsn(Opcodes.ICONST_0); + m.visitVarInsn(Opcodes.ISTORE, 0); + m.visitMethodInsn(Opcodes.INVOKESTATIC, "ExampleKt", "i1", "()I", + false); + m.visitVarInsn(Opcodes.ISTORE, 1); + m.visitVarInsn(Opcodes.ILOAD, 0); + m.visitVarInsn(Opcodes.ILOAD, 1); + m.visitJumpInsn(Opcodes.IF_ICMPGE, label1); + final AbstractInsnNode ignored1 = m.instructions.getLast(); + m.visitLabel(label2); + m.visitVarInsn(Opcodes.ILOAD, 0); + m.visitVarInsn(Opcodes.ISTORE, 2); + m.visitInsn(Opcodes.IINC); + m.visitVarInsn(Opcodes.ILOAD, 0); + m.visitVarInsn(Opcodes.ILOAD, 1); + m.visitJumpInsn(Opcodes.IF_ICMPLT, label2); + final AbstractInsnNode ignored2 = m.instructions.getLast(); + m.visitLabel(label1); + m.visitInsn(Opcodes.RETURN); + + filter.filter(m, context, output); + assertIgnored(new Range(ignored1, ignored1), + new Range(ignored2, ignored2)); + } + + /** + *
+	 * class Example {
+	 *   fun example() {
+	 *     for (i in i1() downTo 0) {}
+	 *   }
+	 *   private fun i1() = 1
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_5_downTo() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "example", "()V", null, null); + final Label label1 = new Label(); + final Label label2 = new Label(); + + m.visitMethodInsn(Opcodes.INVOKESTATIC, "ExampleKt", "i1", "()I", + false); + m.visitVarInsn(Opcodes.ISTORE, 0); + m.visitInsn(Opcodes.ICONST_0); + m.visitVarInsn(Opcodes.ISTORE, 1); + m.visitVarInsn(Opcodes.ILOAD, 1); + m.visitJumpInsn(Opcodes.IF_ICMPGT, label1); + final AbstractInsnNode ignored1 = m.instructions.getLast(); + m.visitLabel(label2); + m.visitVarInsn(Opcodes.ILOAD, 0); + m.visitVarInsn(Opcodes.ISTORE, 1); + m.visitInsn(Opcodes.IINC); + m.visitInsn(Opcodes.ICONST_0); + m.visitVarInsn(Opcodes.ILOAD, 1); + m.visitJumpInsn(Opcodes.IF_ICMPLE, label2); + final AbstractInsnNode ignored2 = m.instructions.getLast(); + m.visitLabel(label1); + m.visitInsn(Opcodes.RETURN); + + filter.filter(m, context, output); + assertIgnored(new Range(ignored1, ignored1), + new Range(ignored2, ignored2)); + } + + /** + *
+	 * class Example {
+	 *   fun example() {
+	 *     val limit = 10
+	 *     for (j in limit downTo i1()) {}
+	 *   }
+	 *   private fun i1() = 1
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_5_downTo_val() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "example", "()V", null, null); + final Label label1 = new Label(); + final Label label2 = new Label(); + + m.visitVarInsn(Opcodes.BIPUSH, 10); + m.visitVarInsn(Opcodes.ISTORE, 0); + m.visitVarInsn(Opcodes.ILOAD, 0); + m.visitVarInsn(Opcodes.ISTORE, 1); + m.visitMethodInsn(Opcodes.INVOKESTATIC, "ExampleKt", "i1", "()I", + false); + m.visitVarInsn(Opcodes.ISTORE, 2); + m.visitVarInsn(Opcodes.ILOAD, 2); + m.visitVarInsn(Opcodes.ILOAD, 1); + m.visitJumpInsn(Opcodes.IF_ICMPGT, label1); + final AbstractInsnNode ignored1 = m.instructions.getLast(); + m.visitLabel(label2); + m.visitVarInsn(Opcodes.ILOAD, 1); + m.visitVarInsn(Opcodes.ISTORE, 3); + m.visitInsn(Opcodes.IINC); + m.visitVarInsn(Opcodes.ILOAD, 3); + m.visitVarInsn(Opcodes.ILOAD, 2); + m.visitJumpInsn(Opcodes.IF_ICMPNE, label2); + final AbstractInsnNode ignored2 = m.instructions.getLast(); + m.visitLabel(label1); + m.visitInsn(Opcodes.RETURN); + + filter.filter(m, context, output); + assertIgnored(new Range(ignored1, ignored1), + new Range(ignored2, ignored2)); + } + +} 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..36e13bc292 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 @@ -44,7 +44,7 @@ public static IFilter all() { new RecordPatternFilter(), // new AnnotationGeneratedFilter(), new KotlinGeneratedFilter(), new KotlinLateinitFilter(), new KotlinWhenFilter(), - new KotlinWhenStringFilter(), + new KotlinWhenStringFilter(), new KotlinForLoopFilter(), new KotlinUnsafeCastOperatorFilter(), new KotlinNotNullOperatorFilter(), new KotlinDefaultArgumentsFilter(), new KotlinInlineFilter(), diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinForLoopFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinForLoopFilter.java new file mode 100644 index 0000000000..6c0a29fa97 --- /dev/null +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinForLoopFilter.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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: + * Fabian Mastenbroek - initial API and implementation + * + *******************************************************************************/ +package org.jacoco.core.internal.analysis.filter; + +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.MethodNode; + +/** + * Filters branches in bytecode that the Kotlin compiler generates for + * for loops as they are not coverable most of the time. + */ +public class KotlinForLoopFilter implements IFilter { + + public void filter(final MethodNode methodNode, + final IFilterContext context, final IFilterOutput output) { + final Matcher matcher = new Matcher(); + for (final AbstractInsnNode node : methodNode.instructions) { + matcher.match(node, output); + } + } + + private static class Matcher extends AbstractMatcher { + + public void match(final AbstractInsnNode start, IFilterOutput output) { + if (start.getOpcode() != Opcodes.IF_ICMPGE + && start.getOpcode() != Opcodes.IF_ICMPGT) { + return; + } + cursor = start; + AbstractInsnNode loopLabelNode = start.getNext(); + if (loopLabelNode instanceof LabelNode) { + Label loopLabel = ((LabelNode) loopLabelNode).getLabel(); + LabelNode jumpTarget = ((JumpInsnNode) cursor).label; + if (isLoop(jumpTarget, loopLabel)) { + output.ignore(start, start); + output.ignore(jumpTarget.getPrevious(), + jumpTarget.getPrevious()); + } + } + } + + private boolean isLoop(LabelNode jumpTarget, Label loopLabel) { + nextIs(Opcodes.ILOAD); + nextIs(Opcodes.ISTORE); + nextIs(Opcodes.IINC); + // follow the jump node + for (AbstractInsnNode j = cursor; j != null; j = j.getNext()) { + if (j == jumpTarget) { + // if the label prior to the jump target matches + // we can be sure that this is the loop we are looking for + AbstractInsnNode previousOpcode = j.getPrevious(); + return ((JumpInsnNode) previousOpcode).label.getLabel() + .equals(loopLabel); + } + } + return false; + } + } +}