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