/* * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /** * @test * @bug 8367530 * @summary Check enhanced exhaustiveness errors * @library /tools/lib * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * jdk.compiler/com.sun.tools.javac.util * @build toolbox.ToolBox toolbox.JavacTask * @run main ExhaustivenessConvenientErrors */ import com.sun.tools.javac.api.ClientCodeWrapper.DiagnosticSourceUnwrapper; import com.sun.tools.javac.util.JCDiagnostic; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import toolbox.JavacTask; import toolbox.Task; import toolbox.TestRunner; import toolbox.ToolBox; public class ExhaustivenessConvenientErrors extends TestRunner { ToolBox tb; public static void main(String... args) throws Exception { new ExhaustivenessConvenientErrors().runTests(); } ExhaustivenessConvenientErrors() { super(System.err); tb = new ToolBox(); } public void runTests() throws Exception { runTests(m -> new Object[] { Paths.get(m.getName()) }); } @Test public void testExhaustiveSealedClasses(Path base) throws Exception { doTest(base, new String[]{""" package lib; public sealed interface S permits A, B {} """, """ package lib; public final class A implements S {} """, """ package lib; public final class B implements S {} """}, """ package test; import lib.*; public class Test { private int test(S obj) { return switch (obj) { case A a -> 0; }; } } """, "lib.B _"); } @Test public void testExhaustiveSealedClassesTransitive(Path base) throws Exception { doTest(base, new String[]{""" package lib; public sealed interface S1 permits S2, A {} """, """ package lib; public sealed interface S2 extends S1 permits S3, B {} """, """ package lib; public sealed interface S3 extends S2 permits C, D {} """, """ package lib; public final class A implements S1 {} """, """ package lib; public final class B implements S2 {} """, """ package lib; public final class C implements S3 {} """, """ package lib; public final class D implements S3 {} """}, """ package test; import lib.*; public class Test { private int test(S1 obj) { return switch (obj) { case A a -> 0; case B a -> 0; case D a -> 0; }; } } """, "lib.C _"); } @Test public void testTrivialRecord(Path base) throws Exception { doTest(base, new String[]{""" package lib; public sealed interface S permits A, B {} """, """ package lib; public final class A implements S {} """, """ package lib; public final class B implements S {} """, """ package lib; public record R(S s) {} """}, """ package test; import lib.*; public class Test { private int test(R r) { return switch (r) { case R(A a) -> 0; }; } } """, "lib.R(lib.B _)"); } @Test public void testNonNestedRecord(Path base) throws Exception { doTest(base, new String[]{""" package lib; public sealed interface S permits A, B {} """, """ package lib; public final class A implements S {} """, """ package lib; public final class B implements S {} """, """ package lib; public record R(S s1, S s2) {} """}, """ package test; import lib.*; public class Test { private int test(R r) { return switch (r) { case R(A a, B b) -> 0; case R(B b, A a) -> 0; }; } } """, "lib.R(lib.A _, lib.A _)", "lib.R(lib.B _, lib.B _)"); } @Test public void testComplex1(Path base) throws Exception { doTest(base, new String[0], """ package test; import lib.*; public class Test { private int test(Root r) { return switch (r) { case Root(R1 _, _, _) -> 0; }; } sealed interface Base {} record R1() implements Base {} record R2() implements Base {} record R3(Base b1, Base b2) implements Base {} record Root(Base b1, Base b2, Base b3) {} } """, "test.Test.Root(test.Test.R2 _, test.Test.Base _, test.Test.Base _)", "test.Test.Root(test.Test.R3 _, test.Test.Base _, test.Test.Base _)"); } @Test public void testComplex2(Path base) throws Exception { doTest(base, new String[0], """ package test; import lib.*; public class Test { private int test(Root r) { return switch (r) { case Root(R1 _, _, _) -> 0; case Root(R2 _, R1 _, _) -> 0; case Root(R2 _, R2 _, R1 _) -> 0; case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0; case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0; case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0; case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0; case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0; case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0; case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0; case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0; case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0; case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0; case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0; case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0; case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0; case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0; case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0; // case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _)) -> 0; }; } sealed interface Base {} record R1() implements Base {} record R2(Base b1, Base b2) implements Base {} record Root(Base b1, Base b2, Base b3) {} } """, "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.R2 _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.R2 _))"); } @Test public void testComplex3(Path base) throws Exception { doTest(base, new String[0], """ package test; public class Test { private int test(Triple p) { return switch (p) { case Triple(B _, _, _) -> 0; case Triple(_, A _, _) -> 0; case Triple(_, _, A _) -> 0; case Triple(A p, C(Nested _, NestedBaseA _), _) -> 0; case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseA _)) -> 0; case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseB _)) -> 0; case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseC _)) -> 0; case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseA _)) -> 0; case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseB _)) -> 0; // case Path(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseC _)) -> 0; }; } record Triple(Base c1, Base c2, Base c3) {} sealed interface Base permits A, B {} record A(boolean key) implements Base { } sealed interface B extends Base {} record C(Nested n, NestedBase b) implements B {} record Nested() {} sealed interface NestedBase {} record NestedBaseA() implements NestedBase {} record NestedBaseB() implements NestedBase {} record NestedBaseC() implements NestedBase {} } """, "test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _), test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _))"); } @Test public void testComplex4(Path base) throws Exception { doTest(base, new String[0], """ package test; import lib.*; public class Test { private int test(Root r) { return switch (r) { case Root(R1 _, _, _) -> 0; case Root(R2 _, R1 _, _) -> 0; case Root(R2 _, R2 _, R1 _) -> 0; case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0; case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0; case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0; case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0; case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0; case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0; // case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0; case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0; case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0; case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0; case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0; case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0; case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0; case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0; case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0; // case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _)) -> 0; }; } sealed interface Base {} record R1() implements Base {} record R2(Base b1, Base b2) implements Base {} record Root(Base b1, Base b2, Base b3) {} } """, "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.Base _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.Base _))"); //ideally, the result would be as follow, but it is difficult to split Base on two distinct places: // "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.R1 _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.R1 _))", // "test.Test.Root(test.Test.R2 _, test.Test.R2(test.Test.R2 _, test.Test.R2 _), test.Test.R2(test.Test.R2 _, test.Test.R2 _))"); } @Test public void testComplex5(Path base) throws Exception { doTest(base, new String[0], """ package test; public class Test { private int test(Triple p) { return switch (p) { case Triple(B _, _, _) -> 0; case Triple(_, A _, _) -> 0; case Triple(_, _, A _) -> 0; // case Triple(A _, C(Nested _, NestedBaseA _), _) -> 0; case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseA _)) -> 0; case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseB _)) -> 0; case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseC _)) -> 0; case Triple(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseA _)) -> 0; case Triple(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseB _)) -> 0; // case Path(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseC _)) -> 0; }; } record Triple(Base c1, Base c2, Base c3) {} sealed interface Base permits A, B {} record A(boolean key) implements Base { } sealed interface B extends Base {} record C(Nested n, NestedBase b) implements B {} record Nested() {} sealed interface NestedBase {} record NestedBaseA() implements NestedBase {} record NestedBaseB() implements NestedBase {} record NestedBaseC() implements NestedBase {} } """, "test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseA _), test.Test.C _)", //the following could be: //test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _), test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _)) "test.Test.Triple(test.Test.A _, test.Test.C(test.Test.Nested _, test.Test.NestedBaseC _), test.Test.C _)"); } @Test public void testNoInfiniteRecursion(Path base) throws Exception { doTest(base, new String[0], """ package test; public class Test { private int test(R r) { return switch (r) { case R(_, _, R(_, _, _, _), String s) -> 0; case R(_, _, R(_, _, _, String str), _) -> 0; }; } } public record R(R r1, R r2, R r3, Object o) {} """, "test.R(test.R _, test.R _, test.R(test.R _, test.R _, test.R _, java.lang.Object _), java.lang.Object _)"); } @Test public void testEnum(Path base) throws Exception { doTest(base, new String[0], """ package test; public class Test { private int test(I i) { return switch (i) { case E.A -> 0; case C _ -> 1; }; } sealed interface I {} enum E implements I {A, B} final class C implements I {} } public record R(R r1, R r2, R r3, Object o) {} """, "test.Test.E.B"); doTest(base, new String[0], """ package test; public class Test { private int test(I i) { return switch (i) { case C _ -> 1; }; } sealed interface I {} enum E implements I {A, B} final class C implements I {} } public record R(R r1, R r2, R r3, Object o) {} """, "test.Test.E _"); } @Test public void testInstantiateComponentTypes(Path base) throws Exception { doTest(base, new String[0], """ package test; public class Test { private int test(Pair> p) { return switch (p) { case Pair(A(A(_)) -> 0; case Pair(A(B(_)) -> 0; case Pair(B(A(_)) -> 0; }; } record Pair(T c) {} sealed interface Base permits A, B {} record A(T c) implements Base {} record B(T c) implements Base {} } """, "test.Test.Pair(test.Test.B(test.Test.B _))"); } @Test public void testNeedToExpandIfRecordExists(Path base) throws Exception { doTest(base, new String[0], """ package test; class Test { sealed interface A { } record B() implements A { } record C(A a) implements A { } void test(A a) { switch (a) { case C(B _) -> throw null; } } } """, "test.Test.B _", "test.Test.C(test.Test.C _)"); } @Test public void testComplex6(Path base) throws Exception { doTest(base, new String[0], """ public class Test { sealed interface Base {} record NoOp() implements Base {} record Const() implements Base {} record Pair(Base n1, Base b2) implements Base {} int t(Base b) { return switch (b) { case NoOp _ -> 0; case Const _ -> 0; case Pair(NoOp _, _) -> 0; case Pair(Const _, _) -> 0; case Pair(Pair _, NoOp _) -> 0; case Pair(Pair _, Const _) -> 0; case Pair(Pair _, Pair(NoOp _, _)) -> 0; case Pair(Pair _, Pair(Const _, _)) -> 0; case Pair(Pair _, Pair(Pair(NoOp _, _), _)) -> 0; case Pair(Pair _, Pair(Pair(Const _, _), _)) -> 0; case Pair(Pair(NoOp _, _), Pair(Pair(Pair _, _), _)) -> 0; case Pair(Pair(Const _, _), Pair(Pair(Pair _, _), _)) -> 0; // case Pair(Pair(Pair _, _), Pair(Pair(Pair _, _), _)) -> 0; }; } } """, "Test.Pair(Test.Pair(Test.Pair _, Test.Base _), Test.Pair(Test.Pair(Test.Pair _, Test.Base _), Test.Base _))"); } private void doTest(Path base, String[] libraryCode, String testCode, String... expectedMissingPatterns) throws IOException { Path current = base.resolve("."); Path libClasses = current.resolve("libClasses"); Files.createDirectories(libClasses); if (libraryCode.length != 0) { Path libSrc = current.resolve("lib-src"); for (String code : libraryCode) { tb.writeJavaFiles(libSrc, code); } new JavacTask(tb) .outdir(libClasses) .files(tb.findJavaFiles(libSrc)) .run(); } Path src = current.resolve("src"); tb.writeJavaFiles(src, testCode); Path classes = current.resolve("libClasses"); Files.createDirectories(libClasses); Set missingPatterns = new HashSet<>(); new JavacTask(tb) .options("-XDrawDiagnostics", "-XDdev", "-Xlint:-preview", "--class-path", libClasses.toString(), "-XDshould-stop.at=FLOW", "-XDshould-stop.ifNoError=FLOW", "-XDexhaustivityTimeout=" + Long.MAX_VALUE) //never timeout .outdir(classes) .files(tb.findJavaFiles(src)) .diagnosticListener(d -> { if ("compiler.err.not.exhaustive.details".equals(d.getCode()) || "compiler.err.not.exhaustive.statement.details".equals(d.getCode())) { if (d instanceof DiagnosticSourceUnwrapper uw) { d = uw.d; } if (d instanceof JCDiagnostic.MultilineDiagnostic diag) { diag.getSubdiagnostics() .stream() .map(fragment -> fragment.getArgs()[0].toString()) .forEach(missingPatterns::add); } } }) .run(Task.Expect.FAIL) .writeAll() .getOutputLines(Task.OutputKind.DIRECT); Set expectedPatterns = new HashSet<>(List.of(expectedMissingPatterns)); if (!expectedPatterns.equals(missingPatterns)) { throw new AssertionError("Incorrect errors, expected: " + expectedPatterns + ", actual: " + missingPatterns); } } }