From fbd009930c38344d08fcfe966165afa11412bf25 Mon Sep 17 00:00:00 2001
From: Evgeny Mandrikov <138671+Godin@users.noreply.github.com>
Date: Tue, 7 Jan 2020 10:05:04 +0100
Subject: [PATCH] Add filter for Records (#990)
---
.../pom.xml | 21 +++
.../test/validation/java14/RecordsTest.java | 27 +++
.../java14/targets/RecordsTarget.java | 52 ++++++
.../analysis/filter/RecordsFilterTest.java | 175 ++++++++++++++++++
.../internal/analysis/filter/Filters.java | 2 +-
.../analysis/filter/RecordsFilter.java | 89 +++++++++
org.jacoco.doc/docroot/doc/changes.html | 7 +
7 files changed, 372 insertions(+), 1 deletion(-)
create mode 100644 org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/RecordsTest.java
create mode 100644 org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/RecordsTarget.java
create mode 100644 org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/RecordsFilterTest.java
create mode 100644 org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/RecordsFilter.java
diff --git a/org.jacoco.core.test.validation.java14/pom.xml b/org.jacoco.core.test.validation.java14/pom.xml
index d0dc6cb5fb..385ee9c322 100644
--- a/org.jacoco.core.test.validation.java14/pom.xml
+++ b/org.jacoco.core.test.validation.java14/pom.xml
@@ -35,4 +35,25 @@
${project.version}
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ --enable-preview
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ --enable-preview
+
+
+
+
diff --git a/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/RecordsTest.java b/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/RecordsTest.java
new file mode 100644
index 0000000000..1f14ab3b23
--- /dev/null
+++ b/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/RecordsTest.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.test.validation.java14;
+
+import org.jacoco.core.test.validation.ValidationTestBase;
+import org.jacoco.core.test.validation.java14.targets.RecordsTarget;
+
+/**
+ * Test of code coverage for records.
+ */
+public class RecordsTest extends ValidationTestBase {
+
+ public RecordsTest() {
+ super(RecordsTarget.class);
+ }
+
+}
diff --git a/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/RecordsTarget.java b/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/RecordsTarget.java
new file mode 100644
index 0000000000..da2b8804c6
--- /dev/null
+++ b/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/RecordsTarget.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.test.validation.java14.targets;
+
+/**
+ * This target exercises records.
+ */
+public class RecordsTarget {
+
+ private record WithoutFields() { // assertFullyCovered()
+ }
+
+ private record WithFields( // assertPartlyCovered()
+ int x // assertEmpty()
+ ) {
+ }
+
+ private record WithCustomMethods(int x) { // assertFullyCovered()
+ public int x() {
+ return x; // assertNotCovered()
+ }
+
+ public String toString() {
+ return ""; // assertNotCovered()
+ }
+
+ public int hashCode() {
+ return 0; // assertNotCovered()
+ }
+
+ public boolean equals(Object object) {
+ return false; // assertNotCovered()
+ }
+ }
+
+ public static void main(String[] args) {
+ new WithoutFields();
+ new WithFields(42);
+ new WithCustomMethods(42);
+ }
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/RecordsFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/RecordsFilterTest.java
new file mode 100644
index 0000000000..b16778cd56
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/RecordsFilterTest.java
@@ -0,0 +1,175 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * 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.Handle;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.MethodNode;
+
+/**
+ * Unit tests for {@link RecordsFilter}.
+ */
+public class RecordsFilterTest extends FilterTestBase {
+
+ private final RecordsFilter filter = new RecordsFilter();
+
+ @Test
+ public void should_filter_generated_toString_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "toString", "()Ljava/lang/String;", null, null);
+ m.visitVarInsn(Opcodes.ALOAD, 0);
+ m.visitInvokeDynamicInsn("toString", "(LPoint;)Ljava/lang/String;",
+ new Handle(Opcodes.H_INVOKESTATIC,
+ "java/lang/runtime/ObjectMethods", "bootstrap",
+ "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;",
+ false));
+ m.visitInsn(Opcodes.ARETURN);
+
+ filter.filter(m, context, output);
+
+ assertMethodIgnored(m);
+ }
+
+ @Test
+ public void should_not_filter_custom_toString_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "toString", "()Ljava/lang/String;", null, null);
+ m.visitLdcInsn("");
+ m.visitInsn(Opcodes.ARETURN);
+
+ filter.filter(m, context, output);
+
+ assertIgnored();
+ }
+
+ @Test
+ public void should_not_filter_non_toString_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "toString", "()V", null, null);
+ m.visitInsn(Opcodes.NOP);
+
+ filter.filter(m, context, output);
+
+ assertIgnored();
+ }
+
+ @Test
+ public void should_filter_generated_hashCode_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "hashCode", "()I", null, null);
+ m.visitVarInsn(Opcodes.ALOAD, 0);
+ m.visitInvokeDynamicInsn("hashCode", "(LPoint;)I", new Handle(
+ Opcodes.H_INVOKESTATIC, "java/lang/runtime/ObjectMethods",
+ "bootstrap",
+ "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;",
+ false));
+ m.visitInsn(Opcodes.IRETURN);
+
+ filter.filter(m, context, output);
+
+ assertMethodIgnored(m);
+ }
+
+ @Test
+ public void should_not_filter_custom_hashCode_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "hashCode", "()I", null, null);
+ m.visitInsn(Opcodes.ICONST_0);
+ m.visitInsn(Opcodes.IRETURN);
+
+ filter.filter(m, context, output);
+
+ assertIgnored();
+ }
+
+ @Test
+ public void should_not_filter_non_hashCode_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "hashCode", "()V", null, null);
+ m.visitInsn(Opcodes.NOP);
+
+ filter.filter(m, context, output);
+
+ assertIgnored();
+ }
+
+ @Test
+ public void should_filter_generated_equals_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "equals", "(Ljava/lang/Object;)Z", null, null);
+ m.visitVarInsn(Opcodes.ALOAD, 0);
+ m.visitVarInsn(Opcodes.ALOAD, 1);
+ m.visitInvokeDynamicInsn("equals", "(LPoint;Ljava/lang/Object;)Z",
+ new Handle(Opcodes.H_INVOKESTATIC,
+ "java/lang/runtime/ObjectMethods", "bootstrap",
+ "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;",
+ false));
+ m.visitInsn(Opcodes.IRETURN);
+
+ filter.filter(m, context, output);
+
+ assertMethodIgnored(m);
+ }
+
+ @Test
+ public void should_not_filter_custom_equals_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "equals", "(Ljava/lang/Object;)Z", null, null);
+ m.visitInsn(Opcodes.ICONST_0);
+ m.visitInsn(Opcodes.IRETURN);
+
+ filter.filter(m, context, output);
+
+ assertIgnored();
+ }
+
+ @Test
+ public void should_not_filter_non_equals_method() {
+ context.superClassName = "java/lang/Record";
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "equals", "()V", null, null);
+ m.visitInsn(Opcodes.NOP);
+
+ filter.filter(m, context, output);
+
+ assertIgnored();
+ }
+
+ @Test
+ public void should_not_filter_non_records() {
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "toString", "()Ljava/lang/String;", null, null);
+ m.visitVarInsn(Opcodes.ALOAD, 0);
+ m.visitInvokeDynamicInsn("toString", "(LPoint;)Ljava/lang/String;",
+ new Handle(Opcodes.H_INVOKESTATIC,
+ "java/lang/runtime/ObjectMethods", "bootstrap",
+ "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;",
+ false));
+ m.visitInsn(Opcodes.ARETURN);
+
+ filter.filter(m, context, output);
+
+ assertIgnored();
+ }
+
+}
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 b7c95276f8..3ceff7bfa0 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
@@ -38,7 +38,7 @@ public static IFilter all() {
new TryWithResourcesEcjFilter(), new FinallyFilter(),
new PrivateEmptyNoArgConstructorFilter(),
new StringSwitchJavacFilter(), new StringSwitchEcjFilter(),
- new EnumEmptyConstructorFilter(),
+ new EnumEmptyConstructorFilter(), new RecordsFilter(),
new AnnotationGeneratedFilter(), new KotlinGeneratedFilter(),
new KotlinLateinitFilter(), new KotlinWhenFilter(),
new KotlinWhenStringFilter(),
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/RecordsFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/RecordsFilter.java
new file mode 100644
index 0000000000..db9e7818d4
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/RecordsFilter.java
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.internal.analysis.filter;
+
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.InvokeDynamicInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+/**
+ * Filters methods toString
, hashCode
and
+ * equals
that compiler generates for records.
+ */
+public final class RecordsFilter implements IFilter {
+
+ public void filter(final MethodNode methodNode,
+ final IFilterContext context, final IFilterOutput output) {
+ if (!"java/lang/Record".equals(context.getSuperClassName())) {
+ return;
+ }
+ final Matcher matcher = new Matcher();
+ if (matcher.isEquals(methodNode) || matcher.isHashCode(methodNode)
+ || matcher.isToString(methodNode)) {
+ output.ignore(methodNode.instructions.getFirst(),
+ methodNode.instructions.getLast());
+ }
+ }
+
+ private static class Matcher extends AbstractMatcher {
+ boolean isToString(final MethodNode m) {
+ if (!"toString".equals(m.name)
+ || !"()Ljava/lang/String;".equals(m.desc)) {
+ return false;
+ }
+ firstIsALoad0(m);
+ nextIsInvokeDynamic("toString");
+ nextIs(Opcodes.ARETURN);
+ return cursor != null;
+ }
+
+ boolean isHashCode(final MethodNode m) {
+ if (!"hashCode".equals(m.name) || !"()I".equals(m.desc)) {
+ return false;
+ }
+ firstIsALoad0(m);
+ nextIsInvokeDynamic("hashCode");
+ nextIs(Opcodes.IRETURN);
+ return cursor != null;
+ }
+
+ boolean isEquals(final MethodNode m) {
+ if (!"equals".equals(m.name)
+ || !"(Ljava/lang/Object;)Z".equals(m.desc)) {
+ return false;
+ }
+ firstIsALoad0(m);
+ nextIs(Opcodes.ALOAD);
+ nextIsInvokeDynamic("equals");
+ nextIs(Opcodes.IRETURN);
+ return cursor != null;
+ }
+
+ private void nextIsInvokeDynamic(final String name) {
+ nextIs(Opcodes.INVOKEDYNAMIC);
+ if (cursor == null) {
+ return;
+ }
+ final InvokeDynamicInsnNode i = (InvokeDynamicInsnNode) cursor;
+ final Handle bsm = i.bsm;
+ if (name.equals(i.name)
+ && "java/lang/runtime/ObjectMethods".equals(bsm.getOwner())
+ && "bootstrap".equals(bsm.getName())) {
+ return;
+ }
+ cursor = null;
+ }
+ }
+
+}
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index be195d9cad..430b23cb7e 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -20,6 +20,13 @@
Change History
Snapshot Build @qualified.bundle.version@ (@build.date@)
+New Features
+
+ - Methods
toString
, hashCode
and equals
+ generated by compiler for records are filtered out during generation of report
+ (GitHub #990).
+
+
Non-functional Changes
- Support for Pack200 was removed in JDK 14. JaCoCo will now throw a detailed