Add filter for bytecode that javac generates for String in switch (#596)

diff --git a/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/StringSwitchTest.java b/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/StringSwitchTest.java
new file mode 100644
index 0000000..93bc55b
--- /dev/null
+++ b/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/StringSwitchTest.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * 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 org.jacoco.core.analysis.ICounter;
+import org.jacoco.core.test.filter.targets.StringSwitch;
+import org.jacoco.core.test.validation.ValidationTestBase;
+import org.junit.Test;
+
+/**
+ * Test of filtering of a bytecode that is generated for a String in switch
+ * statement.
+ */
+public class StringSwitchTest extends ValidationTestBase {
+
+	public StringSwitchTest() {
+		super("src-java7", StringSwitch.class);
+	}
+
+	/**
+	 * {@link StringSwitch#covered(String)}
+	 */
+	@Test
+	public void covered() {
+		if (isJDKCompiler) {
+			assertLine("covered.switch", ICounter.FULLY_COVERED, 0, 4);
+		} else {
+			assertLine("covered.switch", ICounter.PARTLY_COVERED, 2, 7);
+		}
+		assertLine("covered.case1", ICounter.FULLY_COVERED, 0, 0);
+		assertLine("covered.case2", ICounter.FULLY_COVERED, 0, 0);
+		assertLine("covered.case3", ICounter.FULLY_COVERED, 0, 0);
+		assertLine("covered.default", ICounter.FULLY_COVERED, 0, 0);
+	}
+
+	/**
+	 * {@link StringSwitch#notCovered(String)}
+	 */
+	@Test
+	public void notCovered() {
+		assertLine("notCovered", ICounter.NOT_COVERED, isJDKCompiler ? 4 : 9,
+				0);
+	}
+
+	/**
+	 * {@link StringSwitch#handwritten(String)}
+	 */
+	@Test
+	public void handwritten() {
+		assertLine("handwritten.firstSwitch", ICounter.FULLY_COVERED, 2, 1);
+		assertLine("handwritten.ignored", ICounter.FULLY_COVERED);
+		assertLine("handwritten.secondSwitch", ICounter.FULLY_COVERED, 3, 1);
+		assertLine("handwritten.case1", ICounter.FULLY_COVERED);
+		assertLine("handwritten.case2", ICounter.NOT_COVERED);
+	}
+
+}
diff --git a/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/targets/StringSwitch.java b/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/targets/StringSwitch.java
new file mode 100644
index 0000000..623c80e
--- /dev/null
+++ b/org.jacoco.core.test/src-java7/org/jacoco/core/test/filter/targets/StringSwitch.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * 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.nop;
+
+/**
+ * This test target is a switch statement with a String.
+ */
+public class StringSwitch {
+
+	private static void covered(String s) {
+		switch (s) { // $line-covered.switch$
+		case "a":
+			nop("case a"); // $line-covered.case1$
+			break;
+		case "b":
+			nop("case b"); // $line-covered.case2$
+			break;
+		case "\0a":
+			nop("case \0a"); // $line-covered.case3$
+			break;
+		default:
+			nop("case default"); // $line-covered.default$
+			break;
+		}
+	}
+
+	private static void notCovered(String s) {
+		switch (s) { // $line-notCovered$
+		case "a":
+			nop("case a");
+			break;
+		case "b":
+			nop("case b");
+			break;
+		case "\0a":
+			nop("case \0a");
+			break;
+		default:
+			nop("default");
+			break;
+		}
+	}
+
+	private static void handwritten(String s) {
+		int c = -1;
+		switch (s.hashCode()) { // $line-handwritten.firstSwitch$
+		case 97:
+			if ("a".equals(s)) { // $line-handwritten.ignored$
+				c = 0;
+			} else if ("\0a".equals(s)) {
+				c = 1;
+			}
+			break;
+		case 98:
+			if ("b".equals(s)) {
+				c = 2;
+			}
+			break;
+		}
+		switch (c) { // $line-handwritten.secondSwitch$
+		case 0:
+			nop("case a"); // $line-handwritten.case1$
+			break;
+		case 1:
+			nop("case \0a"); // $line-handwritten.case2$
+			break;
+		case 2:
+			nop("case b");
+			break;
+		default:
+			nop("default");
+			break;
+		}
+	}
+
+	public static void main(String[] args) {
+		covered("");
+		covered("a");
+		covered("b");
+		covered("\0a");
+
+		handwritten("a");
+	}
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/StringSwitchJavacFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/StringSwitchJavacFilterTest.java
new file mode 100644
index 0000000..5a7cdef
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/StringSwitchJavacFilterTest.java
@@ -0,0 +1,155 @@
+/*******************************************************************************
+ * 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 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class StringSwitchJavacFilterTest implements IFilterOutput {
+
+	private final IFilter filter = new StringSwitchJavacFilter();
+
+	private final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+			"name", "()V", null, null);
+
+	private AbstractInsnNode fromInclusive;
+	private AbstractInsnNode toInclusive;
+
+	@Test
+	public void should_filter_code_generated_by_javac() {
+		final Label h1 = new Label();
+		final Label h1_2 = new Label();
+		final Label h2 = new Label();
+		final Label secondSwitch = new Label();
+		final Label cases = new Label();
+
+		m.visitInsn(Opcodes.ICONST_M1);
+		m.visitVarInsn(Opcodes.ISTORE, 2);
+
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "hashCode",
+				"()I", false);
+		m.visitLookupSwitchInsn(secondSwitch, new int[] { 97, 98 },
+				new Label[] { h1, h2 });
+		final AbstractInsnNode fromInclusive = m.instructions.getLast();
+
+		m.visitLabel(h1);
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitLdcInsn("a");
+		m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals",
+				"(Ljava/lang/Object;)Z", false);
+		// if not equal "a", then goto next comparison
+		m.visitJumpInsn(Opcodes.IFEQ, h1_2);
+		m.visitInsn(Opcodes.ICONST_0);
+		m.visitVarInsn(Opcodes.ISTORE, 2);
+
+		// goto secondSwitch
+		m.visitJumpInsn(Opcodes.GOTO, secondSwitch);
+
+		m.visitLabel(h1_2);
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitLdcInsn("\0a");
+		m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals",
+				"(Ljava/lang/Object;)Z", false);
+		// if not equal "\0a", then goto second switch
+		m.visitJumpInsn(Opcodes.IFEQ, secondSwitch);
+		m.visitInsn(Opcodes.ICONST_1);
+		m.visitVarInsn(Opcodes.ISTORE, 2);
+
+		// goto secondSwitch
+		m.visitJumpInsn(Opcodes.GOTO, secondSwitch);
+
+		m.visitLabel(h2);
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitLdcInsn("b");
+		m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals",
+				"(Ljava/lang/Object;)Z", false);
+		// if not equal "b", then goto second switch
+		m.visitJumpInsn(Opcodes.IFEQ, secondSwitch);
+		m.visitInsn(Opcodes.ICONST_2);
+		m.visitVarInsn(Opcodes.ISTORE, 2);
+
+		m.visitLabel(secondSwitch);
+		final AbstractInsnNode toInclusive = m.instructions.getLast();
+		m.visitVarInsn(Opcodes.ILOAD, 2);
+		m.visitTableSwitchInsn(0, 2, cases);
+		m.visitLabel(cases);
+
+		filter.filter("Foo", "java/lang/Object", m, this);
+
+		assertEquals(fromInclusive, this.fromInclusive);
+		assertEquals(toInclusive, this.toInclusive);
+	}
+
+	@Test
+	public void should_not_filter_code_generated_by_ECJ() {
+		final Label h1 = new Label();
+		final Label h2 = new Label();
+		final Label cases = new Label();
+
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "hashCode",
+				"()I", false);
+		m.visitTableSwitchInsn(0, 2, cases, h1, h2);
+
+		m.visitLabel(h1);
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitLdcInsn("a");
+		m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals",
+				"(Ljava/lang/Object;)Z", false);
+		// if equal "a", then goto its case
+		m.visitJumpInsn(Opcodes.IFNE, cases);
+
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitLdcInsn("\0a");
+		m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals",
+				"(Ljava/lang/Object;)Z", false);
+		// if equal "\0a", then goto its case
+		m.visitJumpInsn(Opcodes.IFNE, cases);
+
+		// goto default case
+		m.visitJumpInsn(Opcodes.GOTO, cases);
+
+		m.visitLabel(h2);
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitLdcInsn("b");
+		m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals",
+				"(Ljava/lang/Object;)Z", false);
+		// if equal "b", then goto its case
+		m.visitJumpInsn(Opcodes.IFNE, cases);
+
+		// goto default case
+		m.visitJumpInsn(Opcodes.GOTO, cases);
+
+		m.visitLabel(cases);
+
+		filter.filter("Foo", "java/lang/Object", m, this);
+
+		assertNull(this.fromInclusive);
+		assertNull(this.toInclusive);
+	}
+
+	public void ignore(final AbstractInsnNode fromInclusive,
+			final AbstractInsnNode toInclusive) {
+		assertNull(this.fromInclusive);
+		this.fromInclusive = fromInclusive;
+		this.toInclusive = toInclusive;
+	}
+
+}
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/MethodAnalyzer.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/MethodAnalyzer.java
index 8e07e56..328d079 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/MethodAnalyzer.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/MethodAnalyzer.java
@@ -24,6 +24,7 @@
 import org.jacoco.core.internal.analysis.filter.IFilterOutput;
 import org.jacoco.core.internal.analysis.filter.LombokGeneratedFilter;
 import org.jacoco.core.internal.analysis.filter.PrivateEmptyNoArgConstructorFilter;
+import org.jacoco.core.internal.analysis.filter.StringSwitchJavacFilter;
 import org.jacoco.core.internal.analysis.filter.SynchronizedFilter;
 import org.jacoco.core.internal.analysis.filter.SyntheticFilter;
 import org.jacoco.core.internal.analysis.filter.TryWithResourcesEcjFilter;
@@ -49,7 +50,8 @@
 	private static final IFilter[] FILTERS = new IFilter[] { new EnumFilter(),
 			new SyntheticFilter(), new SynchronizedFilter(),
 			new TryWithResourcesJavacFilter(), new TryWithResourcesEcjFilter(),
-			new PrivateEmptyNoArgConstructorFilter(), new LombokGeneratedFilter() };
+			new PrivateEmptyNoArgConstructorFilter(),
+			new StringSwitchJavacFilter(), new LombokGeneratedFilter() };
 
 	private final String className;
 
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/AbstractMatcher.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/AbstractMatcher.java
index 50a295d..c5edb2e 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/AbstractMatcher.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/AbstractMatcher.java
@@ -25,14 +25,13 @@
 
 	AbstractInsnNode cursor;
 
-	final void nextIsAddSuppressed() {
+	final void nextIsInvokeVirtual(final String owner, final String name) {
 		nextIs(Opcodes.INVOKEVIRTUAL);
 		if (cursor == null) {
 			return;
 		}
 		final MethodInsnNode m = (MethodInsnNode) cursor;
-		if ("java/lang/Throwable".equals(m.owner)
-				&& "addSuppressed".equals(m.name)) {
+		if (owner.equals(m.owner) && name.equals(m.name)) {
 			return;
 		}
 		cursor = null;
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/StringSwitchJavacFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/StringSwitchJavacFilter.java
new file mode 100644
index 0000000..2824fce
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/StringSwitchJavacFilter.java
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * 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 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.LookupSwitchInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.TableSwitchInsnNode;
+
+/**
+ * Filters code that is generated by javac for a switch statement with a String.
+ */
+public final class StringSwitchJavacFilter implements IFilter {
+
+	public void filter(final String className, final String superClassName,
+			final MethodNode methodNode, final IFilterOutput output) {
+		AbstractInsnNode i = methodNode.instructions.getFirst();
+		while (i != null) {
+			filter(i, output);
+			i = i.getNext();
+		}
+	}
+
+	/**
+	 * javac generates two switches. First one by {@link String#hashCode()}.
+	 * Number of handlers in the second switch is equal to number of handlers in
+	 * source code, so it is enough to completely filter-out first switch.
+	 * Handler for default case of the first switch - is the second switch.
+	 */
+	private void filter(final AbstractInsnNode start,
+			final IFilterOutput output) {
+		final LabelNode dflt;
+		if (start.getOpcode() == Opcodes.LOOKUPSWITCH) {
+			dflt = ((LookupSwitchInsnNode) start).dflt;
+		} else if (start.getOpcode() == Opcodes.TABLESWITCH) {
+			dflt = ((TableSwitchInsnNode) start).dflt;
+		} else {
+			return;
+		}
+		if (new Matcher().match(start, dflt)) {
+			output.ignore(start, dflt);
+		}
+	}
+
+	private static class Matcher extends AbstractMatcher {
+		boolean match(final AbstractInsnNode start,
+				final AbstractInsnNode secondSwitchLabel) {
+			cursor = start;
+			for (int i = 0; cursor != null && i < 4; i++) {
+				cursor = cursor.getPrevious();
+			}
+			if (cursor == null || cursor.getOpcode() != Opcodes.ICONST_M1) {
+				return false;
+			}
+			nextIsVar(Opcodes.ISTORE, "c");
+			nextIsVar(Opcodes.ALOAD, "s");
+			nextIsInvokeVirtual("java/lang/String", "hashCode");
+			next();
+			while (true) {
+				nextIsVar(Opcodes.ALOAD, "s");
+				nextIs(Opcodes.LDC);
+				nextIsInvokeVirtual("java/lang/String", "equals");
+				// jump to next comparison or second switch
+				nextIs(Opcodes.IFEQ);
+				// ICONST, BIPUSH or SIPUSH
+				next();
+				nextIsVar(Opcodes.ISTORE, "c");
+				if (cursor == null) {
+					return false;
+				}
+				if (cursor.getNext() == secondSwitchLabel) {
+					break;
+				}
+				nextIs(Opcodes.GOTO);
+				if (((JumpInsnNode) cursor).label != secondSwitchLabel) {
+					return false;
+				}
+			}
+			nextIsVar(Opcodes.ILOAD, "c");
+			nextIs(Opcodes.TABLESWITCH);
+			return cursor != null;
+		}
+	}
+
+}
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/TryWithResourcesEcjFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/TryWithResourcesEcjFilter.java
index 7d7d396..aeceeae 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/TryWithResourcesEcjFilter.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/TryWithResourcesEcjFilter.java
@@ -200,7 +200,7 @@
 			// "primaryExc.addSuppressed(suppressedExc)"
 			nextIsVar(Opcodes.ALOAD, "primaryExc");
 			nextIsVar(Opcodes.ALOAD, suppressedExc);
-			nextIsAddSuppressed();
+			nextIsInvokeVirtual("java/lang/Throwable", "addSuppressed");
 			nextIsLabel(endLabel);
 			return cursor != null;
 		}
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/TryWithResourcesJavacFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/TryWithResourcesJavacFilter.java
index 02ed47e..49cacb3 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/TryWithResourcesJavacFilter.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/TryWithResourcesJavacFilter.java
@@ -214,7 +214,7 @@
 				// "primaryExc.addSuppressed(t)"
 				nextIsVar(Opcodes.ALOAD, "primaryExc");
 				nextIsVar(Opcodes.ALOAD, ctx + "t");
-				nextIsAddSuppressed();
+				nextIsInvokeVirtual("java/lang/Throwable", "addSuppressed");
 				nextIs(Opcodes.GOTO);
 				// "r.close()"
 				nextIsClose();
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index dc8d06b..5ae14dd 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -39,6 +39,9 @@
       (GitHub <a href="https://github.com/jacoco/jacoco/issues/513">#513</a>).</li>
   <li>Exclude from a report private empty constructors that do not have arguments
       (GitHub <a href="https://github.com/jacoco/jacoco/issues/529">#529</a>).</li>
+  <li>Exclude from a report a part of bytecode that javac generates for a
+      String in switch statement
+      (GitHub <a href="https://github.com/jacoco/jacoco/issues/596">#596</a>).</li>
   <li>Maven aggregated reports will now also include modules of <code>runtime</code>
       and <code>provided</code> dependencies
       (GitHub <a href="https://github.com/jacoco/jacoco/issues/498">#498</a>,