1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.internal;
21
22 import static com.google.common.truth.Truth.assertWithMessage;
23
24 import java.beans.PropertyDescriptor;
25 import java.io.File;
26 import java.io.IOException;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Locale;
36 import java.util.Map;
37 import java.util.Set;
38 import java.util.stream.Collectors;
39 import java.util.stream.Stream;
40
41 import javax.xml.parsers.ParserConfigurationException;
42
43 import org.apache.commons.beanutils.PropertyUtils;
44 import org.junit.jupiter.api.Test;
45 import org.w3c.dom.Document;
46 import org.w3c.dom.Element;
47 import org.w3c.dom.NodeList;
48 import org.xml.sax.SAXException;
49
50 import com.puppycrawl.tools.checkstyle.AbstractPathTestSupport;
51 import com.puppycrawl.tools.checkstyle.bdd.InlineConfigParser;
52 import com.puppycrawl.tools.checkstyle.bdd.TestInputConfiguration;
53 import com.puppycrawl.tools.checkstyle.bdd.TestInputViolation;
54 import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
55 import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
56 import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
57
58 public class XdocsExampleFileTest {
59
60 private static final Set<String> COMMON_PROPERTIES = Set.of(
61 "severity",
62 "id",
63 "fileExtensions",
64 "tabWidth",
65 "fileContents",
66 "tokens",
67 "javadocTokens",
68 "violateExecutionOnNonTightHtml"
69 );
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 private static final Set<String> UNSUPPORTED_CONFIG_FORMAT_MODULES = Set.of(
85 "checks/header/header/",
86 "checks/header/multifileregexpheader/",
87 "checks/header/regexpheader/",
88 "checks/imports/importcontrol/"
89 );
90
91
92
93
94
95
96
97
98
99
100
101 private static final Set<String> SUPPRESSED_UNIQUENESS_CHECK_MODULES = Set.of(
102 "checks/coding/hiddenfield/",
103 "checks/coding/returncount/",
104 "checks/imports/avoidstarimport/",
105 "checks/javadoc/javadocvariable/",
106 "checks/javadoc/missingjavadoctype/",
107 "checks/naming/illegalidentifiername/",
108 "checks/naming/patternvariablename/",
109 "checks/outertypefilename/",
110 "checks/regexp/regexpmultiline/",
111 "checks/regexp/regexpsingleline/"
112 );
113
114
115
116
117
118
119
120 private static final Set<String> MODULES_WITHOUT_DEFAULT_FIRST_EXAMPLE = Set.of(
121 "checks/translation"
122 );
123
124 @Test
125 public void testAllCheckPropertiesAreUsedInXdocsExamples() throws Exception {
126 final Map<String, Set<String>> usedPropertiesByCheck =
127 XdocUtil.extractUsedPropertiesFromXdocsExamples();
128 final List<String> failures = new ArrayList<>();
129
130 for (Class<?> checkClass : CheckUtil.getCheckstyleChecks()) {
131 final String checkSimpleName = checkClass.getSimpleName();
132
133 final Set<String> definedProperties = Arrays.stream(
134 PropertyUtils.getPropertyDescriptors(checkClass))
135 .filter(descriptor -> descriptor.getWriteMethod() != null)
136 .map(PropertyDescriptor::getName)
137 .filter(property -> !COMMON_PROPERTIES.contains(property))
138 .collect(Collectors.toUnmodifiableSet());
139
140 final Set<String> usedProperties =
141 usedPropertiesByCheck.getOrDefault(checkSimpleName, Collections.emptySet());
142
143 for (String property : definedProperties) {
144 if (!usedProperties.contains(property)) {
145 failures.add("Missing property in xdoc: '"
146 + property + "' of " + checkSimpleName);
147 }
148 }
149 }
150 if (!failures.isEmpty()) {
151 assertWithMessage("Xdocs are missing properties:\n" + String.join("\n", failures))
152 .fail();
153 }
154 }
155
156 @Test
157 public void testAllExampleFilesHaveCorrespondingTestMethods() throws Exception {
158 final Path examplesResources = Path.of("src/xdocs-examples/resources");
159 final Path examplesNonCompilable = Path.of("src/xdocs-examples/resources-noncompilable");
160 final Path examplesTestRoot = Path.of(
161 "src/xdocs-examples/java/com/puppycrawl/tools/checkstyle/checks");
162 final List<String> failures = new ArrayList<>();
163
164 try (Stream<Path> testFiles = Files.walk(examplesTestRoot)) {
165 testFiles
166 .filter(path -> path.toString().endsWith("ExamplesTest.java"))
167 .forEach(testFile -> {
168 try {
169 scanFile(testFile, examplesResources, examplesNonCompilable, failures);
170 }
171 catch (IOException exception) {
172 throw new IllegalStateException("Error processing: "
173 + testFile, exception);
174 }
175 });
176 }
177 if (!failures.isEmpty()) {
178 assertWithMessage("Example files are missing corresponding test methods:\n"
179 + String.join("\n", failures))
180 .fail();
181 }
182 }
183
184 @Test
185 public void testAllExampleFilesAreReferencedInXdocs() throws Exception {
186 final Set<String> referencedPaths = collectReferencedExamplePaths();
187 final Path xdocsExamplesBase = Path.of("src/xdocs-examples");
188 final List<Path> exampleRoots = List.of(
189 xdocsExamplesBase.resolve("resources"),
190 xdocsExamplesBase.resolve("resources-noncompilable")
191 );
192 final List<String> failures = new ArrayList<>();
193
194 for (Path root : exampleRoots) {
195 if (Files.exists(root)) {
196 try (Stream<Path> paths = Files.walk(root)) {
197 paths
198 .filter(path -> {
199 final String fileName = path.getFileName().toString();
200 return Files.isRegularFile(path)
201 && (fileName.startsWith("Example")
202 || fileName.startsWith("UseCase"));
203 })
204 .forEach(exampleFile -> {
205 final String relative = xdocsExamplesBase
206 .relativize(exampleFile)
207 .toString()
208 .replace(File.separatorChar, '/');
209 if (!referencedPaths.contains(relative)) {
210 failures.add(relative);
211 }
212 });
213 }
214 }
215 }
216
217 if (!failures.isEmpty()) {
218 assertWithMessage(
219 "The following example files are not referenced in any xml.template file:\n"
220 + String.join("\n", failures))
221 .fail();
222 }
223 }
224
225 @Test
226 public void testAllModuleExamplesAreBehaviorallyUnique() throws Exception {
227 final Path examplesTestRoot = Path.of(
228 "src/xdocs-examples/java/com/puppycrawl/tools/checkstyle/checks");
229 final Path examplesResources = Path.of("src/xdocs-examples/resources");
230 final Path examplesNonCompilable = Path.of("src/xdocs-examples/resources-noncompilable");
231 final List<String> failures = new ArrayList<>();
232
233 try (Stream<Path> testFiles = Files.walk(examplesTestRoot)) {
234 testFiles
235 .filter(path -> path.toString().endsWith("ExamplesTest.java"))
236 .forEach(testFile -> {
237 try {
238 checkUniquenessForModule(testFile, examplesResources,
239 examplesNonCompilable, failures);
240 }
241 catch (IOException exception) {
242 throw new IllegalStateException("Error processing: "
243 + testFile, exception);
244 }
245 });
246 }
247
248 if (!failures.isEmpty()) {
249 assertWithMessage("Found examples with duplicate behavior:\n"
250 + String.join("\n", failures))
251 .fail();
252 }
253 }
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269 @Test
270 public void testDefaultConfigExampleIsFirst() throws IOException {
271 final List<String> violations = Collections.synchronizedList(new ArrayList<>());
272
273 try (Stream<Path> pathStream = Files.walk(
274 XdocsExamplesAstConsistencyTest.XDOCS_ROOT)) {
275 pathStream
276 .filter(Files::isDirectory)
277 .filter(XdocsExamplesAstConsistencyTest::isModuleDirectory)
278 .parallel()
279 .forEach(dir -> processDirectoryForDefaultConfigOrderCheck(dir, violations));
280 }
281
282 final String message = formatDefaultConfigOrderViolationsMessage(violations);
283
284 assertWithMessage(message)
285 .that(violations)
286 .isEmpty();
287 }
288
289 private static Set<String> collectReferencedExamplePaths() throws Exception {
290 final Set<String> referenced = new HashSet<>();
291
292 for (Path template : XdocUtil.getXdocsTemplatesFilePaths()) {
293 final String input = Files.readString(template);
294 final Document document = XmlUtil.getRawXml(template.toString(), input, input);
295 final NodeList macros = document.getElementsByTagName("macro");
296
297 for (int idx = 0; idx < macros.getLength(); idx++) {
298 final Element macro = (Element) macros.item(idx);
299 if ("example".equals(macro.getAttribute("name"))) {
300 final String path = getMacroParamValue(macro, "path");
301 if (path != null && !path.isEmpty()) {
302 referenced.add(normalizePath(path));
303 }
304 }
305 }
306 }
307 return referenced;
308 }
309
310 private static String getMacroParamValue(Element macro, String paramName) {
311 String result = null;
312 final NodeList params = macro.getElementsByTagName("param");
313
314 for (int idx = 0; idx < params.getLength(); idx++) {
315 final Element param = (Element) params.item(idx);
316 if (paramName.equals(param.getAttribute("name"))) {
317 result = param.getAttribute("value");
318 break;
319 }
320 }
321 return result;
322 }
323
324 private static String normalizePath(String path) {
325 String result = path;
326 if (result.startsWith("/")) {
327 result = result.substring(1);
328 }
329 return result;
330 }
331
332 private static void scanFile(Path testFile, Path examplesResources, Path examplesNonCompilable,
333 List<String> failures)
334 throws IOException {
335 final String testContent = Files.readString(testFile);
336
337 final String className = Path.of("src/xdocs-examples/java").toAbsolutePath()
338 .relativize(testFile.toAbsolutePath()).toString()
339 .replace(File.separator, ".")
340 .replaceFirst("\\.java$", "");
341
342 try {
343 final Class<?> testClass = Class.forName(className);
344 final AbstractPathTestSupport instance = (AbstractPathTestSupport) testClass
345 .getDeclaredConstructor().newInstance();
346 final String packageLocation = instance.getPackageLocation();
347
348 scanExampleDirectory(examplesResources.resolve(packageLocation),
349 testContent, testFile, failures);
350 scanExampleDirectory(examplesNonCompilable.resolve(packageLocation),
351 testContent, testFile, failures);
352 }
353 catch (ReflectiveOperationException exception) {
354 throw new IllegalStateException("Failed to instantiate " + className, exception);
355 }
356 }
357
358 private static void scanExampleDirectory(Path exampleDir, String testContent,
359 Path testFile, List<String> failures) throws IOException {
360 if (Files.exists(exampleDir) && Files.isDirectory(exampleDir)) {
361 try (Stream<Path> exampleFiles = Files.list(exampleDir)) {
362 exampleFiles
363 .filter(path -> {
364 final String fileName = path.getFileName()
365 .toString();
366 return fileName.matches("Example\\d+\\.java");
367 })
368 .forEach(exampleFile -> {
369 final String fileName = exampleFile.getFileName()
370 .toString();
371 if (!testContent.contains("\"" + fileName + "\"")) {
372 failures.add("Missing test for " + fileName + " in "
373 + testFile.getFileName());
374 }
375 });
376 }
377 }
378 }
379
380 private static void checkUniquenessForModule(Path testFile, Path examplesResources,
381 Path examplesNonCompilable, List<String> failures) throws IOException {
382 final String className = Path.of("src/xdocs-examples/java").toAbsolutePath()
383 .relativize(testFile.toAbsolutePath()).toString()
384 .replace(File.separator, ".")
385 .replaceFirst("\\.java$", "");
386
387 try {
388 final Class<?> testClass = Class.forName(className);
389 final AbstractPathTestSupport instance = (AbstractPathTestSupport) testClass
390 .getDeclaredConstructor().newInstance();
391 final String packageLocation = instance.getPackageLocation();
392
393 checkUniquenessInDirectory(examplesResources.resolve(packageLocation), failures);
394 checkUniquenessInDirectory(examplesNonCompilable.resolve(packageLocation), failures);
395 }
396 catch (ReflectiveOperationException exception) {
397 throw new IllegalStateException("Failed to instantiate " + className, exception);
398 }
399 }
400
401 private static void checkUniquenessInDirectory(Path exampleDir, List<String> failures)
402 throws IOException {
403 if (Files.exists(exampleDir) && Files.isDirectory(exampleDir)) {
404 final String normalizedDirPath = exampleDir.toString()
405 .replace(File.separatorChar, '/') + "/";
406
407 final boolean unsupportedFormat = UNSUPPORTED_CONFIG_FORMAT_MODULES.stream()
408 .anyMatch(normalizedDirPath::endsWith);
409
410 if (!unsupportedFormat) {
411 final String moduleName = exampleDir.getFileName().toString();
412 final boolean suppressed = SUPPRESSED_UNIQUENESS_CHECK_MODULES.stream()
413 .anyMatch(normalizedDirPath::endsWith);
414 final Map<String, List<String>> signatureToExamples = collectSignatures(
415 exampleDir, suppressed, failures);
416
417 reportDuplicates(moduleName, suppressed, signatureToExamples, failures);
418 }
419 }
420 }
421
422 private static Map<String, List<String>> collectSignatures(Path exampleDir,
423 boolean suppressed, List<String> failures) throws IOException {
424 final Map<String, List<String>> signatureToExamples = new HashMap<>();
425
426 try (Stream<Path> exampleFiles = Files.list(exampleDir)) {
427 final List<Path> examples = exampleFiles
428 .filter(path -> {
429 return path.getFileName().toString()
430 .matches("Example\\d+\\.java");
431 })
432 .sorted()
433 .toList();
434
435 if (examples.size() >= 2) {
436 for (Path exampleFile : examples) {
437 final String signature = buildSignature(exampleFile, suppressed, failures);
438 if (signature != null) {
439 signatureToExamples
440 .computeIfAbsent(signature, key -> new ArrayList<>())
441 .add(exampleFile.getFileName().toString());
442 }
443 }
444 }
445 }
446
447 return signatureToExamples;
448 }
449
450 private static void reportDuplicates(String moduleName, boolean suppressed,
451 Map<String, List<String>> signatureToExamples, List<String> failures) {
452 if (!suppressed) {
453 signatureToExamples.forEach((signature, examples) -> {
454 if (examples.size() > 1) {
455 failures.add(String.format(Locale.ROOT,
456 "Module '%s': examples %s produce identical violations (%s).",
457 moduleName, examples, signature));
458 }
459 });
460 }
461 }
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492 private static String buildSignature(Path exampleFile, boolean suppressed,
493 List<String> failures) {
494 String signature;
495 try {
496 final TestInputConfiguration parsed =
497 InlineConfigParser.parse(exampleFile.toString());
498 final List<TestInputViolation> violations = parsed.violations();
499
500 final boolean hasUnspecifiedMessage = violations.stream()
501 .anyMatch(violation -> violation.message() == null);
502
503 final int[] sectionBounds = findVisibleSectionBounds(exampleFile);
504
505 if (hasUnspecifiedMessage || sectionBounds == null || violations.isEmpty()) {
506 signature = null;
507 }
508 else {
509 final int startLine = sectionBounds[0];
510 final int endLine = sectionBounds[1];
511
512 final List<TestInputViolation> visibleViolations = violations.stream()
513 .filter(violation -> {
514 return violation.lineNo() > startLine
515 && violation.lineNo() < endLine;
516 })
517 .toList();
518
519 if (visibleViolations.isEmpty()) {
520 signature = null;
521 }
522 else {
523 signature = visibleViolations.stream()
524 .sorted()
525 .map(violation -> {
526 final int relativeLine = violation.lineNo() - startLine;
527 return relativeLine + ":" + violation.message();
528 })
529 .collect(Collectors.joining("|"));
530 }
531 }
532 }
533
534 catch (Exception exception) {
535 if (!suppressed) {
536 failures.add("Failed to parse " + exampleFile + ": " + exception.getMessage());
537 }
538 signature = null;
539 }
540 return signature;
541 }
542
543
544
545
546
547
548
549
550
551 private static int[] findVisibleSectionBounds(Path exampleFile) throws IOException {
552 final List<String> lines = Files.readAllLines(exampleFile);
553 int startLine = -1;
554 int endLine = -1;
555
556 for (int index = 0; index < lines.size(); index++) {
557 final String trimmed = lines.get(index).trim();
558 if (XdocsExamplesAstConsistencyTest.XDOC_START_MARKER.equals(trimmed)) {
559 startLine = index + 1;
560 }
561 else if (XdocsExamplesAstConsistencyTest.XDOC_END_MARKER.equals(trimmed)) {
562 endLine = index + 1;
563 }
564 }
565
566 int[] result = null;
567 if (startLine != -1 && endLine != -1) {
568 result = new int[] {startLine, endLine};
569 }
570 return result;
571 }
572
573
574
575
576
577
578
579
580
581 private static void processDirectoryForDefaultConfigOrderCheck(Path dir,
582 List<String> violations) {
583 try {
584 final List<Path> examples = new ArrayList<>(
585 XdocsExamplesAstConsistencyTest.getExamplePropertyCoverageFiles(dir));
586 examples.addAll(XdocsExamplesAstConsistencyTest
587 .getNonCompilableExamplePropertyCoverageFiles(dir));
588
589 final String moduleName = XdocsExamplesAstConsistencyTest
590 .toModuleClassSimpleName(dir.getFileName().toString());
591 final String relativePath = XdocsExamplesAstConsistencyTest.XDOCS_ROOT
592 .relativize(dir).toString().replace(File.separatorChar, '/');
593
594 if (moduleName != null && !examples.isEmpty()
595 && !XdocsExamplesAstConsistencyTest.isModuleWithNoProperties(examples)
596 && !MODULES_WITHOUT_DEFAULT_FIRST_EXAMPLE.contains(relativePath)) {
597 final String xmlModuleName =
598 XdocsExamplesAstConsistencyTest.stripCheckSuffix(moduleName);
599 checkDefaultConfigExampleOrder(examples, xmlModuleName,
600 relativePath, violations);
601 }
602 }
603 catch (IOException | ParserConfigurationException | SAXException exception) {
604 throw new IllegalStateException("Failed processing directory: " + dir, exception);
605 }
606 }
607
608
609
610
611
612
613
614
615
616
617
618
619 private static void checkDefaultConfigExampleOrder(List<Path> examples,
620 String xmlModuleName, String relativePath, List<String> violations)
621 throws IOException, ParserConfigurationException, SAXException {
622 final Path firstExample = examples.stream()
623 .filter(example -> {
624 return example.getFileName().toString()
625 .matches("Example1(\\..+)?");
626 })
627 .findFirst()
628 .orElse(null);
629
630 if (firstExample != null && !isDefaultConfig(firstExample, xmlModuleName)) {
631 boolean anyDefaultExists = false;
632 for (Path example : examples) {
633 if (isDefaultConfig(example, xmlModuleName)) {
634 anyDefaultExists = true;
635 break;
636 }
637 }
638
639 if (anyDefaultExists) {
640 violations.add("Directory: " + relativePath
641 + "\nDefault-config example exists but is not "
642 + firstExample.getFileName()
643 + " (should be the first example).");
644 }
645 }
646 }
647
648
649
650
651
652
653
654
655
656
657
658 private static boolean isDefaultConfig(Path example, String moduleName)
659 throws IOException, ParserConfigurationException, SAXException {
660 final String xmlBlock =
661 XdocsExamplesAstConsistencyTest.extractXmlConfigBlock(example);
662 final Element moduleElement;
663 if (xmlBlock == null) {
664 moduleElement = null;
665 }
666 else {
667 moduleElement = XdocsExamplesAstConsistencyTest
668 .parseConfigModuleElement(xmlBlock, moduleName);
669 }
670 return moduleElement != null
671 && XdocsExamplesAstConsistencyTest.collectPropertyNames(moduleElement).isEmpty();
672 }
673
674
675
676
677
678
679
680 private static String formatDefaultConfigOrderViolationsMessage(List<String> violations) {
681 final StringBuilder builder = new StringBuilder(1024);
682 if (!violations.isEmpty()) {
683 builder.append("Found ").append(violations.size())
684 .append(" module(s) where the default-config example is not first.\n\n");
685
686 violations.stream()
687 .sorted()
688 .forEach(violation -> builder.append(violation).append("\n\n"));
689 }
690 return builder.toString();
691 }
692
693 }