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;
+ }
+ }
+}