View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.internal;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.io.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.stream.Stream;
35  
36  import org.junit.jupiter.api.Test;
37  
38  import com.puppycrawl.tools.checkstyle.JavaParser;
39  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
40  import com.puppycrawl.tools.checkstyle.api.DetailAST;
41  import com.puppycrawl.tools.checkstyle.api.FileContents;
42  import com.puppycrawl.tools.checkstyle.api.FileText;
43  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
44  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
45  
46  /**
47   * Ensures xdocs Java examples for the same check differ only by comments.
48   *
49   * <p>This test validates that examples with the same code structure maintain
50   * consistency. Examples are grouped explicitly - either all examples must match,
51   * or specific examples can be marked as independent.
52   *
53   * <p>Only code between {@code // xdoc section -- start} and
54   * {@code // xdoc section -- end} markers is compared. Helper code outside
55   * these markers (like interface definitions) can differ between examples.
56   *
57   */
58  public class XdocsExamplesAstConsistencyTest {
59  
60      private static final Path XDOCS_ROOT = Path.of(
61              "src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle"
62      );
63  
64      private static final String COMMON_PATH =
65              "src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/";
66  
67      private static final String XDOC_START_MARKER = "// xdoc section -- start";
68      private static final String XDOC_END_MARKER = "// xdoc section -- end";
69  
70      /**
71       * Examples that cannot be parsed as valid Java.
72       * These files are intentionally non-compilable for documentation purposes.
73       *
74       */
75      private static final Set<String> UNPARSEABLE_EXAMPLES = Set.of(
76              "checks/regexp/regexponfilename/Example1",
77              "checks/translation/Example1",
78              "filters/suppressionxpathsinglefilter/Example14"
79      );
80  
81      /**
82       * Examples that have independent code structure and should not be compared.
83       * These represent different use cases or configurations with different code.
84       *
85       * <p>Format: "directory/ExampleN" where the example has unique code.
86       *
87       * <p>Until: <a href="https://github.com/checkstyle/checkstyle/issues/18435">...</a>
88       */
89      private static final Set<String> SUPPRESSED_EXAMPLES = Set.of(
90              "checks/annotation/annotationonsameline/Example2",
91              "checks/annotation/missingoverride/Example2",
92              "checks/annotation/suppresswarningsholder/Example1",
93              "checks/blocks/emptyblock/Example3",
94              "checks/blocks/emptycatchblock/Example4",
95              "checks/blocks/emptycatchblock/Example5",
96              "checks/blocks/needbraces/Example6",
97              "checks/coding/constructorsdeclarationgrouping/Example2",
98              "checks/coding/covariantequals/Example2",
99              "checks/coding/illegaltoken/Example2",
100             "checks/coding/illegaltokentext/Example3",
101             "checks/coding/illegaltokentext/Example4",
102             "checks/coding/illegaltokentext/Example5",
103             "checks/coding/innerassignment/Example2",
104             "checks/coding/matchxpath/Example2",
105             "checks/coding/matchxpath/Example3",
106             "checks/coding/matchxpath/Example4",
107             "checks/coding/matchxpath/Example5",
108             "checks/coding/missingswitchdefault/Example2",
109             "checks/coding/missingswitchdefault/Example3",
110             "checks/coding/packagedeclaration/Example2",
111             "checks/coding/requirethis/Example5",
112             "checks/coding/requirethis/Example6",
113             "checks/coding/textblockgooglestyleformatting/Example2",
114             "checks/coding/textblockgooglestyleformatting/Example3",
115             "checks/coding/textblockgooglestyleformatting/Example4",
116             "checks/coding/unnecessaryparentheses/Example2",
117             "checks/coding/unnecessaryparentheses/Example3",
118             "checks/coding/unnecessarysemicoloninenumeration/Example2",
119             "checks/coding/useenhancedswitch/Example2",
120             "checks/coding/variabledeclarationusagedistance/Example2",
121             "checks/descendanttoken/Example10",
122             "checks/descendanttoken/Example11",
123             "checks/descendanttoken/Example12",
124             "checks/descendanttoken/Example13",
125             "checks/descendanttoken/Example14",
126             "checks/descendanttoken/Example15",
127             "checks/descendanttoken/Example16",
128             "checks/descendanttoken/Example17",
129             "checks/descendanttoken/Example4",
130             "checks/descendanttoken/Example5",
131             "checks/descendanttoken/Example6",
132             "checks/descendanttoken/Example7",
133             "checks/descendanttoken/Example8",
134             "checks/descendanttoken/Example9",
135             "checks/design/onetoplevelclass/Example3",
136             "checks/design/visibilitymodifier/Example11",
137             "checks/design/visibilitymodifier/Example12",
138             "checks/finalparameters/Example4",
139             "checks/header/header/Example4",
140             "checks/imports/avoidstaticimport/Example2",
141             "checks/imports/customimportorder/Example10",
142             "checks/imports/customimportorder/Example11",
143             "checks/imports/customimportorder/Example12",
144             "checks/imports/customimportorder/Example13",
145             "checks/imports/customimportorder/Example14",
146             "checks/imports/customimportorder/Example15",
147             "checks/imports/customimportorder/Example2",
148             "checks/imports/customimportorder/Example3",
149             "checks/imports/customimportorder/Example4",
150             "checks/imports/customimportorder/Example5",
151             "checks/imports/customimportorder/Example6",
152             "checks/imports/customimportorder/Example7",
153             "checks/imports/customimportorder/Example8",
154             "checks/imports/customimportorder/Example9",
155             "checks/imports/importcontrol/Example12",
156             "checks/imports/importcontrol/Example5",
157             "checks/imports/importcontrol/filters/Example9",
158             "checks/imports/importcontrol/someImports/Example11",
159             "checks/imports/importcontrol/someImports/Example6",
160             "checks/imports/importcontrol/someImports/Example7",
161             "checks/imports/importorder/Example10",
162             "checks/imports/importorder/Example11",
163             "checks/imports/importorder/Example12",
164             "checks/imports/importorder/Example2",
165             "checks/imports/importorder/Example3",
166             "checks/imports/importorder/Example4",
167             "checks/imports/importorder/Example5",
168             "checks/imports/importorder/Example6",
169             "checks/imports/importorder/Example7",
170             "checks/imports/importorder/Example8",
171             "checks/imports/importorder/Example9",
172             "checks/indentation/commentsindentation/Example3",
173             "checks/indentation/commentsindentation/Example4",
174             "checks/indentation/commentsindentation/Example5",
175             "checks/indentation/commentsindentation/Example6",
176             "checks/indentation/commentsindentation/Example7",
177             "checks/indentation/commentsindentation/Example8",
178             "checks/javadoc/javadocleadingasteriskalign/Example2",
179             "checks/javadoc/javadocleadingasteriskalign/Example3",
180             "checks/javadoc/javadocmethod/Example7",
181             "checks/javadoc/javadocmethod/Example8",
182             "checks/javadoc/javadocstyle/Example7",
183             "checks/javadoc/javadoctagcontinuationindentation/Example4",
184             "checks/javadoc/javadocvariable/Example5",
185             "checks/javadoc/missingjavadoctype/Example4",
186             "checks/javadoc/missingjavadoctype/Example5",
187             "checks/metrics/classdataabstractioncoupling/Example11",
188             "checks/metrics/classdataabstractioncoupling/Example2",
189             "checks/metrics/classdataabstractioncoupling/Example3",
190             "checks/metrics/classdataabstractioncoupling/ignore/Example7",
191             "checks/metrics/classdataabstractioncoupling/ignore/Example8",
192             "checks/metrics/classdataabstractioncoupling/ignore/Example9",
193             "checks/metrics/classdataabstractioncoupling/ignore/deeper/Example5",
194             "checks/metrics/classdataabstractioncoupling/ignore/deeper/Example6",
195             "checks/metrics/javancss/Example5",
196             "checks/metrics/npathcomplexity/Example2",
197             "checks/naming/abbreviationaswordinname/Example3",
198             "checks/naming/abbreviationaswordinname/Example4",
199             "checks/naming/abbreviationaswordinname/Example5",
200             "checks/naming/abbreviationaswordinname/Example6",
201             "checks/naming/abbreviationaswordinname/Example7",
202             "checks/naming/interfacetypeparametername/Example2",
203             "checks/naming/lambdaparametername/Example2",
204             "checks/naming/localfinalvariablename/Example3",
205             "checks/naming/localvariablename/Example3",
206             "checks/naming/localvariablename/Example4",
207             "checks/naming/localvariablename/Example5",
208             "checks/naming/membername/Example2",
209             "checks/naming/membername/Example3",
210             "checks/naming/parametername/Example2",
211             "checks/naming/parametername/Example3",
212             "checks/naming/parametername/Example4",
213             "checks/naming/parametername/Example5",
214             "checks/naming/patternvariablename/Example4",
215             "checks/naming/typename/Example2",
216             "checks/naming/typename/Example3",
217             "checks/naming/typename/Example4",
218             "checks/outertypefilename/Example2",
219             "checks/outertypefilename/Example3",
220             "checks/outertypefilename/Example4",
221             "checks/outertypefilename/Example5",
222             "checks/regexp/regexp/Example1",
223             "checks/regexp/regexp/Example10",
224             "checks/regexp/regexp/Example11",
225             "checks/regexp/regexp/Example2",
226             "checks/regexp/regexp/Example3",
227             "checks/regexp/regexp/Example4",
228             "checks/regexp/regexp/Example5",
229             "checks/regexp/regexp/Example6",
230             "checks/regexp/regexp/Example7",
231             "checks/regexp/regexp/Example8",
232             "checks/regexp/regexp/Example9",
233             "checks/regexp/regexpsingleline/Example2",
234             "checks/regexp/regexpsingleline/Example3",
235             "checks/regexp/regexpsingleline/Example4",
236             "checks/regexp/regexpmultiline/Example5",
237             "checks/sizes/lambdabodylength/Example2",
238             "checks/sizes/linelength/Example6",
239             "checks/trailingcomment/Example2",
240             "checks/trailingcomment/Example3",
241             "checks/trailingcomment/Example4",
242             "checks/trailingcomment/Example5",
243             "checks/trailingcomment/Example6",
244             "checks/whitespace/nolinewrap/Example2",
245             "checks/whitespace/nolinewrap/Example3",
246             "checks/whitespace/nolinewrap/Example4",
247             "checks/whitespace/nolinewrap/Example5",
248             "checks/whitespace/nowhitespacebefore/Example2",
249             "checks/whitespace/nowhitespacebefore/Example3",
250             "checks/whitespace/nowhitespacebefore/Example4",
251             "checks/whitespace/operatorwrap/Example2",
252             "checks/whitespace/parenpad/Example2",
253             "checks/whitespace/separatorwrap/Example2",
254             "checks/whitespace/separatorwrap/Example3",
255             "checks/whitespace/singlespaceseparator/Example2",
256             "checks/whitespace/typecastparenpad/Example2",
257             "checks/whitespace/whitespaceafter/Example2",
258             "checks/whitespace/whitespacearound/Example10",
259             "checks/whitespace/whitespacearound/Example2",
260             "checks/whitespace/whitespacearound/Example3",
261             "checks/whitespace/whitespacearound/Example4",
262             "checks/whitespace/whitespacearound/Example5",
263             "checks/whitespace/whitespacearound/Example6",
264             "checks/whitespace/whitespacearound/Example7",
265             "checks/whitespace/whitespacearound/Example8",
266             "checks/whitespace/whitespacearound/Example9",
267             "checks/whitespace/whitespacearound/Example11",
268             "filters/suppressionfilter/Example2",
269             "filters/suppressionfilter/Example3",
270             "filters/suppressionfilter/Example4",
271             "filters/suppressionsinglefilter/Example2",
272             "filters/suppressionsinglefilter/Example3",
273             "filters/suppressionsinglefilter/Example4",
274             "filters/suppressionxpathfilter/Example10",
275             "filters/suppressionxpathfilter/Example11",
276             "filters/suppressionxpathfilter/Example12",
277             "filters/suppressionxpathfilter/Example13",
278             "filters/suppressionxpathfilter/Example14",
279             "filters/suppressionxpathfilter/Example2",
280             "filters/suppressionxpathfilter/Example3",
281             "filters/suppressionxpathfilter/Example4",
282             "filters/suppressionxpathfilter/Example5",
283             "filters/suppressionxpathfilter/Example6",
284             "filters/suppressionxpathfilter/Example7",
285             "filters/suppressionxpathfilter/Example8",
286             "filters/suppressionxpathfilter/Example9",
287             "filters/suppressionxpathsinglefilter/Example10",
288             "filters/suppressionxpathsinglefilter/Example11",
289             "filters/suppressionxpathsinglefilter/Example12",
290             "filters/suppressionxpathsinglefilter/Example13",
291             "filters/suppressionxpathsinglefilter/Example2",
292             "filters/suppressionxpathsinglefilter/Example4",
293             "filters/suppressionxpathsinglefilter/Example5",
294             "filters/suppressionxpathsinglefilter/Example6",
295             "filters/suppressionxpathsinglefilter/Example7",
296             "filters/suppressionxpathsinglefilter/Example8",
297             "filters/suppressionxpathsinglefilter/Example9",
298             "filters/suppresswarningsfilter/Example2",
299             "filters/suppresswithnearbycommentfilter/Example2",
300             "filters/suppresswithnearbycommentfilter/Example3",
301             "filters/suppresswithnearbycommentfilter/Example4",
302             "filters/suppresswithnearbycommentfilter/Example5",
303             "filters/suppresswithnearbycommentfilter/Example6",
304             "filters/suppresswithnearbycommentfilter/Example7",
305             "filters/suppresswithnearbycommentfilter/Example8",
306             "filters/suppresswithnearbytextfilter/Example2",
307             "filters/suppresswithnearbytextfilter/Example3",
308             "filters/suppresswithnearbytextfilter/Example4",
309             "filters/suppresswithnearbytextfilter/Example5",
310             "filters/suppresswithnearbytextfilter/Example6",
311             "filters/suppresswithnearbytextfilter/Example7",
312             "filters/suppresswithnearbytextfilter/Example8",
313             "filters/suppresswithnearbytextfilter/Example9",
314             "filters/suppresswithplaintextcommentfilter/Example5",
315             "filters/suppresswithplaintextcommentfilter/Example9",
316             // No properties in module, multiple very different examples to ease reading
317             "checks/annotation/missingoverrideonrecordaccessor/Example2",
318             // contains ExampleX constructors
319             "checks/naming/methodname/Example3",
320             "checks/naming/methodname/Example4"
321             );
322 
323     /**
324      * Tests that examples with the same code structure maintain consistency.
325      * Examples not marked as independent must have identical AST structure.
326      *
327      * @throws IOException if an I/O error occurs
328      */
329     @Test
330     public void testExamplesDifferOnlyByComments() throws IOException {
331         final List<String> violations = new ArrayList<>();
332 
333         try (Stream<Path> pathStream = Files.walk(XDOCS_ROOT)) {
334             final List<Path> exampleDirs = pathStream
335                     .filter(Files::isDirectory)
336                     .filter(XdocsExamplesAstConsistencyTest::containsMultipleExamples)
337                     .toList();
338 
339             for (Path dir : exampleDirs) {
340                 final List<String> dirViolations = checkExamplesInDirectory(dir);
341                 violations.addAll(dirViolations);
342             }
343         }
344 
345         final String message;
346 
347         if (violations.isEmpty()) {
348             message = "";
349         }
350         else {
351             final StringBuilder builder = new StringBuilder(1024);
352 
353             builder.append("Found ")
354                     .append(violations.size())
355                     .append(" example files with AST mismatches.\n\n");
356 
357             for (String violation : violations) {
358                 builder.append(violation)
359                         .append("\n\n");
360             }
361 
362             builder.append("If these examples have different code intent, "
363                     + "add them to SUPPRESSED_EXAMPLES:\n");
364 
365             for (String violation : violations) {
366                 final String pattern = extractIndependentPattern(violation);
367                 if (pattern != null) {
368                     builder.append('"').append(pattern).append("\",\n");
369                 }
370             }
371 
372             message = builder.toString();
373         }
374 
375         assertWithMessage(message)
376                 .that(violations)
377                 .isEmpty();
378     }
379 
380     /**
381      * Extracts an independent example pattern from a violation message.
382      *
383      * @param violation the violation message
384      * @return the pattern, or null if not found
385      */
386     private static String extractIndependentPattern(String violation) {
387         final String dirPath = extractDirectoryPath(violation);
388         final String fileName = extractMismatchFileName(violation);
389         final String result;
390 
391         if (dirPath != null && fileName != null) {
392             result = dirPath + "/" + fileName.replace(".java", "");
393         }
394         else {
395             result = null;
396         }
397 
398         return result;
399     }
400 
401     /**
402      * Extracts the mismatched filename from a violation message.
403      *
404      * @param violation the violation message
405      * @return the filename, or null if not found
406      */
407     private static String extractMismatchFileName(String violation) {
408         final String prefix = "Mismatch:  ";
409         final int startIndex = violation.indexOf(prefix);
410         final String result;
411 
412         if (startIndex == -1) {
413             result = null;
414         }
415         else {
416             final int endIndex = violation.indexOf('\n', startIndex);
417             if (endIndex == -1) {
418                 result = violation.substring(startIndex + prefix.length()).trim();
419             }
420             else {
421                 result = violation.substring(startIndex + prefix.length(), endIndex).trim();
422             }
423         }
424 
425         return result;
426     }
427 
428     /**
429      * Checks if a specific example is marked as unparseable.
430      *
431      * @param relativePath the relative directory path
432      * @param exampleFileName the example filename (e.g., "Example1.java")
433      * @return true if this example cannot be parsed
434      */
435     private static boolean isExampleUnparseable(String relativePath, String exampleFileName) {
436         final String exampleName = exampleFileName.replace(".java", "");
437         final String fullPath = relativePath + "/" + exampleName;
438         return UNPARSEABLE_EXAMPLES.contains(fullPath);
439     }
440 
441     /**
442      * Checks if a specific example is marked as independent.
443      *
444      * @param relativePath the relative directory path
445      * @param exampleFileName the example filename (e.g., "Example1.java")
446      * @return true if this example is independent
447      */
448     private static boolean isExampleIndependent(String relativePath, String exampleFileName) {
449         final String exampleName = exampleFileName.replace(".java", "");
450         final String fullPath = relativePath + "/" + exampleName;
451         return SUPPRESSED_EXAMPLES.contains(fullPath);
452     }
453 
454     /**
455      * Gets the relative path from the common base path.
456      *
457      * @param dir the directory path
458      * @return the relative path string
459      */
460     private static String getRelativePath(Path dir) {
461         final String fullPath = dir.toString().replace('\\', '/');
462         final String result;
463         if (fullPath.startsWith(COMMON_PATH)) {
464             result = fullPath.substring(COMMON_PATH.length());
465         }
466         else {
467             result = fullPath;
468         }
469         return result;
470     }
471 
472     /**
473      * Extracts the directory path from a violation message.
474      *
475      * @param violation the violation message
476      * @return the directory path, or null if not found
477      */
478     private static String extractDirectoryPath(String violation) {
479         final String prefix = "Directory: ";
480         final int startIndex = violation.indexOf(prefix);
481         final String result;
482 
483         if (startIndex == -1) {
484             result = null;
485         }
486         else {
487             final int endIndex = violation.indexOf('\n', startIndex);
488             if (endIndex == -1) {
489                 result = null;
490             }
491             else {
492                 result = violation.substring(startIndex + prefix.length(), endIndex).trim();
493             }
494         }
495 
496         return result;
497     }
498 
499     /**
500      * Checks if a directory contains multiple example files.
501      *
502      * @param dir the directory to check
503      * @return true if the directory contains 2 or more Example*.java files
504      */
505     private static boolean containsMultipleExamples(Path dir) {
506         try (Stream<Path> pathStream = Files.list(dir)) {
507             return pathStream
508                     .filter(path -> path.getFileName().toString().matches("Example\\d+\\.java"))
509                     .count() > 1;
510         }
511         catch (IOException exception) {
512             throw new IllegalStateException("Failed to list files in directory: " + dir,
513                     exception);
514         }
515     }
516 
517     /**
518      * Checks examples in a directory. Non-independent examples must match.
519      *
520      * @param dir the directory containing example files
521      * @return list of violation messages for mismatches
522      * @throws IOException if an I/O error occurs
523      */
524     private static List<String> checkExamplesInDirectory(Path dir) throws IOException {
525         final List<String> violations = new ArrayList<>();
526         final List<Path> examples = getExampleFiles(dir);
527 
528         if (!examples.isEmpty()) {
529             violations.addAll(compareExamples(dir, examples));
530         }
531 
532         return violations;
533     }
534 
535     /**
536      * Gets all Example*.java files from a directory.
537      *
538      * @param dir the directory to search
539      * @return list of example file paths
540      * @throws IOException if an I/O error occurs
541      */
542     private static List<Path> getExampleFiles(Path dir) throws IOException {
543         final List<Path> examples;
544         try (Stream<Path> pathStream = Files.list(dir)) {
545             examples = pathStream
546                     .filter(path -> path.getFileName().toString().matches("Example\\d+\\.java"))
547                     .sorted(Comparator.comparing(Path::toString))
548                     .toList();
549         }
550         return examples;
551     }
552 
553     /**
554      * Compares examples: groups by AST, validates groups, reports mismatches.
555      *
556      * @param dir the directory containing the examples
557      * @param examples the list of example files
558      * @return list of violation messages for mismatches
559      * @throws IOException if an I/O error occurs
560      */
561     private static List<String> compareExamples(Path dir, List<Path> examples)
562             throws IOException {
563         final List<String> violations = new ArrayList<>();
564         final String relativePath = getRelativePath(dir);
565 
566         final List<Path> regularExamples = new ArrayList<>();
567 
568         for (Path example : examples) {
569             final String fileName = example.getFileName().toString();
570             if (!isExampleIndependent(relativePath, fileName)) {
571                 regularExamples.add(example);
572             }
573         }
574 
575         if (regularExamples.size() > 1) {
576             violations.addAll(validateAllMatch(dir, regularExamples));
577         }
578 
579         return violations;
580     }
581 
582     /**
583      * Validates that all examples in the list have identical AST structure.
584      *
585      * @param dir the directory containing the examples
586      * @param examples the list of examples that must all match
587      * @return list of violation messages for mismatches
588      * @throws IOException if an I/O error occurs
589      */
590     private static List<String> validateAllMatch(Path dir, List<Path> examples)
591             throws IOException {
592         final List<String> violations = new ArrayList<>();
593         final Path reference = examples.getFirst();
594         final String referenceXdocSection = extractXdocSection(reference);
595 
596         try {
597             final DetailAST referenceDetailAst = parseContent(referenceXdocSection);
598             if (referenceDetailAst != null) {
599                 final StructuralAstNode referenceAst = toStructuralAst(referenceDetailAst);
600 
601                 for (int index = 1; index < examples.size(); index++) {
602                     final Path example = examples.get(index);
603                     final String violation = compareSingleExample(
604                             dir, example, reference, referenceAst
605                     );
606                     if (violation != null) {
607                         violations.add(violation);
608                     }
609                 }
610             }
611         }
612         catch (CheckstyleException exception) {
613             // Skip examples that can't be parsed as Java
614         }
615 
616         return violations;
617     }
618 
619     /**
620      * Extracts content between xdoc section markers from a file.
621      *
622      * @param file the file to read
623      * @return the content between markers, or entire file if no markers
624      * @throws IOException if an I/O error occurs
625      */
626     private static String extractXdocSection(Path file) throws IOException {
627         final List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
628         int startIndex = -1;
629         int endIndex = -1;
630 
631         for (int index = 0; index < lines.size(); index++) {
632             final String line = lines.get(index);
633             if (line.contains(XDOC_START_MARKER)) {
634                 startIndex = index + 1;
635             }
636             else if (line.contains(XDOC_END_MARKER)) {
637                 endIndex = index;
638                 break;
639             }
640         }
641 
642         final String result;
643         if (startIndex != -1 && endIndex != -1 && startIndex < endIndex) {
644             result = String.join("\n", lines.subList(startIndex, endIndex));
645         }
646         else {
647             result = String.join("\n", lines);
648         }
649 
650         return result;
651     }
652 
653     /**
654      * Parses Java content string into a DetailAST.
655      *
656      * @param content the Java code content to parse
657      * @return the parsed AST, or null if parsing fails
658      * @throws CheckstyleException if parsing fails
659      */
660     private static DetailAST parseContent(String content) throws CheckstyleException {
661         final FileText text = new FileText(
662                 new File("Example.java").getAbsoluteFile(),
663                 content.lines().toList()
664         );
665         final FileContents contents = new FileContents(text);
666         return JavaParser.parse(contents);
667     }
668 
669     /**
670      * Compares a single example file against the reference.
671      *
672      * @param dir          the directory containing the examples
673      * @param example      the example file to compare
674      * @param reference    the reference file
675      * @param referenceAst the reference AST
676      * @return violation message if mismatch found, null otherwise
677      * @throws IOException if an I/O error occurs
678      */
679     private static String compareSingleExample(Path dir, Path example,
680                                                Path reference,
681                                                StructuralAstNode referenceAst)
682             throws IOException {
683         final String exampleXdocSection = extractXdocSection(example);
684         final String relativePath = getRelativePath(dir);
685         final String exampleFileName = example.getFileName().toString();
686 
687         DetailAST exampleDetailAst = null;
688         try {
689             exampleDetailAst = parseContent(exampleXdocSection);
690         }
691         catch (CheckstyleException exception) {
692             if (!isExampleUnparseable(relativePath, exampleFileName)) {
693                 throw new IllegalStateException(
694                         "Failed to parse example: " + example, exception);
695             }
696         }
697 
698         final String result;
699         if (exampleDetailAst == null) {
700             result = null;
701         }
702         else {
703             final StructuralAstNode ast = toStructuralAst(exampleDetailAst);
704 
705             if (referenceAst.equals(ast)) {
706                 result = null;
707             }
708             else {
709                 result = "Directory: " + relativePath + "\n"
710                         + "Reference: " + reference.getFileName() + "\n"
711                         + "Mismatch:  " + example.getFileName();
712             }
713         }
714 
715         return result;
716     }
717 
718     /**
719      * Converts a DetailAST into a structural representation that excludes comments.
720      *
721      * @param ast the AST to convert
722      * @return structural representation of the AST, or null if the node is a comment
723      */
724     private static StructuralAstNode toStructuralAst(DetailAST ast) {
725         final StructuralAstNode result;
726 
727         if (isCommentNode(ast)) {
728             result = null;
729         }
730         else {
731             final StructuralAstNode node = new StructuralAstNode(ast.getType(), ast.getText());
732 
733             for (DetailAST child = ast.getFirstChild();
734                  child != null;
735                  child = child.getNextSibling()) {
736                 final StructuralAstNode structuralChild = toStructuralAst(child);
737                 if (structuralChild != null) {
738                     node.addChild(structuralChild);
739                 }
740             }
741             result = node;
742         }
743 
744         return result;
745     }
746 
747     /**
748      * Checks if an AST node represents a comment.
749      *
750      * @param ast the AST node to check
751      * @return true if the node is a comment
752      */
753     private static boolean isCommentNode(DetailAST ast) {
754         final int type = ast.getType();
755         return type == TokenTypes.SINGLE_LINE_COMMENT
756                 || type == TokenTypes.BLOCK_COMMENT_BEGIN
757                 || type == TokenTypes.COMMENT_CONTENT;
758     }
759 
760     /**
761      * Represents a structural AST node without comments or source positions.
762      * This allows for pure structural comparison between example files.
763      * Now includes literal text values for semantic comparison.
764      */
765     private static final class StructuralAstNode {
766         private final int type;
767         private final String text;
768         private final List<StructuralAstNode> children = new ArrayList<>();
769 
770         private StructuralAstNode(int type, String text) {
771             this.type = type;
772             if (isLiteralToken(type)) {
773                 this.text = text;
774             }
775             else {
776                 this.text = null;
777             }
778         }
779 
780         /**
781          * Checks if a token type represents a literal that should have its text compared.
782          *
783          * @param tokenType the token type
784          * @return true if the token represents a literal with semantic value
785          */
786         private static boolean isLiteralToken(int tokenType) {
787             return switch (tokenType) {
788                 case TokenTypes.NUM_INT, TokenTypes.NUM_LONG, TokenTypes.NUM_FLOAT,
789                      TokenTypes.NUM_DOUBLE, TokenTypes.STRING_LITERAL,
790                      TokenTypes.CHAR_LITERAL, TokenTypes.LITERAL_TRUE,
791                      TokenTypes.LITERAL_FALSE, TokenTypes.LITERAL_NULL -> true;
792                 default -> false;
793             };
794         }
795 
796         private void addChild(StructuralAstNode child) {
797             children.add(child);
798         }
799 
800         @Override
801         public boolean equals(Object obj) {
802             final boolean result;
803             if (obj instanceof StructuralAstNode other) {
804                 final boolean typeMatch = type == other.type;
805                 final boolean textMatch = Objects.equals(text, other.text);
806                 final boolean childrenMatch = children.equals(other.children);
807                 result = typeMatch && textMatch && childrenMatch;
808             }
809             else {
810                 result = false;
811             }
812             return result;
813         }
814 
815         @Override
816         public int hashCode() {
817             return Objects.hash(type, text, children);
818         }
819 
820         @Override
821         public String toString() {
822             final StringBuilder sb = new StringBuilder(128);
823             sb.append("StructuralAstNode{type=");
824             try {
825                 sb.append(TokenUtil.getTokenName(type));
826             }
827             catch (IllegalArgumentException exception) {
828                 sb.append(type);
829             }
830             if (text != null) {
831                 sb.append(", text='").append(text).append('\'');
832             }
833             if (!children.isEmpty()) {
834                 sb.append(", children=").append(children.size());
835             }
836             sb.append('}');
837             return sb.toString();
838         }
839     }
840 }