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

+ +

Non-functional Changes