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.File;
25 import java.io.IOException;
26 import java.nio.charset.StandardCharsets;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.util.ArrayList;
30 import java.util.Comparator;
31 import java.util.List;
32 import java.util.Objects;
33 import java.util.Set;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import java.util.stream.Stream;
37
38 import org.junit.jupiter.api.Test;
39
40 import com.puppycrawl.tools.checkstyle.JavaParser;
41 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
42 import com.puppycrawl.tools.checkstyle.api.DetailAST;
43 import com.puppycrawl.tools.checkstyle.api.FileContents;
44 import com.puppycrawl.tools.checkstyle.api.FileText;
45 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
46 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 public class XdocsExamplesAstConsistencyTest {
77
78 private static final Path XDOCS_ROOT = Path.of(
79 "src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle"
80 );
81 private static final String XDOC_START_MARKER = "// xdoc section -- start";
82 private static final String XDOC_END_MARKER = "// xdoc section -- end";
83
84
85
86
87
88
89 private static final Set<String> UNPARSEABLE_EXAMPLES = Set.of(
90 "checks/regexp/regexponfilename/Example1",
91 "checks/translation/Example1",
92 "filters/suppressionxpathsinglefilter/Example14"
93 );
94
95
96
97
98
99
100
101
102
103 private static final Set<String> SUPPRESSED_EXAMPLES = Set.of(
104 "checks/annotation/annotationusestyle/Example2",
105 "checks/annotation/annotationusestyle/Example3",
106 "checks/annotation/annotationusestyle/Example4",
107 "checks/annotation/missingoverride/Example2",
108 "checks/annotation/suppresswarnings/Example2",
109 "checks/annotation/suppresswarningsholder/Example2",
110 "checks/annotation/suppresswarningsholder/Example3",
111 "checks/annotation/suppresswarningsholder/Example4",
112 "checks/blocks/emptyblock/Example2",
113 "checks/blocks/emptyblock/Example3",
114 "checks/blocks/leftcurly/Example3",
115 "checks/blocks/needbraces/Example2",
116 "checks/blocks/needbraces/Example3",
117 "checks/blocks/needbraces/Example4",
118 "checks/blocks/needbraces/Example5",
119 "checks/blocks/rightcurly/Example2",
120 "checks/coding/equalsavoidnull/Example2",
121 "checks/coding/explicitinitialization/Example2",
122 "checks/coding/fallthrough/Example2",
123 "checks/coding/fallthrough/Example3",
124 "checks/coding/hiddenfield/Example2",
125 "checks/coding/hiddenfield/Example3",
126 "checks/coding/hiddenfield/Example4",
127 "checks/coding/hiddenfield/Example5",
128 "checks/coding/hiddenfield/Example6",
129 "checks/coding/hiddenfield/Example7",
130 "checks/coding/illegalinstantiation/Example2",
131 "checks/coding/illegalinstantiation/Example3",
132 "checks/coding/illegalsymbol/Example4",
133 "checks/coding/illegalthrows/Example2",
134 "checks/coding/illegalthrows/Example3",
135 "checks/coding/illegaltype/Example2",
136 "checks/coding/illegaltype/Example6",
137 "checks/coding/illegaltype/Example7",
138 "checks/coding/illegaltype/Example8",
139 "checks/coding/magicnumber/Example2",
140 "checks/coding/magicnumber/Example3",
141 "checks/coding/magicnumber/Example4",
142 "checks/coding/magicnumber/Example5",
143 "checks/coding/magicnumber/Example6",
144 "checks/coding/nestedfordepth/Example2",
145 "checks/coding/nestedifdepth/Example2",
146 "checks/coding/onestatementperline/Example2",
147 "checks/coding/packagedeclaration/Example2",
148 "checks/coding/unnecessarysemicolonafteroutertypedeclaration/Example2",
149 "checks/coding/variabledeclarationusagedistance/Example3",
150 "checks/coding/variabledeclarationusagedistance/Example4",
151 "checks/coding/variabledeclarationusagedistance/Example5",
152 "checks/coding/variabledeclarationusagedistance/Example6",
153 "checks/descendanttoken/Example10",
154 "checks/descendanttoken/Example11",
155 "checks/descendanttoken/Example12",
156 "checks/descendanttoken/Example13",
157 "checks/descendanttoken/Example14",
158 "checks/descendanttoken/Example15",
159 "checks/descendanttoken/Example16",
160 "checks/descendanttoken/Example17",
161 "checks/descendanttoken/Example4",
162 "checks/descendanttoken/Example5",
163 "checks/descendanttoken/Example6",
164 "checks/descendanttoken/Example7",
165 "checks/descendanttoken/Example8",
166 "checks/descendanttoken/Example9",
167 "checks/design/visibilitymodifier/Example12",
168 "checks/design/visibilitymodifier/Example2",
169 "checks/design/visibilitymodifier/Example3",
170 "checks/design/visibilitymodifier/Example4",
171 "checks/design/visibilitymodifier/Example5",
172 "checks/design/visibilitymodifier/Example6",
173 "checks/design/visibilitymodifier/Example8",
174 "checks/imports/importcontrol/Example12",
175 "checks/imports/importcontrol/Example5",
176 "checks/imports/importcontrol/filters/Example9",
177 "checks/imports/importcontrol/someImports/Example11",
178 "checks/imports/importcontrol/someImports/Example6",
179 "checks/imports/importcontrol/someImports/Example7",
180 "checks/indentation/indentation/Example7",
181 "checks/javadoc/atclauseorder/Example2",
182 "checks/javadoc/atclauseorder/Example3",
183 "checks/javadoc/javadoccontentlocation/Example2",
184 "checks/javadoc/javadocleadingasteriskalign/Example2",
185 "checks/javadoc/javadocleadingasteriskalign/Example3",
186 "checks/javadoc/javadocmethod/Example2",
187 "checks/javadoc/javadocmethod/Example3",
188 "checks/javadoc/javadocmethod/Example4",
189 "checks/javadoc/javadocmethod/Example6",
190 "checks/javadoc/javadocpackage/legacywithboth/Example3",
191 "checks/javadoc/javadocpackage/nonlegacy/Example1",
192 "checks/javadoc/javadocstyle/Example2",
193 "checks/javadoc/javadocstyle/Example3",
194 "checks/javadoc/javadocstyle/Example4",
195 "checks/javadoc/javadocstyle/Example5",
196 "checks/javadoc/javadocstyle/Example6",
197 "checks/javadoc/javadocstyle/Example7",
198 "checks/javadoc/javadocstyle/Example8",
199 "checks/javadoc/javadoctagcontinuationindentation/Example3",
200 "checks/javadoc/javadocvariable/Example5",
201 "checks/javadoc/missingjavadoctype/Example3",
202 "checks/javadoc/missingjavadoctype/Example4",
203 "checks/javadoc/missingjavadoctype/Example5",
204 "checks/javadoc/writetag/Example3",
205 "checks/javadoc/writetag/Example4",
206 "checks/javadoc/writetag/Example5",
207 "checks/metrics/booleanexpressioncomplexity/Example2",
208 "checks/metrics/booleanexpressioncomplexity/Example3",
209 "checks/metrics/classdataabstractioncoupling/Example2",
210 "checks/metrics/classdataabstractioncoupling/Example3",
211 "checks/metrics/classdataabstractioncoupling/ignore/Example7",
212 "checks/metrics/classdataabstractioncoupling/ignore/Example8",
213 "checks/metrics/classdataabstractioncoupling/ignore/Example9",
214 "checks/metrics/classdataabstractioncoupling/ignore/deeper/Example5",
215 "checks/metrics/classdataabstractioncoupling/ignore/deeper/Example6",
216 "checks/metrics/classfanoutcomplexity/Example2",
217 "checks/metrics/classfanoutcomplexity/Example3",
218 "checks/metrics/classfanoutcomplexity/Example4",
219 "checks/metrics/classfanoutcomplexity/Example5",
220 "checks/metrics/classfanoutcomplexity/Example6",
221 "checks/metrics/cyclomaticcomplexity/Example2",
222 "checks/metrics/cyclomaticcomplexity/Example3",
223 "checks/modifier/interfacememberimpliedmodifier/Example2",
224 "checks/modifier/interfacememberimpliedmodifier/Example3",
225 "checks/modifier/interfacememberimpliedmodifier/Example4",
226 "checks/modifier/redundantmodifier/Example2",
227 "checks/naming/abstractclassname/Example3",
228 "checks/naming/abstractclassname/Example4",
229 "checks/naming/catchparametername/Example2",
230 "checks/naming/methodname/Example4",
231 "checks/naming/packagename/Example2",
232 "checks/naming/patternvariablename/Example2",
233 "checks/naming/patternvariablename/Example3",
234 "checks/naming/patternvariablename/Example4",
235 "checks/naming/typename/Example2",
236 "checks/naming/typename/Example3",
237 "checks/naming/typename/Example4",
238 "checks/newlineatendoffile/Example2",
239 "checks/newlineatendoffile/Example3",
240 "checks/newlineatendoffile/Example5",
241 "checks/regexp/regexp/Example7",
242 "checks/regexp/regexpmultiline/Example5",
243 "checks/regexp/regexpsingleline/Example2",
244 "checks/regexp/regexpsingleline/Example3",
245 "checks/regexp/regexpsingleline/Example4",
246 "checks/regexp/regexpsingleline/Example5",
247 "checks/regexp/regexpsingleline/Example6",
248 "checks/regexp/regexpsinglelinejava/Example2",
249 "checks/regexp/regexpsinglelinejava/Example3",
250 "checks/regexp/regexpsinglelinejava/Example4",
251 "checks/regexp/regexpsinglelinejava/Example5",
252 "checks/sizes/filelength/Example2",
253 "checks/sizes/filelength/Example3",
254 "checks/sizes/lambdabodylength/Example2",
255 "checks/sizes/methodcount/Example2",
256 "checks/sizes/methodcount/Example3",
257 "checks/sizes/methodcount/Example6",
258 "checks/sizes/methodlength/Example3",
259 "checks/sizes/outertypenumber/Example2",
260 "checks/whitespace/separatorwrap/Example2",
261 "checks/whitespace/separatorwrap/Example3",
262 "checks/whitespace/singlespaceseparator/Example2",
263 "checks/whitespace/typecastparenpad/Example2",
264 "checks/whitespace/whitespaceafter/Example2",
265 "checks/todocomment/Example2",
266 "checks/todocomment/Example3",
267 "checks/trailingcomment/Example3",
268 "checks/uncommentedmain/Example2",
269 "checks/uniqueproperties/Example2",
270 "checks/whitespace/emptyforiteratorpad/Example2",
271 "checks/whitespace/parenpad/Example2",
272 "filters/suppressioncommentfilter/Example2",
273 "filters/suppressioncommentfilter/Example3",
274 "filters/suppressioncommentfilter/Example4",
275 "filters/suppressioncommentfilter/Example5",
276 "filters/suppressioncommentfilter/Example6",
277 "filters/suppressioncommentfilter/Example7",
278 "filters/suppressioncommentfilter/Example8",
279 "filters/suppressionsinglefilter/Example2",
280 "filters/suppressionsinglefilter/Example3",
281 "filters/suppressionsinglefilter/Example4",
282 "filters/suppressionxpathfilter/Example10",
283 "filters/suppressionxpathfilter/Example11",
284 "filters/suppressionxpathfilter/Example12",
285 "filters/suppressionxpathfilter/Example13",
286 "filters/suppressionxpathfilter/Example14",
287 "filters/suppressionxpathfilter/Example2",
288 "filters/suppressionxpathfilter/Example3",
289 "filters/suppressionxpathfilter/Example4",
290 "filters/suppressionxpathfilter/Example5",
291 "filters/suppressionxpathfilter/Example6",
292 "filters/suppressionxpathfilter/Example7",
293 "filters/suppressionxpathfilter/Example8",
294 "filters/suppressionxpathfilter/Example9",
295 "filters/suppressionxpathsinglefilter/Example10",
296 "filters/suppressionxpathsinglefilter/Example11",
297 "filters/suppressionxpathsinglefilter/Example12",
298 "filters/suppressionxpathsinglefilter/Example13",
299 "filters/suppressionxpathsinglefilter/Example2",
300 "filters/suppressionxpathsinglefilter/Example3",
301 "filters/suppressionxpathsinglefilter/Example4",
302 "filters/suppressionxpathsinglefilter/Example5",
303 "filters/suppressionxpathsinglefilter/Example6",
304 "filters/suppressionxpathsinglefilter/Example7",
305 "filters/suppressionxpathsinglefilter/Example8",
306 "filters/suppressionxpathsinglefilter/Example9",
307 "filters/suppresswarningsfilter/Example2",
308 "filters/suppresswithnearbycommentfilter/Example2",
309 "filters/suppresswithnearbycommentfilter/Example3",
310 "filters/suppresswithnearbycommentfilter/Example4",
311 "filters/suppresswithnearbycommentfilter/Example5",
312 "filters/suppresswithnearbycommentfilter/Example6",
313 "filters/suppresswithnearbycommentfilter/Example7",
314 "filters/suppresswithnearbycommentfilter/Example8",
315 "filters/suppresswithnearbytextfilter/Example2",
316 "filters/suppresswithnearbytextfilter/Example3",
317 "filters/suppresswithnearbytextfilter/Example4",
318 "filters/suppresswithnearbytextfilter/Example5",
319 "filters/suppresswithnearbytextfilter/Example6",
320 "filters/suppresswithnearbytextfilter/Example7",
321 "filters/suppresswithnearbytextfilter/Example8",
322 "filters/suppresswithnearbytextfilter/Example9",
323 "filters/suppresswithplaintextcommentfilter/Example2",
324 "filters/suppresswithplaintextcommentfilter/Example3",
325 "filters/suppresswithplaintextcommentfilter/Example4",
326 "filters/suppresswithplaintextcommentfilter/Example5",
327 "filters/suppresswithplaintextcommentfilter/Example6",
328 "filters/suppresswithplaintextcommentfilter/Example7",
329 "filters/suppresswithplaintextcommentfilter/Example8",
330
331 "checks/imports/customimportorder/Example10",
332 "checks/imports/customimportorder/Example11",
333 "checks/imports/customimportorder/Example12",
334 "checks/imports/customimportorder/Example13",
335 "checks/imports/customimportorder/Example14",
336 "checks/imports/customimportorder/Example15",
337 "checks/imports/customimportorder/Example2",
338 "checks/imports/customimportorder/Example3",
339 "checks/imports/customimportorder/Example4",
340 "checks/imports/customimportorder/Example5",
341 "checks/imports/customimportorder/Example6",
342 "checks/imports/customimportorder/Example7",
343 "checks/imports/customimportorder/Example8",
344 "checks/imports/customimportorder/Example9",
345 "checks/imports/importorder/Example10",
346 "checks/imports/importorder/Example11",
347 "checks/imports/importorder/Example12",
348 "checks/imports/importorder/Example2",
349 "checks/imports/importorder/Example3",
350 "checks/imports/importorder/Example4",
351 "checks/imports/importorder/Example5",
352 "checks/imports/importorder/Example6",
353 "checks/imports/importorder/Example7",
354 "checks/imports/importorder/Example8",
355 "checks/imports/importorder/Example9",
356
357 "checks/whitespace/parenpad/Example3",
358 "checks/indentation/commentsindentation/Example3",
359 "checks/indentation/commentsindentation/Example4",
360 "checks/indentation/commentsindentation/Example5",
361 "checks/indentation/commentsindentation/Example6",
362 "checks/indentation/commentsindentation/Example7",
363 "checks/indentation/commentsindentation/Example8",
364 "checks/regexp/regexp/Example2",
365 "checks/regexp/regexp/Example3",
366 "checks/regexp/regexp/Example4",
367 "checks/regexp/regexp/Example5",
368 "checks/regexp/regexp/Example8",
369 "checks/regexp/regexp/Example9",
370 "checks/regexp/regexp/Example11",
371 "checks/regexp/regexp/Example10",
372 "checks/regexp/regexp/Example6",
373 "checks/coding/matchxpath/Example2",
374 "checks/coding/matchxpath/Example3",
375 "checks/coding/matchxpath/Example4",
376 "checks/coding/matchxpath/Example5",
377 "checks/coding/matchxpath/Example6",
378 "checks/coding/illegaltokentext/Example3",
379 "checks/coding/illegaltokentext/Example4",
380 "filters/suppresswithplaintextcommentfilter/Example9",
381 "checks/design/visibilitymodifier/Example7",
382 "checks/design/visibilitymodifier/Example9",
383 "checks/design/visibilitymodifier/Example10",
384 "checks/design/visibilitymodifier/Example11",
385 "checks/coding/variabledeclarationusagedistance/Example2",
386 "checks/indentation/indentation/Example4",
387 "filters/suppressionfilter/Example2",
388 "filters/suppressionfilter/Example3",
389 "filters/suppressionfilter/Example4",
390 "checks/trailingcomment/Example4",
391 "checks/trailingcomment/Example5",
392 "checks/trailingcomment/Example6",
393 "checks/coding/requirethis/Example5",
394 "checks/coding/requirethis/Example6",
395 "checks/whitespace/nolinewrap/Example3",
396 "checks/naming/abbreviationaswordinname/Example4",
397 "checks/naming/abbreviationaswordinname/Example6",
398 "checks/naming/abbreviationaswordinname/Example7",
399 "checks/naming/localvariablename/Example3",
400 "checks/naming/localvariablename/Example5",
401 "checks/coding/unnecessaryparentheses/Example3",
402 "checks/whitespace/nowhitespacebefore/Example4",
403 "checks/whitespace/whitespacearound/Example2",
404 "checks/naming/localfinalvariablename/Example2",
405 "checks/blocks/emptycatchblock/Example4",
406 "checks/blocks/emptycatchblock/Example5",
407 "checks/naming/parametername/Example4",
408 "checks/coding/illegalsymbol/Example3",
409 "checks/coding/illegalsymbol/Example5"
410 );
411
412
413
414
415
416
417
418
419 @Test
420 public void testExamplesDifferOnlyByComments() throws IOException {
421 final List<Violation> violations = new ArrayList<>();
422
423 try (Stream<Path> pathStream = Files.walk(XDOCS_ROOT)) {
424 final List<Path> exampleDirs = pathStream
425 .filter(Files::isDirectory)
426 .filter(XdocsExamplesAstConsistencyTest::containsMultipleExamples)
427 .toList();
428
429 for (Path dir : exampleDirs) {
430 final List<Violation> dirViolations = checkExamplesInDirectory(dir);
431 violations.addAll(dirViolations);
432 }
433 }
434
435 final String message;
436
437 if (violations.isEmpty()) {
438 message = "";
439 }
440 else {
441 final StringBuilder builder = new StringBuilder(1024);
442
443 builder.append("Found ")
444 .append(violations.size())
445 .append(" example files with AST mismatches.\n\n");
446
447 for (Violation violation : violations) {
448 builder.append(violation)
449 .append("\n\n");
450 }
451
452 builder.append("If these examples have different code intent, "
453 + "add them to SUPPRESSED_EXAMPLES:\n");
454
455 for (Violation violation : violations) {
456 final String pattern = violation.getSuppressionPattern();
457 builder.append('"').append(pattern).append("\",\n");
458 }
459
460 message = builder.toString();
461 }
462
463 assertWithMessage(message)
464 .that(violations)
465 .isEmpty();
466 }
467
468
469
470
471
472
473
474
475
476
477
478
479 @Test
480 public void testNoBlockCommentMarkers() throws IOException {
481 final List<String> violations = new ArrayList<>();
482
483 try (Stream<Path> pathStream = Files.walk(XDOCS_ROOT)) {
484 pathStream
485 .filter(path -> path.getFileName().toString().matches("Example\\d+\\.java"))
486 .filter(path -> {
487 final String relativePath = getRelativePath(path.getParent());
488 final String fileName = path.getFileName().toString();
489 return !isExampleIndependent(relativePath, fileName);
490 })
491 .sorted()
492 .forEach(path -> {
493 try {
494 violations.addAll(checkForBlockCommentMarkers(path));
495 }
496 catch (IOException exception) {
497 throw new IllegalStateException(
498 "Failed to read file: " + path, exception);
499 }
500 });
501 }
502
503 final String message;
504 if (violations.isEmpty()) {
505 message = "";
506 }
507 else {
508 message = formatBlockCommentMarkerViolationsMessage(violations);
509 }
510
511 assertWithMessage(message)
512 .that(violations)
513 .isEmpty();
514 }
515
516
517
518
519
520
521
522 private static String formatBlockCommentMarkerViolationsMessage(List<String> violations) {
523 final StringBuilder builder = new StringBuilder(1024);
524 builder.append("Found ")
525 .append(violations.size())
526 .append(
527 """
528 example file(s) using block comments as ok/violation markers.
529 Convert them to single-line comments, e.g.:
530 BAD:
531 GOOD:
532
533 """);
534
535 for (String violation : violations) {
536 builder.append(violation).append('\n');
537 }
538
539 return builder.toString();
540 }
541
542
543
544
545
546
547
548
549 private static List<String> checkForBlockCommentMarkers(Path file)
550 throws IOException {
551 final List<String> fileViolations = new ArrayList<>();
552 final String content = Files.readString(file);
553 final Pattern blockCommentPattern = Pattern.compile("(?s)/\\*.*?\\*/");
554 final Matcher matcher = blockCommentPattern.matcher(content);
555
556 while (matcher.find()) {
557 final String block = matcher.group();
558 final String inner = block
559 .replaceAll("^/\\*+", "")
560 .replaceAll("\\*/$", "")
561 .replace("*", "")
562 .strip();
563
564 if (inner.startsWith("ok") || inner.startsWith("violation")) {
565 int lineNo = 1;
566 for (int index = 0; index < matcher.start(); index++) {
567 if (content.charAt(index) == '\n') {
568 lineNo++;
569 }
570 }
571 fileViolations.add(file + ":" + lineNo
572 + " - use single-line comment instead: // "
573 + inner);
574 }
575 }
576 return fileViolations;
577 }
578
579
580
581
582
583
584
585 private static boolean containsMultipleExamples(Path dir) {
586 try (Stream<Path> pathStream = Files.list(dir)) {
587 return pathStream
588 .filter(path -> path.getFileName().toString().matches("Example\\d+\\.java"))
589 .count() > 1;
590 }
591 catch (IOException exception) {
592 throw new IllegalStateException("Failed to list files in directory: " + dir,
593 exception);
594 }
595 }
596
597
598
599
600
601
602
603
604
605
606
607 private static boolean isModuleWithNoProperties(List<Path> examples) throws IOException {
608 boolean result = true;
609 for (Path example : examples) {
610 final String content = Files.readString(example);
611 if (content.contains("<property ")) {
612 result = false;
613 break;
614 }
615 }
616 return result;
617 }
618
619
620
621
622
623
624
625
626 private static List<Violation> checkExamplesInDirectory(Path dir) throws IOException {
627 final List<Violation> violations = new ArrayList<>();
628 final List<Path> examples = getExampleFiles(dir);
629
630 if (!examples.isEmpty()) {
631 violations.addAll(compareExamples(dir, examples));
632 }
633
634 return violations;
635 }
636
637
638
639
640
641
642
643
644 private static List<Path> getExampleFiles(Path dir) throws IOException {
645 final List<Path> examples;
646 try (Stream<Path> pathStream = Files.list(dir)) {
647 examples = pathStream
648 .filter(path -> path.getFileName().toString().matches("Example\\d+\\.java"))
649 .sorted(Comparator.comparing(Path::toString))
650 .toList();
651 }
652 return examples;
653 }
654
655
656
657
658
659
660
661
662 private static boolean isExampleUnparseable(String relativePath, String exampleFileName) {
663 final String exampleName = exampleFileName.replace(".java", "");
664 final String fullPath = relativePath + "/" + exampleName;
665 return UNPARSEABLE_EXAMPLES.contains(fullPath);
666 }
667
668
669
670
671
672
673
674
675 private static boolean isExampleIndependent(String relativePath, String exampleFileName) {
676 final String exampleName = exampleFileName.replace(".java", "");
677 final String fullPath = relativePath + "/" + exampleName;
678 return SUPPRESSED_EXAMPLES.contains(fullPath);
679 }
680
681
682
683
684
685
686
687 private static String getRelativePath(Path dir) {
688 return XDOCS_ROOT.relativize(dir).toString().replace('\\', '/');
689 }
690
691
692
693
694
695
696
697
698 private static List<Violation> compareExamples(Path dir, List<Path> examples)
699 throws IOException {
700 final List<Violation> violations = new ArrayList<>();
701
702 if (!isModuleWithNoProperties(examples)) {
703 final String relativePath = getRelativePath(dir);
704
705 final List<Path> regularExamples = new ArrayList<>();
706
707 for (Path example : examples) {
708 final String fileName = example.getFileName().toString();
709 if (!isExampleIndependent(relativePath, fileName)) {
710 regularExamples.add(example);
711 }
712 }
713
714 if (regularExamples.size() > 1) {
715 violations.addAll(validateExamplesByConstructorPresence(dir, regularExamples));
716 }
717 }
718
719 return violations;
720 }
721
722
723
724
725
726
727
728
729
730 private static List<Violation> validateExamplesByConstructorPresence(Path dir,
731 List<Path> examples)
732 throws IOException {
733 final List<Violation> violations = new ArrayList<>();
734 final List<Path> constructorExamples = new ArrayList<>();
735 final List<Path> nonConstructorExamples = new ArrayList<>();
736
737 for (Path example : examples) {
738 if (containsConstructorDefinition(example)) {
739 constructorExamples.add(example);
740 }
741 else {
742 nonConstructorExamples.add(example);
743 }
744 }
745
746 if (nonConstructorExamples.size() > 1) {
747 violations.addAll(validateAllMatch(dir, nonConstructorExamples));
748 }
749 if (constructorExamples.size() > 1) {
750 violations.addAll(validateAllMatch(dir, constructorExamples));
751 }
752
753 return violations;
754 }
755
756
757
758
759
760
761
762
763 private static boolean containsConstructorDefinition(Path example) throws IOException {
764 final String xdocSection = extractXdocSection(example);
765 boolean result;
766 try {
767 final DetailAST ast = parseContent(xdocSection);
768 result = ast != null && hasDescendantOfType(ast, TokenTypes.CTOR_DEF);
769 }
770 catch (CheckstyleException exception) {
771 result = false;
772 }
773
774 return result;
775 }
776
777
778
779
780
781
782
783
784 private static boolean hasDescendantOfType(DetailAST ast, int tokenType) {
785 boolean result = false;
786 if (ast.getType() == tokenType) {
787 result = true;
788 }
789 else {
790 for (DetailAST child = ast.getFirstChild(); child != null;
791 child = child.getNextSibling()) {
792 if (hasDescendantOfType(child, tokenType)) {
793 result = true;
794 break;
795 }
796 }
797 }
798
799 return result;
800 }
801
802
803
804
805
806
807
808
809
810
811 private static List<Violation> validateAllMatch(Path dir, List<Path> examples)
812 throws IOException {
813 final List<Violation> violations = new ArrayList<>();
814 final Path reference = examples.getFirst();
815 final String referenceXdocSection = extractXdocSection(reference);
816
817 try {
818 final DetailAST referenceDetailAst = parseContent(referenceXdocSection);
819 if (referenceDetailAst != null) {
820 final StructuralAstNode referenceAst = toStructuralAst(referenceDetailAst);
821 final List<String> referenceComments =
822 extractComments(referenceXdocSection);
823 for (int index = 1; index < examples.size(); index++) {
824 final Path example = examples.get(index);
825 final Violation violation = compareSingleExample(
826 dir, example, reference, referenceAst, referenceComments
827 );
828 if (violation != null) {
829 violations.add(violation);
830 }
831 }
832 }
833 }
834 catch (CheckstyleException exception) {
835
836 }
837
838 return violations;
839 }
840
841
842
843
844
845
846
847
848
849
850
851
852
853 private static String extractXdocSection(Path file) throws IOException {
854 final List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
855 int startIndex = -1;
856 int endIndex = -1;
857
858 for (int index = 0; index < lines.size(); index++) {
859 final String line = lines.get(index);
860 if (line.contains(XDOC_START_MARKER)) {
861 startIndex = index + 1;
862 }
863 else if (line.contains(XDOC_END_MARKER)) {
864 endIndex = index;
865 break;
866 }
867 }
868
869 final String result;
870 if (startIndex == -1 || endIndex == -1 || startIndex >= endIndex) {
871 result = String.join("\n", lines);
872 }
873 else {
874 result = String.join("\n", lines.subList(startIndex, endIndex));
875 }
876
877 return result;
878 }
879
880
881
882
883
884
885
886
887 private static DetailAST parseContent(String content) throws CheckstyleException {
888 final FileText text = new FileText(
889 new File("Example.java").getAbsoluteFile(),
890 content.lines().toList()
891 );
892 final FileContents contents = new FileContents(text);
893 return JavaParser.parse(contents);
894 }
895
896
897
898
899
900
901
902
903
904
905
906 private static Violation compareSingleExample(Path dir, Path example,
907 Path reference,
908 StructuralAstNode referenceAst,
909 List<String> referenceComments)
910 throws IOException {
911 final String exampleXdocSection = extractXdocSection(example);
912 final String relativePath = getRelativePath(dir);
913 final String exampleFileName = example.getFileName().toString();
914
915 DetailAST exampleDetailAst = null;
916 try {
917 exampleDetailAst = parseContent(exampleXdocSection);
918 }
919 catch (CheckstyleException exception) {
920 if (!isExampleUnparseable(relativePath, exampleFileName)) {
921 throw new IllegalStateException(
922 "Failed to parse example: " + example, exception);
923 }
924 }
925
926 final Violation result;
927 if (exampleDetailAst == null) {
928 result = null;
929 }
930 else {
931 final StructuralAstNode ast = toStructuralAst(exampleDetailAst);
932
933 final List<String> exampleComments =
934 extractComments(exampleXdocSection);
935
936 if (referenceAst.equals(ast) && referenceComments.equals(exampleComments)) {
937 result = null;
938 }
939 else if (referenceAst.equals(ast)) {
940 result = new Violation(relativePath, reference.getFileName().toString(),
941 example.getFileName().toString(), "Comments mismatch");
942 }
943 else {
944 result = new Violation(relativePath, reference.getFileName().toString(),
945 example.getFileName().toString(), "AST structure mismatch");
946 }
947 }
948
949 return result;
950 }
951
952
953
954
955
956
957
958
959
960
961
962 private static StructuralAstNode toStructuralAst(DetailAST ast) {
963 final boolean ignoreName = isClassOrConstructorName(ast)
964 || isExtendsAnExampleClass(ast);
965 final StructuralAstNode node = new StructuralAstNode(
966 ast.getType(), ast.getText(), ignoreName, ast.getLineNo(), ignoreName
967 );
968
969 for (DetailAST child = ast.getFirstChild();
970 child != null;
971 child = child.getNextSibling()) {
972 final StructuralAstNode structuralChild = toStructuralAst(child);
973 if (structuralChild != null) {
974 node.addChild(structuralChild);
975 }
976 }
977 return node;
978 }
979
980
981
982
983
984
985
986 private static boolean isClassOrConstructorName(DetailAST ast) {
987 final DetailAST parent = ast.getParent();
988 return parent != null
989 && ast.getType() == TokenTypes.IDENT
990 && (parent.getType() == TokenTypes.CLASS_DEF
991 || parent.getType() == TokenTypes.CTOR_DEF);
992 }
993
994
995
996
997
998
999
1000 private static boolean isExtendsAnExampleClass(DetailAST ast) {
1001 final DetailAST parent = ast.getParent();
1002 boolean result = false;
1003 if (parent != null
1004 && ast.getType() == TokenTypes.IDENT
1005 && parent.getType() == TokenTypes.EXTENDS_CLAUSE) {
1006 final DetailAST classDef = parent.getParent();
1007 if (classDef != null && classDef.getType() == TokenTypes.CLASS_DEF) {
1008 final DetailAST className = classDef.findFirstToken(TokenTypes.IDENT);
1009 result = className != null
1010 && className.getText().matches("Example\\d+");
1011 }
1012 }
1013 return result;
1014 }
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036 private static boolean isIgnoredComment(String comment) {
1037 return comment.startsWith("ok")
1038 || comment.startsWith("violation")
1039 || comment.startsWith("filtered violation")
1040 || comment.startsWith("xdoc section")
1041 || comment.contains("// ok")
1042 || comment.contains("// violation")
1043 || comment.matches("\\d+\\s+violations?.*")
1044 || comment.matches("'.*'");
1045 }
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069 private static List<String> extractComments(String content) {
1070 final List<String> comments = new ArrayList<>();
1071 boolean prevLineWasMarker = false;
1072
1073 for (String line : content.lines().toList()) {
1074 final int commentIndex = findCommentStart(line);
1075
1076 if (commentIndex < 0) {
1077 prevLineWasMarker = false;
1078 continue;
1079 }
1080
1081 final String comment =
1082 line.substring(commentIndex + 2).strip();
1083 final boolean isCodeBefore =
1084 !line.substring(0, commentIndex).isBlank();
1085
1086 if (isIgnoredComment(comment)) {
1087 prevLineWasMarker = true;
1088 }
1089 else if (!prevLineWasMarker || isCodeBefore) {
1090 comments.add(comment);
1091 prevLineWasMarker = false;
1092 }
1093 }
1094
1095 return comments;
1096 }
1097
1098
1099
1100
1101
1102
1103
1104
1105 private static int findCommentStart(String line) {
1106 int result = -1;
1107 boolean inString = false;
1108 boolean inChar = false;
1109 boolean escaped = false;
1110
1111 for (int index = 0; index < line.length() - 1; index++) {
1112 final char current = line.charAt(index);
1113
1114 if (escaped) {
1115 escaped = false;
1116 }
1117 else if (current == '\\') {
1118 escaped = true;
1119 }
1120 else if (!inChar && current == '"') {
1121 inString = !inString;
1122 }
1123 else if (!inString && current == '\'') {
1124 inChar = !inChar;
1125 }
1126 else if (isCommentAt(line, index, inString, inChar)) {
1127 result = index;
1128 break;
1129 }
1130 }
1131
1132 return result;
1133 }
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144 private static boolean isCommentAt(String line, int index, boolean inString, boolean inChar) {
1145 return !inString && !inChar
1146 && line.charAt(index) == '/'
1147 && line.charAt(index + 1) == '/';
1148 }
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162 private static final class StructuralAstNode {
1163 private final int type;
1164 private final String text;
1165
1166 private final Integer lineNo;
1167 private final List<StructuralAstNode> children = new ArrayList<>();
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178 private StructuralAstNode(int type, String text, boolean ignoreText,
1179 int lineNo, boolean ignorePosition) {
1180 this.type = type;
1181 if (ignoreText) {
1182 this.text = null;
1183 }
1184 else if (isLiteralToken(type)) {
1185 this.text = text;
1186 }
1187 else {
1188 this.text = null;
1189 }
1190 if (ignorePosition) {
1191 this.lineNo = null;
1192 }
1193 else {
1194 this.lineNo = lineNo;
1195 }
1196 }
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209 private static boolean isLiteralToken(int tokenType) {
1210 return switch (tokenType) {
1211 case TokenTypes.NUM_INT, TokenTypes.NUM_LONG, TokenTypes.NUM_FLOAT,
1212 TokenTypes.NUM_DOUBLE, TokenTypes.STRING_LITERAL,
1213 TokenTypes.CHAR_LITERAL, TokenTypes.LITERAL_TRUE,
1214 TokenTypes.LITERAL_FALSE, TokenTypes.LITERAL_NULL,
1215 TokenTypes.IDENT -> true;
1216 default -> false;
1217 };
1218 }
1219
1220 private void addChild(StructuralAstNode child) {
1221 children.add(child);
1222 }
1223
1224 @Override
1225 public boolean equals(Object obj) {
1226 if (!(obj instanceof StructuralAstNode other)) {
1227 return false;
1228 }
1229 final boolean typeMatch = type == other.type;
1230 final boolean textMatch = Objects.equals(text, other.text);
1231 final boolean lineNoMatch = Objects.equals(lineNo, other.lineNo);
1232 final boolean childrenMatch = children.equals(other.children);
1233 return typeMatch && textMatch && lineNoMatch && childrenMatch;
1234 }
1235
1236 @Override
1237 public int hashCode() {
1238 return Objects.hash(type, text, lineNo, children);
1239 }
1240
1241 @Override
1242 public String toString() {
1243 final StringBuilder sb = new StringBuilder(128);
1244 sb.append("StructuralAstNode{type=");
1245 try {
1246 sb.append(TokenUtil.getTokenName(type));
1247 }
1248 catch (IllegalArgumentException exception) {
1249 sb.append(type);
1250 }
1251 if (text != null) {
1252 sb.append(", text='").append(text).append('\'');
1253 }
1254 if (lineNo != null) {
1255 sb.append(", line=").append(lineNo);
1256 }
1257 if (!children.isEmpty()) {
1258 sb.append(", children=").append(children.size());
1259 }
1260 sb.append('}');
1261 return sb.toString();
1262 }
1263 }
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273 private record Violation(String relativePath, String referenceFileName,
1274 String mismatchFileName, String reason) {
1275 @Override
1276 public String toString() {
1277 return "Directory: " + relativePath + "\n"
1278 + "Reference: " + referenceFileName + "\n"
1279 + "Mismatch: " + mismatchFileName + "\n"
1280 + "Reason: " + reason;
1281 }
1282
1283
1284
1285
1286
1287
1288 String getSuppressionPattern() {
1289 return relativePath + "/" + mismatchFileName.replace(".java", "");
1290 }
1291 }
1292
1293 }