diff options
author | Evgeny Mandrikov <138671+Godin@users.noreply.github.com> | 2023-06-14 23:23:47 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-14 23:23:47 +0200 |
commit | 8271afb7520379535a42cd416855e1c41c2df1d6 (patch) | |
tree | 3a458190f011f5a73d42c7c4454c2b3c3353d934 | |
parent | 41bc4ac859d76fc6a6c4f5fe451f0bc38c1a6111 (diff) | |
download | jacoco-8271afb7520379535a42cd416855e1c41c2df1d6.tar.gz |
Add filter for exhaustive switch expression (#1472)
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> |