Add filter for empty constructor without parameters in enum (#649)

diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/EnumEmptyConstructorFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/EnumEmptyConstructorFilterTest.java
new file mode 100644
index 0000000..a221654
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/EnumEmptyConstructorFilterTest.java
@@ -0,0 +1,151 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2018 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.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;
+import static org.junit.Assert.fail;
+
+public class EnumEmptyConstructorFilterTest implements IFilterOutput {
+
+	private final EnumEmptyConstructorFilter filter = new EnumEmptyConstructorFilter();
+
+	private AbstractInsnNode fromInclusive;
+	private AbstractInsnNode toInclusive;
+
+	@Test
+	public void should_filter() {
+		final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION,
+				Opcodes.ACC_PRIVATE, "<init>", "(Ljava/lang/String;I)V", null,
+				null);
+		m.visitVarInsn(Opcodes.ALOAD, 0);
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitVarInsn(Opcodes.ILOAD, 2);
+		m.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Enum", "<init>",
+				"(Ljava/lang/String;I)V", false);
+		m.visitInsn(Opcodes.RETURN);
+
+		filter.filter("Foo", "java/lang/Enum", m, this);
+
+		assertEquals(m.instructions.getFirst(), fromInclusive);
+		assertEquals(m.instructions.getLast(), toInclusive);
+	}
+
+	/**
+	 * <code><pre>
+	 * enum E {
+	 *   ;
+	 *   private E() {
+	 *     ...
+	 *   }
+	 * }
+	 * </pre></code>
+	 */
+	@Test
+	public void should_not_filter_non_empty_constructor() {
+		final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION,
+				Opcodes.ACC_PRIVATE, "<init>", "(Ljava/lang/String;I)V", null,
+				null);
+		m.visitVarInsn(Opcodes.ALOAD, 0);
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitVarInsn(Opcodes.ILOAD, 2);
+		m.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Enum", "<init>",
+				"(Ljava/lang/String;I)V", false);
+		m.visitInsn(Opcodes.NOP);
+		m.visitInsn(Opcodes.RETURN);
+
+		filter.filter("Foo", "java/lang/Enum", m, this);
+
+		assertNull(fromInclusive);
+		assertNull(toInclusive);
+	}
+
+	/**
+	 * <code><pre>
+	 * enum E {
+	 *   ;
+	 *   private E(long p) {
+	 *   }
+	 * }
+	 * </pre></code>
+	 */
+	@Test
+	public void should_not_filter_constructor_with_additional_parameters() {
+		final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION,
+				Opcodes.ACC_PRIVATE, "<init>", "(Ljava/lang/String;IJ)V", null,
+				null);
+		m.visitVarInsn(Opcodes.ALOAD, 0);
+		m.visitVarInsn(Opcodes.ALOAD, 1);
+		m.visitVarInsn(Opcodes.ILOAD, 2);
+		m.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Enum", "<init>",
+				"(Ljava/lang/String;I)V", false);
+		m.visitInsn(Opcodes.RETURN);
+
+		filter.filter("Foo", "java/lang/Enum", m, this);
+
+		assertNull(fromInclusive);
+		assertNull(toInclusive);
+	}
+
+	/**
+	 * <code><pre>
+	 * enum E {
+	 *   ;
+	 *   private void method(String p1, int p2) {
+	 *   }
+	 * }
+	 * </pre></code>
+	 */
+	@Test
+	public void should_not_filter_non_constructor() {
+		final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION,
+				Opcodes.ACC_PRIVATE, "method", "(Ljava/lang/String;I)V", null,
+				null);
+		m.visitInsn(Opcodes.NOP);
+
+		filter.filter("Foo", "java/lang/Enum", m, this);
+
+		assertNull(fromInclusive);
+		assertNull(toInclusive);
+	}
+
+	@Test
+	public void should_not_filter_non_Enum() {
+		final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION,
+				Opcodes.ACC_PRIVATE, "<init>", "(Ljava/lang/String;I)V", null,
+				null);
+		m.visitInsn(Opcodes.NOP);
+
+		filter.filter("Foo", "java/lang/Object", m, this);
+
+		assertNull(fromInclusive);
+		assertNull(toInclusive);
+	}
+
+	public void ignore(AbstractInsnNode fromInclusive,
+			AbstractInsnNode toInclusive) {
+		assertNull(this.fromInclusive);
+		this.fromInclusive = fromInclusive;
+		this.toInclusive = toInclusive;
+	}
+
+	public void merge(AbstractInsnNode i1, AbstractInsnNode i2) {
+		fail();
+	}
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/filter/EnumConstructorTest.java b/org.jacoco.core.test/src/org/jacoco/core/test/filter/EnumConstructorTest.java
new file mode 100644
index 0000000..fd88787
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/filter/EnumConstructorTest.java
@@ -0,0 +1,62 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2018 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.EnumConstructor;
+import org.jacoco.core.test.validation.ValidationTestBase;
+import org.junit.Test;
+
+/**
+ * Test of filtering of enum constructors.
+ */
+public class EnumConstructorTest extends ValidationTestBase {
+
+	public EnumConstructorTest() {
+		super(EnumConstructor.class);
+	}
+
+	/**
+	 * {@link EnumConstructor.ImplicitConstructor}
+	 */
+	@Test
+	public void implicit_constructor_should_be_filtered() {
+		// without filter next line is partly covered:
+		assertLine("implicitConstructor", ICounter.FULLY_COVERED);
+	}
+
+	/**
+	 * {@link EnumConstructor.ExplicitNonEmptyConstructor#ExplicitNonEmptyConstructor()}
+	 */
+	@Test
+	public void explicit_non_empty_constructor_should_not_be_filtered() {
+		assertLine("explicitNonEmptyConstructor", ICounter.NOT_COVERED);
+	}
+
+	/**
+	 * {@link EnumConstructor.ExplicitEmptyConstructor#ExplicitEmptyConstructor()}
+	 */
+	@Test
+	public void explicit_empty_constructor_should_be_filtered() {
+		// without filter next line is not covered:
+		assertLine("explicitEmptyConstructor", ICounter.EMPTY);
+	}
+
+	/**
+	 * {@link EnumConstructor.ExplicitEmptyConstructor#ExplicitEmptyConstructor(Object)}
+	 */
+	@Test
+	public void explicit_empty_constructor_with_parameters_should_not_be_filtered() {
+		assertLine("explicitEmptyConstructorWithParameter", ICounter.NOT_COVERED);
+	}
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/EnumConstructor.java b/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/EnumConstructor.java
new file mode 100644
index 0000000..c6eb1c5
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/filter/targets/EnumConstructor.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2018 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 an enum constructor.
+ */
+public class EnumConstructor {
+
+	private enum ImplicitConstructor { // $line-implicitConstructor$
+	}
+
+	private enum ExplicitNonEmptyConstructor {
+		;
+
+		ExplicitNonEmptyConstructor() {
+			nop(); // $line-explicitNonEmptyConstructor$
+		}
+	}
+
+	@SuppressWarnings("unused")
+	private enum ExplicitEmptyConstructor {
+		;
+
+		ExplicitEmptyConstructor() {
+		} // $line-explicitEmptyConstructor$
+
+		ExplicitEmptyConstructor(Object p) {
+		} // $line-explicitEmptyConstructorWithParameter$
+	}
+
+	public static void main(String[] args) {
+		ImplicitConstructor.values();
+		ExplicitEmptyConstructor.values();
+		ExplicitNonEmptyConstructor.values();
+	}
+
+}
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 b5aea72..fcd1c88 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
@@ -17,6 +17,7 @@
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.tree.AbstractInsnNode;
 import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
 import org.objectweb.asm.tree.VarInsnNode;
 
 abstract class AbstractMatcher {
@@ -25,6 +26,35 @@
 
 	AbstractInsnNode cursor;
 
+	/**
+	 * Sets {@link #cursor} to first instruction of method if it is
+	 * <code>ALOAD 0</code>, otherwise sets it to <code>null</code>.
+	 */
+	final void firstIsALoad0(final MethodNode methodNode) {
+		cursor = methodNode.instructions.getFirst();
+		skipNonOpcodes();
+		if (cursor.getOpcode() == Opcodes.ALOAD
+				&& ((VarInsnNode) cursor).var == 0) {
+			return;
+		}
+		cursor = null;
+	}
+
+	/**
+	 * Moves {@link #cursor} to next instruction if it is
+	 * <code>INVOKESPECIAL &lt;init&gt;</code> with given owner and descriptor,
+	 * otherwise sets it to <code>null</code>.
+	 */
+	final void nextIsInvokeSuper(final String owner, final String desc) {
+		nextIs(Opcodes.INVOKESPECIAL);
+		MethodInsnNode m = (MethodInsnNode) cursor;
+		if (m != null && owner.equals(m.owner) && "<init>".equals(m.name)
+				&& desc.equals(m.desc)) {
+			return;
+		}
+		cursor = null;
+	}
+
 	final void nextIsInvokeVirtual(final String owner, final String name) {
 		nextIs(Opcodes.INVOKEVIRTUAL);
 		if (cursor == null) {
@@ -76,7 +106,7 @@
 		skipNonOpcodes();
 	}
 
-	final void skipNonOpcodes() {
+	private void skipNonOpcodes() {
 		while (cursor != null && (cursor.getType() == AbstractInsnNode.FRAME
 				|| cursor.getType() == AbstractInsnNode.LABEL
 				|| cursor.getType() == AbstractInsnNode.LINE)) {
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/EnumEmptyConstructorFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/EnumEmptyConstructorFilter.java
new file mode 100644
index 0000000..9bdc507
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/EnumEmptyConstructorFilter.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2018 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.MethodNode;
+
+/**
+ * Filters empty enum constructors.
+ *
+ * Constructor of enum is invoked from static initialization block to create
+ * instance of each enum constant. So it won't be executed if number of enum
+ * constants is zero. Such enums are sometimes used as alternative to classes
+ * with static utilities and private empty constructor. Implicit constructor of
+ * enum created by compiler doesn't have a synthetic flag and refers to a line
+ * of enum definition. Therefore in order to not have partial coverage of enum
+ * definition line in enums without enum constants and similarly to
+ * {@link PrivateEmptyNoArgConstructorFilter filter of private empty
+ * constructors} - empty constructor in enums without additional parameters
+ * should be filtered out even if it is not implicit.
+ */
+public final class EnumEmptyConstructorFilter implements IFilter {
+
+	private static final String CONSTRUCTOR_NAME = "<init>";
+	private static final String CONSTRUCTOR_DESC = "(Ljava/lang/String;I)V";
+
+	public void filter(String className, String superClassName,
+			MethodNode methodNode, IFilterOutput output) {
+		if ("java/lang/Enum".equals(superClassName)
+				&& CONSTRUCTOR_NAME.equals(methodNode.name)
+				&& CONSTRUCTOR_DESC.equals(methodNode.desc)
+				&& new Matcher().match(methodNode, superClassName)) {
+			output.ignore(methodNode.instructions.getFirst(),
+					methodNode.instructions.getLast());
+		}
+	}
+
+	private static class Matcher extends AbstractMatcher {
+		private boolean match(final MethodNode methodNode,
+				final String superClassName) {
+			firstIsALoad0(methodNode);
+			nextIs(Opcodes.ALOAD);
+			nextIs(Opcodes.ILOAD);
+			nextIsInvokeSuper(superClassName, CONSTRUCTOR_DESC);
+			nextIs(Opcodes.RETURN);
+			return cursor != null;
+		}
+	}
+
+}
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 0f34418..40c0dfc 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
@@ -31,7 +31,7 @@
 			new TryWithResourcesJavacFilter(), new TryWithResourcesEcjFilter(),
 			new FinallyFilter(), new PrivateEmptyNoArgConstructorFilter(),
 			new StringSwitchJavacFilter(), new LombokGeneratedFilter(),
-			new GroovyGeneratedFilter());
+			new GroovyGeneratedFilter(), new EnumEmptyConstructorFilter());
 
 	private final IFilter[] filters;
 
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/PrivateEmptyNoArgConstructorFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/PrivateEmptyNoArgConstructorFilter.java
index 08c654e..29214c5 100644
--- a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/PrivateEmptyNoArgConstructorFilter.java
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/PrivateEmptyNoArgConstructorFilter.java
@@ -12,20 +12,21 @@
 package org.jacoco.core.internal.analysis.filter;
 
 import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.tree.MethodInsnNode;
 import org.objectweb.asm.tree.MethodNode;
-import org.objectweb.asm.tree.VarInsnNode;
 
 /**
  * Filters private empty constructors that do not have arguments.
  */
 public final class PrivateEmptyNoArgConstructorFilter implements IFilter {
 
+	private static final String CONSTRUCTOR_NAME = "<init>";
+	private static final String CONSTRUCTOR_DESC = "()V";
+
 	public void filter(final String className, final String superClassName,
 			final MethodNode methodNode, final IFilterOutput output) {
 		if ((methodNode.access & Opcodes.ACC_PRIVATE) != 0
-				&& "<init>".equals(methodNode.name)
-				&& "()V".equals(methodNode.desc)
+				&& CONSTRUCTOR_NAME.equals(methodNode.name)
+				&& CONSTRUCTOR_DESC.equals(methodNode.desc)
 				&& new Matcher().match(methodNode, superClassName)) {
 			output.ignore(methodNode.instructions.getFirst(),
 					methodNode.instructions.getLast());
@@ -35,20 +36,10 @@
 	private static class Matcher extends AbstractMatcher {
 		private boolean match(final MethodNode methodNode,
 				final String superClassName) {
-			cursor = methodNode.instructions.getFirst();
-			skipNonOpcodes();
-			if (cursor.getOpcode() != Opcodes.ALOAD
-					|| ((VarInsnNode) cursor).var != 0) {
-				return false;
-			}
-			nextIs(Opcodes.INVOKESPECIAL);
-			MethodInsnNode m = (MethodInsnNode) cursor;
-			if (m != null && superClassName.equals(m.owner)
-					&& "<init>".equals(m.name) && ("()V").equals(m.desc)) {
-				nextIs(Opcodes.RETURN);
-				return cursor != null;
-			}
-			return false;
+			firstIsALoad0(methodNode);
+			nextIsInvokeSuper(superClassName, CONSTRUCTOR_DESC);
+			nextIs(Opcodes.RETURN);
+			return cursor != null;
 		}
 	}
 
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index 352e2d7..eebd90f 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -20,6 +20,13 @@
 
 <h2>Snapshot Build @qualified.bundle.version@ (@build.date@)</h2>
 
+<h3>New Features</h3>
+<ul>
+  <li>Empty constructor without parameters in enum is filtered out during
+      generation of report
+      (GitHub <a href="https://github.com/jacoco/jacoco/issues/649">#649</a>).</li>
+</ul>
+
 <h2>Release 0.8.0 (2018/01/02)</h2>
 
 <h3>New Features</h3>