diff --git a/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinLateinitTarget.kt b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinLateinitTarget.kt index 950231a3d9..b8828668ce 100644 --- a/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinLateinitTarget.kt +++ b/org.jacoco.core.test.validation.kotlin/src/org/jacoco/core/test/validation/kotlin/targets/KotlinLateinitTarget.kt @@ -19,10 +19,32 @@ import org.jacoco.core.test.validation.targets.Stubs.nop */ object KotlinLateinitTarget { private lateinit var x: String + lateinit var y: String + + private class Example { + private lateinit var x: T + private lateinit var xx: Any + lateinit var y: T + lateinit var yy: Any + fun nop() { + x = Any() as T + xx = Any() + y = Any() as T + yy = Any() + nop(x) // assertFullyCovered() + nop(xx) // assertFullyCovered() + nop(y) // assertFullyCovered() + nop(yy) // assertFullyCovered() + } + } @JvmStatic fun main(args: Array) { x = "" + y = "" + nop(x) // assertFullyCovered() + nop(y) // assertFullyCovered() + Example().nop() // assertFullyCovered() } } diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinLateinitFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinLateinitFilterTest.java index f0aef7e88d..aeb9fa8cee 100644 --- a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinLateinitFilterTest.java +++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/KotlinLateinitFilterTest.java @@ -49,8 +49,8 @@ public void testLateinitBranchIsFiltered() { "kotlin/jvm/internal/Intrinsics", "throwUninitializedPropertyAccessException", "(Ljava/lang/String;)V", false); - final AbstractInsnNode expectedTo = m.instructions.getLast(); m.visitLabel(l2); + final AbstractInsnNode expectedTo = m.instructions.getLast(); m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/os/PowerManager$WakeLock", "acquire", "", false); @@ -61,34 +61,111 @@ public void testLateinitBranchIsFiltered() { /** *
-	 * class Example {
-	 *   private lateinit var x: String
-	 *   fun example() = x
+	 * class LateinitStringPrivate {
+	 *     private lateinit var member: String
+	 *
+	 *     fun get(): String = member
 	 * }
 	 * 
*/ @Test - public void should_filter_Kotlin_1_5() { + public void should_filter_Kotlin_1_5_private() { final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, - "example", "()Ljava/lang/String;", null, null); + "get", "()Ljava/lang/String;", null, null); Label label = new Label(); m.visitVarInsn(Opcodes.ALOAD, 0); - m.visitFieldInsn(Opcodes.GETFIELD, "Example", "x", + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitStringPrivate", "member", "Ljava/lang/String;"); m.visitVarInsn(Opcodes.ASTORE, 1); m.visitVarInsn(Opcodes.ALOAD, 1); m.visitJumpInsn(Opcodes.IFNONNULL, label); final AbstractInsnNode expectedFrom = m.instructions.getLast(); - m.visitLdcInsn("x"); + m.visitLdcInsn("member"); m.visitMethodInsn(Opcodes.INVOKESTATIC, "kotlin/jvm/internal/Intrinsics", "throwUninitializedPropertyAccessException", "(Ljava/lang/String;)V", false); m.visitInsn(Opcodes.ACONST_NULL); m.visitInsn(Opcodes.ATHROW); + m.visitLabel(label); final AbstractInsnNode expectedTo = m.instructions.getLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitInsn(Opcodes.ARETURN); + + filter.filter(m, context, output); + + assertIgnored(new Range(expectedFrom, expectedTo)); + } + + /** + *
+	 * class LateinitStringPublic {
+	 *     lateinit var member: String
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_5_public() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "getMember", "()Ljava/lang/String;", null, null); + Label label = new Label(); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitStringPublic", "member", + "Ljava/lang/String;"); + m.visitVarInsn(Opcodes.ASTORE, 1); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitJumpInsn(Opcodes.IFNULL, label); + final AbstractInsnNode expectedFrom = m.instructions.getLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitInsn(Opcodes.ARETURN); m.visitLabel(label); + m.visitLdcInsn("member"); + m.visitMethodInsn(Opcodes.INVOKESTATIC, + "kotlin/jvm/internal/Intrinsics", + "throwUninitializedPropertyAccessException", + "(Ljava/lang/String;)V", false); + m.visitInsn(Opcodes.ACONST_NULL); + m.visitInsn(Opcodes.ATHROW); + final AbstractInsnNode expectedTo = m.instructions.getLast(); + + filter.filter(m, context, output); + + assertIgnored(new Range(expectedFrom, expectedTo)); + } + + /** + *
+	 * class LateinitStringPrivate {
+	 *     private lateinit var member: String
+	 *
+	 *     fun get(): String = member
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_5_30_private() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "get", "()Ljava/lang/String;", null, null); + Label l1 = new Label(); + Label l2 = new Label(); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitStringPrivate", "member", + "Ljava/lang/String;"); + m.visitVarInsn(Opcodes.ASTORE, 1); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitJumpInsn(Opcodes.IFNONNULL, l1); + final AbstractInsnNode expectedFrom = m.instructions.getLast(); + m.visitLdcInsn("member"); + m.visitMethodInsn(Opcodes.INVOKESTATIC, + "kotlin/jvm/internal/Intrinsics", + "throwUninitializedPropertyAccessException", + "(Ljava/lang/String;)V", false); + m.visitInsn(Opcodes.ACONST_NULL); + m.visitJumpInsn(Opcodes.GOTO, l2); + m.visitLabel(l1); + final AbstractInsnNode expectedTo = m.instructions.getLast(); m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitLabel(l2); m.visitInsn(Opcodes.ARETURN); filter.filter(m, context, output); @@ -96,4 +173,229 @@ public void should_filter_Kotlin_1_5() { assertIgnored(new Range(expectedFrom, expectedTo)); } + /** + *
+	 * class LateinitStringPublic {
+	 *     lateinit var member: String
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_5_30_public() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "getMember", "()Ljava/lang/String;", null, null); + Label label = new Label(); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitStringPublic", "member", + "Ljava/lang/String;"); + m.visitVarInsn(Opcodes.ASTORE, 1); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitJumpInsn(Opcodes.IFNULL, label); + final AbstractInsnNode expectedFrom = m.instructions.getLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitInsn(Opcodes.ARETURN); + m.visitLabel(label); + m.visitLdcInsn("member"); + m.visitMethodInsn(Opcodes.INVOKESTATIC, + "kotlin/jvm/internal/Intrinsics", + "throwUninitializedPropertyAccessException", + "(Ljava/lang/String;)V", false); + m.visitInsn(Opcodes.ACONST_NULL); + m.visitInsn(Opcodes.ARETURN); + final AbstractInsnNode expectedTo = m.instructions.getLast(); + + filter.filter(m, context, output); + + assertIgnored(new Range(expectedFrom, expectedTo)); + } + + /** + *
+	 * class LateinitGenericPrivate {
+	 *     private lateinit var member: T
+	 *
+	 *     fun get(): T = member
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_5_30_private_generic() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "get", "()Ljava/lang/String;", null, null); + Label l1 = new Label(); + Label l2 = new Label(); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitGenericPrivate", "member", + "Ljava/lang/Object;"); + m.visitVarInsn(Opcodes.ASTORE, 1); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitJumpInsn(Opcodes.IFNONNULL, l1); + final AbstractInsnNode expectedFrom = m.instructions.getLast(); + m.visitLdcInsn("member"); + m.visitMethodInsn(Opcodes.INVOKESTATIC, + "kotlin/jvm/internal/Intrinsics", + "throwUninitializedPropertyAccessException", + "(Ljava/lang/String;)V", false); + m.visitFieldInsn(Opcodes.GETSTATIC, "kotlin/Unit", "INSTANCE", + "Lkotlin/Unit;"); + m.visitJumpInsn(Opcodes.GOTO, l2); + m.visitLabel(l1); + final AbstractInsnNode expectedTo = m.instructions.getLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitLabel(l2); + m.visitInsn(Opcodes.ARETURN); + + filter.filter(m, context, output); + + assertIgnored(new Range(expectedFrom, expectedTo)); + } + + /** + *
+	 * class LateinitGenericPublic {
+	 *     lateinit var member: T
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_5_30_public_generic() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "getMember", "()Ljava/lang/String;", null, null); + Label label = new Label(); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitGenericPublic", "member", + "Ljava/lang/Object;"); + m.visitVarInsn(Opcodes.ASTORE, 1); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitJumpInsn(Opcodes.IFNULL, label); + final AbstractInsnNode expectedFrom = m.instructions.getLast(); + m.visitVarInsn(Opcodes.ALOAD, 1); + m.visitInsn(Opcodes.ARETURN); + m.visitLabel(label); + m.visitLdcInsn("member"); + m.visitMethodInsn(Opcodes.INVOKESTATIC, + "kotlin/jvm/internal/Intrinsics", + "throwUninitializedPropertyAccessException", + "(Ljava/lang/String;)V", false); + m.visitFieldInsn(Opcodes.GETSTATIC, "kotlin/Unit", "INSTANCE", + "Lkotlin/Unit;"); + m.visitInsn(Opcodes.ARETURN); + final AbstractInsnNode expectedTo = m.instructions.getLast(); + + filter.filter(m, context, output); + + assertIgnored(new Range(expectedFrom, expectedTo)); + } + + /** + *
+	 * class LateinitStringPrivate {
+	 *     private lateinit var member: String
+	 *
+	 *     fun get(): String = member
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_6_private() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "get", "()Ljava/lang/String;", null, null); + Label label = new Label(); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitStringPrivate", "member", + "Ljava/lang/String;"); + m.visitInsn(Opcodes.DUP); + m.visitJumpInsn(Opcodes.IFNONNULL, label); + final AbstractInsnNode expectedFrom = m.instructions.getLast(); + m.visitInsn(Opcodes.POP); + m.visitLdcInsn("member"); + m.visitMethodInsn(Opcodes.INVOKESTATIC, + "kotlin/jvm/internal/Intrinsics", + "throwUninitializedPropertyAccessException", + "(Ljava/lang/String;)V", false); + m.visitInsn(Opcodes.ACONST_NULL); + m.visitLabel(label); + final AbstractInsnNode expectedTo = m.instructions.getLast(); + m.visitInsn(Opcodes.ARETURN); + + filter.filter(m, context, output); + + assertIgnored(new Range(expectedFrom, expectedTo)); + } + + /** + *
+	 * class LateinitGenericPrivate {
+	 *     private lateinit var member: T
+	 *
+	 *     fun get(): T = member
+	 * }
+	 * 
+ */ + @Test + public void should_filter_Kotlin_1_6_private_generic() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "get", "()Ljava/lang/String;", null, null); + Label label = new Label(); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitGenericPrivate", "member", + "Ljava/lang/Object;"); + m.visitInsn(Opcodes.DUP); + m.visitJumpInsn(Opcodes.IFNONNULL, label); + final AbstractInsnNode expectedFrom = m.instructions.getLast(); + m.visitInsn(Opcodes.POP); + m.visitLdcInsn("member"); + m.visitMethodInsn(Opcodes.INVOKESTATIC, + "kotlin/jvm/internal/Intrinsics", + "throwUninitializedPropertyAccessException", + "(Ljava/lang/String;)V", false); + m.visitFieldInsn(Opcodes.GETSTATIC, "kotlin/Unit", "INSTANCE", + "Lkotlin/Unit;"); + m.visitLabel(label); + final AbstractInsnNode expectedTo = m.instructions.getLast(); + m.visitInsn(Opcodes.ARETURN); + + filter.filter(m, context, output); + + assertIgnored(new Range(expectedFrom, expectedTo)); + } + + /** + *
+	 * class LateinitStringPublic {
+	 *     lateinit var member: String
+	 * }
+	 * 
+ * + * Kotlin 1.7.21 contains an additional frame node before the option pop. + */ + @Test + public void should_filter_Kotlin_1_7_21() { + final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0, + "get", "()Ljava/lang/String;", null, null); + Label label = new Label(); + m.visitVarInsn(Opcodes.ALOAD, 0); + m.visitFieldInsn(Opcodes.GETFIELD, "LateinitStringPublic", "member", + "Ljava/lang/String;"); + m.visitInsn(Opcodes.DUP); + m.visitJumpInsn(Opcodes.IFNULL, label); + final AbstractInsnNode expectedFrom = m.instructions.getLast(); + m.visitInsn(Opcodes.ARETURN); + m.visitLabel(label); + m.visitFrame(Opcodes.F_SAME1, 0, new Object[] {}, 1, + new String[] { "java/lang/String" }); + m.visitInsn(Opcodes.POP); + m.visitLdcInsn("member"); + m.visitMethodInsn(Opcodes.INVOKESTATIC, + "kotlin/jvm/internal/Intrinsics", + "throwUninitializedPropertyAccessException", + "(Ljava/lang/String;)V", false); + m.visitInsn(Opcodes.ACONST_NULL); + m.visitInsn(Opcodes.ARETURN); + final AbstractInsnNode expectedTo = m.instructions.getLast(); + + filter.filter(m, context, output); + + assertIgnored(new Range(expectedFrom, expectedTo)); + } } diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinLateinitFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinLateinitFilter.java index 36e573dde2..176ea3d2fa 100644 --- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinLateinitFilter.java +++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinLateinitFilter.java @@ -14,6 +14,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FrameNode; import org.objectweb.asm.tree.JumpInsnNode; import org.objectweb.asm.tree.MethodNode; @@ -27,33 +28,71 @@ 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); + final AbstractInsnNode to = matcher.match(node); + if (to != null) { + output.ignore(node, to); + } } } private static class Matcher extends AbstractMatcher { - public void match(final AbstractInsnNode start, - final IFilterOutput output) { - if (Opcodes.IFNONNULL != start.getOpcode()) { - return; + public AbstractInsnNode match(final AbstractInsnNode start) { + + if (Opcodes.IFNONNULL != start.getOpcode() + && Opcodes.IFNULL != start.getOpcode()) { + return null; } + cursor = start; + if (Opcodes.IFNULL == start.getOpcode()) { + // we're looking for the + // throwUninitializedPropertyAccessException instruction, which + // is in the "null" branch, so we have to follow this jump. If + // we have an IFNONNULL instruction, we are already in die + // "null" branch and don't have to jump. + cursor = ((JumpInsnNode) start).label; + } + + AbstractInsnNode optionalFrame = cursor.getNext(); + if (optionalFrame != null && optionalFrame instanceof FrameNode + && ((FrameNode) optionalFrame).type == Opcodes.F_SAME1) { + next(); + } + + AbstractInsnNode optionalPop = cursor.getNext(); + if (optionalPop != null && optionalPop.getOpcode() == Opcodes.POP) { + // Kotlin 1.6.0 DUPs the lateinit variable and POPs it here, + // previous versions instead load the variable twice. To be + // compatible with both, we can just skip the POP. + next(); + } + nextIs(Opcodes.LDC); nextIsInvoke(Opcodes.INVOKESTATIC, "kotlin/jvm/internal/Intrinsics", "throwUninitializedPropertyAccessException", "(Ljava/lang/String;)V"); - if (cursor != null - && skipNonOpcodes(cursor.getNext()) != skipNonOpcodes( - ((JumpInsnNode) start).label)) { - nextIs(Opcodes.ACONST_NULL); - nextIs(Opcodes.ATHROW); + if (cursor == null) { + return null; } - if (cursor != null) { - output.ignore(start, cursor); + if (Opcodes.IFNONNULL == start.getOpcode()) { + // ignore everything until the jump target of our IFNONNULL. + return ((JumpInsnNode) start).label; + } else { + // ignore everything until the next ARETURN or ATHROW + // instruction; or until the end of the function. + while (cursor.getOpcode() != Opcodes.ATHROW + && cursor.getOpcode() != Opcodes.ARETURN) { + AbstractInsnNode next = cursor.getNext(); + if (next == null) { + break; + } + cursor = next; + } + return cursor; } } }