Add support for guard clauses in Java 21 switch expressions

This PR adds support for `switch` statements where a `case` has a guard clause.

See Issue #983

Fixes #988

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/google-java-format/pull/988 from TheCK:master 4771486db7d8aab84eb4ecf8e68af2612d0c2b5c
PiperOrigin-RevId: 588913297
diff --git a/core/pom.xml b/core/pom.xml
index 038e4ed..d1363fe 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -226,7 +226,7 @@
     <profile>
       <id>jdk11</id>
       <activation>
-        <jdk>(,17)</jdk>
+        <jdk>[11,17)</jdk>
       </activation>
       <build>
         <plugins>
@@ -236,6 +236,7 @@
             <configuration>
               <excludes>
                 <exclude>**/Java17InputAstVisitor.java</exclude>
+                <exclude>**/Java21InputAstVisitor.java</exclude>
               </excludes>
             </configuration>
           </plugin>
@@ -243,6 +244,32 @@
             <artifactId>maven-javadoc-plugin</artifactId>
             <configuration>
               <excludePackageNames>com.google.googlejavaformat.java.java17</excludePackageNames>
+              <excludePackageNames>com.google.googlejavaformat.java.java21</excludePackageNames>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>jdk17</id>
+      <activation>
+        <jdk>[17,21)</jdk>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+              <excludes>
+                <exclude>**/Java21InputAstVisitor.java</exclude>
+              </excludes>
+            </configuration>
+          </plugin>
+          <plugin>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <excludePackageNames>com.google.googlejavaformat.java.java21</excludePackageNames>
             </configuration>
           </plugin>
         </plugins>
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
index 9ff702d..5aa7a12 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
@@ -151,16 +151,14 @@
     OpsBuilder builder = new OpsBuilder(javaInput, javaOutput);
     // Output the compilation unit.
     JavaInputAstVisitor visitor;
-    if (Runtime.version().feature() >= 17) {
-      try {
-        visitor =
-            Class.forName("com.google.googlejavaformat.java.java17.Java17InputAstVisitor")
-                .asSubclass(JavaInputAstVisitor.class)
-                .getConstructor(OpsBuilder.class, int.class)
-                .newInstance(builder, options.indentationMultiplier());
-      } catch (ReflectiveOperationException e) {
-        throw new LinkageError(e.getMessage(), e);
-      }
+    if (Runtime.version().feature() >= 21) {
+      visitor =
+          createVisitor(
+              "com.google.googlejavaformat.java.java21.Java21InputAstVisitor", builder, options);
+    } else if (Runtime.version().feature() >= 17) {
+      visitor =
+          createVisitor(
+              "com.google.googlejavaformat.java.java17.Java17InputAstVisitor", builder, options);
     } else {
       visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier());
     }
@@ -173,6 +171,18 @@
     javaOutput.flush();
   }
 
+  private static JavaInputAstVisitor createVisitor(
+      final String className, final OpsBuilder builder, final JavaFormatterOptions options) {
+    try {
+      return Class.forName(className)
+          .asSubclass(JavaInputAstVisitor.class)
+          .getConstructor(OpsBuilder.class, int.class)
+          .newInstance(builder, options.indentationMultiplier());
+    } catch (ReflectiveOperationException e) {
+      throw new LinkageError(e.getMessage(), e);
+    }
+  }
+
   static boolean errorDiagnostic(Diagnostic<?> input) {
     if (input.getKind() != Diagnostic.Kind.ERROR) {
       return false;
diff --git a/core/src/main/java/com/google/googlejavaformat/java/java17/Java17InputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/java17/Java17InputAstVisitor.java
index a0561e2..97bb2ff 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/java17/Java17InputAstVisitor.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/java17/Java17InputAstVisitor.java
@@ -29,6 +29,7 @@
 import com.sun.source.tree.CaseTree;
 import com.sun.source.tree.ClassTree;
 import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.ExpressionTree;
 import com.sun.source.tree.InstanceOfTree;
 import com.sun.source.tree.ModifiersTree;
 import com.sun.source.tree.ModuleTree;
@@ -238,6 +239,15 @@
       }
       builder.close();
     }
+
+    final ExpressionTree guard = getGuard(node);
+    if (guard != null) {
+      builder.space();
+      token("when");
+      builder.space();
+      scan(guard, null);
+    }
+
     switch (node.getCaseKind()) {
       case STATEMENT:
         token(":");
@@ -267,4 +277,8 @@
     }
     return null;
   }
+
+  protected ExpressionTree getGuard(final CaseTree node) {
+    return null;
+  }
 }
diff --git a/core/src/main/java/com/google/googlejavaformat/java/java21/Java21InputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/java21/Java21InputAstVisitor.java
new file mode 100644
index 0000000..a96ef99
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/java21/Java21InputAstVisitor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The google-java-format Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.java21;
+
+import com.google.googlejavaformat.OpsBuilder;
+import com.google.googlejavaformat.java.java17.Java17InputAstVisitor;
+import com.sun.source.tree.CaseTree;
+import com.sun.source.tree.ExpressionTree;
+
+/**
+ * Extends {@link Java17InputAstVisitor} with support for AST nodes that were added or modified in
+ * Java 21.
+ */
+public class Java21InputAstVisitor extends Java17InputAstVisitor {
+
+  public Java21InputAstVisitor(OpsBuilder builder, int indentMultiplier) {
+    super(builder, indentMultiplier);
+  }
+
+  @Override
+  protected ExpressionTree getGuard(final CaseTree node) {
+    return node.getGuard();
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java
index 61a4346..688b24d 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java
@@ -52,6 +52,7 @@
           .putAll(15, "I603")
           .putAll(16, "I588")
           .putAll(17, "I683", "I684", "I696")
+          .putAll(21, "SwitchGuardClause")
           .build();
 
   @Parameters(name = "{index}: {0}")
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.input
new file mode 100644
index 0000000..25df580
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.input
@@ -0,0 +1,9 @@
+class SwitchGuardClause {
+  boolean test(Object x) {
+    return switch (x) {
+      case String s when s.length() < 5 -> true;
+      case Integer i -> false;
+      default -> true;
+    };
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.output
new file mode 100644
index 0000000..25df580
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.output
@@ -0,0 +1,9 @@
+class SwitchGuardClause {
+  boolean test(Object x) {
+    return switch (x) {
+      case String s when s.length() < 5 -> true;
+      case Integer i -> false;
+      default -> true;
+    };
+  }
+}