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.io.ByteArrayInputStream;
25 import java.io.File;
26 import java.io.IOException;
27 import java.nio.charset.StandardCharsets;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.Comparator;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Locale;
36 import java.util.Objects;
37 import java.util.Set;
38 import java.util.concurrent.ConcurrentHashMap;
39 import java.util.concurrent.ConcurrentMap;
40 import java.util.function.Predicate;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43 import java.util.stream.Stream;
44
45 import javax.xml.parsers.DocumentBuilder;
46 import javax.xml.parsers.DocumentBuilderFactory;
47 import javax.xml.parsers.ParserConfigurationException;
48
49 import org.junit.jupiter.api.Test;
50 import org.w3c.dom.Document;
51 import org.w3c.dom.Element;
52 import org.w3c.dom.Node;
53 import org.w3c.dom.NodeList;
54 import org.xml.sax.SAXException;
55
56 import com.puppycrawl.tools.checkstyle.JavaParser;
57 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
58 import com.puppycrawl.tools.checkstyle.api.DetailAST;
59 import com.puppycrawl.tools.checkstyle.api.FileContents;
60 import com.puppycrawl.tools.checkstyle.api.FileText;
61 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
62 import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
63 import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
64 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
65
66
67
68
69
70 public class XdocsExamplesAstConsistencyTest {
71
72 public static final String XDOC_START_MARKER = "// xdoc section - start";
73 public static final String XDOC_END_MARKER = "// xdoc section - end";
74
75 public static final Path XDOCS_ROOT = Path.of(
76 "src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle"
77 );
78
79 private static final Path XDOCS_NONCOMPILABLE_ROOT = Path.of(
80 "src/xdocs-examples/resources-noncompilable/com/puppycrawl/tools/checkstyle"
81 );
82
83 private static final Pattern BLOCK_COMMENT_PATTERN = Pattern.compile("(?s)/\\*.*?\\*/");
84
85
86
87
88 private static final Set<String> UNPARSEABLE_EXAMPLES = Set.of(
89 "checks/regexp/regexponfilename/Example1",
90 "checks/translation/Example1",
91 "filters/suppressionxpathsinglefilter/Example7"
92 );
93
94
95
96
97 private static final Set<String> IGNORED_PROPERTIES_FOR_COVERAGE = Set.of(
98 "violateExecutionOnNonTightHtml"
99 );
100
101
102
103
104 private static final ConcurrentMap<String, Integer> PROPERTY_COUNT_CACHE =
105 new ConcurrentHashMap<>();
106
107
108
109
110 private static final ConcurrentMap<String, String> MODULE_SIMPLE_NAME_CACHE =
111 buildModuleSimpleNameIndex();
112
113
114
115
116
117
118 private static final Set<String> SUPPRESSED_EXAMPLES = Set.of(
119
120 "checks/imports/customimportorder/Example10",
121 "checks/imports/customimportorder/Example11",
122 "checks/imports/customimportorder/Example12",
123 "checks/imports/customimportorder/Example13",
124 "checks/imports/customimportorder/Example14",
125 "checks/imports/customimportorder/Example15",
126 "checks/imports/customimportorder/Example2",
127 "checks/imports/customimportorder/Example3",
128 "checks/imports/customimportorder/Example4",
129 "checks/imports/customimportorder/Example5",
130 "checks/imports/customimportorder/Example6",
131 "checks/imports/customimportorder/Example7",
132 "checks/imports/customimportorder/Example8",
133 "checks/imports/customimportorder/Example9",
134 "checks/imports/importorder/Example10",
135 "checks/imports/importorder/Example11",
136 "checks/imports/importorder/Example12",
137 "checks/imports/importorder/Example2",
138 "checks/imports/importorder/Example3",
139 "checks/imports/importorder/Example4",
140 "checks/imports/importorder/Example5",
141 "checks/imports/importorder/Example6",
142 "checks/imports/importorder/Example7",
143 "checks/imports/importorder/Example8",
144 "checks/imports/importorder/Example9"
145 );
146
147
148
149
150
151
152 private static final Set<String> EXAMPLE_DEFAULT_CONFIG_SUPPRESSED_MODULES = Set.of(
153 "checks/coding/returncount",
154 "checks/descendanttoken",
155 "checks/imports/importcontrol",
156 "filters/severitymatchfilter",
157 "filters/suppressionsinglefilter",
158 "filters/suppressionxpathfilter",
159 "filters/suppresswithplaintextcommentfilter"
160 );
161
162
163
164
165
166
167
168 private static final Set<String> EXAMPLE_COUNT_SUPPRESSED_MODULES = Set.of(
169 "checks/javadoc/javadocpackage",
170 "checks/sizes/linelength",
171 "checks/whitespace/emptylineseparator",
172 "filters/suppresswarningsfilter"
173 );
174
175
176
177
178
179
180 @Test
181 public void testExamplesDifferOnlyByComments() throws IOException {
182 final List<Violation> violations = new ArrayList<>();
183
184 try (Stream<Path> pathStream = Files.walk(XDOCS_ROOT)) {
185 final List<Path> exampleDirs = pathStream
186 .filter(Files::isDirectory)
187 .filter(XdocsExamplesAstConsistencyTest::isModuleDirectory)
188 .filter(XdocsExamplesAstConsistencyTest::containsMultipleExamples)
189 .toList();
190
191 for (Path dir : exampleDirs) {
192 final List<Violation> dirViolations = checkExamplesInDirectory(dir);
193 violations.addAll(dirViolations);
194 }
195 }
196
197 final String message;
198
199 if (violations.isEmpty()) {
200 message = "";
201 }
202 else {
203 final StringBuilder builder = new StringBuilder(1024);
204
205 builder.append("Found ")
206 .append(violations.size())
207 .append(" example files with AST mismatches.\n\n");
208
209 for (Violation violation : violations) {
210 builder.append(violation)
211 .append("\n\n");
212 }
213
214 builder.append(
215 """
216 Note: a mismatch reason of "line numbers differ only" usually means \
217 an example has an extra/missing blank line or shifted code relative to its \
218 reference - fix the line alignment before considering suppression.
219 """);
220
221 for (Violation violation : violations) {
222 final String pattern = violation.getSuppressionPattern();
223 builder.append('"').append(pattern).append("\",\n");
224 }
225
226 message = builder.toString();
227 }
228
229 assertWithMessage(message)
230 .that(violations)
231 .isEmpty();
232 }
233
234
235
236
237
238
239 @Test
240 public void testNoBlockCommentMarkers() throws IOException {
241 final List<String> violations = new ArrayList<>();
242
243 try (Stream<Path> pathStream = Files.walk(XDOCS_ROOT)) {
244 pathStream
245 .filter(path -> path.getFileName().toString().matches("Example\\d+\\.java"))
246 .filter(path -> {
247 final String relativePath = getRelativePath(path.getParent());
248 final String fileName = path.getFileName().toString();
249 return !isExampleIndependent(relativePath, fileName);
250 })
251 .sorted()
252 .forEach(path -> {
253 try {
254 violations.addAll(checkForBlockCommentMarkers(path));
255 }
256 catch (IOException exception) {
257 throw new IllegalStateException(
258 "Failed to read file: " + path, exception);
259 }
260 });
261 }
262
263 final String message;
264 if (violations.isEmpty()) {
265 message = "";
266 }
267 else {
268 message = formatBlockCommentMarkerViolationsMessage(violations);
269 }
270
271 assertWithMessage(message)
272 .that(violations)
273 .isEmpty();
274 }
275
276
277
278
279
280
281 @Test
282 public void testExampleCountMatchesPropertyCount() throws IOException {
283 final List<String> violations = Collections.synchronizedList(new ArrayList<>());
284
285 try (Stream<Path> pathStream = Files.walk(XDOCS_ROOT)) {
286 pathStream
287 .filter(Files::isDirectory)
288 .filter(XdocsExamplesAstConsistencyTest::isModuleDirectory)
289 .parallel()
290 .forEach(dir -> processDirectory(dir, violations));
291 }
292
293 final String message = formatViolationsMessage(violations);
294
295 assertWithMessage(message)
296 .that(violations)
297 .isEmpty();
298 }
299
300
301
302
303
304
305 @Test
306 public void testEveryPropertyHasAnExample() throws IOException {
307 final List<String> violations = Collections.synchronizedList(new ArrayList<>());
308
309 try (Stream<Path> pathStream = Files.walk(XDOCS_ROOT)) {
310 pathStream
311 .filter(Files::isDirectory)
312 .filter(XdocsExamplesAstConsistencyTest::isModuleDirectory)
313 .parallel()
314 .forEach(dir -> processDirectoryForPropertyCoverage(dir, violations));
315 }
316
317 final String message = formatPropertyCoverageViolationsMessage(violations);
318
319 assertWithMessage(message)
320 .that(violations)
321 .isEmpty();
322 }
323
324 @Test
325 public void testEveryModuleHasDefaultConfigExample() throws IOException {
326 final List<String> violations = Collections.synchronizedList(new ArrayList<>());
327
328 try (Stream<Path> pathStream = Files.walk(XDOCS_ROOT)) {
329 pathStream
330 .filter(Files::isDirectory)
331 .filter(XdocsExamplesAstConsistencyTest::isModuleDirectory)
332 .parallel()
333 .forEach(dir -> processDirectoryForDefaultConfigCheck(dir, violations));
334 }
335
336 final String message = formatDefaultConfigViolationsMessage(violations);
337
338 assertWithMessage(message)
339 .that(violations)
340 .isEmpty();
341 }
342
343
344
345
346
347
348
349 private static void processDirectoryForDefaultConfigCheck(Path dir, List<String> violations) {
350 try {
351 final List<Path> examples = new ArrayList<>(getExamplePropertyCoverageFiles(dir));
352 examples.addAll(getNonCompilableExamplePropertyCoverageFiles(dir));
353
354 final String moduleName = toModuleClassSimpleName(dir.getFileName().toString());
355 final String relativePath = getRelativePath(dir);
356
357 if (moduleName != null && !examples.isEmpty() && !isModuleWithNoProperties(examples)
358 && !EXAMPLE_DEFAULT_CONFIG_SUPPRESSED_MODULES.contains(relativePath)) {
359 final String xmlModuleName = stripCheckSuffix(moduleName);
360 boolean hasDefaultConfig = false;
361
362 for (Path example : examples) {
363 if (hasExampleDefaultConfig(example, xmlModuleName)) {
364 hasDefaultConfig = true;
365 break;
366 }
367 }
368
369 if (!hasDefaultConfig) {
370 violations.add("Directory: " + relativePath
371 + "\nNo example uses the default configuration "
372 + "(module element with zero configured properties).");
373 }
374 }
375 }
376 catch (IOException | ParserConfigurationException | SAXException exception) {
377 throw new IllegalStateException("Failed processing directory: " + dir, exception);
378 }
379 }
380
381
382
383
384
385
386
387
388
389
390
391 private static boolean hasExampleDefaultConfig(Path example, String xmlModuleName)
392 throws IOException, ParserConfigurationException, SAXException {
393 final String xmlBlock = extractXmlConfigBlock(example);
394 final Element moduleElement;
395 if (xmlBlock == null) {
396 moduleElement = null;
397 }
398 else {
399 moduleElement = parseConfigModuleElement(xmlBlock, xmlModuleName);
400 }
401 return moduleElement != null && collectPropertyNames(moduleElement).isEmpty();
402 }
403
404
405
406
407
408
409
410 private static String formatDefaultConfigViolationsMessage(List<String> violations) {
411 final StringBuilder builder = new StringBuilder(1024);
412 if (!violations.isEmpty()) {
413 builder.append(String.format(Locale.ROOT,
414 "Found %d modules with no example demonstrating the default"
415 + "configuration.%n%n",
416 violations.size()));
417
418 violations.stream()
419 .sorted()
420 .forEach(violation -> builder.append(violation).append("\n\n"));
421 }
422 return builder.toString();
423 }
424
425
426
427
428
429
430
431
432
433 private static List<Path> collectFilesWithinModule(Path dir,
434 Predicate<Path> fileFilter) throws IOException {
435 final List<Path> result = new ArrayList<>();
436
437 try (Stream<Path> pathStream = Files.list(dir)) {
438 for (Path entry : pathStream.toList()) {
439 if (Files.isDirectory(entry)) {
440 if (!isModuleDirectory(entry)) {
441 result.addAll(collectFilesWithinModule(entry, fileFilter));
442 }
443 }
444 else if (fileFilter.test(entry)) {
445 result.add(entry);
446 }
447 }
448 }
449
450 return result;
451 }
452
453
454
455
456
457
458
459 public static boolean isModuleDirectory(Path dir) {
460 return toModuleClassSimpleName(dir.getFileName().toString()) != null;
461 }
462
463 private static void processDirectoryForPropertyCoverage(Path dir, List<String> violations) {
464 try {
465 final List<Path> examples = new ArrayList<>(getExamplePropertyCoverageFiles(dir));
466 examples.addAll(getNonCompilableExamplePropertyCoverageFiles(dir));
467
468 if (examples.size() > 1) {
469 final String violation = checkPropertyCoverage(dir, examples);
470 if (violation != null) {
471 violations.add(violation);
472 }
473 }
474 }
475 catch (IOException exception) {
476 throw new IllegalStateException("Failed processing directory: " + dir, exception);
477 }
478 }
479
480
481
482
483
484
485
486
487 public static List<Path> getNonCompilableExamplePropertyCoverageFiles(Path dir)
488 throws IOException {
489 final String relativePath = getRelativePath(dir);
490 final Path nonCompilableDir = XDOCS_NONCOMPILABLE_ROOT.resolve(relativePath);
491
492 List<Path> examples = List.of();
493 if (Files.isDirectory(nonCompilableDir)) {
494 examples = getExamplePropertyCoverageFiles(nonCompilableDir);
495 }
496 return examples;
497 }
498
499
500
501
502
503
504
505 private static String formatPropertyCoverageViolationsMessage(List<String> violations) {
506 final StringBuilder builder = new StringBuilder(1024);
507 if (!violations.isEmpty()) {
508 builder.append("Found ").append(violations.size())
509 .append(" module(s) with a documented property not covered by any example.\n\n");
510
511 violations.stream()
512 .sorted()
513 .forEach(violation -> builder.append(violation).append("\n\n"));
514
515 builder.append("If intentional add to EXAMPLE_PROPERTY_COVERAGE_SUPPRESSED_MODULES.\n");
516 }
517 return builder.toString();
518 }
519
520
521
522
523
524
525
526
527
528 private static String checkPropertyCoverage(Path dir, List<Path> examples)
529 throws IOException {
530 String result = null;
531
532 if (!isModuleWithNoProperties(examples)) {
533
534 final String moduleName = toModuleClassSimpleName(dir.getFileName().toString());
535
536 if (moduleName != null) {
537 final Set<String> documentedProperties = resolveDocumentedPropertyNames(dir);
538
539 if (!documentedProperties.isEmpty()) {
540 final String xmlModuleName = stripCheckSuffix(moduleName);
541
542 final Set<String> configuredProperties = new HashSet<>();
543 for (Path example : examples) {
544 configuredProperties.addAll(
545 extractConfiguredPropertyNames(example,
546 xmlModuleName));
547 }
548
549 final Set<String> uncoveredProperties = new HashSet<>(documentedProperties);
550 uncoveredProperties.removeAll(configuredProperties);
551 uncoveredProperties.removeAll(IGNORED_PROPERTIES_FOR_COVERAGE);
552
553 if (!uncoveredProperties.isEmpty()) {
554 final String relativePath = getRelativePath(dir);
555 result = "Directory: " + relativePath
556 + "\nDocumented properties: " + documentedProperties
557 + "\nProperties with no covering example: " + uncoveredProperties;
558 }
559 }
560 }
561 }
562
563 return result;
564 }
565
566
567
568
569
570
571
572 private static Set<String> resolveDocumentedPropertyNames(Path dir) {
573 final String moduleName = toModuleClassSimpleName(dir.getFileName().toString());
574 Set<String> result = Set.of();
575
576 if (moduleName != null) {
577 result = XdocUtil.getDocumentedProperties(moduleName);
578 }
579
580 return result;
581 }
582
583
584
585
586
587
588
589
590
591 private static Set<String> extractConfiguredPropertyNames(Path example, String moduleName)
592 throws IOException {
593 Set<String> result = Set.of();
594 final String xmlBlock = extractXmlConfigBlock(example);
595
596 if (xmlBlock != null) {
597 try {
598 final Element moduleElement = parseConfigModuleElement(xmlBlock, moduleName);
599 if (moduleElement != null) {
600 result = collectPropertyNames(moduleElement);
601 }
602 }
603 catch (ParserConfigurationException | SAXException exception) {
604 throw new IllegalStateException(
605 "Failed to parse example config XML: " + example, exception);
606 }
607 }
608
609 return result;
610 }
611
612
613
614
615
616
617
618
619
620
621 public static Element parseConfigModuleElement(String xmlBlock, String moduleName)
622 throws ParserConfigurationException, SAXException {
623 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
624 factory.setValidating(false);
625 factory.setNamespaceAware(false);
626 factory.setFeature(
627 "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
628 factory.setFeature(
629 "http://xml.org/sax/features/external-general-entities", false);
630 factory.setFeature(
631 "http://xml.org/sax/features/external-parameter-entities", false);
632
633 final DocumentBuilder builder = factory.newDocumentBuilder();
634 final Document document;
635 try {
636 document = builder.parse(
637 new ByteArrayInputStream(xmlBlock.getBytes(StandardCharsets.UTF_8)));
638 }
639 catch (IOException exception) {
640 throw new IllegalStateException("Failed to parse in-memory XML block", exception);
641 }
642
643 return findModuleElement(document.getDocumentElement(), moduleName);
644 }
645
646
647
648
649
650
651
652
653 private static Element findModuleElement(Element element, String moduleName) {
654 Element result = null;
655
656 if (moduleName.equals(element.getAttribute("name"))) {
657 result = element;
658 }
659 else {
660 final NodeList children = element.getChildNodes();
661 for (int index = 0; result == null && index < children.getLength(); index++) {
662 final Node node = children.item(index);
663 if (node instanceof Element childElement
664 && "module".equals(node.getNodeName())) {
665 result = findModuleElement(childElement, moduleName);
666 }
667 }
668 }
669
670 return result;
671 }
672
673
674
675
676
677
678
679 public static Set<String> collectPropertyNames(Element moduleElement) {
680 final Set<String> names = new HashSet<>();
681 final NodeList children = moduleElement.getChildNodes();
682
683 for (int index = 0; index < children.getLength(); index++) {
684 final Node node = children.item(index);
685 if (node instanceof Element childElement
686 && "property".equals(node.getNodeName())) {
687 names.add(childElement.getAttribute("name"));
688 }
689 }
690
691 return names;
692 }
693
694
695
696
697
698
699
700
701 public static String extractXmlConfigBlock(Path file) throws IOException {
702 final String content = Files.readString(file);
703 String result = null;
704
705 final int startMarker = content.indexOf("/*xml");
706 if (startMarker >= 0) {
707 final int contentStart = startMarker + "/*xml".length();
708 final int endMarker = content.indexOf("*/", contentStart);
709 if (endMarker >= 0) {
710 result = content.substring(contentStart, endMarker).strip();
711 }
712 }
713
714 return result;
715 }
716
717
718
719
720 private static void processDirectory(Path dir, List<String> violations) {
721 try {
722 final List<Path> examples = getExampleFiles(dir);
723 if (examples.size() > 1) {
724 final String violation = checkExampleCount(dir, examples);
725 if (violation != null) {
726 violations.add(violation);
727 }
728 }
729 }
730 catch (IOException exception) {
731 throw new IllegalStateException("Failed processing directory: " + dir, exception);
732 }
733 }
734
735
736
737
738
739
740
741 private static String formatViolationsMessage(List<String> violations) {
742 final StringBuilder builder = new StringBuilder(1024);
743 if (!violations.isEmpty()) {
744 builder.append("Found ").append(violations.size())
745 .append(" module(s) whose example count does not match property count + 1.\n\n");
746
747 violations.stream()
748 .sorted()
749 .forEach(violation -> builder.append(violation).append("\n\n"));
750 }
751 return builder.toString();
752 }
753
754
755
756
757
758
759
760
761
762 private static String checkExampleCount(Path dir, List<Path> examples) throws IOException {
763 String result = null;
764
765 if (!isModuleWithNoProperties(examples)) {
766 final String relativePath = getRelativePath(dir);
767 final String moduleName = toModuleClassSimpleName(dir.getFileName().toString());
768 final String xmlModuleName;
769 if (moduleName == null) {
770 xmlModuleName = null;
771 }
772 else {
773 xmlModuleName = stripCheckSuffix(moduleName);
774 }
775
776 final List<Path> regularExamples = examples.stream()
777 .filter(example -> {
778 return !isExampleIndependent(
779 relativePath, example.getFileName().toString());
780 })
781 .filter(example -> {
782 return xmlModuleName == null
783 || !hasConfiguresOnlyIgnoredProperties(example, xmlModuleName);
784 })
785 .toList();
786
787 result = validateExampleCount(dir, relativePath, regularExamples);
788 }
789 return result;
790 }
791
792
793
794
795
796
797
798
799
800
801 private static String validateExampleCount(Path dir, String relativePath,
802 List<Path> regularExamples) throws IOException {
803 String result = null;
804 final int propertyCount = resolvePropertyCount(dir);
805
806 if (propertyCount >= 0 && regularExamples.size() > 1
807 && !EXAMPLE_COUNT_SUPPRESSED_MODULES.contains(relativePath)) {
808 final List<Path> parseableExamples = new ArrayList<>();
809 for (Path example : regularExamples) {
810 if (isActuallyParseable(example)) {
811 parseableExamples.add(example);
812 }
813 }
814
815 if (parseableExamples.isEmpty()) {
816 result = checkPropertyCoverageFallback(dir, relativePath,
817 regularExamples, propertyCount);
818 }
819 else {
820 final int largestAstGroupSize =
821 findLargestAstMatchingGroupSize(parseableExamples);
822 final int expected = propertyCount + 1;
823
824 if (largestAstGroupSize != expected) {
825 result = "Directory: " + relativePath
826 + "\nProperties: " + propertyCount
827 + "\nExpected AST-matching examples: " + expected
828 + "\nActual largest AST-matching group: " + largestAstGroupSize
829 + " (of " + parseableExamples.size() + " total example files)";
830 }
831 }
832 }
833 return result;
834 }
835
836
837
838
839
840
841
842
843
844 private static boolean hasConfiguresOnlyIgnoredProperties(Path example, String xmlModuleName) {
845 final boolean result;
846 try {
847 final Set<String> configured = new HashSet<>(
848 extractConfiguredPropertyNames(example, xmlModuleName));
849 result = !configured.isEmpty()
850 && IGNORED_PROPERTIES_FOR_COVERAGE.containsAll(configured);
851 }
852 catch (IOException exception) {
853 throw new IllegalStateException("Failed to read example: " + example, exception);
854 }
855 return result;
856 }
857
858
859
860
861
862
863
864
865 private static boolean isActuallyParseable(Path example) throws IOException {
866 final String xdocSection = extractXdocSection(example);
867 boolean result;
868 try {
869 result = parseContent(xdocSection) != null;
870 }
871 catch (CheckstyleException exception) {
872 result = false;
873 }
874 return result;
875 }
876
877
878
879
880
881
882
883
884
885
886
887 private static String checkPropertyCoverageFallback(Path dir, String relativePath,
888 List<Path> examples, int propertyCount) throws IOException {
889 String result = null;
890
891 final Set<String> documentedProperties = resolveDocumentedPropertyNames(dir);
892 if (!documentedProperties.isEmpty()) {
893 final String moduleName = toModuleClassSimpleName(dir.getFileName().toString());
894 final String xmlModuleName = stripCheckSuffix(moduleName);
895
896 final Set<String> configuredProperties = new HashSet<>();
897 for (Path example : examples) {
898 configuredProperties.addAll(
899 extractConfiguredPropertyNames(example, xmlModuleName));
900 }
901
902 final Set<String> uncoveredProperties = new HashSet<>(documentedProperties);
903 uncoveredProperties.removeAll(configuredProperties);
904 uncoveredProperties.removeAll(IGNORED_PROPERTIES_FOR_COVERAGE);
905
906 if (uncoveredProperties.isEmpty()) {
907 final int expected = propertyCount + 1;
908 if (examples.size() < expected) {
909 result = "Directory: " + relativePath
910 + "\nProperties: " + propertyCount
911 + "\nExpected examples (at least, including a baseline): " + expected
912 + "\nActual example count: " + examples.size();
913 }
914 }
915 else {
916 result = "Directory: " + relativePath
917 + "\nProperties: " + propertyCount
918 + "\nProperties with no covering example (pseudo-path format): "
919 + uncoveredProperties;
920 }
921 }
922
923 return result;
924 }
925
926
927
928
929
930
931
932
933
934 private static int findLargestAstMatchingGroupSize(List<Path> examples) throws IOException {
935 final List<Path> ctorExamples = new ArrayList<>();
936 final List<Path> nonCtorExamples = new ArrayList<>();
937
938 for (Path example : examples) {
939 if (containsConstructorDefinition(example)) {
940 ctorExamples.add(example);
941 }
942 else {
943 nonCtorExamples.add(example);
944 }
945 }
946
947 return Math.max(
948 largestGroupWithinSubset(nonCtorExamples),
949 largestGroupWithinSubset(ctorExamples)
950 );
951 }
952
953
954
955
956
957
958
959
960 private static int largestGroupWithinSubset(List<Path> examples) throws IOException {
961 final List<StructuralAstNode> asts = new ArrayList<>();
962
963 for (Path example : examples) {
964 try {
965 final String xdocSection = extractXdocSection(example);
966 final DetailAST detailAst = parseContent(xdocSection);
967 if (detailAst != null) {
968 asts.add(toStructuralAst(detailAst));
969 }
970 }
971 catch (CheckstyleException exception) {
972
973 }
974 }
975
976 int best = 0;
977 for (StructuralAstNode candidate : asts) {
978 int count = 0;
979 for (StructuralAstNode other : asts) {
980 if (candidate.equals(other)) {
981 count++;
982 }
983 }
984 best = Math.max(best, count);
985 }
986 return best;
987 }
988
989
990
991
992
993
994
995
996 public static boolean isModuleWithNoProperties(List<Path> examples) throws IOException {
997 boolean noProperties = true;
998
999 for (Path example : examples) {
1000 final String content = Files.readString(example);
1001
1002 if (content.contains("<property ")) {
1003 noProperties = false;
1004 break;
1005 }
1006 }
1007
1008 return noProperties;
1009 }
1010
1011
1012
1013
1014
1015
1016
1017 private static int resolvePropertyCount(Path dir) {
1018 final String moduleName = toModuleClassSimpleName(dir.getFileName().toString());
1019 int result = -1;
1020
1021 if (moduleName != null) {
1022 result = PROPERTY_COUNT_CACHE.computeIfAbsent(moduleName,
1023 XdocsExamplesAstConsistencyTest::loadPropertyCount);
1024 }
1025
1026 return result;
1027 }
1028
1029
1030
1031
1032
1033
1034
1035 private static int loadPropertyCount(String moduleName) {
1036 final Set<String> properties = new HashSet<>(
1037 XdocUtil.getDocumentedProperties(moduleName));
1038 properties.removeAll(IGNORED_PROPERTIES_FOR_COVERAGE);
1039 return properties.size();
1040 }
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051 public static String toModuleClassSimpleName(String dirName) {
1052 return MODULE_SIMPLE_NAME_CACHE.get(dirName.toLowerCase(Locale.ROOT));
1053 }
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063 private static ConcurrentMap<String, String> buildModuleSimpleNameIndex() {
1064 final ConcurrentMap<String, String> index = new ConcurrentHashMap<>();
1065
1066 try {
1067 for (Class<?> moduleClass : CheckUtil.getCheckstyleModules()) {
1068 final String simpleName = moduleClass.getSimpleName();
1069 final String stem = stripCheckSuffix(simpleName);
1070 index.putIfAbsent(stem.toLowerCase(Locale.ROOT), simpleName);
1071 }
1072 }
1073 catch (IOException exception) {
1074 throw new IllegalStateException("Failed to build module simple name index",
1075 exception);
1076 }
1077
1078 return index;
1079 }
1080
1081
1082
1083
1084
1085
1086
1087 public static String stripCheckSuffix(String simpleName) {
1088 String result = simpleName;
1089 if (simpleName.endsWith("Check")) {
1090 result = simpleName.substring(0, simpleName.length() - "Check".length());
1091 }
1092 return result;
1093 }
1094
1095
1096
1097
1098
1099
1100
1101 private static String formatBlockCommentMarkerViolationsMessage(List<String> violations) {
1102 final StringBuilder builder = new StringBuilder(1024);
1103 builder.append("Found ")
1104 .append(violations.size())
1105 .append(
1106 """
1107 example file(s) using block comments as ok/violation markers.
1108 Convert them to single-line comments, e.g.:
1109 BAD:
1110 GOOD:
1111
1112 """);
1113
1114 for (String violation : violations) {
1115 builder.append(violation).append('\n');
1116 }
1117
1118 return builder.toString();
1119 }
1120
1121
1122
1123
1124
1125
1126
1127
1128 private static List<String> checkForBlockCommentMarkers(Path file)
1129 throws IOException {
1130 final List<String> fileViolations = new ArrayList<>();
1131 final String content = Files.readString(file);
1132 final Matcher matcher = BLOCK_COMMENT_PATTERN.matcher(content);
1133
1134 while (matcher.find()) {
1135 final String block = matcher.group();
1136 final String inner = block
1137 .replaceAll("^/\\*+", "")
1138 .replaceAll("\\*/$", "")
1139 .replace("*", "")
1140 .strip();
1141
1142 if (inner.startsWith("ok") || inner.startsWith("violation")) {
1143 int lineNo = 1;
1144 for (int index = 0; index < matcher.start(); index++) {
1145 if (content.charAt(index) == '\n') {
1146 lineNo++;
1147 }
1148 }
1149 fileViolations.add(file + ":" + lineNo
1150 + " - use single-line comment instead: // "
1151 + inner);
1152 }
1153 }
1154 return fileViolations;
1155 }
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165 private static boolean containsMultipleExamples(Path dir) {
1166 try {
1167 return collectFilesWithinModule(dir,
1168 path -> path.getFileName().toString().matches("Example\\d+\\.java"))
1169 .size() > 1;
1170 }
1171 catch (IOException exception) {
1172 throw new IllegalStateException("Failed to list files in directory: " + dir,
1173 exception);
1174 }
1175 }
1176
1177
1178
1179
1180
1181
1182
1183
1184 private static List<Violation> checkExamplesInDirectory(Path dir) throws IOException {
1185 final List<Violation> violations = new ArrayList<>();
1186 final List<Path> examples = getExampleFiles(dir);
1187
1188 if (!examples.isEmpty()) {
1189 violations.addAll(compareExamples(dir, examples));
1190 }
1191
1192 return violations;
1193 }
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204 private static List<Path> getExampleFiles(Path dir) throws IOException {
1205 final List<Path> examples = collectFilesWithinModule(dir,
1206 path -> path.getFileName().toString().matches("Example\\d+\\.java"));
1207 return examples.stream()
1208 .sorted(Comparator.comparing(Path::toString))
1209 .toList();
1210 }
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224 public static List<Path> getExamplePropertyCoverageFiles(Path dir) throws IOException {
1225 final List<Path> examples = collectFilesWithinModule(dir, path -> {
1226 return path.getFileName().toString().matches("Example\\d+(\\..+)?")
1227 && hasXmlConfigBlock(path);
1228 });
1229 return examples.stream()
1230 .sorted(Comparator.comparing(Path::toString))
1231 .toList();
1232 }
1233
1234
1235
1236
1237
1238
1239
1240
1241 private static boolean hasXmlConfigBlock(Path file) {
1242 final boolean result;
1243 try {
1244 result = extractXmlConfigBlock(file) != null;
1245 }
1246 catch (IOException exception) {
1247 throw new IllegalStateException("Failed to read file: " + file, exception);
1248 }
1249 return result;
1250 }
1251
1252
1253
1254
1255
1256
1257
1258
1259 private static boolean isExampleUnparseable(String relativePath, String exampleFileName) {
1260 final String exampleName = exampleFileName.replace(".java", "");
1261 final String fullPath = relativePath + "/" + exampleName;
1262 return UNPARSEABLE_EXAMPLES.contains(fullPath);
1263 }
1264
1265
1266
1267
1268
1269
1270
1271
1272 private static boolean isExampleIndependent(String relativePath, String exampleFileName) {
1273 final String exampleName = exampleFileName.replace(".java", "");
1274 final String fullPath = relativePath + "/" + exampleName;
1275 return SUPPRESSED_EXAMPLES.contains(fullPath);
1276 }
1277
1278
1279
1280
1281
1282
1283
1284 private static String getRelativePath(Path dir) {
1285 return XDOCS_ROOT.relativize(dir).toString().replace('\\', '/');
1286 }
1287
1288
1289
1290
1291
1292
1293
1294
1295 private static List<Violation> compareExamples(Path dir, List<Path> examples)
1296 throws IOException {
1297 final List<Violation> violations = new ArrayList<>();
1298
1299 if (!isModuleWithNoProperties(examples)) {
1300 final String relativePath = getRelativePath(dir);
1301
1302 final List<Path> regularExamples = new ArrayList<>();
1303
1304 for (Path example : examples) {
1305 final String fileName = example.getFileName().toString();
1306 if (!isExampleIndependent(relativePath, fileName)) {
1307 regularExamples.add(example);
1308 }
1309 }
1310
1311 if (regularExamples.size() > 1) {
1312 violations.addAll(validateExamplesByConstructorPresence(dir, regularExamples));
1313 }
1314 }
1315
1316 return violations;
1317 }
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327 private static List<Violation> validateExamplesByConstructorPresence(Path dir,
1328 List<Path> examples)
1329 throws IOException {
1330 final List<Violation> violations = new ArrayList<>();
1331 final List<Path> constructorExamples = new ArrayList<>();
1332 final List<Path> nonConstructorExamples = new ArrayList<>();
1333
1334 for (Path example : examples) {
1335 if (containsConstructorDefinition(example)) {
1336 constructorExamples.add(example);
1337 }
1338 else {
1339 nonConstructorExamples.add(example);
1340 }
1341 }
1342
1343 if (nonConstructorExamples.size() > 1) {
1344 violations.addAll(validateAllMatch(dir, nonConstructorExamples));
1345 }
1346 if (constructorExamples.size() > 1) {
1347 violations.addAll(validateAllMatch(dir, constructorExamples));
1348 }
1349
1350 return violations;
1351 }
1352
1353
1354
1355
1356
1357
1358
1359
1360 private static boolean containsConstructorDefinition(Path example) throws IOException {
1361 final String xdocSection = extractXdocSection(example);
1362 boolean result;
1363 try {
1364 final DetailAST ast = parseContent(xdocSection);
1365 result = ast != null && hasDescendantOfType(ast, TokenTypes.CTOR_DEF);
1366 }
1367 catch (CheckstyleException exception) {
1368 result = false;
1369 }
1370
1371 return result;
1372 }
1373
1374
1375
1376
1377
1378
1379
1380
1381 private static boolean hasDescendantOfType(DetailAST ast, int tokenType) {
1382 boolean result = false;
1383 if (ast.getType() == tokenType) {
1384 result = true;
1385 }
1386 else {
1387 for (DetailAST child = ast.getFirstChild(); child != null;
1388 child = child.getNextSibling()) {
1389 if (hasDescendantOfType(child, tokenType)) {
1390 result = true;
1391 break;
1392 }
1393 }
1394 }
1395
1396 return result;
1397 }
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408 private static List<Violation> validateAllMatch(Path dir, List<Path> examples)
1409 throws IOException {
1410 final List<Violation> violations = new ArrayList<>();
1411 final Path reference = examples.getFirst();
1412 final String referenceXdocSection = extractXdocSection(reference);
1413
1414 try {
1415 final DetailAST referenceDetailAst = parseContent(referenceXdocSection);
1416 if (referenceDetailAst != null) {
1417 final StructuralAstNode referenceAst = toStructuralAst(referenceDetailAst);
1418 final List<String> referenceComments =
1419 extractComments(referenceXdocSection);
1420 for (int index = 1; index < examples.size(); index++) {
1421 final Path example = examples.get(index);
1422 final Violation violation = compareSingleExample(
1423 dir, example, reference, referenceAst, referenceComments
1424 );
1425 if (violation != null) {
1426 violations.add(violation);
1427 }
1428 }
1429 }
1430 }
1431 catch (CheckstyleException exception) {
1432
1433 }
1434
1435 return violations;
1436 }
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448 private static String extractXdocSection(Path file) throws IOException {
1449 final List<String> lines = Files.readAllLines(file);
1450 int startIndex = -1;
1451 int endIndex = -1;
1452
1453 for (int index = 0; index < lines.size(); index++) {
1454 final String line = lines.get(index);
1455 if (line.contains(XDOC_START_MARKER)) {
1456 startIndex = index + 1;
1457 }
1458 else if (line.contains(XDOC_END_MARKER)) {
1459 endIndex = index;
1460 break;
1461 }
1462 }
1463
1464 final String result;
1465 if (startIndex == -1 || endIndex == -1 || startIndex >= endIndex) {
1466 result = String.join("\n", lines);
1467 }
1468 else {
1469 result = String.join("\n", lines.subList(startIndex, endIndex));
1470 }
1471
1472 return result;
1473 }
1474
1475
1476
1477
1478
1479
1480
1481
1482 private static DetailAST parseContent(String content) throws CheckstyleException {
1483 final FileText text = new FileText(
1484 new File("Example.java").getAbsoluteFile(),
1485 content.lines().toList()
1486 );
1487 final FileContents contents = new FileContents(text);
1488 return JavaParser.parse(contents);
1489 }
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501 private static Violation compareSingleExample(Path dir, Path example,
1502 Path reference,
1503 StructuralAstNode referenceAst,
1504 List<String> referenceComments)
1505 throws IOException {
1506 final String exampleXdocSection = extractXdocSection(example);
1507 final String relativePath = getRelativePath(dir);
1508 final String exampleFileName = example.getFileName().toString();
1509
1510 DetailAST exampleDetailAst = null;
1511 try {
1512 exampleDetailAst = parseContent(exampleXdocSection);
1513 }
1514 catch (CheckstyleException exception) {
1515 if (!isExampleUnparseable(relativePath, exampleFileName)) {
1516 throw new IllegalStateException(
1517 "Failed to parse example: " + example, exception);
1518 }
1519 }
1520
1521 final Violation result;
1522 if (exampleDetailAst == null) {
1523 result = null;
1524 }
1525 else {
1526 final StructuralAstNode ast = toStructuralAst(exampleDetailAst);
1527
1528 final List<String> exampleComments =
1529 extractComments(exampleXdocSection);
1530
1531 if (referenceAst.equals(ast) && referenceComments.equals(exampleComments)) {
1532 result = null;
1533 }
1534 else if (referenceAst.equals(ast)) {
1535 result = new Violation(relativePath, reference.getFileName().toString(),
1536 example.getFileName().toString(), "Comments mismatch");
1537 }
1538 else if (referenceAst.equalsIgnoringLineNumbers(ast)) {
1539 result = new Violation(relativePath, reference.getFileName().toString(),
1540 example.getFileName().toString(),
1541 "AST structure mismatch (line numbers differ only - "
1542 + "check for added/removed blank lines or shifted code)");
1543 }
1544 else {
1545 result = new Violation(relativePath, reference.getFileName().toString(),
1546 example.getFileName().toString(), "AST structure mismatch");
1547 }
1548 }
1549
1550 return result;
1551 }
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561 private static StructuralAstNode toStructuralAst(DetailAST ast) {
1562 final boolean ignoreName = isTypeName(ast)
1563 || isConstructorName(ast)
1564 || isExtendsAnExampleClass(ast);
1565 final StructuralAstNode node = new StructuralAstNode(
1566 ast.getType(), ast.getText(), ignoreName, ast.getLineNo(), ignoreName
1567 );
1568
1569 for (DetailAST child = ast.getFirstChild();
1570 child != null;
1571 child = child.getNextSibling()) {
1572 final StructuralAstNode structuralChild = toStructuralAst(child);
1573 if (structuralChild != null) {
1574 node.addChild(structuralChild);
1575 }
1576 }
1577 return node;
1578 }
1579
1580
1581
1582
1583
1584
1585
1586 private static boolean isTypeName(DetailAST ast) {
1587 final DetailAST parent = ast.getParent();
1588 return parent != null
1589 && ast.getType() == TokenTypes.IDENT
1590 && (parent.getType() == TokenTypes.CLASS_DEF
1591 || parent.getType() == TokenTypes.INTERFACE_DEF
1592 || parent.getType() == TokenTypes.ENUM_DEF
1593 || parent.getType() == TokenTypes.RECORD_DEF
1594 || parent.getType() == TokenTypes.ANNOTATION_DEF);
1595 }
1596
1597
1598
1599
1600
1601
1602
1603 private static boolean isConstructorName(DetailAST ast) {
1604 final DetailAST parent = ast.getParent();
1605 return parent != null
1606 && ast.getType() == TokenTypes.IDENT
1607 && parent.getType() == TokenTypes.CTOR_DEF;
1608 }
1609
1610
1611
1612
1613
1614
1615
1616 private static boolean isExtendsAnExampleClass(DetailAST ast) {
1617 final DetailAST parent = ast.getParent();
1618 boolean result = false;
1619 if (parent != null
1620 && ast.getType() == TokenTypes.IDENT
1621 && parent.getType() == TokenTypes.EXTENDS_CLAUSE) {
1622 final DetailAST classDef = parent.getParent();
1623 if (classDef != null && classDef.getType() == TokenTypes.CLASS_DEF) {
1624 final DetailAST className = classDef.findFirstToken(TokenTypes.IDENT);
1625 result = className != null
1626 && className.getText().matches("Example\\d+");
1627 }
1628 }
1629 return result;
1630 }
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642 private static boolean isIgnoredComment(String comment) {
1643 return comment.startsWith("ok")
1644 || comment.startsWith("violation")
1645 || comment.startsWith("filtered violation")
1646 || comment.startsWith("xdoc section")
1647 || comment.contains("// ok")
1648 || comment.contains("// violation")
1649 || comment.matches("\\d+\\s+violations?.*")
1650 || comment.matches("'.*'");
1651 }
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664 private static List<String> extractComments(String content) {
1665 final List<String> comments = new ArrayList<>();
1666 comments.addAll(extractJavadocComments(content));
1667 comments.addAll(extractSingleLineComments(content));
1668 return comments;
1669 }
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680 private static List<String> extractJavadocComments(String content) {
1681 final List<String> javadocComments = new ArrayList<>();
1682 final Matcher javadocMatcher = Pattern.compile("/\\*\\*[\\s\\S]*?\\*/").matcher(content);
1683
1684 while (javadocMatcher.find()) {
1685 final String originalJavadoc = javadocMatcher.group().strip();
1686 final StringBuilder filteredJavadoc = new StringBuilder(256);
1687
1688 for (String line : originalJavadoc.lines().toList()) {
1689 filteredJavadoc.append(stripInlineMarkers(line)).append('\n');
1690 }
1691
1692 final String filteredJavadocStr = filteredJavadoc.toString().strip();
1693 if (!filteredJavadocStr.isEmpty()) {
1694 javadocComments.add(filteredJavadocStr);
1695 }
1696 }
1697
1698 return javadocComments;
1699 }
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710 private static String stripInlineMarkers(String line) {
1711 String result = line;
1712 final int commentIndex = findCommentStart(line);
1713
1714 if (commentIndex >= 0) {
1715 final String comment = line.substring(commentIndex + 2).strip();
1716 if (isIgnoredComment(comment)) {
1717 result = line.substring(0, commentIndex).stripTrailing();
1718 }
1719 }
1720
1721 return result;
1722 }
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732 private static List<String> extractSingleLineComments(String content) {
1733 final List<String> comments = new ArrayList<>();
1734 boolean prevLineWasMarker = false;
1735
1736 for (String line : content.lines().toList()) {
1737 final int commentIndex = findCommentStart(line);
1738
1739 if (commentIndex < 0) {
1740 prevLineWasMarker = false;
1741 continue;
1742 }
1743
1744 final String comment = line.substring(commentIndex + 2).strip();
1745 final boolean isCodeBefore = !line.substring(0, commentIndex).isBlank();
1746
1747 if (isIgnoredComment(comment)) {
1748 prevLineWasMarker = true;
1749 }
1750 else if (!prevLineWasMarker || isCodeBefore) {
1751 comments.add(comment);
1752 prevLineWasMarker = false;
1753 }
1754 }
1755
1756 return comments;
1757 }
1758
1759
1760
1761
1762
1763
1764
1765
1766 private static int findCommentStart(String line) {
1767 int result = -1;
1768 boolean inString = false;
1769 boolean inChar = false;
1770 boolean escaped = false;
1771
1772 for (int index = 0; index < line.length() - 1; index++) {
1773 final char current = line.charAt(index);
1774
1775 if (escaped) {
1776 escaped = false;
1777 }
1778 else if (current == '\\') {
1779 escaped = true;
1780 }
1781 else if (!inChar && current == '"') {
1782 inString = !inString;
1783 }
1784 else if (!inString && current == '\'') {
1785 inChar = !inChar;
1786 }
1787 else if (isCommentAt(line, index, inString, inChar)) {
1788 result = index;
1789 break;
1790 }
1791 }
1792
1793 return result;
1794 }
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805 private static boolean isCommentAt(String line, int index, boolean inString, boolean inChar) {
1806 return !inString && !inChar
1807 && line.charAt(index) == '/'
1808 && line.charAt(index + 1) == '/';
1809 }
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823 private static final class StructuralAstNode {
1824 private final int type;
1825 private final String text;
1826
1827 private final Integer lineNo;
1828 private final List<StructuralAstNode> children = new ArrayList<>();
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839 private StructuralAstNode(int type, String text, boolean ignoreText,
1840 int lineNo, boolean ignorePosition) {
1841 this.type = type;
1842 if (ignoreText) {
1843 this.text = null;
1844 }
1845 else if (isLiteralToken(type)) {
1846 this.text = text;
1847 }
1848 else {
1849 this.text = null;
1850 }
1851 if (ignorePosition) {
1852 this.lineNo = null;
1853 }
1854 else {
1855 this.lineNo = lineNo;
1856 }
1857 }
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870 private static boolean isLiteralToken(int tokenType) {
1871 return switch (tokenType) {
1872 case TokenTypes.NUM_INT, TokenTypes.NUM_LONG, TokenTypes.NUM_FLOAT,
1873 TokenTypes.NUM_DOUBLE, TokenTypes.STRING_LITERAL,
1874 TokenTypes.CHAR_LITERAL, TokenTypes.LITERAL_TRUE,
1875 TokenTypes.LITERAL_FALSE, TokenTypes.LITERAL_NULL,
1876 TokenTypes.IDENT -> true;
1877 default -> false;
1878 };
1879 }
1880
1881 private void addChild(StructuralAstNode child) {
1882 children.add(child);
1883 }
1884
1885 @Override
1886 public boolean equals(Object obj) {
1887 if (!(obj instanceof StructuralAstNode other)) {
1888 return false;
1889 }
1890 final boolean typeMatch = type == other.type;
1891 final boolean textMatch = Objects.equals(text, other.text);
1892 final boolean lineNoMatch = Objects.equals(lineNo, other.lineNo);
1893 final boolean childrenMatch = children.equals(other.children);
1894 return typeMatch && textMatch && lineNoMatch && childrenMatch;
1895 }
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907 private boolean equalsIgnoringLineNumbers(StructuralAstNode other) {
1908 final boolean typeMatch = type == other.type;
1909 final boolean textMatch = Objects.equals(text, other.text);
1910 boolean childrenMatch = children.size() == other.children.size();
1911 if (childrenMatch) {
1912 for (int index = 0; index < children.size(); index++) {
1913 if (!children.get(index)
1914 .equalsIgnoringLineNumbers(other.children.get(index))) {
1915 childrenMatch = false;
1916 break;
1917 }
1918 }
1919 }
1920 return typeMatch && textMatch && childrenMatch;
1921 }
1922
1923 @Override
1924 public int hashCode() {
1925 return Objects.hash(type, text, lineNo, children);
1926 }
1927
1928 @Override
1929 public String toString() {
1930 final StringBuilder sb = new StringBuilder(128);
1931 sb.append("StructuralAstNode{type=");
1932 try {
1933 sb.append(TokenUtil.getTokenName(type));
1934 }
1935 catch (IllegalArgumentException exception) {
1936 sb.append(type);
1937 }
1938 if (text != null) {
1939 sb.append(", text='").append(text).append('\'');
1940 }
1941 if (lineNo != null) {
1942 sb.append(", line=").append(lineNo);
1943 }
1944 if (!children.isEmpty()) {
1945 sb.append(", children=").append(children.size());
1946 }
1947 sb.append('}');
1948 return sb.toString();
1949 }
1950 }
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960 private record Violation(String relativePath, String referenceFileName,
1961 String mismatchFileName, String reason) {
1962 @Override
1963 public String toString() {
1964 return "Directory: " + relativePath + "\n"
1965 + "Reference: " + referenceFileName + "\n"
1966 + "Mismatch: " + mismatchFileName + "\n"
1967 + "Reason: " + reason;
1968 }
1969
1970
1971
1972
1973
1974
1975 String getSuppressionPattern() {
1976 return relativePath + "/" + mismatchFileName.replace(".java", "");
1977 }
1978 }
1979
1980 }