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 org.checkstyle.suppressionxpathfilter;
  
  import static com.google.common.truth.Truth.assertWithMessage;
  
  import java.io.File;
  import java.io.Writer;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.util.List;
  import java.util.Locale;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import org.checkstyle.base.AbstractCheckstyleModuleTestSupport;
  import org.junit.jupiter.api.io.TempDir;
  
  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
  import com.puppycrawl.tools.checkstyle.JavaParser;
  import com.puppycrawl.tools.checkstyle.TreeWalker;
  import com.puppycrawl.tools.checkstyle.api.DetailAST;
  import com.puppycrawl.tools.checkstyle.api.FileText;
  import com.puppycrawl.tools.checkstyle.filters.SuppressionXpathFilter;
  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
  import com.puppycrawl.tools.checkstyle.xpath.XpathQueryGenerator;
  
  public abstract class AbstractXpathTestSupport extends AbstractCheckstyleModuleTestSupport {
  
      private static final int DEFAULT_TAB_WIDTH = 4;
  
      private static final String DELIMITER = " | \n";
  
      private static final Pattern LINE_COLUMN_NUMBER_REGEX =
              Pattern.compile("(\\d+):(\\d+):");
  
      /**
       * The temporary folder to hold intermediate files.
       */
      @TempDir
      public Path temporaryFolder;
  
      /**
       * Returns name of the check.
       *
       * @return name of the check as a String.
       */
      protected abstract String getCheckName();
  
      @Override
      protected String getPackageLocation() {
          final String subpackage = getCheckName().toLowerCase(Locale.ENGLISH)
                  .replace("check", "");
          return "org/checkstyle/suppressionxpathfilter/" + subpackage;
      }
  
      /**
       * Returns a list of XPath queries to locate the violation nodes in a Java file.
       *
       * @param fileToProcess the Java file to be processed. {@link File} type object.
       * @param position the position of violation in the file. {@link ViolationPosition} object.
       * @return a list of strings containing XPath queries for locating violation nodes.
       * @throws Exception can throw exceptions while accessing file contents.
       */
      private static List<String> generateXpathQueries(File fileToProcess,
                                                       ViolationPosition position)
              throws Exception {
          final FileText fileText = new FileText(fileToProcess,
                  StandardCharsets.UTF_8.name());
          final DetailAST rootAst = JavaParser.parseFile(fileToProcess,
                  JavaParser.Options.WITH_COMMENTS);
          final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst,
                  position.violationLineNumber, position.violationColumnNumber,
                  fileText, DEFAULT_TAB_WIDTH);
  
          return queryGenerator.generate();
      }
  
      /**
       * Verify generated XPath queries by comparing with expected queries.
       *
       * @param generatedXpathQueries a list of generated XPath queries.
       * @param expectedXpathQueries a list of expected XPath queries.
       */
      private static void verifyXpathQueries(List<String> generatedXpathQueries,
                                             List<String> expectedXpathQueries) {
          assertWithMessage("Generated queries do not match expected ones")
              .that(generatedXpathQueries)
              .isEqualTo(expectedXpathQueries);
      }
  
      /**
       * Returns the path to the generated Suppressions XPath config file.
       * This method creates a Suppressions config file containing the Xpath queries for
       * any check and returns the path to that file.
       *
       * @param checkName the name of the check that is run.
       * @param xpathQueries a list of generated XPath queries for violations in a file.
       * @return path(String) to the generated Suppressions config file.
       * @throws Exception can throw exceptions when writing/creating the config file.
       */
      private String createSuppressionsXpathConfigFile(String checkName,
                                                       List<String> xpathQueries)
              throws Exception {
          final Path suppressionsXpathConfigPath =
                  Files.createTempFile(temporaryFolder, "", "");
          try (Writer bw = Files.newBufferedWriter(suppressionsXpathConfigPath,
                  StandardCharsets.UTF_8)) {
              bw.write("<?xml version=\"1.0\"?>\n");
              bw.write("<!DOCTYPE suppressions PUBLIC\n");
              bw.write("    \"-//Checkstyle//DTD SuppressionXpathFilter ");
              bw.write("Experimental Configuration 1.2//EN\"\n");
              bw.write("    \"https://checkstyle.org/dtds/");
              bw.write("suppressions_1_2_xpath_experimental.dtd\">\n");
              bw.write("<suppressions>\n");
              bw.write("   <suppress-xpath\n");
              bw.write("       checks=\"");
              bw.write(checkName);
              bw.write("\"\n");
              bw.write("       query=\"");
              bw.write(String.join(DELIMITER, xpathQueries));
              bw.write("\"/>\n");
              bw.write("</suppressions>");
          }
  
          return suppressionsXpathConfigPath.toString();
      }
  
      /**
       * Returns the config {@link DefaultConfiguration} for the created Suppression XPath filter.
       *
       * @param checkName the name of the check that is run.
       * @param xpathQueries a list of generated XPath queries for violations in a file.
       * @return the default config for Suppressions XPath filter based on check and xpath queries.
       * @throws Exception can throw exceptions when creating config.
       */
      private DefaultConfiguration createSuppressionXpathFilter(String checkName,
                                             List<String> xpathQueries) throws Exception {
          final DefaultConfiguration suppressionXpathFilterConfig =
                  createModuleConfig(SuppressionXpathFilter.class);
          suppressionXpathFilterConfig.addProperty("file",
                  createSuppressionsXpathConfigFile(checkName, xpathQueries));
  
          return suppressionXpathFilterConfig;
      }
  
      /**
       * Extract line no and column nos from String of expected violations.
       *
       * @param expectedViolations the expected violations generated for the check.
       * @return instance of type {@link ViolationPosition} which contains the line and column nos.
       */
      private static ViolationPosition extractLineAndColumnNumber(String... expectedViolations) {
          ViolationPosition violation = null;
          final Matcher matcher =
                  LINE_COLUMN_NUMBER_REGEX.matcher(expectedViolations[0]);
          if (matcher.find()) {
              final int violationLineNumber = Integer.parseInt(matcher.group(1));
              final int violationColumnNumber = Integer.parseInt(matcher.group(2));
              violation = new ViolationPosition(violationLineNumber, violationColumnNumber);
          }
          return violation;
      }
  
      /**
       * Runs three verifications:
       * First one executes checker with defined module configuration and compares output with
       * expected violations.
       * Second one generates xpath queries based on violation message and compares them with expected
       * xpath queries.
       * Third one constructs new configuration with {@code SuppressionXpathFilter} using generated
       * xpath queries, executes checker and checks if no violation occurred.
       *
       * @param moduleConfig module configuration.
       * @param fileToProcess input class file.
       * @param expectedViolations expected violation messages.
       * @param expectedXpathQueries expected generated xpath queries.
       * @throws Exception if an error occurs
       */
      protected void runVerifications(DefaultConfiguration moduleConfig,
                                    File fileToProcess,
                                    String[] expectedViolations,
                                    List<String> expectedXpathQueries) throws Exception {
          final ViolationPosition position =
                  extractLineAndColumnNumber(expectedViolations);
          final List<String> generatedXpathQueries =
                  generateXpathQueries(fileToProcess, position);
  
          final DefaultConfiguration treeWalkerConfigWithXpath =
                  createModuleConfig(TreeWalker.class);
          treeWalkerConfigWithXpath.addChild(moduleConfig);
          treeWalkerConfigWithXpath.addChild(createSuppressionXpathFilter(moduleConfig.getName(),
                  generatedXpathQueries));
  
          final Integer[] warnList = getLinesWithWarn(fileToProcess.getPath());
          verify(moduleConfig, fileToProcess.getPath(), expectedViolations, warnList);
          verifyXpathQueries(generatedXpathQueries, expectedXpathQueries);
          verify(treeWalkerConfigWithXpath, fileToProcess.getPath(), CommonUtil.EMPTY_STRING_ARRAY);
      }
  
      private static final class ViolationPosition {
          private final int violationLineNumber;
          private final int violationColumnNumber;
  
          /**
           * Constructor of the class.
           *
           * @param violationLineNumber line no of the violation produced for the check.
           * @param violationColumnNumber column no of the violation produced for the check.
           */
          private ViolationPosition(int violationLineNumber,
                                int violationColumnNumber) {
              this.violationLineNumber = violationLineNumber;
              this.violationColumnNumber = violationColumnNumber;
          }
      }
  }
  