Skip to content

Commit fbd0099

Browse files
Godinmarchof
authored andcommitted
Add filter for Records (jacoco#990)
1 parent 6bcce69 commit fbd0099

File tree

7 files changed

+372
-1
lines changed

7 files changed

+372
-1
lines changed

org.jacoco.core.test.validation.java14/pom.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,25 @@
3535
<version>${project.version}</version>
3636
</dependency>
3737
</dependencies>
38+
39+
<build>
40+
<plugins>
41+
<plugin>
42+
<groupId>org.apache.maven.plugins</groupId>
43+
<artifactId>maven-compiler-plugin</artifactId>
44+
<configuration>
45+
<compilerArgs>
46+
<arg>--enable-preview</arg>
47+
</compilerArgs>
48+
</configuration>
49+
</plugin>
50+
<plugin>
51+
<groupId>org.apache.maven.plugins</groupId>
52+
<artifactId>maven-surefire-plugin</artifactId>
53+
<configuration>
54+
<argLine>--enable-preview</argLine>
55+
</configuration>
56+
</plugin>
57+
</plugins>
58+
</build>
3859
</project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
3+
* This program and the accompanying materials are made available under
4+
* the terms of the Eclipse Public License 2.0 which is available at
5+
* http://www.eclipse.org/legal/epl-2.0
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Evgeny Mandrikov - initial API and implementation
11+
*
12+
*******************************************************************************/
13+
package org.jacoco.core.test.validation.java14;
14+
15+
import org.jacoco.core.test.validation.ValidationTestBase;
16+
import org.jacoco.core.test.validation.java14.targets.RecordsTarget;
17+
18+
/**
19+
* Test of code coverage for records.
20+
*/
21+
public class RecordsTest extends ValidationTestBase {
22+
23+
public RecordsTest() {
24+
super(RecordsTarget.class);
25+
}
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
3+
* This program and the accompanying materials are made available under
4+
* the terms of the Eclipse Public License 2.0 which is available at
5+
* http://www.eclipse.org/legal/epl-2.0
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Evgeny Mandrikov - initial API and implementation
11+
*
12+
*******************************************************************************/
13+
package org.jacoco.core.test.validation.java14.targets;
14+
15+
/**
16+
* This target exercises records.
17+
*/
18+
public class RecordsTarget {
19+
20+
private record WithoutFields() { // assertFullyCovered()
21+
}
22+
23+
private record WithFields( // assertPartlyCovered()
24+
int x // assertEmpty()
25+
) {
26+
}
27+
28+
private record WithCustomMethods(int x) { // assertFullyCovered()
29+
public int x() {
30+
return x; // assertNotCovered()
31+
}
32+
33+
public String toString() {
34+
return ""; // assertNotCovered()
35+
}
36+
37+
public int hashCode() {
38+
return 0; // assertNotCovered()
39+
}
40+
41+
public boolean equals(Object object) {
42+
return false; // assertNotCovered()
43+
}
44+
}
45+
46+
public static void main(String[] args) {
47+
new WithoutFields();
48+
new WithFields(42);
49+
new WithCustomMethods(42);
50+
}
51+
52+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
3+
* This program and the accompanying materials are made available under
4+
* the terms of the Eclipse Public License 2.0 which is available at
5+
* http://www.eclipse.org/legal/epl-2.0
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Evgeny Mandrikov - initial API and implementation
11+
*
12+
*******************************************************************************/
13+
package org.jacoco.core.internal.analysis.filter;
14+
15+
import org.jacoco.core.internal.instr.InstrSupport;
16+
import org.junit.Test;
17+
import org.objectweb.asm.Handle;
18+
import org.objectweb.asm.Opcodes;
19+
import org.objectweb.asm.tree.MethodNode;
20+
21+
/**
22+
* Unit tests for {@link RecordsFilter}.
23+
*/
24+
public class RecordsFilterTest extends FilterTestBase {
25+
26+
private final RecordsFilter filter = new RecordsFilter();
27+
28+
@Test
29+
public void should_filter_generated_toString_method() {
30+
context.superClassName = "java/lang/Record";
31+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
32+
"toString", "()Ljava/lang/String;", null, null);
33+
m.visitVarInsn(Opcodes.ALOAD, 0);
34+
m.visitInvokeDynamicInsn("toString", "(LPoint;)Ljava/lang/String;",
35+
new Handle(Opcodes.H_INVOKESTATIC,
36+
"java/lang/runtime/ObjectMethods", "bootstrap",
37+
"(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;",
38+
false));
39+
m.visitInsn(Opcodes.ARETURN);
40+
41+
filter.filter(m, context, output);
42+
43+
assertMethodIgnored(m);
44+
}
45+
46+
@Test
47+
public void should_not_filter_custom_toString_method() {
48+
context.superClassName = "java/lang/Record";
49+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
50+
"toString", "()Ljava/lang/String;", null, null);
51+
m.visitLdcInsn("");
52+
m.visitInsn(Opcodes.ARETURN);
53+
54+
filter.filter(m, context, output);
55+
56+
assertIgnored();
57+
}
58+
59+
@Test
60+
public void should_not_filter_non_toString_method() {
61+
context.superClassName = "java/lang/Record";
62+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
63+
"toString", "()V", null, null);
64+
m.visitInsn(Opcodes.NOP);
65+
66+
filter.filter(m, context, output);
67+
68+
assertIgnored();
69+
}
70+
71+
@Test
72+
public void should_filter_generated_hashCode_method() {
73+
context.superClassName = "java/lang/Record";
74+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
75+
"hashCode", "()I", null, null);
76+
m.visitVarInsn(Opcodes.ALOAD, 0);
77+
m.visitInvokeDynamicInsn("hashCode", "(LPoint;)I", new Handle(
78+
Opcodes.H_INVOKESTATIC, "java/lang/runtime/ObjectMethods",
79+
"bootstrap",
80+
"(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;",
81+
false));
82+
m.visitInsn(Opcodes.IRETURN);
83+
84+
filter.filter(m, context, output);
85+
86+
assertMethodIgnored(m);
87+
}
88+
89+
@Test
90+
public void should_not_filter_custom_hashCode_method() {
91+
context.superClassName = "java/lang/Record";
92+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
93+
"hashCode", "()I", null, null);
94+
m.visitInsn(Opcodes.ICONST_0);
95+
m.visitInsn(Opcodes.IRETURN);
96+
97+
filter.filter(m, context, output);
98+
99+
assertIgnored();
100+
}
101+
102+
@Test
103+
public void should_not_filter_non_hashCode_method() {
104+
context.superClassName = "java/lang/Record";
105+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
106+
"hashCode", "()V", null, null);
107+
m.visitInsn(Opcodes.NOP);
108+
109+
filter.filter(m, context, output);
110+
111+
assertIgnored();
112+
}
113+
114+
@Test
115+
public void should_filter_generated_equals_method() {
116+
context.superClassName = "java/lang/Record";
117+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
118+
"equals", "(Ljava/lang/Object;)Z", null, null);
119+
m.visitVarInsn(Opcodes.ALOAD, 0);
120+
m.visitVarInsn(Opcodes.ALOAD, 1);
121+
m.visitInvokeDynamicInsn("equals", "(LPoint;Ljava/lang/Object;)Z",
122+
new Handle(Opcodes.H_INVOKESTATIC,
123+
"java/lang/runtime/ObjectMethods", "bootstrap",
124+
"(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;",
125+
false));
126+
m.visitInsn(Opcodes.IRETURN);
127+
128+
filter.filter(m, context, output);
129+
130+
assertMethodIgnored(m);
131+
}
132+
133+
@Test
134+
public void should_not_filter_custom_equals_method() {
135+
context.superClassName = "java/lang/Record";
136+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
137+
"equals", "(Ljava/lang/Object;)Z", null, null);
138+
m.visitInsn(Opcodes.ICONST_0);
139+
m.visitInsn(Opcodes.IRETURN);
140+
141+
filter.filter(m, context, output);
142+
143+
assertIgnored();
144+
}
145+
146+
@Test
147+
public void should_not_filter_non_equals_method() {
148+
context.superClassName = "java/lang/Record";
149+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
150+
"equals", "()V", null, null);
151+
m.visitInsn(Opcodes.NOP);
152+
153+
filter.filter(m, context, output);
154+
155+
assertIgnored();
156+
}
157+
158+
@Test
159+
public void should_not_filter_non_records() {
160+
final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
161+
"toString", "()Ljava/lang/String;", null, null);
162+
m.visitVarInsn(Opcodes.ALOAD, 0);
163+
m.visitInvokeDynamicInsn("toString", "(LPoint;)Ljava/lang/String;",
164+
new Handle(Opcodes.H_INVOKESTATIC,
165+
"java/lang/runtime/ObjectMethods", "bootstrap",
166+
"(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;",
167+
false));
168+
m.visitInsn(Opcodes.ARETURN);
169+
170+
filter.filter(m, context, output);
171+
172+
assertIgnored();
173+
}
174+
175+
}

