aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Mandrikov <138671+Godin@users.noreply.github.com>2023-06-14 23:23:47 +0200
committerGitHub <noreply@github.com>2023-06-14 23:23:47 +0200
commit8271afb7520379535a42cd416855e1c41c2df1d6 (patch)
tree3a458190f011f5a73d42c7c4454c2b3c3353d934
parent41bc4ac859d76fc6a6c4f5fe451f0bc38c1a6111 (diff)
downloadjacoco-8271afb7520379535a42cd416855e1c41c2df1d6.tar.gz
Add filter for exhaustive switch expression (#1472)
-rw-r--r--jacoco/pom.xml4
-rw-r--r--org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/SwitchExpressionsTest.java16
-rw-r--r--org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/SwitchExpressionsTarget.java4
-rw-r--r--org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/ExhaustiveSwitchFilterTest.java314
-rw-r--r--org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/ExhaustiveSwitchFilter.java113
-rw-r--r--org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/Filters.java1
-rw-r--r--org.jacoco.doc/docroot/doc/changes.html3
7 files changed, 435 insertions, 20 deletions
diff --git a/jacoco/pom.xml b/jacoco/pom.xml
index 82956db3..65c7e39e 100644
--- a/jacoco/pom.xml
+++ b/jacoco/pom.xml
@@ -111,8 +111,8 @@
<configuration>
<rules>
<requireFilesSize>
- <maxsize>4500000</maxsize>
- <minsize>3400000</minsize>
+ <maxsize>4600000</maxsize>
+ <minsize>4000000</minsize>
<files>
<file>${project.build.directory}/jacoco-${qualified.bundle.version}.zip</file>
</files>
diff --git a/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/SwitchExpressionsTest.java b/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/SwitchExpressionsTest.java
index 0f47ad67..974eb87d 100644
--- a/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/SwitchExpressionsTest.java
+++ b/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/SwitchExpressionsTest.java
@@ -25,20 +25,4 @@ public class SwitchExpressionsTest extends ValidationTestBase {
super(SwitchExpressionsTarget.class);
}
- public void assertExhaustiveSwitchExpression(Line line) {
- if (isJDKCompiler) {
- assertPartlyCovered(line, 1, 3);
- } else {
- assertFullyCovered(line, 1, 3);
- }
- }
-
- public void assertExhaustiveSwitchExpressionLastCase(Line line) {
- if (isJDKCompiler) {
- assertFullyCovered(line);
- } else {
- assertPartlyCovered(line);
- }
- }
-
}
diff --git a/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/SwitchExpressionsTarget.java b/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/SwitchExpressionsTarget.java
index 48f7a61d..04715a5a 100644
--- a/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/SwitchExpressionsTarget.java
+++ b/org.jacoco.core.test.validation.java14/src/org/jacoco/core/test/validation/java14/targets/SwitchExpressionsTarget.java
@@ -101,10 +101,10 @@ public class SwitchExpressionsTarget {
private static void exhaustiveSwitchExpression(Stubs.Enum e) {
- nop(switch (e) { // assertExhaustiveSwitchExpression()
+ nop(switch (e) { // assertFullyCovered(0, 3)
case A -> i1(); // assertFullyCovered()
case B -> i1(); // assertFullyCovered()
- case C -> i1(); // assertExhaustiveSwitchExpressionLastCase()
+ case C -> i1(); // assertFullyCovered()
}); // assertEmpty()
}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/ExhaustiveSwitchFilterTest.java b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/ExhaustiveSwitchFilterTest.java
new file mode 100644
index 00000000..20620fff
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/internal/analysis/filter/ExhaustiveSwitchFilterTest.java
@@ -0,0 +1,314 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2023 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.Label;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link ExhaustiveSwitchFilter}.
+ */
+public class ExhaustiveSwitchFilterTest extends FilterTestBase {
+
+ private final IFilter filter = new ExhaustiveSwitchFilter();
+
+ /**
+ * <pre>
+ * enum E {
+ * A, B, C
+ * }
+ *
+ * int example(E e) {
+ * return switch (e) {
+ * case A -> 1;
+ * case B -> 2;
+ * case C -> 3;
+ * };
+ * }
+ * </pre>
+ */
+ @Test
+ public void should_filter_when_default_branch_has_LineNumber_of_switch() {
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "Example", "()I", null, null);
+
+ final Label start = new Label();
+ final Label end = new Label();
+ m.visitLabel(start);
+ m.visitLineNumber(0, start);
+ m.visitFieldInsn(Opcodes.GETSTATIC, "Example$1", "$SwitchMap$Example$E",
+ "[I");
+ m.visitVarInsn(Opcodes.ALOAD, 0);
+ m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "Example$E", "ordinal", "()I",
+ false);
+ m.visitInsn(Opcodes.IALOAD);
+
+ final Label dflt = new Label();
+ final Label case1 = new Label();
+ final Label case2 = new Label();
+ final Label case3 = new Label();
+ m.visitLookupSwitchInsn(dflt, new int[] { 1, 2, 3 },
+ new Label[] { case1, case2, case3 });
+ final AbstractInsnNode switchNode = m.instructions.getLast();
+ final Set<AbstractInsnNode> newTargets = new HashSet<AbstractInsnNode>();
+
+ m.visitLabel(dflt);
+ final Range range = new Range();
+ range.fromInclusive = m.instructions.getLast();
+ m.visitLineNumber(0, dflt);
+ m.visitTypeInsn(Opcodes.NEW, "java/lang/IncompatibleClassChangeError");
+ m.visitInsn(Opcodes.DUP);
+ m.visitMethodInsn(Opcodes.INVOKESPECIAL,
+ "java/lang/IncompatibleClassChangeError", "<init>", "()V",
+ false);
+ m.visitInsn(Opcodes.ATHROW);
+ range.toInclusive = m.instructions.getLast();
+
+ m.visitLabel(case1);
+ m.visitInsn(Opcodes.ICONST_1);
+ newTargets.add(m.instructions.getLast());
+ m.visitJumpInsn(Opcodes.GOTO, end);
+
+ m.visitLabel(case2);
+ m.visitInsn(Opcodes.ICONST_2);
+ newTargets.add(m.instructions.getLast());
+
+ m.visitLabel(case3);
+ m.visitInsn(Opcodes.ICONST_3);
+ newTargets.add(m.instructions.getLast());
+
+ m.visitLabel(end);
+ m.visitInsn(Opcodes.IRETURN);
+
+ filter.filter(m, context, output);
+
+ assertIgnored(range);
+ assertReplacedBranches(switchNode, newTargets);
+ }
+
+ /**
+ * <pre>
+ * enum E {
+ * A, B, C
+ * }
+ *
+ * int example(E e) {
+ * return switch (e) {
+ * case A -> 1;
+ * case B -> 2;
+ * case C -> 3;
+ * };
+ * }
+ * </pre>
+ */
+ @Test
+ public void should_filter_when_default_branch_has_no_LineNumber() {
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "Example", "()I", null, null);
+
+ final Label start = new Label();
+ final Label end = new Label();
+ m.visitLabel(start);
+ m.visitLineNumber(0, start);
+ m.visitFieldInsn(Opcodes.GETSTATIC, "Example$1", "$SwitchMap$Example$E",
+ "[I");
+ m.visitVarInsn(Opcodes.ALOAD, 0);
+ m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "Example$E", "ordinal", "()I",
+ false);
+ m.visitInsn(Opcodes.IALOAD);
+
+ final Label dflt = new Label();
+ final Label case1 = new Label();
+ final Label case2 = new Label();
+ final Label case3 = new Label();
+ m.visitLookupSwitchInsn(dflt, new int[] { 1, 2, 3 },
+ new Label[] { case1, case2, case3 });
+ final AbstractInsnNode switchNode = m.instructions.getLast();
+ final Set<AbstractInsnNode> newTargets = new HashSet<AbstractInsnNode>();
+
+ m.visitLabel(dflt);
+ final Range range = new Range();
+ range.fromInclusive = m.instructions.getLast();
+ m.visitTypeInsn(Opcodes.NEW, "java/lang/IncompatibleClassChangeError");
+ m.visitInsn(Opcodes.DUP);
+ m.visitMethodInsn(Opcodes.INVOKESPECIAL,
+ "java/lang/IncompatibleClassChangeError", "<init>", "()V",
+ false);
+ m.visitInsn(Opcodes.ATHROW);
+ range.toInclusive = m.instructions.getLast();
+
+ m.visitLabel(case1);
+ m.visitInsn(Opcodes.ICONST_1);
+ newTargets.add(m.instructions.getLast());
+ m.visitJumpInsn(Opcodes.GOTO, end);
+
+ m.visitLabel(case2);
+ m.visitInsn(Opcodes.ICONST_2);
+ newTargets.add(m.instructions.getLast());
+
+ m.visitLabel(case3);
+ m.visitInsn(Opcodes.ICONST_3);
+ newTargets.add(m.instructions.getLast());
+
+ m.visitLabel(end);
+ m.visitInsn(Opcodes.IRETURN);
+
+ filter.filter(m, context, output);
+
+ assertIgnored(range);
+ assertReplacedBranches(switchNode, newTargets);
+ }
+
+ /**
+ * <pre>
+ * enum E {
+ * A, B, C
+ * }
+ *
+ * int example(E e) {
+ * return switch (e) {
+ * case A -> 1;
+ * case B -> 2;
+ * case C -> 3;
+ * };
+ * }
+ * </pre>
+ */
+ @Test
+ public void should_filter_when_default_branch_throws_Java_21_MatchException() {
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "Example", "()I", null, null);
+
+ final Label start = new Label();
+ final Label end = new Label();
+ m.visitLabel(start);
+ m.visitLineNumber(0, start);
+ m.visitFieldInsn(Opcodes.GETSTATIC, "Example$1", "$SwitchMap$Example$E",
+ "[I");
+ m.visitVarInsn(Opcodes.ALOAD, 0);
+ m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "Example$E", "ordinal", "()I",
+ false);
+ m.visitInsn(Opcodes.IALOAD);
+
+ final Label dflt = new Label();
+ final Label case1 = new Label();
+ final Label case2 = new Label();
+ final Label case3 = new Label();
+ m.visitLookupSwitchInsn(dflt, new int[] { 1, 2, 3 },
+ new Label[] { case1, case2, case3 });
+ final AbstractInsnNode switchNode = m.instructions.getLast();
+ final Set<AbstractInsnNode> newTargets = new HashSet<AbstractInsnNode>();
+
+ m.visitLabel(dflt);
+ final Range range = new Range();
+ range.fromInclusive = m.instructions.getLast();
+ m.visitTypeInsn(Opcodes.NEW, "java/lang/MatchException");
+ m.visitInsn(Opcodes.DUP);
+ m.visitInsn(Opcodes.ACONST_NULL);
+ m.visitInsn(Opcodes.ACONST_NULL);
+ m.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/MatchException",
+ "<init>", "(Ljava/lang/String;Ljava/lang/Throwable;)V", false);
+ m.visitInsn(Opcodes.ATHROW);
+ range.toInclusive = m.instructions.getLast();
+
+ m.visitLabel(case1);
+ m.visitInsn(Opcodes.ICONST_1);
+ newTargets.add(m.instructions.getLast());
+ m.visitJumpInsn(Opcodes.GOTO, end);
+
+ m.visitLabel(case2);
+ m.visitInsn(Opcodes.ICONST_2);
+ newTargets.add(m.instructions.getLast());
+
+ m.visitLabel(case3);
+ m.visitInsn(Opcodes.ICONST_3);
+ newTargets.add(m.instructions.getLast());
+
+ m.visitLabel(end);
+ m.visitInsn(Opcodes.IRETURN);
+
+ filter.filter(m, context, output);
+
+ assertIgnored(range);
+ assertReplacedBranches(switchNode, newTargets);
+ }
+
+ /**
+ * <pre>
+ * enum E {
+ * A, B, C
+ * }
+ *
+ * int example(E e) {
+ * return switch (e) {
+ * case A -> 1;
+ * case B -> 2;
+ * default -> throw new IncompatibleClassChangeError();
+ * };
+ * }
+ * </pre>
+ */
+ @Test
+ public void should_not_filter_when_default_branch_has_LineNumber_different_from_switch() {
+ final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
+ "Example", "()I", null, null);
+
+ final Label start = new Label();
+ final Label end = new Label();
+ m.visitLabel(start);
+ m.visitLineNumber(0, start);
+ m.visitFieldInsn(Opcodes.GETSTATIC, "Example$1", "$SwitchMap$Example$E",
+ "[I");
+ m.visitVarInsn(Opcodes.ALOAD, 0);
+ m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "Example$E", "ordinal", "()I",
+ false);
+ m.visitInsn(Opcodes.IALOAD);
+
+ final Label dflt = new Label();
+ final Label case1 = new Label();
+ final Label case2 = new Label();
+ m.visitLookupSwitchInsn(dflt, new int[] { 1, 2 },
+ new Label[] { case1, case2 });
+
+ m.visitLabel(dflt);
+ m.visitLineNumber(1, dflt);
+ m.visitTypeInsn(Opcodes.NEW, "java/lang/IncompatibleClassChangeError");
+ m.visitInsn(Opcodes.DUP);
+ m.visitMethodInsn(Opcodes.INVOKESPECIAL,
+ "java/lang/IncompatibleClassChangeError", "<init>", "()V",
+ false);
+ m.visitInsn(Opcodes.ATHROW);
+
+ m.visitLabel(case1);
+ m.visitInsn(Opcodes.ICONST_1);
+ m.visitJumpInsn(Opcodes.GOTO, end);
+
+ m.visitLabel(case2);
+ m.visitInsn(Opcodes.ICONST_2);
+
+ m.visitLabel(end);
+ m.visitInsn(Opcodes.IRETURN);
+
+ filter.filter(m, context, output);
+
+ assertIgnored();
+ }
+
+}
diff --git a/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/ExhaustiveSwitchFilter.java b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/ExhaustiveSwitchFilter.java
new file mode 100644
index 00000000..4a5e90f3
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/ExhaustiveSwitchFilter.java
@@ -0,0 +1,113 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2023 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.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.LabelNode;
+import org.objectweb.asm.tree.LineNumberNode;
+import org.objectweb.asm.tree.LookupSwitchInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.TableSwitchInsnNode;
+import org.objectweb.asm.tree.TypeInsnNode;
+
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Filters default branch generated by compilers for exhaustive switch
+ * expressions.
+ */
+final class ExhaustiveSwitchFilter implements IFilter {
+
+ public void filter(final MethodNode methodNode,
+ final IFilterContext context, final IFilterOutput output) {
+ final Matcher matcher = new Matcher();
+ int line = -1;
+ for (final AbstractInsnNode i : methodNode.instructions) {
+ if (i.getType() == AbstractInsnNode.LINE) {
+ line = ((LineNumberNode) i).line;
+ }
+ matcher.match(i, line, output);
+ }
+ }
+
+ private static class Matcher extends AbstractMatcher {
+ public void match(final AbstractInsnNode start, final int line,
+ final IFilterOutput output) {
+ final LabelNode dflt;
+ final List<LabelNode> labels;
+ if (start.getOpcode() == Opcodes.LOOKUPSWITCH) {
+ dflt = ((LookupSwitchInsnNode) start).dflt;
+ labels = ((LookupSwitchInsnNode) start).labels;
+ } else if (start.getOpcode() == Opcodes.TABLESWITCH) {
+ dflt = ((TableSwitchInsnNode) start).dflt;
+ labels = ((TableSwitchInsnNode) start).labels;
+ } else {
+ return;
+ }
+
+ cursor = skipToLineNumberOrInstruction(dflt);
+ if (cursor == null) {
+ return;
+ }
+ if (cursor.getType() == AbstractInsnNode.LINE) {
+ if (line != ((LineNumberNode) cursor).line) {
+ return;
+ }
+ cursor = skipNonOpcodes(cursor);
+ }
+ if (cursor == null || cursor.getOpcode() != Opcodes.NEW) {
+ return;
+ }
+ if ("java/lang/MatchException"
+ .equals(((TypeInsnNode) cursor).desc)) {
+ // since Java 21
+ nextIs(Opcodes.DUP);
+ nextIs(Opcodes.ACONST_NULL);
+ nextIs(Opcodes.ACONST_NULL);
+ nextIsInvoke(Opcodes.INVOKESPECIAL, "java/lang/MatchException",
+ "<init>", "(Ljava/lang/String;Ljava/lang/Throwable;)V");
+ } else if ("java/lang/IncompatibleClassChangeError"
+ .equals(((TypeInsnNode) cursor).desc)) {
+ // prior to Java 21
+ nextIs(Opcodes.DUP);
+ nextIsInvoke(Opcodes.INVOKESPECIAL,
+ "java/lang/IncompatibleClassChangeError", "<init>",
+ "()V");
+ } else {
+ return;
+ }
+ nextIs(Opcodes.ATHROW);
+ if (cursor == null) {
+ return;
+ }
+ output.ignore(dflt, cursor);
+ final HashSet<AbstractInsnNode> replacements = new HashSet<AbstractInsnNode>();
+ for (final AbstractInsnNode label : labels) {
+ replacements.add(skipNonOpcodes(label));
+ }
+ output.replaceBranches(start, replacements);
+ }
+
+ private static AbstractInsnNode skipToLineNumberOrInstruction(
+ AbstractInsnNode cursor) {
+ while (cursor != null && (cursor.getType() == AbstractInsnNode.FRAME
+ || cursor.getType() == AbstractInsnNode.LABEL)) {
+ cursor = cursor.getNext();
+ }
+ return cursor;
+ }
+ }
+
+}
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 3889d43d..d8c17cea 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
@@ -40,6 +40,7 @@ public final class Filters implements IFilter {
new PrivateEmptyNoArgConstructorFilter(), new AssertFilter(),
new StringSwitchJavacFilter(), new StringSwitchFilter(),
new EnumEmptyConstructorFilter(), new RecordsFilter(),
+ new ExhaustiveSwitchFilter(), //
new RecordPatternFilter(), //
new AnnotationGeneratedFilter(), new KotlinGeneratedFilter(),
new KotlinLateinitFilter(), new KotlinWhenFilter(),
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index 9c4cfedf..5f8e0293 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -24,6 +24,9 @@
<ul>
<li>Experimental support for Java 22 class files
(GitHub <a href="https://github.com/jacoco/jacoco/issues/1479">#1479</a>).</li>
+ <li>Part of bytecode generated by the Java compilers for exhaustive switch
+ expressions is filtered out during generation of report
+ (GitHub <a href="https://github.com/jacoco/jacoco/issues/1472">#1472</a>).</li>
<li>Part of bytecode generated by the Java compilers for record patterns is
filtered out during generation of report
(GitHub <a href="https://github.com/jacoco/jacoco/issues/1473">#1473</a>).</li>