View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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 com.puppycrawl.tools.checkstyle.internal;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.io.IOException;
25  import java.nio.file.DirectoryStream;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.function.Function;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  import java.util.stream.Collectors;
38  import java.util.stream.Stream;
39  
40  import org.junit.jupiter.api.BeforeEach;
41  import org.junit.jupiter.api.Test;
42  
43  import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
44  import com.puppycrawl.tools.checkstyle.Definitions;
45  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
46  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck;
47  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocStyleCheck;
48  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTypeCheck;
49  import com.puppycrawl.tools.checkstyle.checks.javadoc.WriteTagCheck;
50  import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
51  
52  public class XpathRegressionTest extends AbstractModuleTestSupport {
53  
54      // Checks that not compatible with SuppressionXpathFilter
55      public static final Set<String> INCOMPATIBLE_CHECK_NAMES = Set.of(
56              "NoCodeInFile (reason is that AST is not generated for a file not containing code)",
57              "Regexp (reason is at  #7759)",
58              "RegexpSinglelineJava (reason is at  #7759)"
59      );
60  
61      // Javadoc checks are not compatible with SuppressionXpathFilter
62      // till https://github.com/checkstyle/checkstyle/issues/5770
63      // then all of them should be added to #INCOMPATIBLE_CHECK_NAMES
64      // and this field should be removed
65      public static final Set<String> INCOMPATIBLE_JAVADOC_CHECK_NAMES = Set.of(
66                      "AtclauseOrder",
67                      "JavadocBlockTagLocation",
68                      "JavadocMethod",
69                      "JavadocMissingLeadingAsterisk",
70                      "JavadocLeadingAsteriskAlign",
71                      "JavadocMissingWhitespaceAfterAsterisk",
72                      "JavadocParagraph",
73                      "JavadocStyle",
74                      "JavadocTagContinuationIndentation",
75                      "JavadocType",
76                      "MissingDeprecated",
77                      "NonEmptyAtclauseDescription",
78                      "RequireEmptyLineBeforeBlockTagGroup",
79                      "SingleLineJavadoc",
80                      "SummaryJavadoc",
81                      "WriteTag"
82      );
83  
84      // Older regex-based checks that are under INCOMPATIBLE_JAVADOC_CHECK_NAMES
85      // but not subclasses of AbstractJavadocCheck.
86      private static final Set<Class<?>> REGEXP_JAVADOC_CHECKS = Set.of(
87                      JavadocStyleCheck.class,
88                      JavadocMethodCheck.class,
89                      JavadocTypeCheck.class,
90                      WriteTagCheck.class
91      );
92  
93      // Modules that will never have xpath support ever because they not report violations
94      private static final Set<String> NO_VIOLATION_MODULES = Set.of(
95              "SuppressWarningsHolder"
96      );
97  
98      private static final Set<String> SIMPLE_CHECK_NAMES = getSimpleCheckNames();
99      private static final Map<String, String> ALLOWED_DIRECTORY_AND_CHECKS =
100         getAllowedDirectoryAndChecks();
101 
102     private static final Set<String> INTERNAL_MODULES = getInternalModules();
103     private static final DirectoryStream.Filter<Path> IS_DIRECTORY = path
104             -> path.toFile().isDirectory();
105 
106     private Path javaDir;
107     private Path inputDir;
108 
109     private static Set<String> getSimpleCheckNames() {
110         try {
111             return CheckUtil.getSimpleNames(CheckUtil.getCheckstyleChecks());
112         }
113         catch (IOException exc) {
114             throw new ExceptionInInitializerError(exc);
115         }
116     }
117 
118     private static Map<String, String> getAllowedDirectoryAndChecks() {
119         return SIMPLE_CHECK_NAMES
120             .stream()
121             .collect(Collectors.toUnmodifiableMap(
122                 id -> id.toLowerCase(Locale.ENGLISH), Function.identity()));
123     }
124 
125     private static Set<String> getInternalModules() {
126         return Definitions.INTERNAL_MODULES.stream()
127             .map(moduleName -> {
128                 final String[] packageTokens = moduleName.split("\\.");
129                 return packageTokens[packageTokens.length - 1];
130             })
131             .collect(Collectors.toUnmodifiableSet());
132     }
133 
134     @BeforeEach
135     public void setUp() throws Exception {
136         javaDir = Path.of("src/it/java/" + getPackageLocation());
137         inputDir = Path.of(getPath(""));
138     }
139 
140     @Override
141     protected String getPackageLocation() {
142         return "org/checkstyle/suppressionxpathfilter";
143     }
144 
145     @Override
146     protected String getResourceLocation() {
147         return "it";
148     }
149 
150     @Test
151     public void validateIncompatibleJavadocCheckNames() throws IOException {
152         // subclasses of AbstractJavadocCheck
153         final Set<Class<?>> abstractJavadocCheckNames = CheckUtil.getCheckstyleChecks()
154                 .stream()
155                 .filter(AbstractJavadocCheck.class::isAssignableFrom)
156                 .collect(Collectors.toCollection(HashSet::new));
157         // add the extra checks
158         abstractJavadocCheckNames.addAll(REGEXP_JAVADOC_CHECKS);
159         final Set<String> abstractJavadocCheckSimpleNames =
160                 CheckUtil.getSimpleNames(abstractJavadocCheckNames);
161         abstractJavadocCheckSimpleNames.removeAll(INTERNAL_MODULES);
162         assertWithMessage("INCOMPATIBLE_JAVADOC_CHECK_NAMES should contains all descendants "
163                     + "of AbstractJavadocCheck")
164             .that(abstractJavadocCheckSimpleNames)
165             .isEqualTo(INCOMPATIBLE_JAVADOC_CHECK_NAMES);
166     }
167 
168     @Test
169     public void validateIntegrationTestClassNames() throws Exception {
170         final Set<String> compatibleChecks = new HashSet<>();
171         final Pattern pattern = Pattern.compile("^XpathRegression(.+)Test\\.java$");
172         try (Stream<Path> javaPathsStream = Files.walk(Paths.get(javaDir.toString()))) {
173             final List<Path> javaPaths = javaPathsStream.filter(Files::isRegularFile).toList();
174 
175             for (Path path : javaPaths) {
176                 assertWithMessage(path + " is not a regular file")
177                         .that(Files.isRegularFile(path))
178                         .isTrue();
179                 final String filename = path.toFile().getName();
180                 if (filename.startsWith("Abstract")) {
181                     continue;
182                 }
183 
184                 final Matcher matcher = pattern.matcher(filename);
185                 assertWithMessage(
186                             "Invalid test file: " + filename + ", expected pattern: " + pattern)
187                         .that(matcher.matches())
188                         .isTrue();
189 
190                 final String check = matcher.group(1);
191                 assertWithMessage("Unknown check '" + check + "' in test file: " + filename)
192                         .that(SIMPLE_CHECK_NAMES)
193                         .contains(check);
194 
195                 assertWithMessage(
196                             "Check '" + check + "' is now compatible with SuppressionXpathFilter."
197                                 + " Please update the todo list in"
198                                 + " XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES")
199                         .that(INCOMPATIBLE_CHECK_NAMES.contains(check))
200                         .isFalse();
201                 compatibleChecks.add(check);
202             }
203         }
204 
205         // Ensure that all lists are up-to-date
206         final Set<String> allChecks = new HashSet<>(SIMPLE_CHECK_NAMES);
207         allChecks.removeAll(INCOMPATIBLE_JAVADOC_CHECK_NAMES);
208         allChecks.removeAll(INCOMPATIBLE_CHECK_NAMES);
209         allChecks.removeAll(Set.of("Regexp", "RegexpSinglelineJava", "NoCodeInFile"));
210         allChecks.removeAll(NO_VIOLATION_MODULES);
211         allChecks.removeAll(compatibleChecks);
212         allChecks.removeAll(INTERNAL_MODULES);
213 
214         assertWithMessage("XpathRegressionTest is missing for [" + String.join(", ", allChecks)
215                 + "]. Please add them to src/it/java/org/checkstyle/suppressionxpathfilter")
216                         .that(allChecks)
217                         .isEmpty();
218     }
219 
220     @Test
221     public void validateInputFiles() throws Exception {
222         try (DirectoryStream<Path> dirs = Files.newDirectoryStream(inputDir, IS_DIRECTORY);
223              Stream<Path> testPathsStream = Files.walk(Paths.get(javaDir.toString()))) {
224             final List<Path> testDirs = testPathsStream.filter(Files::isDirectory).toList();
225 
226             for (Path dir : dirs) {
227                 // input directory must be named in lower case
228                 assertWithMessage(dir + " is not a directory")
229                         .that(Files.isDirectory(dir))
230                         .isTrue();
231                 final String dirName = dir.toFile().getName();
232                 assertWithMessage("Invalid directory name: " + dirName)
233                         .that(ALLOWED_DIRECTORY_AND_CHECKS.containsKey(dirName)
234                             || isDirNameModuleCategoryName(dirName, testDirs))
235                         .isTrue();
236 
237                 // input directory must be connected to an existing test
238                 final String check = ALLOWED_DIRECTORY_AND_CHECKS.get(dirName);
239                 final Path javaPath = javaDir.resolve("XpathRegression" + check + "Test.java");
240                 assertWithMessage("Input directory '" + dir
241                             + "' is not connected to Java test case: " + javaPath)
242                         .that(Files.exists(javaPath)
243                             || isDirNameModuleCategoryName(dirName, testDirs))
244                         .isTrue();
245 
246                 // input files should be named correctly
247                 validateInputDirectory(dir);
248             }
249         }
250     }
251 
252     private static boolean isDirNameModuleCategoryName(String dirName, List<Path> dirPaths) {
253         return dirPaths.stream().anyMatch(someDir -> someDir.toString().contains(dirName));
254     }
255 
256     private static void validateInputDirectory(Path checkDir) throws IOException {
257         final Pattern pattern = Pattern.compile("^InputXpath(.+)\\.java$");
258         final String check = ALLOWED_DIRECTORY_AND_CHECKS.get(checkDir.toFile().getName());
259 
260         try (DirectoryStream<Path> inputPaths = Files.newDirectoryStream(checkDir)) {
261             for (Path inputPath : inputPaths) {
262                 final String filename = inputPath.toFile().getName();
263                 if (filename.endsWith("java")) {
264                     final Matcher matcher = pattern.matcher(filename);
265                     assertWithMessage(
266                               "Invalid input file '" + inputPath
267                               + "', expected pattern:" + pattern)
268                             .that(matcher.matches())
269                             .isTrue();
270 
271                     final String remaining = matcher.group(1);
272                     assertWithMessage("Check name '" + check
273                                 + "' should be included in input file: " + inputPath)
274                             .that(remaining)
275                             .startsWith(check);
276                 }
277             }
278         }
279     }
280 }