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.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   * Ensures xdocs Java examples for the same check differ only by comments.
50   *
51   * <p>This test validates that examples with the same code structure maintain
52   * consistency. Examples are grouped explicitly - either all examples must match,
53   * or specific examples can be marked as independent.
54   *
55   * <p>Only code between {@code // xdoc section -- start} and
56   * {@code // xdoc section -- end} markers is compared. Helper code outside
57   * these markers (like interface definitions) can differ between examples.
58   *
59   * <p>Line numbers within the extracted xdoc section are also compared, ensuring
60   * that structurally identical statements appear on the same relative lines across
61   * examples. Because {@code parseContent} re-parses only the extracted section
62   * (starting at line 1), line numbers are always section-relative and are safe
63   * to compare directly between examples.
64   *
65   * <p>Single-line comments starting with {@code ok}, {@code violation}, or
66   * {@code xdoc section} are excluded from comparison as they are documentation
67   * markers. All other single-line comments, as well as Javadoc and block comments,
68   * are included in the structural comparison.
69   *
70   * <p>Block comments used as {@code ok} or {@code violation} markers
71   * (e.g. {@code /* ok, allowMissingReturnTag is true *}{@code /}) are forbidden;
72   * use single-line {@code //} comments instead. The
73   * {@link #testNoBlockCommentMarkers()} test enforces this.
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       * Examples that cannot be parsed as valid Java.
86       * These files are intentionally non-compilable for documentation purposes.
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       * Examples that have independent code structure and should not be compared.
97       * These represent different use cases or configurations with different code.
98       *
99       * <p>Format: "directory/ExampleN" where the example has unique code.
100      *
101      * <p>Until: <a href="https://github.com/checkstyle/checkstyle/issues/18435">...</a>
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             // Note: customImport/ImportOrder changes import group ORDER affecting AST structure
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             // until https://github.com/checkstyle/checkstyle/issues/19891
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      * Tests that examples with the same code structure maintain consistency.
414      * Examples not marked as independent must have identical AST structure,
415      * including the line numbers of each node within the xdoc section.
416      *
417      * @throws IOException if an I/O error occurs
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      * Tests that no example file uses block comments as {@code ok} or
470      * {@code violation} markers. All such markers must use single-line
471      * comments instead. For example:
472      * <pre>
473      *   BAD:  &#47;* ok, allowMissingReturnTag is true *&#47;
474      *   GOOD: // ok, allowMissingReturnTag is true
475      * </pre>
476      *
477      * @throws IOException if an I/O error occurs
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      * Formats the violation message for block comment markers.
518      *
519      * @param violations the list of violations
520      * @return formatted message
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:  /* ok, allowMissingReturnTag is true */
531                           GOOD: // ok, allowMissingReturnTag is true
532 
533                         """);
534 
535         for (String violation : violations) {
536             builder.append(violation).append('\n');
537         }
538 
539         return builder.toString();
540     }
541 
542     /**
543      * Checks a single example file for block comments used as ok/violation markers.
544      *
545      * @param file the example file to check
546      * @return the list of violation messages
547      * @throws IOException if an I/O error occurs
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      * Checks if a directory contains multiple example files.
581      *
582      * @param dir the directory to check
583      * @return true if the directory contains 2 or more Example*.java files
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      * Checks whether none of the examples in this directory define any module properties.
599      * When a module has no configurable properties, its examples may intentionally use
600      * very different code to demonstrate different behaviours, so consistency checking
601      * is not meaningful.
602      *
603      * @param examples the list of example files in the directory
604      * @return true if no example file contains a {@code <property} element in its XML config
605      * @throws IOException if an I/O error occurs reading an example file
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      * Checks examples in a directory. Non-independent examples must match.
621      *
622      * @param dir the directory containing example files
623      * @return list of violation messages for mismatches
624      * @throws IOException if an I/O error occurs
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      * Gets all Example*.java files from a directory.
639      *
640      * @param dir the directory to search
641      * @return list of example file paths
642      * @throws IOException if an I/O error occurs
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      * Checks if a specific example is marked as unparseable.
657      *
658      * @param relativePath the relative directory path
659      * @param exampleFileName the example filename (e.g., "Example1.java")
660      * @return true if this example cannot be parsed
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      * Checks if a specific example is marked as independent.
670      *
671      * @param relativePath the relative directory path
672      * @param exampleFileName the example filename (e.g., "Example1.java")
673      * @return true if this example is independent
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      * Gets the relative path from the common base path.
683      *
684      * @param dir the directory path
685      * @return the relative path string
686      */
687     private static String getRelativePath(Path dir) {
688         return XDOCS_ROOT.relativize(dir).toString().replace('\\', '/');
689     }
690 
691     /**
692      * Compares examples: groups by AST, validates groups, reports mismatches.
693      *
694      * @param dir the directory containing the examples
695      * @param examples the list of example files
696      * @return list of violation messages for mismatches
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      * Validates examples by comparing files with and without constructors separately.
724      *
725      * @param dir the directory containing examples
726      * @param examples the list of examples that must be validated
727      * @return list of violation messages for mismatches
728      * @throws IOException if an I/O error occurs
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      * Checks whether an example contains at least one constructor definition.
758      *
759      * @param example the example file path
760      * @return true if the parsed xdoc section contains a constructor definition
761      * @throws IOException if an I/O error occurs
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      * Checks whether an AST contains a descendant of the given token type.
779      *
780      * @param ast the AST root to inspect
781      * @param tokenType the token type to find
782      * @return true if a matching node is found
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      * Validates that all examples in the list have identical AST structure,
804      * including identical line numbers for each node within the xdoc section.
805      *
806      * @param dir the directory containing the examples
807      * @param examples the list of examples that must all match
808      * @return list of violation messages for mismatches
809      * @throws IOException if an I/O error occurs
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             // Skip examples that can't be parsed as Java
836         }
837 
838         return violations;
839     }
840 
841     /**
842      * Extracts content between xdoc section markers from a file.
843      *
844      * <p>The extracted lines are re-joined and parsed fresh by {@link #parseContent}, so
845      * AST line numbers are always relative to the start of the extracted section (line 1).
846      * This makes line-number comparisons between examples independent of any difference in
847      * header length (license block, imports, etc.) above the marker.
848      *
849      * @param file the file to read
850      * @return the content between markers, or entire file if no markers
851      * @throws IOException if an I/O error occurs
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      * Parses Java content string into a DetailAST.
882      *
883      * @param content the Java code content to parse
884      * @return the parsed AST, or null if parsing fails
885      * @throws CheckstyleException if parsing fails
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      * Compares a single example file against the reference.
898      *
899      * @param dir          the directory containing the examples
900      * @param example      the example file to compare
901      * @param reference    the reference file
902      * @param referenceAst the reference AST
903      * @return violation message if mismatch found, null otherwise
904      * @throws IOException if an I/O error occurs
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      * Converts a DetailAST into a structural representation that excludes only
954      * {@code ok}, {@code violation}, and {@code xdoc section} single-line comments.
955      * All other single-line comments, as well as Javadoc and block comments, are
956      * included in the comparison.
957      *
958      * @param ast the AST to convert
959      * @return structural representation of the AST, or null if the node is a
960      *         skippable comment
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      * Checks if an AST node is an identifier representing class or constructor name.
982      *
983      * @param ast the AST node to check
984      * @return true if the node is a class or constructor name identifier
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      * Checks if an AST node is an identifier in an extends clause of an example class.
996      *
997      * @param ast the AST node to check
998      * @return true if the node is an identifier in an extends clause of an example class
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      * Checks whether a comment is a documentation marker that should be
1018      * excluded from structural comparison.
1019      *
1020      * <p>Skipped prefixes:
1021      * <ul>
1022      *   <li>{@code ok} - suppressed-violation marker</li>
1023      *   <li>{@code violation} - violation marker (including {@code filtered violation})</li>
1024      *   <li>{@code xdoc section} - section boundary marker</li>
1025      *   <li>{@code N violation(s)} - count-style marker, e.g. {@code 3 violations}</li>
1026      *   <li>A single-quoted string starting and ending with a single-quote character
1027      *       continuation line, e.g. {@code //    'Expected }&#64;{@code param tag for p1.'}.
1028      *       These lines appear below a count-style or
1029      *       {@code violation above} marker and may be separated from it by a blank line,
1030      *       so they cannot be reliably caught by the continuation-chain logic alone.</li>
1031      * </ul>
1032      *
1033      * @param comment the stripped comment text (everything after {@code //})
1034      * @return true if the comment is a marker that should be ignored
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      * Extracts comments that participate in comparison.
1049      *
1050      * <p>The following comments are ignored:
1051      * <ul>
1052      *   <li>{@code ok}</li>
1053      *   <li>{@code violation} (including {@code filtered violation}
1054      *       and count-style {@code N violations})</li>
1055      *   <li>xdoc section markers</li>
1056      *   <li>standalone continuation lines immediately following a skipped marker —
1057      *       e.g. <code>// no space after '{'</code> after {@code // 3 violations}</li>
1058      * </ul>
1059      *
1060      * <p>A standalone continuation line is one where there is no code before the
1061      * double-slash on that line, and the previous comment line was a skipped marker.
1062      * Inline trailing comments on code lines are always evaluated independently.
1063      *
1064      * <p>All other comments are included in comparison.
1065      *
1066      * @param content example content
1067      * @return comments participating in comparison
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      * Finds the starting index of a single-line comment ({@code //}) in a given line,
1100      * ignoring comments within string literals or character literals.
1101      *
1102      * @param line the string line to search
1103      * @return the index of the first {@code //}, or -1 if not found or within a literal
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      * Checks if a single-line comment starts at the given index.
1137      *
1138      * @param line current line
1139      * @param index current index
1140      * @param inString whether currently inside a string literal
1141      * @param inChar whether currently inside a character literal
1142      * @return true if comment starts at index
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      * Represents a structural AST node without skippable comments.
1152      * This allows for structural comparison between example files.
1153      * Includes literal text values and identifier names for semantic comparison,
1154      * and line numbers for positional consistency validation.
1155      *
1156      * <p>Line numbers are section-relative (line 1 = first line of the extracted
1157      * xdoc section) because {@link #parseContent} re-parses only the extracted
1158      * section string. Class and constructor name nodes have their line number
1159      * set to {@code null} so that intentional name differences do not cause
1160      * false positives.
1161      */
1162     private static final class StructuralAstNode {
1163         private final int type;
1164         private final String text;
1165         /** Section-relative line number; null when position is intentionally ignored. */
1166         private final Integer lineNo;
1167         private final List<StructuralAstNode> children = new ArrayList<>();
1168 
1169         /**
1170          * Constructs a structural AST node.
1171          *
1172          * @param type           the token type
1173          * @param text           the token text from the source
1174          * @param ignoreText     if true, the text field is not stored (class/ctor names)
1175          * @param lineNo         the line number of this node within the parsed section
1176          * @param ignorePosition if true, the line number is not stored (class/ctor names)
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          * Checks if a token type represents a value whose text should be compared.
1200          * This includes numeric, string, boolean, null literals, and identifiers.
1201          * Identifiers are included so that differences in variable names, parameter
1202          * names, annotation names, etc. are detected as mismatches.
1203          * Class and constructor name identifiers are excluded via the
1204          * {@code ignoreText} flag set in {@link #toStructuralAst}.
1205          *
1206          * @param tokenType the token type
1207          * @return true if the token text carries semantic value
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      * Represents a violation found during consistency check.
1267      *
1268      * @param relativePath      relative directory path
1269      * @param referenceFileName reference file name
1270      * @param mismatchFileName  mismatch file name
1271      * @param reason            mismatch reason
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          * Gets the pattern to use for suppression.
1285          *
1286          * @return the suppression pattern
1287          */
1288         /* package */ String getSuppressionPattern() {
1289             return relativePath + "/" + mismatchFileName.replace(".java", "");
1290         }
1291     }
1292 
1293 }