aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Mandrikov <Godin@users.noreply.github.com>2017-12-19 23:58:18 +0100
committerGitHub <noreply@github.com>2017-12-19 23:58:18 +0100
commitff00194751d5d432168d842d089793e18d4011e5 (patch)
tree35c3d6df410bb6b026220b529be35967d5173cda
parent00f06ecccf9d6e18f25a488be72a63c9b9393af1 (diff)
downloadjacoco-ff00194751d5d432168d842d089793e18d4011e5.tar.gz
Add filter for duplicates of finally blocks (#604)
-rw-r--r--org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java18
-rw-r--r--org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FinallyFilterTest.java421
-rw-r--r--org.jacoco.core.test/src/org/jacoco/core/test/filter/FinallyTest.java266
-rw-r--r--org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/Finally.java148
-rw-r--r--org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java57
-rw-r--r--org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java9
-rw-r--r--org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java2
-rw-r--r--org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/FinallyFilter.java227
-rw-r--r--org.jacoco.doc/docroot/doc/changes.html3
9 files changed, 1092 insertions, 59 deletions
diff --git a/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java b/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java
index 6c9db09c..7517fb55 100644
--- a/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java
+++ b/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/TryWithResourcesTest.java
@@ -44,7 +44,7 @@ public class TryWithResourcesTest extends ValidationTestBase {
// without filter next line has branches:
assertLine("test.close", ICounter.EMPTY);
assertLine("test.catch", ICounter.NOT_COVERED);
- assertLine("test.finally", ICounter.PARTLY_COVERED);
+ assertLine("test.finally", ICounter.FULLY_COVERED);
}
/**
@@ -62,7 +62,7 @@ public class TryWithResourcesTest extends ValidationTestBase {
// without filter next line has branches:
assertLine("test2.close", ICounter.EMPTY);
assertLine("test2.catch", ICounter.NOT_COVERED);
- assertLine("test2.finally", ICounter.PARTLY_COVERED);
+ assertLine("test2.finally", ICounter.FULLY_COVERED);
assertLine("test2.after", ICounter.FULLY_COVERED);
}
@@ -109,14 +109,14 @@ public class TryWithResourcesTest extends ValidationTestBase {
assertLine("nested.try2", ICounter.FULLY_COVERED);
assertLine("nested.body", ICounter.FULLY_COVERED);
assertLine("nested.catch2", ICounter.NOT_COVERED);
- assertLine("nested.finally2", ICounter.PARTLY_COVERED);
+ assertLine("nested.finally2", ICounter.FULLY_COVERED);
// next lines not covered on exceptional path:
- assertLine("nested.try3", ICounter.PARTLY_COVERED, 0, 0);
- assertLine("nested.open3", ICounter.PARTLY_COVERED, 0, 0);
- assertLine("nested.body3", ICounter.PARTLY_COVERED, 0, 0);
+ assertLine("nested.try3", ICounter.FULLY_COVERED, 0, 0);
+ assertLine("nested.open3", ICounter.FULLY_COVERED, 0, 0);
+ assertLine("nested.body3", ICounter.FULLY_COVERED, 0, 0);
assertLine("nested.catch3", ICounter.NOT_COVERED);
- assertLine("nested.finally3", ICounter.PARTLY_COVERED, 0, 0);
+ assertLine("nested.finally3", ICounter.FULLY_COVERED, 0, 0);
// without filter next lines have branches:
assertLine("nested.close3", ICounter.EMPTY);
@@ -132,12 +132,12 @@ public class TryWithResourcesTest extends ValidationTestBase {
// without filter next line covered partly:
assertLine("returnInCatch.try1", ICounter.FULLY_COVERED);
assertLine("returnInCatch.open", ICounter.FULLY_COVERED);
- assertLine("returnInCatch.finally1", ICounter.PARTLY_COVERED, 5, 1);
+ assertLine("returnInCatch.finally1", ICounter.PARTLY_COVERED, 1, 1);
// without filter next line has branches:
assertLine("returnInCatch.close", ICounter.EMPTY);
assertLine("returnInCatch.try2", ICounter.EMPTY);
- assertLine("returnInCatch.finally2", ICounter.PARTLY_COVERED, 5, 1);
+ assertLine("returnInCatch.finally2", ICounter.PARTLY_COVERED, 1, 1);
}
/*
diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FinallyFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FinallyFilterTest.java
new file mode 100644
index 00000000..64ad884d
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/FinallyFilterTest.java
@@ -0,0 +1,421 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.internal.analysis.filter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.HashSet;
+import java.util.Set;
+
+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;
+
+public class FinallyFilterTest implements IFilterOutput {
+
+ private final IFilter filter = new FinallyFilter();
+
+ private final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "name", "()V", null, null);
+
+ /**
+ * <pre>
+ * try {
+ * ...
+ * if (...) {
+ * ...
+ * return;
+ * } else {
+ * ...
+ * return;
+ * }
+ * } finally {
+ * ...
+ * }
+ * </pre>
+ */
+ @Test
+ public void should_analyze_control_flow() {
+ final Label start1 = new Label();
+ final Label end1 = new Label();
+ final Label start2 = new Label();
+ final Label end2 = new Label();
+ final Label finallyStart = new Label();
+
+ m.visitTryCatchBlock(start1, end1, finallyStart, null);
+ m.visitTryCatchBlock(start2, end2, finallyStart, null);
+
+ m.visitLabel(start1);
+ // jump to another region associated with same handler:
+ m.visitJumpInsn(Opcodes.IFEQ, start2);
+ m.visitInsn(Opcodes.NOP);
+ m.visitLabel(end1);
+
+ m.visitInsn(Opcodes.NOP); // finally block
+ shouldMergeLast();
+ m.visitInsn(Opcodes.RETURN);
+
+ m.visitLabel(start2);
+ m.visitInsn(Opcodes.NOP);
+ m.visitLabel(end2);
+ m.visitInsn(Opcodes.NOP); // finally block
+ shouldMergeLast();
+ m.visitInsn(Opcodes.RETURN);
+
+ m.visitLabel(finallyStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.NOP); // finally block
+ shouldMergeLast();
+ m.visitVarInsn(Opcodes.ALOAD, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.ATHROW);
+ shouldIgnoreLast();
+
+ execute();
+ }
+
+ // === try/catch/finally ===
+
+ @Test
+ public void javac_try_catch_finally() {
+ final Label tryStart = new Label();
+ final Label tryEnd = new Label();
+ final Label catchStart = new Label();
+ final Label catchEnd = new Label();
+ final Label finallyStart = new Label();
+ final Label finallyEnd = new Label();
+
+ m.visitTryCatchBlock(tryStart, tryEnd, catchStart,
+ "java/lang/Exception");
+ m.visitTryCatchBlock(catchStart, catchEnd, finallyStart, null);
+ m.visitTryCatchBlock(tryStart, tryEnd, finallyStart, null);
+
+ m.visitLabel(tryStart);
+ m.visitInsn(Opcodes.NOP); // try body
+ m.visitLabel(tryEnd);
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitJumpInsn(Opcodes.GOTO, finallyEnd);
+ shouldIgnoreLast();
+
+ m.visitLabel(catchStart);
+ m.visitInsn(Opcodes.NOP); // catch body
+ m.visitLabel(catchEnd);
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitInsn(Opcodes.ATHROW);
+
+ m.visitLabel(finallyStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitVarInsn(Opcodes.ALOAD, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.ATHROW);
+ shouldIgnoreLast();
+ m.visitLabel(finallyEnd);
+
+ m.visitInsn(Opcodes.NOP);
+
+ execute();
+ }
+
+ @Test
+ public void ecj_try_catch_finally() {
+ final Label tryStart = new Label();
+ final Label tryEnd = new Label();
+ final Label catchStart = new Label();
+ final Label catchEnd = new Label();
+ final Label finallyStart = new Label();
+ final Label finallyEnd = new Label();
+ final Label after = new Label();
+
+ m.visitTryCatchBlock(tryStart, tryEnd, catchStart,
+ "java/lang/Exception");
+ m.visitTryCatchBlock(tryStart, catchEnd, finallyStart, null);
+
+ m.visitLabel(tryStart);
+ m.visitInsn(Opcodes.NOP); // try body
+ m.visitLabel(tryEnd);
+ m.visitJumpInsn(Opcodes.GOTO, finallyEnd);
+
+ m.visitLabel(catchStart);
+ m.visitInsn(Opcodes.POP);
+ m.visitInsn(Opcodes.NOP); // catch body
+ m.visitLabel(catchEnd);
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitJumpInsn(Opcodes.GOTO, after);
+ shouldIgnoreLast();
+
+ m.visitLabel(finallyStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitVarInsn(Opcodes.ALOAD, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.ATHROW);
+ shouldIgnoreLast();
+ m.visitLabel(finallyEnd);
+
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitLabel(after);
+ m.visitInsn(Opcodes.NOP);
+
+ execute();
+ }
+
+ // === empty catch ===
+
+ /**
+ * javac 1.5 - 1.7
+ */
+ @Test
+ public void javac_empty_catch() {
+ final Label tryStart = new Label();
+ final Label tryEnd = new Label();
+ final Label catchStart = new Label();
+ final Label catchEnd = new Label();
+ final Label finallyStart = new Label();
+ final Label finallyEnd = new Label();
+
+ m.visitTryCatchBlock(tryStart, tryEnd, catchStart,
+ "java/lang/Exception");
+ m.visitTryCatchBlock(tryStart, tryEnd, finallyStart, null);
+ m.visitTryCatchBlock(catchStart, catchEnd, finallyStart, null);
+ // actually one more useless TryCatchBlock for ASTORE in finally
+
+ m.visitLabel(tryStart);
+ m.visitInsn(Opcodes.NOP); // try body
+ m.visitLabel(tryEnd);
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitJumpInsn(Opcodes.GOTO, finallyEnd);
+ shouldIgnoreLast();
+
+ m.visitLabel(catchStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ m.visitLabel(catchEnd);
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitJumpInsn(Opcodes.GOTO, finallyEnd);
+ shouldIgnoreLast();
+
+ m.visitLabel(finallyStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitVarInsn(Opcodes.ALOAD, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.ATHROW);
+ shouldIgnoreLast();
+ m.visitLabel(finallyEnd);
+
+ m.visitInsn(Opcodes.NOP);
+
+ execute();
+ }
+
+ /**
+ * javac >= 1.8
+ *
+ * Probably related to https://bugs.openjdk.java.net/browse/JDK-7093325
+ */
+ @Test
+ public void javac_8_empty_catch() throws Exception {
+ final Label tryStart = new Label();
+ final Label tryEnd = new Label();
+ final Label catchStart = new Label();
+ final Label finallyStart = new Label();
+ final Label finallyEnd = new Label();
+
+ m.visitTryCatchBlock(tryStart, tryEnd, catchStart,
+ "java/lang/Exception");
+ m.visitTryCatchBlock(tryStart, tryEnd, finallyStart, null);
+
+ m.visitLabel(tryStart);
+ m.visitInsn(Opcodes.NOP); // try body
+ m.visitLabel(tryEnd);
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitJumpInsn(Opcodes.GOTO, finallyEnd);
+ shouldIgnoreLast();
+ shouldIgnoreLast();
+
+ m.visitLabel(catchStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitJumpInsn(Opcodes.GOTO, finallyEnd);
+ shouldIgnoreLast();
+
+ m.visitLabel(finallyStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitVarInsn(Opcodes.ALOAD, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.ATHROW);
+ shouldIgnoreLast();
+ m.visitLabel(finallyEnd);
+
+ execute();
+ }
+
+ @Test
+ public void ecj_empty_catch() {
+ final Label tryStart = new Label();
+ final Label tryEnd = new Label();
+ final Label catchStart = new Label();
+ final Label catchEnd = new Label();
+ final Label finallyStart = new Label();
+ final Label finallyEnd = new Label();
+ final Label after = new Label();
+
+ m.visitTryCatchBlock(tryStart, tryEnd, catchStart,
+ "java/lang/Exception");
+ m.visitTryCatchBlock(tryStart, catchEnd, finallyStart, null);
+
+ m.visitLabel(tryStart);
+ m.visitInsn(Opcodes.NOP); // try body
+ m.visitLabel(tryEnd);
+ m.visitJumpInsn(Opcodes.GOTO, finallyEnd);
+
+ m.visitLabel(catchStart);
+ m.visitInsn(Opcodes.POP);
+ m.visitLabel(catchEnd);
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitJumpInsn(Opcodes.GOTO, after);
+ shouldIgnoreLast();
+
+ m.visitLabel(finallyStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitVarInsn(Opcodes.ALOAD, 1);
+ shouldIgnoreLast();
+ m.visitInsn(Opcodes.ATHROW);
+ shouldIgnoreLast();
+ m.visitLabel(finallyEnd);
+
+ m.visitInsn(Opcodes.NOP); // finally body
+ shouldMergeLast();
+ m.visitLabel(after);
+ m.visitInsn(Opcodes.NOP);
+
+ execute();
+ }
+
+ // === always completes abruptly ===
+
+ @Test
+ public void javac_always_completes_abruptly() {
+ final Label tryStart = new Label();
+ final Label tryEnd = new Label();
+ final Label finallyStart = new Label();
+
+ m.visitTryCatchBlock(tryStart, tryEnd, finallyStart, null);
+
+ m.visitLabel(tryStart);
+ m.visitInsn(Opcodes.NOP); // try body
+ m.visitLabel(tryEnd);
+ m.visitInsn(Opcodes.RETURN); // finally body
+
+ m.visitLabel(finallyStart);
+ m.visitVarInsn(Opcodes.ASTORE, 1);
+ m.visitInsn(Opcodes.RETURN); // finally body
+
+ execute();
+ }
+
+ @Test
+ public void ecj_always_completes_abruptly() {
+ final Label tryStart = new Label();
+ final Label tryEnd = new Label();
+ final Label finallyStart = new Label();
+
+ m.visitTryCatchBlock(tryStart, tryEnd, tryEnd, null);
+
+ m.visitLabel(tryStart);
+ m.visitInsn(Opcodes.NOP); // try body
+ m.visitJumpInsn(Opcodes.GOTO, finallyStart);
+ m.visitLabel(tryEnd);
+
+ m.visitInsn(Opcodes.POP);
+ m.visitLabel(finallyStart);
+ m.visitInsn(Opcodes.RETURN); // finally body
+
+ execute();
+ }
+
+ private final Set<AbstractInsnNode> expectedIgnored = new HashSet<AbstractInsnNode>();
+ private final Set<AbstractInsnNode> actualIgnored = new HashSet<AbstractInsnNode>();
+ private final Set<AbstractInsnNode> expectedMerged = new HashSet<AbstractInsnNode>();
+ private final Set<AbstractInsnNode> actualMerged = new HashSet<AbstractInsnNode>();
+
+ private void shouldMergeLast() {
+ expectedMerged.add(m.instructions.getLast());
+ }
+
+ private void shouldIgnoreLast() {
+ expectedIgnored.add(m.instructions.getLast());
+ }
+
+ private void execute() {
+ filter.filter("", "", m, this);
+ assertEquals("ignored", toIndexes(expectedIgnored),
+ toIndexes(actualIgnored));
+ assertEquals("merged", toIndexes(expectedMerged),
+ toIndexes(actualMerged));
+ }
+
+ @SuppressWarnings("boxing")
+ private Set<Integer> toIndexes(Set<AbstractInsnNode> set) {
+ final Set<Integer> result = new HashSet<Integer>();
+ for (final AbstractInsnNode i : set) {
+ result.add(m.instructions.indexOf(i));
+ }
+ return result;
+ }
+
+ public void ignore(final AbstractInsnNode fromInclusive,
+ final AbstractInsnNode toInclusive) {
+ for (AbstractInsnNode i = fromInclusive; i != toInclusive; i = i
+ .getNext()) {
+ actualIgnored.add(i);
+ }
+ actualIgnored.add(toInclusive);
+ }
+
+ public void merge(final AbstractInsnNode i1, final AbstractInsnNode i2) {
+ if (actualMerged.isEmpty() || actualMerged.contains(i1)
+ || actualMerged.contains(i2)) {
+ actualMerged.add(i1);
+ actualMerged.add(i2);
+ } else {
+ fail();
+ }
+ }
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/filter/FinallyTest.java b/org.jacoco.core.test/src/org/jacoco/core/test/filter/FinallyTest.java
new file mode 100644
index 00000000..7658a118
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/filter/FinallyTest.java
@@ -0,0 +1,266 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.test.filter;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.jacoco.core.analysis.ICounter;
+import org.jacoco.core.test.TargetLoader;
+import org.jacoco.core.test.filter.targets.Finally;
+import org.jacoco.core.test.validation.Source;
+import org.jacoco.core.test.validation.ValidationTestBase;
+import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.LineNumberNode;
+import org.objectweb.asm.tree.MethodNode;
+
+/**
+ * Test of filtering of duplicated bytecode that is generated for finally block.
+ */
+public class FinallyTest extends ValidationTestBase {
+
+ private static boolean isJDK8 = !Pattern.compile("1\\.[567]\\.0_(\\d++)")
+ .matcher(System.getProperty("java.version")).matches();
+
+ public FinallyTest() {
+ super(Finally.class);
+ }
+
+ /**
+ * {@link Finally#example(boolean)}
+ */
+ @Test
+ public void example() {
+ if (isJDKCompiler) {
+ assertLine("example.0", ICounter.EMPTY);
+ } else {
+ assertLine("example.0", ICounter.FULLY_COVERED);
+ }
+ assertLine("example.1", ICounter.FULLY_COVERED, 0, 2);
+ assertLine("example.2", ICounter.FULLY_COVERED);
+ assertLine("example.3", ICounter.EMPTY);
+ assertLine("example.4", ICounter.EMPTY);
+ }
+
+ /**
+ * GOTO instructions at the end of duplicates of finally block might have
+ * line number of a last instruction of finally block and hence lead to
+ * unexpected coverage results, like for example in case of ECJ for
+ * {@link Finally#catchNotExecuted()}, {@link Finally#emptyCatch()}. So we
+ * decided to ignore them, even if they can correspond to a real break
+ * statement.
+ * <p>
+ * See also <a href=
+ * "https://bugs.openjdk.java.net/browse/JDK-8180141">JDK-8180141</a> and
+ * <a href=
+ * "https://bugs.openjdk.java.net/browse/JDK-7008643">JDK-7008643</a>.
+ * <p>
+ * {@link Finally#breakStatement()}
+ */
+ @Test
+ public void breakStatement() {
+ assertLine("breakStatement", ICounter.EMPTY);
+
+ assertLine("breakStatement.1", ICounter.FULLY_COVERED);
+ assertLine("breakStatement.2", ICounter.EMPTY);
+ }
+
+ /**
+ * {@link Finally#catchNotExecuted()}
+ */
+ @Test
+ public void catchNotExecuted() {
+ assertLine("catchNotExecuted.catch", ICounter.NOT_COVERED);
+ assertLine("catchNotExecuted.0", ICounter.EMPTY);
+ assertLine("catchNotExecuted.1", ICounter.FULLY_COVERED);
+ assertLine("catchNotExecuted.2", ICounter.EMPTY);
+ }
+
+ /**
+ * {@link Finally#emptyCatch()}
+ */
+ @Test
+ public void emptyCatch() {
+ assertLine("emptyCatch.0", ICounter.EMPTY);
+ assertLine("emptyCatch.1", ICounter.FULLY_COVERED);
+ assertLine("emptyCatch.2", ICounter.EMPTY);
+ }
+
+ /**
+ * {@link Finally#twoRegions()}
+ */
+ @Test
+ public void twoRegions() {
+ assertLine("twoRegions.0", ICounter.EMPTY);
+ if (isJDKCompiler && !isJDK8) {
+ // https://bugs.openjdk.java.net/browse/JDK-7008643
+ assertLine("twoRegions.1", ICounter.PARTLY_COVERED);
+ assertLine("twoRegions.return.1", ICounter.EMPTY);
+ assertLine("twoRegions.return.2", ICounter.EMPTY);
+ } else {
+ assertLine("twoRegions.1", ICounter.FULLY_COVERED);
+ assertLine("twoRegions.return.1", ICounter.FULLY_COVERED);
+ assertLine("twoRegions.return.2", ICounter.NOT_COVERED);
+ }
+ assertLine("twoRegions.2", ICounter.EMPTY);
+
+ assertLine("twoRegions.if", ICounter.FULLY_COVERED);
+ assertLine("twoRegions.region.1", ICounter.FULLY_COVERED);
+ assertLine("twoRegions.region.2", ICounter.NOT_COVERED);
+ }
+
+ /**
+ * {@link Finally#nested()}
+ */
+ @Test
+ public void nested() {
+ if (isJDKCompiler) {
+ assertLine("nested.0", ICounter.EMPTY);
+ } else {
+ assertLine("nested.0", ICounter.FULLY_COVERED);
+ }
+ assertLine("nested.1", ICounter.EMPTY);
+ assertLine("nested.2", ICounter.FULLY_COVERED);
+ if (isJDKCompiler) {
+ assertLine("nested.3", ICounter.EMPTY);
+ } else {
+ assertLine("nested.3", ICounter.FULLY_COVERED);
+ }
+ assertLine("nested.4", ICounter.FULLY_COVERED);
+ }
+
+ /**
+ * {@link Finally#emptyTry()}
+ */
+ @Test
+ public void emptyTry() {
+ assertLine("emptyTry.0", ICounter.EMPTY);
+ if (!isJDKCompiler || isJDK8) {
+ assertLine("emptyTry.1", ICounter.FULLY_COVERED);
+ assertLine("emptyTry.2", ICounter.EMPTY);
+ } else {
+ // compiler bug fixed in javac >= 1.8:
+ assertLine("emptyTry.1", ICounter.PARTLY_COVERED);
+ assertLine("emptyTry.2", ICounter.FULLY_COVERED);
+ }
+ }
+
+ /**
+ * {@link Finally#alwaysCompletesAbruptly()}
+ */
+ @Test
+ public void alwaysCompletesAbruptly() {
+ if (isJDKCompiler) {
+ // uncovered case:
+ assertLine("alwaysCompletesAbruptly.0", ICounter.EMPTY);
+ assertLine("alwaysCompletesAbruptly.1", ICounter.PARTLY_COVERED);
+ } else {
+ assertLine("alwaysCompletesAbruptly.0", ICounter.PARTLY_COVERED);
+ assertLine("alwaysCompletesAbruptly.1", ICounter.FULLY_COVERED);
+ }
+ assertLine("alwaysCompletesAbruptly.2", ICounter.EMPTY);
+ }
+
+ /**
+ * This test studies placement of GOTO instructions.
+ */
+ @Test
+ public void gotos() throws IOException {
+ final Source source = Source.getSourceFor("src", Finally.class);
+
+ final ClassNode classNode = new ClassNode();
+ new ClassReader(TargetLoader.getClassDataAsBytes(Finally.class))
+ .accept(classNode, 0);
+ final Set<String> tags = new HashSet<String>();
+ for (final MethodNode m : classNode.methods) {
+ if ("main".equals(m.name)) {
+ // skip it
+ continue;
+ }
+ int lineNumber = -1;
+ for (AbstractInsnNode i = m.instructions
+ .getFirst(); i != null; i = i.getNext()) {
+ if (AbstractInsnNode.LINE == i.getType()) {
+ lineNumber = ((LineNumberNode) i).line;
+ }
+ if (Opcodes.GOTO == i.getOpcode()) {
+ final String line = source.getLine(lineNumber);
+ if (line.indexOf('$') < 0) {
+ throw new AssertionError(
+ "No tag at line " + lineNumber);
+ }
+ final String tag = line.substring(
+ line.indexOf('$') + "$line-".length(),
+ line.lastIndexOf('$'));
+ tags.add(tag);
+ }
+ }
+ }
+
+ final Set<String> expected = new HashSet<String>();
+
+ if (isJDKCompiler) {
+ expected.add("example.2");
+ } else {
+ expected.add("example.0");
+ }
+
+ expected.add("breakStatement.for");
+ if (isJDKCompiler) {
+ expected.add("breakStatement.1");
+ expected.add("breakStatement.2");
+ } else {
+ expected.add("breakStatement");
+ }
+
+ if (isJDKCompiler) {
+ expected.add("emptyCatch.2");
+ } else {
+ expected.add("emptyCatch");
+ expected.add("emptyCatch.1");
+ }
+
+ if (isJDKCompiler) {
+ expected.add("catchNotExecuted.2");
+ } else {
+ expected.add("catchNotExecuted");
+ expected.add("catchNotExecuted.1");
+ }
+
+ if (isJDKCompiler) {
+ expected.add("nested.5");
+ expected.add("nested.6");
+ } else {
+ expected.add("nested.0");
+ expected.add("nested.3");
+ }
+
+ if (isJDKCompiler && !isJDK8) {
+ expected.add("emptyTry.2");
+ }
+
+ if (!isJDKCompiler) {
+ expected.add("alwaysCompletesAbruptly.0");
+ }
+
+ assertEquals(expected, tags);
+ }
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/Finally.java b/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/Finally.java
new file mode 100644
index 00000000..04950e91
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/Finally.java
@@ -0,0 +1,148 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.test.filter.targets;
+
+import static org.jacoco.core.test.validation.targets.Stubs.ex;
+import static org.jacoco.core.test.validation.targets.Stubs.f;
+import static org.jacoco.core.test.validation.targets.Stubs.nop;
+import static org.jacoco.core.test.validation.targets.Stubs.t;
+
+public class Finally {
+
+ /**
+ * <pre>
+ * InputStream in = null;
+ * try {
+ * in = ...;
+ * ...
+ * } finally {
+ * if (in != null) {
+ * in.close();
+ * }
+ * }
+ * </pre>
+ */
+ private static void example(boolean t) {
+ Object in = null;
+ try {
+ in = open(t);
+ } finally { // $line-example.0$
+ if (in != null) { // $line-example.1$
+ nop(); // $line-example.2$
+ } // $line-example.3$
+ } // $line-example.4$
+ }
+
+ private static Object open(boolean t) {
+ ex(t);
+ return new Object();
+ }
+
+ private static void breakStatement() {
+ for (int i = 0; i < 1; i++) { // $line-breakStatement.for$
+ try {
+ if (f()) {
+ break; // $line-breakStatement$
+ }
+ } finally {
+ nop("finally"); // $line-breakStatement.1$
+ } // $line-breakStatement.2$
+ }
+ }
+
+ private static void catchNotExecuted() {
+ try {
+ nop("try");
+ } catch (Exception e) { // $line-catchNotExecuted$
+ nop("catch"); // $line-catchNotExecuted.catch$
+ } finally { // $line-catchNotExecuted.0$
+ nop("finally"); // $line-catchNotExecuted.1$
+ } // $line-catchNotExecuted.2$
+ }
+
+ private static void emptyCatch() {
+ try {
+ nop("try");
+ } catch (Exception e) { // $line-emptyCatch$
+ // empty
+ } finally { // $line-emptyCatch.0$
+ nop("finally"); // $line-emptyCatch.1$
+ } // $line-emptyCatch.2$
+ }
+
+ private static void twoRegions() {
+ try {
+ // jump to another region associated with same handler:
+ if (t()) { // $line-twoRegions.if$
+ nop(); // $line-twoRegions.region.1$
+ return; // $line-twoRegions.return.1$
+ } else {
+ nop(); // $line-twoRegions.region.2$
+ return; // $line-twoRegions.return.2$
+ }
+ } finally { // $line-twoRegions.0$
+ nop(); // $line-twoRegions.1$
+ } // $line-twoRegions.2$
+ }
+
+ private static void nested() {
+ try {
+ nop();
+ } finally { // $line-nested.0$
+ try { // $line-nested.1$
+ nop(); // $line-nested.2$
+ } finally { // $line-nested.3$
+ nop(); // $line-nested.4$
+ } // $line-nested.5$
+ } // $line-nested.6$
+ }
+
+ private static void emptyTry() {
+ try {
+ // empty
+ } finally { // $line-emptyTry.0$
+ nop(); // $line-emptyTry.1$
+ } // $line-emptyTry.2$
+ }
+
+ @SuppressWarnings("finally")
+ private static void alwaysCompletesAbruptly() {
+ try {
+ nop();
+ } finally { // $line-alwaysCompletesAbruptly.0$
+ return; // $line-alwaysCompletesAbruptly.1$
+ } // $line-alwaysCompletesAbruptly.2$
+ }
+
+ public static void main(String[] args) {
+ example(false);
+ try {
+ example(true);
+ } catch (Exception ignore) {
+ }
+
+ breakStatement();
+
+ catchNotExecuted();
+
+ emptyCatch();
+
+ twoRegions();
+
+ nested();
+
+ emptyTry();
+
+ alwaysCompletesAbruptly();
+ }
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java
index e1baec7d..3ec77f33 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ExceptionsTest.java
@@ -15,27 +15,11 @@ import org.jacoco.core.analysis.ICounter;
import org.jacoco.core.test.validation.targets.Target03;
import org.junit.Test;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
/**
* Tests of exception based control flow.
*/
public class ExceptionsTest extends ValidationTestBase {
- /**
- * https://bugs.openjdk.java.net/browse/JDK-8180660
- */
- private static final boolean isJDK8u152;
- private static final boolean isJDK10;
-
- static {
- final Matcher m = Pattern.compile("1\\.8\\.0_(\\d++)(-ea)?")
- .matcher(System.getProperty("java.version"));
- isJDK8u152 = m.matches() && Integer.parseInt(m.group(1)) >= 152;
- isJDK10 = System.getProperty("java.version").startsWith("10");
- }
-
public ExceptionsTest() {
super(Target03.class);
}
@@ -109,45 +93,25 @@ public class ExceptionsTest extends ValidationTestBase {
ICounter.FULLY_COVERED);
// 7. Finally Block Without Exception Thrown
- // Finally block is yellow as the exception path is missing.
assertLine("noExceptionFinally.beforeBlock", ICounter.FULLY_COVERED);
assertLine("noExceptionFinally.tryBlock", ICounter.FULLY_COVERED);
assertLine("noExceptionFinally.finally",
- isJDKCompiler ? ICounter.EMPTY : ICounter.PARTLY_COVERED);
- assertLine("noExceptionFinally.finallyBlock", ICounter.PARTLY_COVERED);
- if (!isJDKCompiler) {
- assertLine("noExceptionFinally.finallyBlockEnd",
- ICounter.NOT_COVERED);
- } else if (isJDK8u152 || isJDK10) {
- assertLine("noExceptionFinally.finallyBlockEnd",
- ICounter.PARTLY_COVERED);
- } else {
- assertLine("noExceptionFinally.finallyBlockEnd",
- ICounter.FULLY_COVERED);
- }
+ isJDKCompiler ? ICounter.EMPTY : ICounter.FULLY_COVERED);
+ assertLine("noExceptionFinally.finallyBlock", ICounter.FULLY_COVERED);
+ assertLine("noExceptionFinally.finallyBlockEnd", ICounter.EMPTY);
assertLine("noExceptionFinally.afterBlock", ICounter.FULLY_COVERED);
// 8. Finally Block With Implicit Exception
- // Finally block is yellow as the non-exception path is missing.
assertLine("implicitExceptionFinally.beforeBlock",
ICounter.FULLY_COVERED);
assertLine("implicitExceptionFinally.before", ICounter.FULLY_COVERED);
assertLine("implicitExceptionFinally.exception", ICounter.NOT_COVERED);
assertLine("implicitExceptionFinally.after", ICounter.NOT_COVERED);
assertLine("implicitExceptionFinally.finally",
- isJDKCompiler ? ICounter.EMPTY : ICounter.PARTLY_COVERED);
+ isJDKCompiler ? ICounter.EMPTY : ICounter.NOT_COVERED);
assertLine("implicitExceptionFinally.finallyBlock",
- ICounter.PARTLY_COVERED);
- if (!isJDKCompiler) {
- assertLine("implicitExceptionFinally.finallyBlockEnd",
- ICounter.FULLY_COVERED);
- } else if (isJDK8u152 || isJDK10) {
- assertLine("implicitExceptionFinally.finallyBlockEnd",
- ICounter.PARTLY_COVERED);
- } else {
- assertLine("implicitExceptionFinally.finallyBlockEnd",
- ICounter.NOT_COVERED);
- }
+ ICounter.FULLY_COVERED);
+ assertLine("implicitExceptionFinally.finallyBlockEnd", ICounter.EMPTY);
assertLine("implicitExceptionFinally.afterBlock", ICounter.NOT_COVERED);
// 9. Finally Block With Exception Thrown Explicitly
@@ -159,13 +123,8 @@ public class ExceptionsTest extends ValidationTestBase {
isJDKCompiler ? ICounter.EMPTY : ICounter.FULLY_COVERED);
assertLine("explicitExceptionFinally.finallyBlock",
ICounter.FULLY_COVERED);
- if (!isJDKCompiler || isJDK8u152 || isJDK10) {
- assertLine("explicitExceptionFinally.finallyBlockEnd",
- ICounter.FULLY_COVERED);
- } else {
- assertLine("explicitExceptionFinally.finallyBlockEnd",
- ICounter.EMPTY);
- }
+ assertLine("explicitExceptionFinally.finallyBlockEnd",
+ isJDKCompiler ? ICounter.EMPTY : ICounter.FULLY_COVERED);
assertLine("explicitExceptionFinally.afterBlock", ICounter.EMPTY);
}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java
index 97b49e4b..8200fcf7 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/targets/Stubs.java
@@ -108,6 +108,15 @@ public class Stubs {
}
/**
+ * Throws a {@link RuntimeException} if given argument is <code>true</code>.
+ */
+ public static void ex(boolean t) {
+ if (t) {
+ throw new StubException();
+ }
+ }
+
+ /**
* Directly executes the given runnable.
*/
public static void exec(Runnable task) {
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 4b2a64a9..8696151a 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
@@ -29,7 +29,7 @@ public final class Filters implements IFilter {
public static final IFilter ALL = new Filters(new EnumFilter(),
new SyntheticFilter(), new SynchronizedFilter(),
new TryWithResourcesJavacFilter(), new TryWithResourcesEcjFilter(),
- new PrivateEmptyNoArgConstructorFilter(),
+ new FinallyFilter(), new PrivateEmptyNoArgConstructorFilter(),
new StringSwitchJavacFilter(), new LombokGeneratedFilter(),
new GroovyGeneratedFilter());
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/FinallyFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/FinallyFilter.java
new file mode 100644
index 00000000..0ab6ca2f
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/FinallyFilter.java
@@ -0,0 +1,227 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.internal.analysis.filter;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.JumpInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.TryCatchBlockNode;
+import org.objectweb.asm.tree.VarInsnNode;
+
+/**
+ * Filters duplicates of finally blocks that compiler generates.
+ *
+ * To understand algorithm of filtering, consider following example:
+ *
+ * <pre>
+ * try {
+ * if (x) {
+ * a();
+ * return; // 1
+ * }
+ * b(); // 2
+ * } catch (Exception e) {
+ * c(); // 3
+ * } finally {
+ * d(); // 4
+ * }
+ * </pre>
+ *
+ * There are 4 <b>distinct</b> points of exit out of these "try/catch/finally"
+ * blocks - three without exception, and one with Throwable if it is thrown
+ * prior to reaching first three points of exit.
+ *
+ * "finally" block must be executed just before these points, so there must be 4
+ * copies of its bytecode instructions.
+ *
+ * One of them handles Throwable ("catch-any") and must cover all instructions
+ * of "try/catch" blocks. But must not cover instructions of other duplicates,
+ * because instructions of "finally" block also can cause Throwable to be
+ * thrown.
+ *
+ * Therefore there will be multiple {@link MethodNode#tryCatchBlocks} with
+ * {@link TryCatchBlockNode#type} null with same
+ * {@link TryCatchBlockNode#handler} for different non-intersecting bytecode
+ * regions ({@link TryCatchBlockNode#start}, {@link TryCatchBlockNode#end}).
+ *
+ * And each exit out of these regions, except one that handles Throwable, will
+ * contain duplicate of "finally" block.
+ *
+ * To determine exits out of these regions, they all must be processed together
+ * at once, because execution can branch from one region to another (like it is
+ * in given example due to "if" statement).
+ */
+public final class FinallyFilter implements IFilter {
+
+ public void filter(final String className, final String superClassName,
+ final MethodNode methodNode, final IFilterOutput output) {
+ for (final TryCatchBlockNode tryCatchBlock : methodNode.tryCatchBlocks) {
+ if (tryCatchBlock.type == null) {
+ filter(output, methodNode.tryCatchBlocks, tryCatchBlock);
+ }
+ }
+ }
+
+ private static void filter(final IFilterOutput output,
+ final List<TryCatchBlockNode> tryCatchBlocks,
+ final TryCatchBlockNode catchAnyBlock) {
+ final AbstractInsnNode e = next(catchAnyBlock.handler);
+ final int size = size(e);
+ if (size <= 0) {
+ return;
+ }
+
+ // Determine instructions inside regions
+ final Set<AbstractInsnNode> inside = new HashSet<AbstractInsnNode>();
+ for (final TryCatchBlockNode t : tryCatchBlocks) {
+ if (t.handler == catchAnyBlock.handler) {
+ AbstractInsnNode i = t.start;
+ while (i != t.end) {
+ inside.add(i);
+ i = i.getNext();
+ }
+ }
+ }
+
+ // Find and merge duplicates at exits of regions
+ for (final TryCatchBlockNode t : tryCatchBlocks) {
+ if (t.handler == catchAnyBlock.handler) {
+ boolean continues = false;
+ AbstractInsnNode i = t.start;
+
+ while (i != t.end) {
+ switch (i.getType()) {
+ case AbstractInsnNode.FRAME:
+ case AbstractInsnNode.LINE:
+ case AbstractInsnNode.LABEL:
+ break;
+ case AbstractInsnNode.JUMP_INSN:
+ final AbstractInsnNode jumpTarget = next(
+ ((JumpInsnNode) i).label);
+ if (!inside.contains(jumpTarget)) {
+ merge(output, size, e, jumpTarget);
+ }
+ continues = i.getOpcode() != Opcodes.GOTO;
+ break;
+ default:
+ switch (i.getOpcode()) {
+ case Opcodes.IRETURN:
+ case Opcodes.LRETURN:
+ case Opcodes.FRETURN:
+ case Opcodes.DRETURN:
+ case Opcodes.ARETURN:
+ case Opcodes.RETURN:
+ case Opcodes.ATHROW:
+ continues = false;
+ break;
+ default:
+ continues = true;
+ break;
+ }
+ break;
+ }
+ i = i.getNext();
+ }
+
+ i = next(i);
+ if (continues && !inside.contains(i)) {
+ merge(output, size, e, i);
+ }
+ }
+
+ if (t != catchAnyBlock && t.start == catchAnyBlock.start
+ && t.end == catchAnyBlock.end) {
+ final AbstractInsnNode i = next(next(t.handler));
+ if (!inside.contains(i)) {
+ // javac's empty catch - merge after ASTORE
+ merge(output, size, e, i);
+ }
+ }
+ }
+ }
+
+ private static void merge(final IFilterOutput output, final int size,
+ AbstractInsnNode e, AbstractInsnNode n) {
+ if (!isSame(size, e, n)) {
+ return;
+ }
+ output.ignore(e, e);
+ e = next(e);
+ for (int i = 0; i < size; i++) {
+ output.merge(e, n);
+ e = next(e);
+ n = next(n);
+ }
+ output.ignore(e, next(e));
+
+ if (n != null && n.getOpcode() == Opcodes.GOTO) {
+ // goto instructions at the end of non-executed duplicates
+ // cause partial coverage of last line of finally block,
+ // so should be ignored
+ output.ignore(n, n);
+ }
+ }
+
+ private static boolean isSame(final int size, AbstractInsnNode e,
+ AbstractInsnNode n) {
+ e = next(e);
+ for (int i = 0; i < size; i++) {
+ if (n == null || e.getOpcode() != n.getOpcode()) {
+ return false;
+ }
+ e = next(e);
+ n = next(n);
+ }
+ return true;
+ }
+
+ /**
+ * @return number of instructions inside given "catch-any" handler
+ */
+ private static int size(AbstractInsnNode i) {
+ if (Opcodes.ASTORE != i.getOpcode()) {
+ // when always completes abruptly
+ return 0;
+ }
+ final int var = ((VarInsnNode) i).var;
+ int size = -1;
+ do {
+ size++;
+ i = next(i);
+ if (i == null) {
+ // when always completes abruptly
+ return 0;
+ }
+ } while (!(Opcodes.ALOAD == i.getOpcode()
+ && var == ((VarInsnNode) i).var));
+ i = next(i);
+ if (Opcodes.ATHROW != i.getOpcode()) {
+ return 0;
+ }
+ return size;
+ }
+
+ private static AbstractInsnNode next(AbstractInsnNode i) {
+ do {
+ i = i.getNext();
+ } while (i != null && (AbstractInsnNode.FRAME == i.getType()
+ || AbstractInsnNode.LABEL == i.getType()
+ || AbstractInsnNode.LINE == i.getType()));
+ return i;
+ }
+
+}
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index 9035dc36..ee78110f 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -34,6 +34,9 @@
<li>Exclude from a report a part of bytecode that compiler generates for a
try-with-resources statement
(GitHub <a href="https://github.com/jacoco/jacoco/issues/500">#500</a>).</li>
+ <li>During creation of a report merge duplicate instructions that compiler generates
+ for finally blocks
+ (GitHub <a href="https://github.com/jacoco/jacoco/issues/604">#604</a>).</li>
<li>Exclude from a report methods which are annotated with <code>@lombok.Generated</code>.
Initial analysis and contribution by RĂ¼diger zu Dohna
(GitHub <a href="https://github.com/jacoco/jacoco/issues/513">#513</a>).</li>