Skip to content

Commit

Permalink
Add filter for Records (#990)
Browse files Browse the repository at this point in the history
  • Loading branch information
Godin authored and marchof committed Jan 7, 2020
1 parent 6bcce69 commit fbd0099
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 1 deletion.
21 changes: 21 additions & 0 deletions org.jacoco.core.test.validation.java14/pom.xml
Expand Up @@ -35,4 +35,25 @@
<version>${project.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}

}
@@ -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);
}

}
@@ -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();
}

}
Expand Up @@ -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(),
Expand Down
@@ -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 <code>toString</code>, <code>hashCode</code> and
* <code>equals</code> 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;
}
}

}
7 changes: 7 additions & 0 deletions org.jacoco.doc/docroot/doc/changes.html
Expand Up @@ -20,6 +20,13 @@ <h1>Change History</h1>

<h2>Snapshot Build @qualified.bundle.version@ (@build.date@)</h2>

<h3>New Features</h3>
<ul>
<li>Methods <code>toString</code>, <code>hashCode</code> and <code>equals</code>
generated by compiler for records are filtered out during generation of report
(GitHub <a href="https://github.com/jacoco/jacoco/issues/990">#990</a>).</li>
</ul>

<h3>Non-functional Changes</h3>
<ul>
<li>Support for Pack200 was removed in JDK 14. JaCoCo will now throw a detailed
Expand Down

0 comments on commit fbd0099

Please sign in to comment.