View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package org.checkstyle.suppressionxpathfilter;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.io.File;
25  import java.io.Writer;
26  import java.nio.charset.StandardCharsets;
27  import java.nio.file.Files;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.UUID;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  import org.checkstyle.base.AbstractCheckstyleModuleTestSupport;
35  import org.junit.jupiter.api.io.TempDir;
36  
37  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
38  import com.puppycrawl.tools.checkstyle.JavaParser;
39  import com.puppycrawl.tools.checkstyle.TreeWalker;
40  import com.puppycrawl.tools.checkstyle.api.DetailAST;
41  import com.puppycrawl.tools.checkstyle.api.FileText;
42  import com.puppycrawl.tools.checkstyle.filters.SuppressionXpathFilter;
43  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
44  import com.puppycrawl.tools.checkstyle.xpath.XpathQueryGenerator;
45  
46  public abstract class AbstractXpathTestSupport extends AbstractCheckstyleModuleTestSupport {
47  
48      private static final int DEFAULT_TAB_WIDTH = 4;
49  
50      private static final String DELIMITER = " | \n";
51  
52      private static final Pattern LINE_COLUMN_NUMBER_REGEX =
53              Pattern.compile("(\\d+):(\\d+):");
54  
55      /**
56       * The temporary folder to hold intermediate files.
57       */
58      @TempDir
59      public File temporaryFolder;
60  
61      /**
62       * Returns name of the check.
63       *
64       * @return name of the check as a String.
65       */
66      protected abstract String getCheckName();
67  
68      @Override
69      protected String getPackageLocation() {
70          final String subpackage = getCheckName().toLowerCase(Locale.ENGLISH)
71                  .replace("check", "");
72          return "org/checkstyle/suppressionxpathfilter/" + subpackage;
73      }
74  
75      /**
76       * Returns a list of XPath queries to locate the violation nodes in a Java file.
77       *
78       * @param fileToProcess the Java file to be processed. {@link File} type object.
79       * @param position the position of violation in the file. {@link ViolationPosition} object.
80       * @return a list of strings containing XPath queries for locating violation nodes.
81       * @throws Exception can throw exceptions while accessing file contents.
82       */
83      private static List<String> generateXpathQueries(File fileToProcess,
84                                                       ViolationPosition position)
85              throws Exception {
86          final FileText fileText = new FileText(fileToProcess,
87                  StandardCharsets.UTF_8.name());
88          final DetailAST rootAst = JavaParser.parseFile(fileToProcess,
89                  JavaParser.Options.WITH_COMMENTS);
90          final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst,
91                  position.violationLineNumber, position.violationColumnNumber,
92                  fileText, DEFAULT_TAB_WIDTH);
93  
94          return queryGenerator.generate();
95      }
96  
97      /**
98       * Verify generated XPath queries by comparing with expected queries.
99       *
100      * @param generatedXpathQueries a list of generated XPath queries.
101      * @param expectedXpathQueries a list of expected XPath queries.
102      */
103     private static void verifyXpathQueries(List<String> generatedXpathQueries,
104                                            List<String> expectedXpathQueries) {
105         assertWithMessage("Generated queries do not match expected ones")
106             .that(generatedXpathQueries)
107             .isEqualTo(expectedXpathQueries);
108     }
109 
110     /**
111      * Returns the path to the generated Suppressions XPath config file.
112      * This method creates a Suppressions config file containing the Xpath queries for
113      * any check and returns the path to that file.
114      *
115      * @param checkName the name of the check that is run.
116      * @param xpathQueries a list of generated XPath queries for violations in a file.
117      * @return path(String) to the generated Suppressions config file.
118      * @throws Exception can throw exceptions when writing/creating the config file.
119      */
120     private String createSuppressionsXpathConfigFile(String checkName,
121                                                      List<String> xpathQueries)
122             throws Exception {
123         final String uniqueFileName =
124                 "suppressions_xpath_config_" + UUID.randomUUID() + ".xml";
125         final File suppressionsXpathConfigPath = new File(temporaryFolder, uniqueFileName);
126         try (Writer bw = Files.newBufferedWriter(suppressionsXpathConfigPath.toPath(),
127                 StandardCharsets.UTF_8)) {
128             bw.write("<?xml version=\"1.0\"?>\n");
129             bw.write("<!DOCTYPE suppressions PUBLIC\n");
130             bw.write("    \"-//Checkstyle//DTD SuppressionXpathFilter ");
131             bw.write("Experimental Configuration 1.2//EN\"\n");
132             bw.write("    \"https://checkstyle.org/dtds/");
133             bw.write("suppressions_1_2_xpath_experimental.dtd\">\n");
134             bw.write("<suppressions>\n");
135             bw.write("   <suppress-xpath\n");
136             bw.write("       checks=\"");
137             bw.write(checkName);
138             bw.write("\"\n");
139             bw.write("       query=\"");
140             bw.write(String.join(DELIMITER, xpathQueries));
141             bw.write("\"/>\n");
142             bw.write("</suppressions>");
143         }
144 
145         return suppressionsXpathConfigPath.toString();
146     }
147 
148     /**
149      * Returns the config {@link DefaultConfiguration} for the created Suppression XPath filter.
150      *
151      * @param checkName the name of the check that is run.
152      * @param xpathQueries a list of generated XPath queries for violations in a file.
153      * @return the default config for Suppressions XPath filter based on check and xpath queries.
154      * @throws Exception can throw exceptions when creating config.
155      */
156     private DefaultConfiguration createSuppressionXpathFilter(String checkName,
157                                            List<String> xpathQueries) throws Exception {
158         final DefaultConfiguration suppressionXpathFilterConfig =
159                 createModuleConfig(SuppressionXpathFilter.class);
160         suppressionXpathFilterConfig.addProperty("file",
161                 createSuppressionsXpathConfigFile(checkName, xpathQueries));
162 
163         return suppressionXpathFilterConfig;
164     }
165 
166     /**
167      * Extract line no and column nos from String of expected violations.
168      *
169      * @param expectedViolations the expected violations generated for the check.
170      * @return instance of type {@link ViolationPosition} which contains the line and column nos.
171      */
172     private static ViolationPosition extractLineAndColumnNumber(String... expectedViolations) {
173         ViolationPosition violation = null;
174         final Matcher matcher =
175                 LINE_COLUMN_NUMBER_REGEX.matcher(expectedViolations[0]);
176         if (matcher.find()) {
177             final int violationLineNumber = Integer.parseInt(matcher.group(1));
178             final int violationColumnNumber = Integer.parseInt(matcher.group(2));
179             violation = new ViolationPosition(violationLineNumber, violationColumnNumber);
180         }
181         return violation;
182     }
183 
184     /**
185      * Runs three verifications:
186      * First one executes checker with defined module configuration and compares output with
187      * expected violations.
188      * Second one generates xpath queries based on violation message and compares them with expected
189      * xpath queries.
190      * Third one constructs new configuration with {@code SuppressionXpathFilter} using generated
191      * xpath queries, executes checker and checks if no violation occurred.
192      *
193      * @param moduleConfig module configuration.
194      * @param fileToProcess input class file.
195      * @param expectedViolation expected violation message.
196      * @param expectedXpathQueries expected generated xpath queries.
197      * @throws Exception if an error occurs
198      * @throws IllegalArgumentException if length of expectedViolation is more than 1
199      */
200     protected void runVerifications(DefaultConfiguration moduleConfig,
201                                   File fileToProcess,
202                                   String[] expectedViolation,
203                                   List<String> expectedXpathQueries) throws Exception {
204         if (expectedViolation.length != 1) {
205             throw new IllegalArgumentException(
206                     "Expected violations should contain exactly one element."
207                             + " Multiple violations are not supported."
208             );
209         }
210 
211         final ViolationPosition position =
212                 extractLineAndColumnNumber(expectedViolation);
213         final List<String> generatedXpathQueries =
214                 generateXpathQueries(fileToProcess, position);
215 
216         final DefaultConfiguration treeWalkerConfigWithXpath =
217                 createModuleConfig(TreeWalker.class);
218         treeWalkerConfigWithXpath.addChild(moduleConfig);
219         treeWalkerConfigWithXpath.addChild(createSuppressionXpathFilter(moduleConfig.getName(),
220                 generatedXpathQueries));
221 
222         final Integer[] warnList = getLinesWithWarn(fileToProcess.getPath());
223         verify(moduleConfig, fileToProcess.getPath(), expectedViolation, warnList);
224         verifyXpathQueries(generatedXpathQueries, expectedXpathQueries);
225         verify(treeWalkerConfigWithXpath, fileToProcess.getPath(), CommonUtil.EMPTY_STRING_ARRAY);
226     }
227 
228     private static final class ViolationPosition {
229         private final int violationLineNumber;
230         private final int violationColumnNumber;
231 
232         /**
233          * Constructor of the class.
234          *
235          * @param violationLineNumber line no of the violation produced for the check.
236          * @param violationColumnNumber column no of the violation produced for the check.
237          */
238         private ViolationPosition(int violationLineNumber,
239                               int violationColumnNumber) {
240             this.violationLineNumber = violationLineNumber;
241             this.violationColumnNumber = violationColumnNumber;
242         }
243     }
244 }