code: ///////////////////////////////////////////////////////////////////////////////////////////////
  // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
  // Copyright (C) 2001-2023 the original author or authors.
  //
  // This library is free software; you can redistribute it and/or
  // modify it under the terms of the GNU Lesser General Public
  // License as published by the Free Software Foundation; either
  // version 2.1 of the License, or (at your option) any later version.
  //
  // This library 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
  // Lesser General Public License for more details.
  //
  // You should have received a copy of the GNU Lesser General Public
  // License along with this library; if not, write to the Free Software
  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  ///////////////////////////////////////////////////////////////////////////////////////////////
  
  package com.puppycrawl.tools.checkstyle;
  
  import static com.google.common.collect.ImmutableSet.toImmutableSet;
  import static com.google.common.truth.Truth.assertWithMessage;
  
  import java.io.File;
  import java.lang.reflect.Method;
  import java.lang.reflect.Modifier;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Set;
  import java.util.stream.Collectors;
  
  import org.antlr.v4.runtime.CharStream;
  import org.antlr.v4.runtime.CharStreams;
  import org.antlr.v4.runtime.CommonTokenStream;
  import org.junit.jupiter.api.Test;
  
  import com.google.common.collect.ImmutableSet;
  import com.puppycrawl.tools.checkstyle.api.DetailAST;
  import com.puppycrawl.tools.checkstyle.api.FileContents;
  import com.puppycrawl.tools.checkstyle.api.FileText;
  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
  import com.puppycrawl.tools.checkstyle.grammar.java.JavaLanguageLexer;
  import com.puppycrawl.tools.checkstyle.grammar.java.JavaLanguageParser;
  import com.puppycrawl.tools.checkstyle.grammar.java.JavaLanguageParserBaseVisitor;
  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
  
  public class JavaAstVisitorTest extends AbstractModuleTestSupport {
  
      /**
       * If a visit method is not overridden, we should explain why we do not 'visit' the
       * parse tree at this node and construct an AST. Reasons could include that we have
       * no terminal symbols (tokens) in the corresponding production rule, or that
       * we handle the construction of this particular AST in its parent node. If we
       * have a production rule where we have terminal symbols (tokens), but we do not build
       * an AST from tokens in the rule context, the rule is extraneous.
       */
      private static final Set<String> VISIT_METHODS_NOT_OVERRIDDEN = Set.of(
              // no tokens in production rule, so no AST to build
              "visitClassOrInterfaceOrPrimitiveType",
              "visitNonWildcardTypeArgs",
              "visitStat",
              "visitAnnotationConstantRest",
              "visitSwitchLabeledRule",
              "visitLocalVariableDeclaration",
              "visitTypes",
              "visitSwitchStat",
              "visitSwitchPrimary",
              "visitClassDef",
              "visitInterfaceMemberDeclaration",
              "visitMemberDeclaration",
              "visitLiteralPrimary",
              "visitPatternDefinition",
              "visitLocalType",
              "visitLocalTypeDeclaration",
              "visitRecordBodyDeclaration",
              "visitResource",
              "visitVariableInitializer",
              "visitLambdaBody",
              "visitPatternVariableDef",
  
              // AST built in parent rule
              "visitCreatedNameExtended",
              "visitSuperSuffixSimple",
              "visitFieldAccessNoIdent",
              "visitClassType",
              "visitClassOrInterfaceTypeExtended",
              "visitQualifiedNameExtended",
              "visitGuard"
      );
  
      @Override
      protected String getPackageLocation() {
          return "com/puppycrawl/tools/checkstyle/javaastvisitor";
      }
  
      @Test
      public void testAllVisitMethodsAreOverridden() {
          final Method[] baseVisitMethods = JavaLanguageParserBaseVisitor
                  .class.getDeclaredMethods();
          final Method[] visitMethods = JavaAstVisitor.class.getDeclaredMethods();
  
          final Set<String> filteredBaseVisitMethodNames = Arrays.stream(baseVisitMethods)
                  .filter(method -> !VISIT_METHODS_NOT_OVERRIDDEN.contains(method.getName()))
                  .filter(method -> method.getName().contains("visit"))
                  .filter(method -> method.getModifiers() == Modifier.PUBLIC)
                  .map(Method::getName)
                  .collect(Collectors.toSet());
  
          final ImmutableSet<String> filteredVisitMethodNames = Arrays.stream(visitMethods)
                  .filter(method -> method.getName().contains("visit"))
                  // remove overridden 'visit' method from ParseTreeVisitor interface in
                  // JavaAstVisitor
                  .filter(method -> !"visit".equals(method.getName()))
                  .filter(method -> method.getModifiers() == Modifier.PUBLIC)
                  .map(Method::getName)
                  .collect(toImmutableSet());
  
          final String message = "Visit methods in 'JavaLanguageParserBaseVisitor' generated from "
                  + "production rules and labeled alternatives in 'JavaLanguageParser.g4' should "
                  + "be overridden in 'JavaAstVisitor' or be added to 'VISIT_METHODS_NOT_OVERRIDDEN' "
                  + "with comment explaining why.";
  
          assertWithMessage(message)
                  .that(filteredVisitMethodNames)
                  .containsExactlyElementsIn(filteredBaseVisitMethodNames);
      }
  
      @Test
      public void testOrderOfVisitMethodsAndProductionRules() throws Exception {
          // Order of BaseVisitor's generated 'visit' methods match the order of
          // production rules in 'JavaLanguageParser.g4'.
          final String baseVisitorFilename = "target/generated-sources/antlr/com/puppycrawl"
                  + "/tools/checkstyle/grammar/java/JavaLanguageParserBaseVisitor.java";
          final DetailAST baseVisitorAst = JavaParser.parseFile(new File(baseVisitorFilename),
                              JavaParser.Options.WITHOUT_COMMENTS);
  
          final String visitorFilename = "src/main/java/com/puppycrawl/tools/checkstyle"
                  + "/JavaAstVisitor.java";
          final DetailAST visitorAst = JavaParser.parseFile(new File(visitorFilename),
                              JavaParser.Options.WITHOUT_COMMENTS);
  
          final List<String> orderedBaseVisitorMethodNames =
                  getOrderedVisitMethodNames(baseVisitorAst);
          final List<String> orderedVisitorMethodNames =
                  getOrderedVisitMethodNames(visitorAst);
  
          orderedBaseVisitorMethodNames.removeAll(VISIT_METHODS_NOT_OVERRIDDEN);
  
          // remove overridden 'visit' method from ParseTreeVisitor interface in JavaAstVisitor
          orderedVisitorMethodNames.remove("visit");
  
          assertWithMessage("Visit methods in 'JavaAstVisitor' should appear in same order as "
                  + "production rules and labeled alternatives in 'JavaLanguageParser.g4'.")
                  .that(orderedVisitorMethodNames)
                  .containsExactlyElementsIn(orderedBaseVisitorMethodNames)
                  .inOrder();
      }
  
      /**
       * Finds all {@code visit...} methods in a source file, and collects
       * the method names into a list. This method counts on the simple structure
       * of 'JavaAstVisitor' and 'JavaLanguageParserBaseVisitor'.
       *
       * @param root the root of the AST to extract method names from
       * @return list of all {@code visit...} method names
       */
      private static List<String> getOrderedVisitMethodNames(DetailAST root) {
          final List<String> orderedVisitMethodNames = new ArrayList<>();
  
          DetailAST classDef = root.getFirstChild();
          while (classDef.getType() != TokenTypes.CLASS_DEF) {
              classDef = classDef.getNextSibling();
          }
  
          final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
          DetailAST objBlockChild = objBlock.findFirstToken(TokenTypes.METHOD_DEF);
          while (objBlockChild != null) {
              if (isVisitMethod(objBlockChild)) {
                  orderedVisitMethodNames.add(objBlockChild
                          .findFirstToken(TokenTypes.IDENT)
                          .getText());
              }
              objBlockChild = objBlockChild.getNextSibling();
          }
          return orderedVisitMethodNames;
      }
  
      /**
       * Checks if given AST is a visit method.
       *
       * @param objBlockChild AST to check
       * @return true if AST is a visit method
       */
      private static boolean isVisitMethod(DetailAST objBlockChild) {
          return objBlockChild.getType() == TokenTypes.METHOD_DEF
                  && objBlockChild.findFirstToken(TokenTypes.IDENT).getText().contains("visit");
      }
  
      @Test
      public void testNullSelfInAddLastSibling() throws Exception {
          final Method addLastSibling = JavaAstVisitor.class
                  .getDeclaredMethod("addLastSibling", DetailAstImpl.class, DetailAstImpl.class);
          addLastSibling.setAccessible(true);
          assertWithMessage("Method should not throw exception.")
                  .that(addLastSibling.invoke(JavaAstVisitor.class, null, null))
                  .isNull();
      }
      /**
       * This test exists to kill surviving mutation from pitest removing expression AST building
       * optimization in {@link JavaAstVisitor#visitBinOp(JavaLanguageParser.BinOpContext)}.
       * We do not use {@link JavaParser#parse(FileContents)} here due to DFA clearing hack.
       *
       * <p>
       * Reason: we have iterative expression AST building to avoid stackoverflow
       * in {@link JavaAstVisitor#visitBinOp(JavaLanguageParser.BinOpContext)}. In actual
       * generated parser, we avoid stackoverflow thanks to the left recursive expression
       * rule (eliminating unnecessary recursive calls to hierarchical expression production rules).
       * However, ANTLR's ParserATNSimulator has no such optimization. So, the number of recursive
       * calls to ParserATNSimulator#closure when calling ParserATNSimulator#clearDFA causes a
       * StackOverflow error. We avoid this by using the single argument constructor (thus not
       * forcing DFA clearing) in this test.
       * </p>
       *
       * @throws Exception if input file does not exist
       */
  
      @Test
      public void testNoStackOverflowOnDeepStringConcat() throws Exception {
          final File file =
                  new File(getPath("InputJavaAstVisitorNoStackOverflowOnDeepStringConcat.java"));
          final FileText fileText = new FileText(file, StandardCharsets.UTF_8.name());
          final FileContents contents = new FileContents(fileText);
  
          final String fullText = contents.getText().getFullText().toString();
          final CharStream codePointCharStream = CharStreams.fromString(fullText);
          final JavaLanguageLexer lexer = new JavaLanguageLexer(codePointCharStream, true);
          lexer.setCommentListener(contents);
  
          final CommonTokenStream tokenStream = new CommonTokenStream(lexer);
          final JavaLanguageParser parser = new JavaLanguageParser(tokenStream);
  
          final JavaLanguageParser.CompilationUnitContext compilationUnit = parser.compilationUnit();
  
          // We restrict execution to use limited resources here, so that we can
          // kill surviving pitest mutation from removal of nested binary operation
          // optimization in JavaAstVisitor#visitBinOp. Limited resources (small stack size)
          // ensure that we throw a StackOverflowError if optimization is removed.
          final DetailAST root = TestUtil.getResultWithLimitedResources(
                  () -> new JavaAstVisitor(tokenStream).visit(compilationUnit)
          );
  
          assertWithMessage("File parsing and AST building should complete successfully.")
                  .that(root)
                  .isNotNull();
      }
  }
  