org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static IFilter all() {
3838
new TryWithResourcesEcjFilter(), new FinallyFilter(),
3939
new PrivateEmptyNoArgConstructorFilter(),
4040
new StringSwitchJavacFilter(), new StringSwitchEcjFilter(),
41-
new EnumEmptyConstructorFilter(),
41+
new EnumEmptyConstructorFilter(), new RecordsFilter(),
4242
new AnnotationGeneratedFilter(), new KotlinGeneratedFilter(),
4343
new KotlinLateinitFilter(), new KotlinWhenFilter(),
4444
new KotlinWhenStringFilter(),
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
3+
* This program and the accompanying materials are made available under
4+
* the terms of the Eclipse Public License 2.0 which is available at
5+
* http://www.eclipse.org/legal/epl-2.0
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Evgeny Mandrikov - initial API and implementation
11+
*
12+
*******************************************************************************/
13+
package org.jacoco.core.internal.analysis.filter;
14+
15+
import org.objectweb.asm.Handle;
16+
import org.objectweb.asm.Opcodes;
17+
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
18+
import org.objectweb.asm.tree.MethodNode;
19+
20+
/**
21+
* Filters methods <code>toString</code>, <code>hashCode</code> and
22+
* <code>equals</code> that compiler generates for records.
23+
*/
24+
public final class RecordsFilter implements IFilter {
25+
26+
public void filter(final MethodNode methodNode,
27+
final IFilterContext context, final IFilterOutput output) {
28+
if (!"java/lang/Record".equals(context.getSuperClassName())) {
29+
return;
30+
}
31+
final Matcher matcher = new Matcher();
32+
if (matcher.isEquals(methodNode) || matcher.isHashCode(methodNode)
33+
|| matcher.isToString(methodNode)) {
34+
output.ignore(methodNode.instructions.getFirst(),
35+
methodNode.instructions.getLast());
36+
}
37+
}
38+
39+
private static class Matcher extends AbstractMatcher {
40+
boolean isToString(final MethodNode m) {
41+
if (!"toString".equals(m.name)
42+
|| !"()Ljava/lang/String;".equals(m.desc)) {
43+
return false;
44+
}
45+
firstIsALoad0(m);
46+
nextIsInvokeDynamic("toString");
47+
nextIs(Opcodes.ARETURN);
48+
return cursor != null;
49+
}
50+
51+
boolean isHashCode(final MethodNode m) {
52+
if (!"hashCode".equals(m.name) || !"()I".equals(m.desc)) {
53+
return false;
54+
}
55+
firstIsALoad0(m);
56+
nextIsInvokeDynamic("hashCode");
57+
nextIs(Opcodes.IRETURN);
58+
return cursor != null;
59+
}
60+
61+
boolean isEquals(final MethodNode m) {
62+
if (!"equals".equals(m.name)
63+
|| !"(Ljava/lang/Object;)Z".equals(m.desc)) {
64+
return false;
65+
}
66+
firstIsALoad0(m);
67+
nextIs(Opcodes.ALOAD);
68+
nextIsInvokeDynamic("equals");
69+
nextIs(Opcodes.IRETURN);
70+
return cursor != null;
71+
}
72+
73+
private void nextIsInvokeDynamic(final String name) {
74+
nextIs(Opcodes.INVOKEDYNAMIC);
75+
if (cursor == null) {
76+
return;
77+
}
78+
final InvokeDynamicInsnNode i = (InvokeDynamicInsnNode) cursor;
79+
final Handle bsm = i.bsm;
80+
if (name.equals(i.name)
81+
&& "java/lang/runtime/ObjectMethods".equals(bsm.getOwner())
82+
&& "bootstrap".equals(bsm.getName())) {
83+
return;
84+
}
85+
cursor = null;
86+
}
87+
}
88+
89+
}

org.jacoco.doc/docroot/doc/changes.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ <h1>Change History</h1>
2020

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

23+
<h3>New Features</h3>
24+
<ul>
25+
<li>Methods <code>toString</code>, <code>hashCode</code> and <code>equals</code>
26+
generated by compiler for records are filtered out during generation of report
27+
(GitHub <a href="https://github.com/jacoco/jacoco/issues/990">#990</a>).</li>
28+
</ul>
29+
2330
<h3>Non-functional Changes</h3>
2431
<ul>
2532
<li>Support for Pack200 was removed in JDK 14. JaCoCo will now throw a detailed

0 commit comments

Comments
 (0)