View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.collect.ImmutableList.toImmutableList;
23  import static com.google.common.truth.Truth.assertWithMessage;
24  import static java.lang.Integer.parseInt;
25  
26  import java.beans.PropertyDescriptor;
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.StringReader;
30  import java.lang.reflect.Array;
31  import java.lang.reflect.Field;
32  import java.lang.reflect.ParameterizedType;
33  import java.net.URI;
34  import java.nio.file.Files;
35  import java.nio.file.Path;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.BitSet;
39  import java.util.Collection;
40  import java.util.Collections;
41  import java.util.HashMap;
42  import java.util.HashSet;
43  import java.util.Iterator;
44  import java.util.List;
45  import java.util.Locale;
46  import java.util.Map;
47  import java.util.NoSuchElementException;
48  import java.util.Optional;
49  import java.util.Properties;
50  import java.util.Set;
51  import java.util.TreeSet;
52  import java.util.regex.Matcher;
53  import java.util.regex.Pattern;
54  import java.util.stream.Collectors;
55  import java.util.stream.IntStream;
56  import java.util.stream.Stream;
57  
58  import javax.xml.parsers.DocumentBuilder;
59  import javax.xml.parsers.DocumentBuilderFactory;
60  
61  import org.apache.commons.beanutils.PropertyUtils;
62  import org.junit.jupiter.api.BeforeAll;
63  import org.junit.jupiter.api.Test;
64  import org.junit.jupiter.api.io.TempDir;
65  import org.w3c.dom.Document;
66  import org.w3c.dom.Element;
67  import org.w3c.dom.Node;
68  import org.w3c.dom.NodeList;
69  import org.xml.sax.InputSource;
70  
71  import com.puppycrawl.tools.checkstyle.Checker;
72  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
73  import com.puppycrawl.tools.checkstyle.ConfigurationLoader.IgnoredModulesOptions;
74  import com.puppycrawl.tools.checkstyle.ModuleFactory;
75  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
76  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
77  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
78  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
79  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
80  import com.puppycrawl.tools.checkstyle.api.Configuration;
81  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
82  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
83  import com.puppycrawl.tools.checkstyle.internal.annotation.PreserveOrder;
84  import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
85  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
86  import com.puppycrawl.tools.checkstyle.internal.utils.XdocGenerator;
87  import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
88  import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
89  import com.puppycrawl.tools.checkstyle.site.SiteUtil;
90  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
91  
92  /**
93   * Generates xdocs pages from templates and performs validations.
94   * Before running this test, the following commands have to be executed:
95   * - mvn clean compile - Required for next command
96   * - mvn plexus-component-metadata:generate-metadata - Required to find custom macros and parser
97   */
98  public class XdocsPagesTest {
99      private static final Path SITE_PATH = Path.of("src/site/site.xml");
100     private static final Path CHECKSTYLE_JS_PATH = Path.of(
101         "src/site/resources/js/checkstyle.js");
102 
103     private static final Path AVAILABLE_CHECKS_PATH = Path.of("src/site/xdoc/checks.xml");
104     private static final Path AVAILABLE_FILE_FILTERS_PATH = Path.of(
105         "src/site/xdoc/filefilters/index.xml");
106     private static final Path AVAILABLE_FILTERS_PATH = Path.of("src/site/xdoc/filters/index.xml");
107 
108     private static final Pattern VERSION = Pattern.compile("\\d+\\.\\d+(\\.\\d+)?");
109 
110     private static final Pattern DESCRIPTION_VERSION = Pattern
111             .compile("^Since Checkstyle \\d+\\.\\d+(\\.\\d+)?");
112 
113     private static final Pattern END_OF_SENTENCE = Pattern.compile("(.*?\\.)\\s", Pattern.DOTALL);
114 
115     private static final List<String> XML_FILESET_LIST = List.of(
116             "TreeWalker",
117             "name=\"Checker\"",
118             "name=\"Header\"",
119             "name=\"LineLength\"",
120             "name=\"Translation\"",
121             "name=\"SeverityMatchFilter\"",
122             "name=\"SuppressWithNearbyTextFilter\"",
123             "name=\"SuppressWithPlainTextCommentFilter\"",
124             "name=\"SuppressionFilter\"",
125             "name=\"SuppressionSingleFilter\"",
126             "name=\"SuppressWarningsFilter\"",
127             "name=\"BeforeExecutionExclusionFileFilter\"",
128             "name=\"RegexpHeader\"",
129             "name=\"MultiFileRegexpHeader\"",
130             "name=\"RegexpOnFilename\"",
131             "name=\"RegexpSingleline\"",
132             "name=\"RegexpMultiline\"",
133             "name=\"JavadocPackage\"",
134             "name=\"NewlineAtEndOfFile\"",
135             "name=\"OrderedProperties\"",
136             "name=\"UniqueProperties\"",
137             "name=\"FileLength\"",
138             "name=\"FileTabCharacter\""
139     );
140 
141     private static final Set<String> CHECK_PROPERTIES = getProperties(AbstractCheck.class);
142     private static final Set<String> JAVADOC_CHECK_PROPERTIES =
143             getProperties(AbstractJavadocCheck.class);
144     private static final Set<String> FILESET_PROPERTIES = getProperties(AbstractFileSetCheck.class);
145 
146     private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
147             "Checker.classLoader",
148             "Checker.classloader",
149             "Checker.moduleClassLoader",
150             "Checker.moduleFactory",
151             "TreeWalker.classLoader",
152             "TreeWalker.moduleFactory",
153             "TreeWalker.cacheFile",
154             "TreeWalker.upChild",
155             "SuppressWithNearbyCommentFilter.fileContents",
156             "SuppressionCommentFilter.fileContents"
157     );
158 
159     private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
160             // static field (all upper case)
161             "SuppressWarningsHolder.aliasList",
162             // loads string into memory similar to file
163             "Header.header",
164             "RegexpHeader.header",
165             // property is an int, but we cut off excess to accommodate old versions
166             "RedundantModifier.jdkVersion",
167             // until https://github.com/checkstyle/checkstyle/issues/13376
168             "CustomImportOrder.customImportOrderRules"
169     );
170 
171     private static final Set<String> SUN_MODULES = Collections.unmodifiableSet(
172         CheckUtil.getConfigSunStyleModules());
173     // ignore the not yet properly covered modules while testing newly added ones
174     // add proper sections to the coverage report and integration tests
175     // and then remove this list eventually
176     private static final Set<String> IGNORED_SUN_MODULES = Set.of(
177             "ArrayTypeStyle",
178             "AvoidNestedBlocks",
179             "AvoidStarImport",
180             "ConstantName",
181             "DesignForExtension",
182             "EmptyBlock",
183             "EmptyForIteratorPad",
184             "EmptyStatement",
185             "EqualsHashCode",
186             "FileLength",
187             "FileTabCharacter",
188             "FinalClass",
189             "FinalParameters",
190             "GenericWhitespace",
191             "HiddenField",
192             "HideUtilityClassConstructor",
193             "IllegalImport",
194             "IllegalInstantiation",
195             "InnerAssignment",
196             "InterfaceIsType",
197             "JavadocMethod",
198             "JavadocPackage",
199             "JavadocStyle",
200             "JavadocType",
201             "JavadocVariable",
202             "LeftCurly",
203             "LineLength",
204             "LocalFinalVariableName",
205             "LocalVariableName",
206             "MagicNumber",
207             "MemberName",
208             "MethodLength",
209             "MethodName",
210             "MethodParamPad",
211             "MissingJavadocMethod",
212             "MissingSwitchDefault",
213             "ModifierOrder",
214             "NeedBraces",
215             "NewlineAtEndOfFile",
216             "NoWhitespaceAfter",
217             "NoWhitespaceBefore",
218             "OperatorWrap",
219             "PackageName",
220             "ParameterName",
221             "ParameterNumber",
222             "ParenPad",
223             "RedundantImport",
224             "RedundantModifier",
225             "RegexpSingleline",
226             "RightCurly",
227             "SimplifyBooleanExpression",
228             "SimplifyBooleanReturn",
229             "StaticVariableName",
230             "TodoComment",
231             "Translation",
232             "TypecastParenPad",
233             "TypeName",
234             "UnusedImports",
235             "UpperEll",
236             "VisibilityModifier",
237             "WhitespaceAfter",
238             "WhitespaceAround"
239     );
240 
241     private static final Set<String> GOOGLE_MODULES = Collections.unmodifiableSet(
242         CheckUtil.getConfigGoogleStyleModules());
243 
244     private static final Set<String> NON_MODULE_XDOC = Set.of(
245         "config_system_properties.xml",
246         "sponsoring.xml",
247         "consulting.xml",
248         "index.xml",
249         "extending.xml",
250         "contributing.xml",
251         "running.xml",
252         "checks.xml",
253         "property_types.xml",
254         "google_style.xml",
255         "sun_style.xml",
256         "style_configs.xml",
257         "writingfilters.xml",
258         "writingfilefilters.xml",
259         "eclipse.xml",
260         "netbeans.xml",
261         "idea.xml",
262         "beginning_development.xml",
263         "writingchecks.xml",
264         "config.xml",
265         "report_issue.xml",
266         "result_reports.xml"
267     );
268 
269     private static final String NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH =
270             " names must be in alphabetical order at " + SITE_PATH;
271 
272     @TempDir
273     private static File temporaryFolder;
274 
275     /**
276      * Generate xdoc content from templates before validation.
277      * This method will be removed once
278      * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved.
279      *
280      * @throws Exception if something goes wrong
281      */
282     @BeforeAll
283     public static void generateXdocContent() throws Exception {
284         XdocGenerator.generateXdocContent(temporaryFolder);
285     }
286 
287     @Test
288     public void testAllChecksPresentOnAvailableChecksPage() throws Exception {
289         final String availableChecks = Files.readString(AVAILABLE_CHECKS_PATH);
290 
291         CheckUtil.getSimpleNames(CheckUtil.getCheckstyleChecks())
292             .stream()
293             .filter(checkName -> {
294                 return !"ClassAndPropertiesSettersJavadocScraper".equals(checkName);
295             })
296             .forEach(checkName -> {
297                 if (!isPresent(availableChecks, checkName)) {
298                     assertWithMessage(
299                             checkName + " is not correctly listed on Available Checks page"
300                                     + " - add it to " + AVAILABLE_CHECKS_PATH).fail();
301                 }
302             });
303     }
304 
305     private static boolean isPresent(String availableChecks, String checkName) {
306         final String linkPattern = String.format(Locale.ROOT,
307                 "(?s).*<a href=\"[^\"]+#%1$s\">([\\r\\n\\s])*%1$s([\\r\\n\\s])*</a>.*",
308                 checkName);
309         return availableChecks.matches(linkPattern);
310     }
311 
312     @Test
313     public void testAllConfigsHaveLinkInSite() throws Exception {
314         final String siteContent = Files.readString(SITE_PATH);
315 
316         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
317             final String expectedFile = path.toString()
318                     .replace(".xml", ".html")
319                     .replaceAll("\\\\", "/")
320                     .replaceAll("src[\\\\/]site[\\\\/]xdoc[\\\\/]", "");
321             final boolean isConfigHtmlFile = Pattern.matches("config_[a-z]+.html", expectedFile);
322             final boolean isChecksIndexHtmlFile = "checks/index.html".equals(expectedFile);
323             final boolean isOldReleaseNotes = path.toString().contains("releasenotes_");
324             final boolean isInnerPage = "report_issue.html".equals(expectedFile);
325 
326             if (!isConfigHtmlFile && !isChecksIndexHtmlFile
327                 && !isOldReleaseNotes && !isInnerPage) {
328                 final String expectedLink = String.format(Locale.ROOT, "href=\"%s\"", expectedFile);
329                 assertWithMessage("Expected to find link to '" + expectedLink + "' in " + SITE_PATH)
330                         .that(siteContent)
331                         .contains(expectedLink);
332             }
333         }
334     }
335 
336     @Test
337     public void testAllModulesPageInSyncWithModuleSummaries() throws Exception {
338         validateModulesSyncWithTheirSummaries(AVAILABLE_CHECKS_PATH,
339             (Path path) -> {
340                 final String fileName = path.getFileName().toString();
341                 return isNonModulePage(fileName) || !path.toString().contains("checks");
342             });
343 
344         validateModulesSyncWithTheirSummaries(AVAILABLE_FILTERS_PATH,
345             (Path path) -> {
346                 final String fileName = path.getFileName().toString();
347                 return isNonModulePage(fileName)
348                     || path.toString().contains("checks")
349                     || path.toString().contains("filefilters");
350             });
351 
352         validateModulesSyncWithTheirSummaries(AVAILABLE_FILE_FILTERS_PATH,
353             (Path path) -> {
354                 final String fileName = path.getFileName().toString();
355                 return isNonModulePage(fileName) || !path.toString().contains("filefilters");
356             });
357     }
358 
359     private static void validateModulesSyncWithTheirSummaries(Path availablePagePath,
360                                                               PredicateProcess skipPredicate)
361             throws Exception {
362         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
363             if (skipPredicate.hasFit(path)) {
364                 continue;
365             }
366 
367             final String fileName = path.getFileName().toString();
368             final Map<String, String> summaries = readSummaries(availablePagePath);
369             final NodeList subsectionSources = getTagSourcesNode(path, "subsection");
370 
371             for (int position = 0; position < subsectionSources.getLength(); position++) {
372                 final Node subsection = subsectionSources.item(position);
373                 final String subsectionName = XmlUtil.getNameAttributeOfNode(subsection);
374                 if (!"Description".equals(subsectionName)) {
375                     continue;
376                 }
377 
378                 final String moduleName = XmlUtil.getNameAttributeOfNode(
379                     subsection.getParentNode());
380                 final Matcher matcher = END_OF_SENTENCE.matcher(subsection.getTextContent());
381                 assertWithMessage(
382                     "The first sentence of the \"Description\" subsection for the module "
383                         + moduleName + " in the file \"" + fileName + "\" should end with a period")
384                     .that(matcher.find())
385                     .isTrue();
386 
387                 final String firstSentence = XmlUtil.sanitizeXml(matcher.group(1));
388 
389                 assertWithMessage("The summary for module " + moduleName
390                         + " in the file \"" + availablePagePath + "\""
391                         + " should match the first sentence of the \"Description\" subsection"
392                         + " for this module in the file \"" + fileName + "\"")
393                     .that(summaries.get(moduleName))
394                     .isEqualTo(firstSentence);
395             }
396         }
397     }
398 
399     @Test
400     public void testCategoryIndexPageTableInSyncWithAllChecksPageTable() throws Exception {
401         final Map<String, String> summaries = readSummaries(AVAILABLE_CHECKS_PATH);
402         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
403             final String fileName = path.getFileName().toString();
404             if (!"index.xml".equals(fileName)
405                     // Filters are excluded because they are not included in the main checks.xml
406                     // file and have their own separate validation in
407                     // testAllFiltersIndexPageTable()
408                     || path.getParent().toString().contains("filters")) {
409                 continue;
410             }
411 
412             final NodeList sources = getTagSourcesNode(path, "tr");
413 
414             for (int position = 0; position < sources.getLength(); position++) {
415                 final Node tableRow = sources.item(position);
416                 final Iterator<Node> cells = XmlUtil
417                         .findChildElementsByTag(tableRow, "td").iterator();
418                 final String checkName = XmlUtil.sanitizeXml(cells.next().getTextContent());
419                 final String description = XmlUtil.sanitizeXml(cells.next().getTextContent());
420                 assertWithMessage("The summary for check " + checkName
421                         + " in the file \"" + path + "\""
422                         + " should match the summary"
423                         + " for this check in the file \"" + AVAILABLE_CHECKS_PATH + "\"")
424                     .that(description)
425                     .isEqualTo(summaries.get(checkName));
426             }
427         }
428     }
429 
430     @Test
431     public void testAllFiltersIndexPageTable() throws Exception {
432         validateFilterTypeIndexPage(AVAILABLE_FILTERS_PATH);
433         validateFilterTypeIndexPage(AVAILABLE_FILE_FILTERS_PATH);
434     }
435 
436     private static void validateFilterTypeIndexPage(Path availablePath)
437             throws Exception {
438         final NodeList tableRowSources = getTagSourcesNode(availablePath, "tr");
439 
440         for (int position = 0; position < tableRowSources.getLength(); position++) {
441             final Node tableRow = tableRowSources.item(position);
442             final Iterator<Node> tdCells = XmlUtil
443                 .findChildElementsByTag(tableRow, "td").iterator();
444 
445             assertWithMessage("Filter name cell at row " + (position + 1)
446                 + " in " + availablePath + " should exist")
447                 .that(tdCells.hasNext())
448                 .isTrue();
449             final Node nameCell = tdCells.next();
450             final String filterName = XmlUtil.sanitizeXml(nameCell.getTextContent().trim());
451 
452             assertWithMessage("Description cell for " + filterName
453                 + " in index.xml should exist")
454                 .that(tdCells.hasNext())
455                 .isTrue();
456 
457             assertWithMessage("Filter name at row " + (position + 1) + " in " + availablePath
458                     + " should not be empty")
459                 .that(filterName)
460                 .isNotEmpty();
461 
462             final Node descriptionCell = tdCells.next();
463             final String description = XmlUtil.sanitizeXml(
464                 descriptionCell.getTextContent().trim());
465 
466             assertWithMessage("Filter description for " + filterName
467                 + " in " + availablePath + " should not be empty")
468                 .that(description)
469                 .isNotEmpty();
470 
471             assertWithMessage("Filter description for " + filterName
472                 + " in " + availablePath + " should end with a period")
473                 .that(description.charAt(description.length() - 1))
474                 .isEqualTo('.');
475         }
476     }
477 
478     private static NodeList getTagSourcesNode(Path availablePath, String tagName)
479             throws Exception {
480         final String input = Files.readString(availablePath);
481         final Document document = XmlUtil.getRawXml(
482             availablePath.toString(), input, input);
483 
484         return document.getElementsByTagName(tagName);
485     }
486 
487     @Test
488     public void testAlphabetOrderInNames() throws Exception {
489         final NodeList nodes = getTagSourcesNode(SITE_PATH, "item");
490 
491         for (int nodeIndex = 0; nodeIndex < nodes.getLength(); nodeIndex++) {
492             final Node current = nodes.item(nodeIndex);
493 
494             if ("Checks".equals(XmlUtil.getNameAttributeOfNode(current))) {
495                 final List<String> groupNames = getNames(current);
496                 final List<String> groupNamesSorted = groupNames.stream()
497                         .sorted()
498                         .toList();
499 
500                 assertWithMessage("Group" + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
501                         .that(groupNames)
502                         .containsExactlyElementsIn(groupNamesSorted)
503                         .inOrder();
504 
505                 Node groupNode = current.getFirstChild();
506                 int index = 0;
507                 final int totalGroups = XmlUtil.getChildrenElements(current).size();
508                 while (index < totalGroups) {
509                     if ("item".equals(groupNode.getNodeName())) {
510                         final List<String> checkNames = getNames(groupNode);
511                         final List<String> checkNamesSorted = checkNames.stream()
512                                 .sorted()
513                                 .toList();
514                         assertWithMessage("Check" + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
515                                 .that(checkNames)
516                                 .containsExactlyElementsIn(checkNamesSorted)
517                                 .inOrder();
518                         index++;
519                     }
520                     groupNode = groupNode.getNextSibling();
521                 }
522             }
523             if ("Filters".equals(XmlUtil.getNameAttributeOfNode(current))) {
524                 final List<String> filterNames = getNames(current);
525                 final List<String> filterNamesSorted = filterNames.stream()
526                         .sorted()
527                         .toList();
528                 assertWithMessage("Filter" + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
529                         .that(filterNames)
530                         .containsExactlyElementsIn(filterNamesSorted)
531                         .inOrder();
532             }
533             if ("File Filters".equals(XmlUtil.getNameAttributeOfNode(current))) {
534                 final List<String> fileFilterNames = getNames(current);
535                 final List<String> fileFilterNamesSorted = fileFilterNames.stream()
536                         .sorted()
537                         .toList();
538                 assertWithMessage("File Filter" + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
539                         .that(fileFilterNames)
540                         .containsExactlyElementsIn(fileFilterNamesSorted)
541                         .inOrder();
542             }
543         }
544     }
545 
546     @Test
547     public void testAlphabetOrderAtIndexPages() throws Exception {
548         final Path allChecks = Path.of("src/site/xdoc/checks.xml");
549         validateOrder(allChecks, "Check");
550 
551         final String[] groupNames = {"annotation", "blocks", "design",
552             "coding", "header", "imports", "javadoc", "metrics",
553             "misc", "modifier", "naming", "regexp", "sizes", "whitespace"};
554         for (String name : groupNames) {
555             final Path checks = Path.of("src/site/xdoc/checks/" + name + "/index.xml");
556             validateOrder(checks, "Check");
557         }
558         validateOrder(AVAILABLE_FILTERS_PATH, "Filter");
559 
560         final Path fileFilters = Path.of("src/site/xdoc/filefilters/index.xml");
561         validateOrder(fileFilters, "File Filter");
562     }
563 
564     public static void validateOrder(Path path, String name) throws Exception {
565         final NodeList nodes = getTagSourcesNode(path, "div");
566 
567         for (int nodeIndex = 0; nodeIndex < nodes.getLength(); nodeIndex++) {
568             final Node current = nodes.item(nodeIndex);
569             final List<String> names = getNamesFromIndexPage(current);
570             final List<String> namesSorted = names.stream()
571                     .sorted()
572                     .toList();
573 
574             assertWithMessage(name + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH + path)
575                     .that(names)
576                     .containsExactlyElementsIn(namesSorted)
577                     .inOrder();
578         }
579     }
580 
581     private static List<String> getNamesFromIndexPage(Node node) {
582         final List<String> result = new ArrayList<>();
583         final Set<Node> children = XmlUtil.findChildElementsByTag(node, "a");
584 
585         Node current = node.getFirstChild();
586         Node treeNode = current;
587         boolean getFirstChild = false;
588         int index = 0;
589         while (current != null && index < children.size()) {
590             if ("tr".equals(current.getNodeName())) {
591                 treeNode = current.getNextSibling();
592             }
593             if ("a".equals(current.getNodeName())) {
594                 final String name = current.getFirstChild().getTextContent()
595                     .replace(" ", "").replace("\n", "");
596                 result.add(name);
597                 current = treeNode;
598                 getFirstChild = false;
599                 index++;
600             }
601             else if (getFirstChild) {
602                 current = current.getFirstChild();
603                 getFirstChild = false;
604             }
605             else {
606                 current = current.getNextSibling();
607                 getFirstChild = true;
608             }
609         }
610         return result;
611     }
612 
613     private static List<String> getNames(Node node) {
614         final Set<Node> children = XmlUtil.getChildrenElements(node);
615         final List<String> result = new ArrayList<>();
616         Node current = node.getFirstChild();
617         int index = 0;
618         while (index < children.size()) {
619             if ("item".equals(current.getNodeName())) {
620                 final String name = XmlUtil.getNameAttributeOfNode(current);
621                 result.add(name);
622                 index++;
623             }
624             current = current.getNextSibling();
625         }
626         return result;
627     }
628 
629     private static Map<String, String> readSummaries(Path availablePath) throws Exception {
630         final NodeList rows = getTagSourcesNode(availablePath, "tr");
631         final Map<String, String> result = new HashMap<>();
632 
633         for (int position = 0; position < rows.getLength(); position++) {
634             final Node row = rows.item(position);
635             final Iterator<Node> cells = XmlUtil.findChildElementsByTag(row, "td").iterator();
636             final String name = XmlUtil.sanitizeXml(cells.next().getTextContent());
637             final String summary = XmlUtil.sanitizeXml(cells.next().getTextContent());
638 
639             result.put(name, summary);
640         }
641 
642         return result;
643     }
644 
645     @Test
646     public void testAllSubSections() throws Exception {
647         for (Path path : XdocUtil.getXdocsFilePaths()) {
648             final String fileName = path.getFileName().toString();
649             final NodeList subSections = getTagSourcesNode(path, "subsection");
650 
651             for (int position = 0; position < subSections.getLength(); position++) {
652                 final Node subSection = subSections.item(position);
653                 final Node name = subSection.getAttributes().getNamedItem("name");
654 
655                 assertWithMessage("All sub-sections in '" + fileName + "' must have a name")
656                     .that(name)
657                     .isNotNull();
658 
659                 final Node id = subSection.getAttributes().getNamedItem("id");
660 
661                 assertWithMessage("All sub-sections in '" + fileName + "' must have an id")
662                     .that(id)
663                     .isNotNull();
664 
665                 // Checks and filters have their own xdocs files, so the section name
666                 // is the same as the section id by default.
667                 String sectionName = XmlUtil.getNameAttributeOfNode(subSection.getParentNode());
668                 final String nameString = name.getNodeValue();
669                 final String subsectionId = id.getNodeValue();
670                 final String expectedId;
671 
672                 if ("google_style.xml".equals(fileName)) {
673                     sectionName = "Google";
674                     expectedId = (sectionName + "_" + nameString).replace(' ', '_');
675                 }
676                 else if ("sun_style.xml".equals(fileName)) {
677                     sectionName = "Sun";
678                     expectedId = (sectionName + "_" + nameString).replace(' ', '_');
679                 }
680                 else if ((path.toString().contains("filters")
681                         || path.toString().contains("checks"))
682                         && !subsectionId.startsWith(sectionName)) {
683                     expectedId = nameString.replace(' ', '_');
684                 }
685                 else {
686                     expectedId = (sectionName + "_" + nameString).replace(' ', '_');
687                 }
688 
689                 assertWithMessage(fileName + " sub-section " + nameString + " for section "
690                         + sectionName + " must match")
691                     .that(subsectionId)
692                     .isEqualTo(expectedId);
693             }
694         }
695     }
696 
697     @Test
698     public void testAllXmlExamples() throws Exception {
699         for (Path path : XdocUtil.getXdocsFilePaths()) {
700             final String fileName = path.getFileName().toString();
701             final NodeList sources = getTagSourcesNode(path, "source");
702 
703             for (int position = 0; position < sources.getLength(); position++) {
704                 final String unserializedSource = sources.item(position).getTextContent()
705                         .replace("...", "").trim();
706 
707                 if (unserializedSource.length() > 1 && (unserializedSource.charAt(0) != '<'
708                         || unserializedSource.charAt(unserializedSource.length() - 1) != '>'
709                         // no dtd testing yet
710                         || unserializedSource.contains("<!"))) {
711                     continue;
712                 }
713 
714                 final String code = buildXml(unserializedSource);
715                 // validate only
716                 XmlUtil.getRawXml(fileName, code, unserializedSource);
717 
718                 // can't test ant structure, or old and outdated checks
719                 assertWithMessage("Xml is invalid, old or has outdated structure")
720                         .that(fileName.startsWith("anttask")
721                                 || fileName.startsWith("releasenotes")
722                                 || fileName.startsWith("writingjavadocchecks")
723                                 || isValidCheckstyleXml(fileName, code, unserializedSource))
724                         .isTrue();
725             }
726         }
727     }
728 
729     private static String buildXml(String unserializedSource) throws IOException {
730         // not all examples come with the full xml structure
731         String code = unserializedSource
732             // don't corrupt our own cachefile
733             .replace("target/cachefile", "target/cachefile-test");
734 
735         if (!hasFileSetClass(code)) {
736             code = "<module name=\"TreeWalker\">\n" + code + "\n</module>";
737         }
738         if (!code.contains("name=\"Checker\"")) {
739             code = "<module name=\"Checker\">\n" + code + "\n</module>";
740         }
741         if (!code.startsWith("<?xml")) {
742             final String dtdPath = new File(
743                     "src/main/resources/com/puppycrawl/tools/checkstyle/configuration_1_3.dtd")
744                     .getCanonicalPath();
745 
746             code = "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC "
747                     + "\"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\" \"" + dtdPath
748                     + "\">\n" + code;
749         }
750         return code;
751     }
752 
753     private static boolean hasFileSetClass(String xml) {
754         boolean found = false;
755 
756         for (String find : XML_FILESET_LIST) {
757             if (xml.contains(find)) {
758                 found = true;
759                 break;
760             }
761         }
762 
763         return found;
764     }
765 
766     private static boolean isValidCheckstyleXml(String fileName, String code,
767                                                 String unserializedSource)
768             throws IOException, CheckstyleException {
769         // can't process non-existent examples, or out of context snippets
770         if (!code.contains("com.mycompany") && !code.contains("checkstyle-packages")
771                 && !code.contains("MethodLimit") && !code.contains("<suppress ")
772                 && !code.contains("<suppress-xpath ")
773                 && !code.contains("<import-control ")
774                 && !unserializedSource.startsWith("<property ")
775                 && !unserializedSource.startsWith("<taskdef ")) {
776             // validate checkstyle structure and contents
777             try {
778                 final Properties properties = new Properties();
779 
780                 properties.setProperty("checkstyle.header.file",
781                         new File("config/java.header").getCanonicalPath());
782                 properties.setProperty("config.folder",
783                         new File("config").getCanonicalPath());
784 
785                 final PropertiesExpander expander = new PropertiesExpander(properties);
786                 final Configuration config = ConfigurationLoader.loadConfiguration(new InputSource(
787                         new StringReader(code)), expander, IgnoredModulesOptions.EXECUTE);
788                 final Checker checker = new Checker();
789 
790                 try {
791                     final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
792                     checker.setModuleClassLoader(moduleClassLoader);
793                     checker.configure(config);
794                 }
795                 finally {
796                     checker.destroy();
797                 }
798             }
799             catch (CheckstyleException exc) {
800                 throw new CheckstyleException(fileName + " has invalid Checkstyle xml ("
801                         + exc.getMessage() + "): " + unserializedSource, exc);
802             }
803         }
804         return true;
805     }
806 
807     @Test
808     public void testAllCheckSections() throws Exception {
809         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
810 
811         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
812             final String fileName = path.getFileName().toString();
813 
814             if (isNonModulePage(fileName)) {
815                 continue;
816             }
817 
818             final NodeList sources = getTagSourcesNode(path, "section");
819             String lastSectionName = null;
820 
821             for (int position = 0; position < sources.getLength(); position++) {
822                 final Node section = sources.item(position);
823                 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
824 
825                 if ("Content".equals(sectionName) || "Overview".equals(sectionName)) {
826                     assertWithMessage(fileName + " section '" + sectionName + "' should be first")
827                         .that(lastSectionName)
828                         .isNull();
829                     continue;
830                 }
831 
832                 assertWithMessage(
833                         fileName + " section '" + sectionName + "' shouldn't end with 'Check'")
834                                 .that(sectionName.endsWith("Check"))
835                                 .isFalse();
836                 if (lastSectionName != null) {
837                     assertWithMessage(fileName + " section '" + sectionName
838                             + "' is out of order compared to '" + lastSectionName + "'")
839                                     .that(sectionName.toLowerCase(Locale.ENGLISH).compareTo(
840                                             lastSectionName.toLowerCase(Locale.ENGLISH)) >= 0)
841                                     .isTrue();
842                 }
843 
844                 validateCheckSection(moduleFactory, fileName, sectionName, section);
845 
846                 lastSectionName = sectionName;
847             }
848         }
849     }
850 
851     public static boolean isNonModulePage(String fileName) {
852         return NON_MODULE_XDOC.contains(fileName)
853             || fileName.startsWith("releasenotes")
854             || Pattern.matches("config_[a-z]+.xml", fileName);
855     }
856 
857     @Test
858     public void testAllCheckSectionsEx() throws Exception {
859         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
860 
861         final Path path = Path.of(XdocUtil.DIRECTORY_PATH + "/config.xml");
862         final String fileName = path.getFileName().toString();
863 
864         final NodeList sources = getTagSourcesNode(path, "section");
865 
866         for (int position = 0; position < sources.getLength(); position++) {
867             final Node section = sources.item(position);
868             final String sectionName = XmlUtil.getNameAttributeOfNode(section);
869 
870             if (!"Checker".equals(sectionName) && !"TreeWalker".equals(sectionName)) {
871                 continue;
872             }
873 
874             validateCheckSection(moduleFactory, fileName, sectionName, section);
875         }
876     }
877 
878     private static void validateCheckSection(ModuleFactory moduleFactory, String fileName,
879             String sectionName, Node section) throws Exception {
880         final Object instance;
881 
882         try {
883             instance = moduleFactory.createModule(sectionName);
884         }
885         catch (CheckstyleException exc) {
886             throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, exc);
887         }
888 
889         int subSectionPos = 0;
890         for (Node subSection : XmlUtil.getChildrenElements(section)) {
891             if (subSectionPos == 0 && "p".equals(subSection.getNodeName())) {
892                 validateSinceDescriptionSection(fileName, sectionName, subSection);
893                 continue;
894             }
895 
896             final String subSectionName = XmlUtil.getNameAttributeOfNode(subSection);
897 
898             // can be in different orders, and completely optional
899             if ("Notes".equals(subSectionName)
900                     || "Rule Description".equals(subSectionName)
901                     || "Metadata".equals(subSectionName)) {
902                 continue;
903             }
904 
905             // optional sections that can be skipped if they have nothing to report
906             if (subSectionPos == 1 && !"Properties".equals(subSectionName)) {
907                 validatePropertySection(fileName, sectionName, null, instance);
908                 subSectionPos++;
909             }
910             if (subSectionPos == 4 && !"Violation Messages".equals(subSectionName)) {
911                 validateViolationSection(fileName, sectionName, null, instance);
912                 subSectionPos++;
913             }
914 
915             assertWithMessage(fileName + " section '" + sectionName + "' should be in order")
916                 .that(subSectionName)
917                 .isEqualTo(getSubSectionName(subSectionPos));
918 
919             switch (subSectionPos) {
920                 case 0 -> validateDescriptionSection(fileName, sectionName, subSection);
921                 case 1 -> validatePropertySection(fileName, sectionName, subSection, instance);
922                 case 3 -> validateUsageExample(fileName, sectionName, subSection);
923                 case 4 -> validateViolationSection(fileName, sectionName, subSection, instance);
924                 case 5 -> validatePackageSection(fileName, sectionName, subSection, instance);
925                 case 6 -> validateParentSection(fileName, sectionName, subSection);
926                 default -> {
927                     // no code by design
928                 }
929             }
930 
931             subSectionPos++;
932         }
933 
934         if ("Checker".equals(sectionName)) {
935             assertWithMessage(fileName + " section '" + sectionName
936                     + "' should contain up to 'Package' sub-section")
937                     .that(subSectionPos)
938                     .isGreaterThan(5);
939         }
940         else {
941             assertWithMessage(fileName + " section '" + sectionName
942                     + "' should contain up to 'Parent' sub-section")
943                     .that(subSectionPos)
944                     .isGreaterThan(6);
945         }
946     }
947 
948     private static void validateSinceDescriptionSection(String fileName, String sectionName,
949             Node subSection) {
950         assertWithMessage(fileName + " section '" + sectionName
951                     + "' should have a valid version at the start of the description like:\n"
952                     + DESCRIPTION_VERSION.pattern())
953                 .that(DESCRIPTION_VERSION.matcher(subSection.getTextContent().trim()).find())
954                 .isTrue();
955     }
956 
957     private static Object getSubSectionName(int subSectionPos) {
958         return switch (subSectionPos) {
959             case 0 -> "Description";
960             case 1 -> "Properties";
961             case 2 -> "Examples";
962             case 3 -> "Example of Usage";
963             case 4 -> "Violation Messages";
964             case 5 -> "Package";
965             case 6 -> "Parent Module";
966             default -> null;
967         };
968     }
969 
970     private static void validateDescriptionSection(String fileName, String sectionName,
971             Node subSection) {
972         if ("config_filters.xml".equals(fileName) && "SuppressionXpathFilter".equals(sectionName)) {
973             validateListOfSuppressionXpathFilterIncompatibleChecks(subSection);
974         }
975     }
976 
977     private static void validateListOfSuppressionXpathFilterIncompatibleChecks(Node subSection) {
978         assertWithMessage(
979             "Incompatible check list should match XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES")
980             .that(getListById(subSection, "SuppressionXpathFilter_IncompatibleChecks"))
981             .isEqualTo(XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES);
982         final Set<String> suppressionXpathFilterJavadocChecks = getListById(subSection,
983                 "SuppressionXpathFilter_JavadocChecks");
984         assertWithMessage(
985             "Javadoc check list should match XpathRegressionTest.INCOMPATIBLE_JAVADOC_CHECK_NAMES")
986             .that(suppressionXpathFilterJavadocChecks)
987             .isEqualTo(XpathRegressionTest.INCOMPATIBLE_JAVADOC_CHECK_NAMES);
988     }
989 
990     private static void validatePropertySection(String fileName, String sectionName,
991             Node subSection, Object instance) throws Exception {
992         final Set<String> properties = getProperties(instance.getClass());
993         final Class<?> clss = instance.getClass();
994 
995         fixCapturedProperties(sectionName, instance, clss, properties);
996 
997         if (subSection != null) {
998             assertWithMessage(fileName + " section '" + sectionName
999                     + "' should have no properties to show")
1000                 .that(properties)
1001                 .isNotEmpty();
1002 
1003             final Set<Node> nodes = XmlUtil.getChildrenElements(subSection);
1004             assertWithMessage(fileName + " section '" + sectionName
1005                     + "' subsection 'Properties' should have one child node")
1006                 .that(nodes)
1007                 .hasSize(1);
1008 
1009             final Node div = nodes.iterator().next();
1010             assertWithMessage(fileName + " section '" + sectionName
1011                         + "' subsection 'Properties' has unexpected child node")
1012                 .that(div.getNodeName())
1013                 .isEqualTo("div");
1014             final String wrapperMessage = fileName + " section '" + sectionName
1015                     + "' subsection 'Properties' wrapping div for table needs the"
1016                     + " class 'wrapper'";
1017             assertWithMessage(wrapperMessage)
1018                     .that(div.hasAttributes())
1019                     .isTrue();
1020             assertWithMessage(wrapperMessage)
1021                 .that(div.getAttributes().getNamedItem("class").getNodeValue())
1022                 .isNotNull();
1023             assertWithMessage(wrapperMessage)
1024                     .that(div.getAttributes().getNamedItem("class").getNodeValue())
1025                     .contains("wrapper");
1026 
1027             final Node table = XmlUtil.getFirstChildElement(div);
1028             assertWithMessage(fileName + " section '" + sectionName
1029                     + "' subsection 'Properties' has unexpected child node")
1030                 .that(table.getNodeName())
1031                 .isEqualTo("table");
1032 
1033             validatePropertySectionPropertiesOrder(fileName, sectionName, table, properties);
1034 
1035             validatePropertySectionProperties(fileName, sectionName, table, instance,
1036                     properties);
1037         }
1038 
1039         assertWithMessage(
1040                 fileName + " section '" + sectionName + "' should show properties: " + properties)
1041             .that(properties)
1042             .isEmpty();
1043     }
1044 
1045     private static void validatePropertySectionPropertiesOrder(String fileName, String sectionName,
1046                                                                Node table, Set<String> properties) {
1047         final Set<Node> rows = XmlUtil.getChildrenElements(table);
1048         final List<String> orderedPropertyNames = new ArrayList<>(properties);
1049         final List<String> tablePropertyNames = new ArrayList<>();
1050 
1051         // javadocTokens and tokens should be last
1052         if (orderedPropertyNames.contains("javadocTokens")) {
1053             orderedPropertyNames.remove("javadocTokens");
1054             orderedPropertyNames.add("javadocTokens");
1055         }
1056         if (orderedPropertyNames.contains("tokens")) {
1057             orderedPropertyNames.remove("tokens");
1058             orderedPropertyNames.add("tokens");
1059         }
1060 
1061         rows
1062             .stream()
1063             // First row is header row
1064             .skip(1)
1065             .forEach(row -> {
1066                 final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
1067                 assertWithMessage(fileName + " section '" + sectionName
1068                         + "' should have the requested columns")
1069                     .that(columns)
1070                     .hasSize(5);
1071 
1072                 final String propertyName = columns.get(0).getTextContent();
1073                 tablePropertyNames.add(propertyName);
1074             });
1075 
1076         assertWithMessage(fileName + " section '" + sectionName
1077                 + "' should have properties in the requested order")
1078             .that(tablePropertyNames)
1079             .isEqualTo(orderedPropertyNames);
1080     }
1081 
1082     private static void fixCapturedProperties(String sectionName, Object instance, Class<?> clss,
1083             Set<String> properties) {
1084         // remove global properties that don't need documentation
1085         if (hasParentModule(sectionName)) {
1086             if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
1087                 properties.removeAll(JAVADOC_CHECK_PROPERTIES);
1088 
1089                 // override
1090                 properties.add("violateExecutionOnNonTightHtml");
1091             }
1092             else if (AbstractCheck.class.isAssignableFrom(clss)) {
1093                 properties.removeAll(CHECK_PROPERTIES);
1094             }
1095         }
1096         if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
1097             properties.removeAll(FILESET_PROPERTIES);
1098 
1099             // override
1100             properties.add("fileExtensions");
1101         }
1102 
1103         // remove undocumented properties
1104         new HashSet<>(properties).stream()
1105             .filter(prop -> UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + "." + prop))
1106             .forEach(properties::remove);
1107 
1108         if (AbstractCheck.class.isAssignableFrom(clss)) {
1109             final AbstractCheck check = (AbstractCheck) instance;
1110 
1111             final int[] acceptableTokens = check.getAcceptableTokens();
1112             Arrays.sort(acceptableTokens);
1113             final int[] defaultTokens = check.getDefaultTokens();
1114             Arrays.sort(defaultTokens);
1115             final int[] requiredTokens = check.getRequiredTokens();
1116             Arrays.sort(requiredTokens);
1117 
1118             if (!Arrays.equals(acceptableTokens, defaultTokens)
1119                     || !Arrays.equals(acceptableTokens, requiredTokens)) {
1120                 properties.add("tokens");
1121             }
1122         }
1123 
1124         if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
1125             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
1126 
1127             final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
1128             Arrays.sort(acceptableJavadocTokens);
1129             final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
1130             Arrays.sort(defaultJavadocTokens);
1131             final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
1132             Arrays.sort(requiredJavadocTokens);
1133 
1134             if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
1135                     || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
1136                 properties.add("javadocTokens");
1137             }
1138         }
1139     }
1140 
1141     private static void validatePropertySectionProperties(String fileName, String sectionName,
1142             Node table, Object instance, Set<String> properties) throws Exception {
1143         boolean skip = true;
1144         boolean didJavadocTokens = false;
1145         boolean didTokens = false;
1146 
1147         for (Node row : XmlUtil.getChildrenElements(table)) {
1148             final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
1149 
1150             assertWithMessage(fileName + " section '" + sectionName
1151                     + "' should have the requested columns")
1152                 .that(columns)
1153                 .hasSize(5);
1154 
1155             if (skip) {
1156                 assertWithMessage(fileName + " section '" + sectionName
1157                                 + "' should have the specific title")
1158                     .that(columns.get(0).getTextContent())
1159                     .isEqualTo("name");
1160                 assertWithMessage(fileName + " section '" + sectionName
1161                                 + "' should have the specific title")
1162                     .that(columns.get(1).getTextContent())
1163                     .isEqualTo("description");
1164                 assertWithMessage(fileName + " section '" + sectionName
1165                                 + "' should have the specific title")
1166                     .that(columns.get(2).getTextContent())
1167                     .isEqualTo("type");
1168                 assertWithMessage(fileName + " section '" + sectionName
1169                                 + "' should have the specific title")
1170                     .that(columns.get(3).getTextContent())
1171                     .isEqualTo("default value");
1172                 assertWithMessage(fileName + " section '" + sectionName
1173                                 + "' should have the specific title")
1174                     .that(columns.get(4).getTextContent())
1175                     .isEqualTo("since");
1176 
1177                 skip = false;
1178                 continue;
1179             }
1180 
1181             assertWithMessage(fileName + " section '" + sectionName
1182                         + "' should have token properties last")
1183                     .that(didTokens)
1184                     .isFalse();
1185 
1186             final String propertyName = columns.get(0).getTextContent();
1187             assertWithMessage(fileName + " section '" + sectionName
1188                         + "' should not contain the property: " + propertyName)
1189                     .that(properties.remove(propertyName))
1190                     .isTrue();
1191 
1192             if ("tokens".equals(propertyName)) {
1193                 final AbstractCheck check = (AbstractCheck) instance;
1194                 validatePropertySectionPropertyTokens(fileName, sectionName, check, columns);
1195                 didTokens = true;
1196             }
1197             else if ("javadocTokens".equals(propertyName)) {
1198                 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
1199                 validatePropertySectionPropertyJavadocTokens(fileName, sectionName, check, columns);
1200                 didJavadocTokens = true;
1201             }
1202             else {
1203                 assertWithMessage(fileName + " section '" + sectionName
1204                         + "' should have javadoc token properties next to last, before tokens")
1205                                 .that(didJavadocTokens)
1206                                 .isFalse();
1207 
1208                 validatePropertySectionPropertyEx(fileName, sectionName, instance, columns,
1209                         propertyName);
1210             }
1211 
1212             assertWithMessage("%s section '%s' should have a version for %s",
1213                             fileName, sectionName, propertyName)
1214                     .that(columns.get(4).getTextContent().trim())
1215                     .isNotEmpty();
1216             assertWithMessage("%s section '%s' should have a valid version for %s",
1217                             fileName, sectionName, propertyName)
1218                     .that(columns.get(4).getTextContent().trim())
1219                     .matches(VERSION);
1220         }
1221     }
1222 
1223     private static void validatePropertySectionPropertyEx(String fileName, String sectionName,
1224             Object instance, List<Node> columns, String propertyName) throws Exception {
1225         assertWithMessage("%s section '%s' should have a description for %s",
1226                         fileName, sectionName, propertyName)
1227                 .that(columns.get(1).getTextContent().trim())
1228                 .isNotEmpty();
1229         assertWithMessage("%s section '%s' should have a description for %s"
1230                         + " that starts with uppercase character",
1231                         fileName, sectionName, propertyName)
1232                 .that(Character.isUpperCase(columns.get(1).getTextContent().trim().charAt(0)))
1233                 .isTrue();
1234 
1235         final String actualTypeName = columns.get(2).getTextContent().replace("\n", "")
1236                 .replace("\r", "").replaceAll(" +", " ").trim();
1237 
1238         assertWithMessage(
1239                 fileName + " section '" + sectionName + "' should have a type for " + propertyName)
1240                         .that(actualTypeName)
1241                         .isNotEmpty();
1242 
1243         final Field field = getField(instance.getClass(), propertyName);
1244         final Class<?> fieldClass = getFieldClass(fileName, sectionName, instance, field,
1245                 propertyName);
1246 
1247         final String expectedTypeName = Optional.ofNullable(field)
1248                 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
1249                 .map(propertyType -> propertyType.value().getDescription())
1250                 .map(SiteUtil::simplifyTypeName)
1251                 .orElseGet(fieldClass::getSimpleName);
1252         final String expectedValue = getModulePropertyExpectedValue(sectionName, propertyName,
1253                 field, fieldClass, instance);
1254 
1255         assertWithMessage(fileName + " section '" + sectionName
1256                         + "' should have the type for " + propertyName)
1257             .that(actualTypeName)
1258             .isEqualTo(expectedTypeName);
1259 
1260         if (expectedValue != null) {
1261             final String actualValue = columns.get(3).getTextContent().trim()
1262                     .replaceAll("\\s+", " ")
1263                     .replaceAll("\\s,", ",");
1264 
1265             assertWithMessage(fileName + " section '" + sectionName
1266                             + "' should have the value for " + propertyName)
1267                 .that(actualValue)
1268                 .isEqualTo(expectedValue);
1269         }
1270     }
1271 
1272     private static void validatePropertySectionPropertyTokens(String fileName, String sectionName,
1273             AbstractCheck check, List<Node> columns) {
1274         assertWithMessage(fileName + " section '" + sectionName
1275                         + "' should have the basic token description")
1276             .that(columns.get(1).getTextContent())
1277             .isEqualTo("tokens to check");
1278 
1279         final String acceptableTokenText = columns.get(2).getTextContent().trim();
1280         String expectedAcceptableTokenText = "subset of tokens "
1281                 + CheckUtil.getTokenText(check.getAcceptableTokens(),
1282                 check.getRequiredTokens());
1283         if (isAllTokensAcceptable(check)) {
1284             expectedAcceptableTokenText = "set of any supported tokens";
1285         }
1286         assertWithMessage(fileName + " section '" + sectionName
1287                         + "' should have all the acceptable tokens")
1288             .that(acceptableTokenText
1289                         .replaceAll("\\s+", " ")
1290                         .replaceAll("\\s,", ",")
1291                         .replaceAll("\\s\\.", "."))
1292             .isEqualTo(expectedAcceptableTokenText);
1293         assertWithMessage(fileName + "'s acceptable token section: " + sectionName
1294                 + "should have ',' & '.' at beginning of the next corresponding lines.")
1295                         .that(isInvalidTokenPunctuation(acceptableTokenText))
1296                         .isFalse();
1297 
1298         final String defaultTokenText = columns.get(3).getTextContent().trim();
1299         final String expectedDefaultTokenText = CheckUtil.getTokenText(check.getDefaultTokens(),
1300                 check.getRequiredTokens());
1301         if (expectedDefaultTokenText.isEmpty()) {
1302             assertWithMessage("Empty tokens should have 'empty' string in xdoc")
1303                 .that(defaultTokenText)
1304                 .isEqualTo("empty");
1305         }
1306         else {
1307             assertWithMessage(fileName + " section '" + sectionName
1308                     + "' should have all the default tokens")
1309                 .that(defaultTokenText
1310                             .replaceAll("\\s+", " ")
1311                             .replaceAll("\\s,", ",")
1312                             .replaceAll("\\s\\.", "."))
1313                 .isEqualTo(expectedDefaultTokenText);
1314             assertWithMessage(fileName + "'s default token section: " + sectionName
1315                     + "should have ',' or '.' at beginning of the next corresponding lines.")
1316                             .that(isInvalidTokenPunctuation(defaultTokenText))
1317                             .isFalse();
1318         }
1319 
1320     }
1321 
1322     private static boolean isAllTokensAcceptable(AbstractCheck check) {
1323         return Arrays.equals(check.getAcceptableTokens(), TokenUtil.getAllTokenIds());
1324     }
1325 
1326     private static void validatePropertySectionPropertyJavadocTokens(String fileName,
1327             String sectionName, AbstractJavadocCheck check, List<Node> columns) {
1328         assertWithMessage(fileName + " section '" + sectionName
1329                         + "' should have the basic token javadoc description")
1330             .that(columns.get(1).getTextContent())
1331             .isEqualTo("javadoc tokens to check");
1332 
1333         final String acceptableTokenText = columns.get(2).getTextContent().trim();
1334         assertWithMessage(fileName + " section '" + sectionName
1335                         + "' should have all the acceptable javadoc tokens")
1336             .that(acceptableTokenText
1337                         .replaceAll("\\s+", " ")
1338                         .replaceAll("\\s,", ",")
1339                         .replaceAll("\\s\\.", "."))
1340             .isEqualTo("subset of javadoc tokens "
1341                         + CheckUtil.getJavadocTokenText(check.getAcceptableJavadocTokens(),
1342                 check.getRequiredJavadocTokens()));
1343         assertWithMessage(fileName + "'s acceptable javadoc token section: " + sectionName
1344                 + "should have ',' & '.' at beginning of the next corresponding lines.")
1345                         .that(isInvalidTokenPunctuation(acceptableTokenText))
1346                         .isFalse();
1347 
1348         final String defaultTokenText = columns.get(3).getTextContent().trim();
1349         assertWithMessage(fileName + " section '" + sectionName
1350                         + "' should have all the default javadoc tokens")
1351             .that(defaultTokenText
1352                         .replaceAll("\\s+", " ")
1353                         .replaceAll("\\s,", ",")
1354                         .replaceAll("\\s\\.", "."))
1355             .isEqualTo(CheckUtil.getJavadocTokenText(check.getDefaultJavadocTokens(),
1356                 check.getRequiredJavadocTokens()));
1357         assertWithMessage(fileName + "'s default javadoc token section: " + sectionName
1358                 + "should have ',' & '.' at beginning of the next corresponding lines.")
1359                         .that(isInvalidTokenPunctuation(defaultTokenText))
1360                         .isFalse();
1361     }
1362 
1363     private static boolean isInvalidTokenPunctuation(String tokenText) {
1364         return Pattern.compile("\\w,").matcher(tokenText).find()
1365                 || Pattern.compile("\\w\\.").matcher(tokenText).find();
1366     }
1367 
1368     /**
1369      * Gets the name of the bean property's default value for the class.
1370      *
1371      * @param sectionName The name of the section/module being worked on
1372      * @param propertyName The property name to work with
1373      * @param field The bean property's field
1374      * @param fieldClass The bean property's type
1375      * @param instance The class instance to work with
1376      * @return String form of property's default value
1377      * @noinspection IfStatementWithTooManyBranches
1378      * @noinspectionreason IfStatementWithTooManyBranches - complex nature of getting properties
1379      *      from XML files requires giant if/else statement
1380      */
1381     private static String getModulePropertyExpectedValue(String sectionName, String propertyName,
1382             Field field, Class<?> fieldClass, Object instance) throws Exception {
1383         String result = null;
1384 
1385         if (field != null) {
1386             final Object value = field.get(instance);
1387 
1388             if ("Checker".equals(sectionName) && "localeCountry".equals(propertyName)) {
1389                 result = "default locale country for the Java Virtual Machine";
1390             }
1391             else if ("Checker".equals(sectionName) && "localeLanguage".equals(propertyName)) {
1392                 result = "default locale language for the Java Virtual Machine";
1393             }
1394             else if ("Checker".equals(sectionName) && "charset".equals(propertyName)) {
1395                 result = "UTF-8";
1396             }
1397             else if ("charset".equals(propertyName)) {
1398                 result = "the charset property of the parent"
1399                     + " <a href=\"https://checkstyle.org/config.html#Checker\">Checker</a> module";
1400             }
1401             else if ("PropertyCacheFile".equals(fieldClass.getSimpleName())) {
1402                 result = "null (no cache file)";
1403             }
1404             else if (fieldClass == boolean.class) {
1405                 result = value.toString();
1406             }
1407             else if (fieldClass == int.class) {
1408                 result = value.toString();
1409             }
1410             else if (fieldClass == int[].class) {
1411                 result = getIntArrayPropertyValue(value);
1412             }
1413             else if (fieldClass == double[].class) {
1414                 result = Arrays.toString((double[]) value).replace("[", "").replace("]", "")
1415                         .replace(".0", "");
1416                 if (result.isEmpty()) {
1417                     result = "{}";
1418                 }
1419             }
1420             else if (fieldClass == String[].class) {
1421                 final boolean preserveOrder = hasPreserveOrderAnnotation(field);
1422                 result = getStringArrayPropertyValue(propertyName, value, preserveOrder);
1423             }
1424             else if (fieldClass == URI.class || fieldClass == String.class) {
1425                 if (value != null) {
1426                     result = value.toString();
1427                 }
1428             }
1429             else if (fieldClass == Pattern.class) {
1430                 if (value != null) {
1431                     result = value.toString().replace("\n", "\\n").replace("\t", "\\t")
1432                             .replace("\r", "\\r").replace("\f", "\\f");
1433                 }
1434             }
1435             else if (fieldClass == Pattern[].class) {
1436                 result = getPatternArrayPropertyValue(value);
1437             }
1438             else if (fieldClass.isEnum()) {
1439                 if (value != null) {
1440                     result = value.toString().toLowerCase(Locale.ENGLISH);
1441                 }
1442             }
1443             else if (fieldClass == AccessModifierOption[].class) {
1444                 result = Arrays.toString((Object[]) value).replace("[", "").replace("]", "");
1445             }
1446             else {
1447                 assertWithMessage("Unknown property type: " + fieldClass.getSimpleName()).fail();
1448             }
1449 
1450             if (result == null) {
1451                 result = "null";
1452             }
1453         }
1454 
1455         return result;
1456     }
1457 
1458     private static boolean hasPreserveOrderAnnotation(Field field) {
1459         return field != null && field.isAnnotationPresent(PreserveOrder.class);
1460     }
1461 
1462     /**
1463      * Gets the name of the bean property's default value for the Pattern array class.
1464      *
1465      * @param fieldValue The bean property's value
1466      * @return String form of property's default value
1467      */
1468     private static String getPatternArrayPropertyValue(Object fieldValue) {
1469         Object value = fieldValue;
1470         String result;
1471         if (value instanceof Collection<?> collection) {
1472             final Pattern[] newArray = new Pattern[collection.size()];
1473             final Iterator<?> iterator = collection.iterator();
1474             int index = 0;
1475 
1476             while (iterator.hasNext()) {
1477                 final Object next = iterator.next();
1478                 newArray[index] = (Pattern) next;
1479                 index++;
1480             }
1481 
1482             value = newArray;
1483         }
1484 
1485         if (value != null && Array.getLength(value) > 0) {
1486             final String[] newArray = new String[Array.getLength(value)];
1487 
1488             for (int i = 0; i < newArray.length; i++) {
1489                 newArray[i] = ((Pattern) Array.get(value, i)).pattern();
1490             }
1491 
1492             result = Arrays.toString(newArray).replace("[", "").replace("]", "");
1493         }
1494         else {
1495             result = "";
1496         }
1497 
1498         if (result.isEmpty()) {
1499             result = "{}";
1500         }
1501         return result;
1502     }
1503 
1504     /**
1505      * Gets the name of the bean property's default value for the string array class.
1506      *
1507      * @param propertyName The bean property's name
1508      * @param value The bean property's value
1509      * @param preserveOrder whether to preserve the original order
1510      * @return String form of property's default value
1511      */
1512     private static String getStringArrayPropertyValue(String propertyName, Object value,
1513             boolean preserveOrder) {
1514         String result;
1515         if (value == null) {
1516             result = "";
1517         }
1518         else {
1519             final Stream<?> valuesStream;
1520             if (value instanceof Collection<?> collection) {
1521                 valuesStream = collection.stream();
1522             }
1523             else {
1524                 final Object[] array = (Object[]) value;
1525                 valuesStream = Arrays.stream(array);
1526             }
1527 
1528             Stream<String> stringStream = valuesStream.map(String.class::cast);
1529 
1530             if (!preserveOrder) {
1531                 stringStream = stringStream.sorted();
1532             }
1533 
1534             result = stringStream.collect(Collectors.joining(", "));
1535 
1536         }
1537 
1538         if (result.isEmpty()) {
1539             if ("fileExtensions".equals(propertyName)) {
1540                 result = "all files";
1541             }
1542             else {
1543                 result = "{}";
1544             }
1545         }
1546         return result;
1547     }
1548 
1549     /**
1550      * Returns the name of the bean property's default value for the int array class.
1551      *
1552      * @param value The bean property's value.
1553      * @return String form of property's default value.
1554      */
1555     private static String getIntArrayPropertyValue(Object value) {
1556         final IntStream stream;
1557         if (value instanceof Collection<?> collection) {
1558             stream = collection.stream()
1559                     .mapToInt(number -> (int) number);
1560         }
1561         else if (value instanceof BitSet set) {
1562             stream = set.stream();
1563         }
1564         else {
1565             stream = Arrays.stream((int[]) value);
1566         }
1567         String result = stream
1568                 .mapToObj(TokenUtil::getTokenName)
1569                 .sorted()
1570                 .collect(Collectors.joining(", "));
1571         if (result.isEmpty()) {
1572             result = "{}";
1573         }
1574         return result;
1575     }
1576 
1577     /**
1578      * Returns the bean property's field.
1579      *
1580      * @param fieldClass The bean property's type
1581      * @param propertyName The bean property's name
1582      * @return the bean property's field
1583      */
1584     private static Field getField(Class<?> fieldClass, String propertyName) {
1585         Field result = null;
1586         Class<?> currentClass = fieldClass;
1587 
1588         while (!Object.class.equals(currentClass)) {
1589             try {
1590                 result = currentClass.getDeclaredField(propertyName);
1591                 result.trySetAccessible();
1592                 break;
1593             }
1594             catch (NoSuchFieldException ignored) {
1595                 currentClass = currentClass.getSuperclass();
1596             }
1597         }
1598 
1599         return result;
1600     }
1601 
1602     private static Class<?> getFieldClass(String fileName, String sectionName, Object instance,
1603             Field field, String propertyName) throws Exception {
1604         Class<?> result = null;
1605 
1606         if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD.contains(sectionName + "." + propertyName)) {
1607             final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1608                     propertyName);
1609             result = descriptor.getPropertyType();
1610         }
1611         if (field != null && result == null) {
1612             result = field.getType();
1613         }
1614         if (result == null) {
1615             assertWithMessage(
1616                     fileName + " section '" + sectionName + "' could not find field "
1617                             + propertyName)
1618                     .fail();
1619         }
1620         if (field != null && (result == List.class || result == Set.class)) {
1621             final ParameterizedType type = (ParameterizedType) field.getGenericType();
1622             final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1623 
1624             if (parameterClass == Integer.class) {
1625                 result = int[].class;
1626             }
1627             else if (parameterClass == String.class) {
1628                 result = String[].class;
1629             }
1630             else if (parameterClass == Pattern.class) {
1631                 result = Pattern[].class;
1632             }
1633             else {
1634                 assertWithMessage("Unknown parameterized type: " + parameterClass.getSimpleName())
1635                         .fail();
1636             }
1637         }
1638         else if (result == BitSet.class) {
1639             result = int[].class;
1640         }
1641 
1642         return result;
1643     }
1644 
1645     private static Set<String> getListById(Node subSection, String id) {
1646         Set<String> result = null;
1647         final Node node = XmlUtil.findChildElementById(subSection, id);
1648         if (node != null) {
1649             result = XmlUtil.getChildrenElements(node)
1650                     .stream()
1651                     .map(Node::getTextContent)
1652                     .collect(Collectors.toUnmodifiableSet());
1653         }
1654         return result;
1655     }
1656 
1657     private static void validateViolationSection(String fileName, String sectionName,
1658                                                  Node subSection,
1659                                                  Object instance) throws Exception {
1660         final Class<?> clss = instance.getClass();
1661         final Set<Field> fields = CheckUtil.getCheckMessages(clss, true);
1662         final Set<String> list = new TreeSet<>();
1663 
1664         for (Field field : fields) {
1665             // below is required for package/private classes
1666             field.trySetAccessible();
1667 
1668             list.add(field.get(null).toString());
1669         }
1670 
1671         final StringBuilder expectedText = new StringBuilder(120);
1672 
1673         for (String s : list) {
1674             expectedText.append(s);
1675             expectedText.append('\n');
1676         }
1677 
1678         if (!expectedText.isEmpty()) {
1679             expectedText.append(
1680                 """
1681                 All messages can be customized if the default message doesn't suit you.
1682                 Please see the documentation to learn how to.
1683                 """);
1684         }
1685 
1686         if (subSection == null) {
1687             assertWithMessage(fileName + " section '" + sectionName
1688                     + "' should have the expected error keys")
1689                 .that(expectedText.toString())
1690                 .isEqualTo("");
1691         }
1692         else {
1693             final String subsectionTextContent = subSection.getTextContent()
1694                     .replaceAll("\n\\s+", "\n")
1695                     .replaceAll("\\s+", " ")
1696                     .trim();
1697             assertWithMessage(fileName + " section '" + sectionName
1698                             + "' should have the expected error keys")
1699                 .that(subsectionTextContent)
1700                 .isEqualTo(expectedText.toString().replaceAll("\n", " ").trim());
1701 
1702             for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1703                 final String url = node.getAttributes().getNamedItem("href").getTextContent();
1704                 final String linkText = node.getTextContent().trim();
1705                 final String expectedUrl;
1706 
1707                 if ("see the documentation".equals(linkText)) {
1708                     expectedUrl = "../../config.html#Custom_messages";
1709                 }
1710                 else {
1711                     expectedUrl = "https://github.com/search?q="
1712                             + "path%3Asrc%2Fmain%2Fresources%2F"
1713                             + clss.getPackage().getName().replace(".", "%2F")
1714                             + "%20path%3A**%2Fmessages*.properties+repo%3Acheckstyle%2F"
1715                             + "checkstyle+%22" + linkText + "%22";
1716                 }
1717 
1718                 assertWithMessage(fileName + " section '" + sectionName
1719                         + "' should have matching url for '" + linkText + "'")
1720                     .that(url)
1721                     .isEqualTo(expectedUrl);
1722             }
1723         }
1724     }
1725 
1726     private static void validateUsageExample(String fileName, String sectionName, Node subSection) {
1727         final String text = subSection.getTextContent()
1728             .replace("Checkstyle Style", "")
1729             .replace("Google Style", "")
1730             .replace("Sun Style", "")
1731             .replace("Checkstyle's Import Control Config", "")
1732             .trim();
1733 
1734         assertWithMessage(fileName + " section '" + sectionName
1735                 + "' has unknown text in 'Example of Usage': " + text)
1736             .that(text)
1737             .isEmpty();
1738 
1739         boolean hasCheckstyle = false;
1740         boolean hasGoogle = false;
1741         boolean hasSun = false;
1742 
1743         for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1744             final String url = node.getAttributes().getNamedItem("href").getTextContent();
1745             final String linkText = node.getTextContent().trim();
1746             String expectedUrl = null;
1747 
1748             if ("Checkstyle Style".equals(linkText)) {
1749                 hasCheckstyle = true;
1750                 expectedUrl = "https://github.com/search?q="
1751                         + "path%3Aconfig%20path%3A**%2Fcheckstyle-checks.xml+"
1752                         + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName;
1753             }
1754             else if ("Google Style".equals(linkText)) {
1755                 hasGoogle = true;
1756                 expectedUrl = "https://github.com/search?q="
1757                         + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2Fgoogle_checks.xml+"
1758                         + "repo%3Acheckstyle%2Fcheckstyle+"
1759                         + sectionName;
1760 
1761                 assertWithMessage(fileName + " section '" + sectionName
1762                             + "' should be in google_checks.xml or not reference 'Google Style'")
1763                         .that(GOOGLE_MODULES)
1764                         .contains(sectionName);
1765             }
1766             else if ("Sun Style".equals(linkText)) {
1767                 hasSun = true;
1768                 expectedUrl = "https://github.com/search?q="
1769                         + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2Fsun_checks.xml+"
1770                         + "repo%3Acheckstyle%2Fcheckstyle+"
1771                         + sectionName;
1772 
1773                 assertWithMessage(fileName + " section '" + sectionName
1774                             + "' should be in sun_checks.xml or not reference 'Sun Style'")
1775                         .that(SUN_MODULES)
1776                         .contains(sectionName);
1777             }
1778             else if ("Checkstyle's Import Control Config".equals(linkText)) {
1779                 expectedUrl = "https://github.com/checkstyle/checkstyle/blob/master/config/"
1780                     + "import-control.xml";
1781             }
1782 
1783             assertWithMessage(fileName + " section '" + sectionName
1784                     + "' should have matching url")
1785                 .that(url)
1786                 .isEqualTo(expectedUrl);
1787         }
1788 
1789         assertWithMessage(fileName + " section '" + sectionName
1790                     + "' should have a checkstyle section")
1791                 .that(hasCheckstyle)
1792                 .isTrue();
1793         assertWithMessage(fileName + " section '" + sectionName
1794                     + "' should have a google section since it is in it's config")
1795                 .that(hasGoogle || !GOOGLE_MODULES.contains(sectionName))
1796                 .isTrue();
1797         assertWithMessage(fileName + " section '" + sectionName
1798                     + "' should have a sun section since it is in it's config")
1799                 .that(hasSun || !SUN_MODULES.contains(sectionName))
1800                 .isTrue();
1801     }
1802 
1803     private static void validatePackageSection(String fileName, String sectionName,
1804             Node subSection, Object instance) {
1805         assertWithMessage(fileName + " section '" + sectionName
1806                         + "' should have matching package")
1807             .that(subSection.getTextContent().trim())
1808             .isEqualTo(instance.getClass().getPackage().getName());
1809     }
1810 
1811     private static void validateParentSection(String fileName, String sectionName,
1812             Node subSection) {
1813         final String expected;
1814 
1815         if (!"TreeWalker".equals(sectionName) && hasParentModule(sectionName)) {
1816             expected = "TreeWalker";
1817         }
1818         else {
1819             expected = "Checker";
1820         }
1821 
1822         assertWithMessage(fileName + " section '" + sectionName + "' should have matching parent")
1823             .that(subSection.getTextContent().trim())
1824             .isEqualTo(expected);
1825     }
1826 
1827     private static boolean hasParentModule(String sectionName) {
1828         final String search = "\"" + sectionName + "\"";
1829         boolean result = true;
1830 
1831         for (String find : XML_FILESET_LIST) {
1832             if (find.contains(search)) {
1833                 result = false;
1834                 break;
1835             }
1836         }
1837 
1838         return result;
1839     }
1840 
1841     private static Set<String> getProperties(Class<?> clss) {
1842         final Set<String> result = new TreeSet<>();
1843         final PropertyDescriptor[] map = PropertyUtils.getPropertyDescriptors(clss);
1844 
1845         for (PropertyDescriptor p : map) {
1846             if (p.getWriteMethod() != null) {
1847                 result.add(p.getName());
1848             }
1849         }
1850 
1851         return result;
1852     }
1853 
1854     @Test
1855     public void testAllStyleRules() throws Exception {
1856         for (Path path : XdocUtil.getXdocsStyleFilePaths(XdocUtil.getXdocsFilePaths())) {
1857             final String fileName = path.getFileName().toString();
1858             final String styleName = fileName.substring(0, fileName.lastIndexOf('_'));
1859             final NodeList sources = getTagSourcesNode(path, "tr");
1860 
1861             final Set<String> styleChecks = switch (styleName) {
1862                 case "google" -> new HashSet<>(GOOGLE_MODULES);
1863                 case "sun" -> {
1864                     final Set<String> checks = new HashSet<>(SUN_MODULES);
1865                     checks.removeAll(IGNORED_SUN_MODULES);
1866                     yield checks;
1867                 }
1868                 default -> {
1869                     assertWithMessage("Missing modules list for style file '" + fileName + "'")
1870                             .fail();
1871                     yield null;
1872                 }
1873             };
1874 
1875             String lastRuleName = null;
1876             String[] lastRuleNumberParts = null;
1877 
1878             for (int position = 0; position < sources.getLength(); position++) {
1879                 final Node row = sources.item(position);
1880                 final List<Node> columns = new ArrayList<>(
1881                         XmlUtil.findChildElementsByTag(row, "td"));
1882 
1883                 if (columns.isEmpty()) {
1884                     continue;
1885                 }
1886 
1887                 final String ruleName = columns.get(1).getTextContent().trim();
1888                 lastRuleNumberParts = validateRuleNameOrder(
1889                         fileName, lastRuleName, lastRuleNumberParts, ruleName);
1890 
1891                 if (!"--".equals(ruleName)) {
1892                     validateStyleAnchors(XmlUtil.findChildElementsByTag(columns.get(0), "a"),
1893                             fileName, ruleName);
1894                 }
1895 
1896                 validateStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"),
1897                         XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, styleName,
1898                         ruleName);
1899 
1900                 lastRuleName = ruleName;
1901             }
1902 
1903             // these modules aren't documented, but are added to the config
1904             styleChecks.remove("BeforeExecutionExclusionFileFilter");
1905             styleChecks.remove("SuppressionFilter");
1906             styleChecks.remove("SuppressionXpathFilter");
1907             styleChecks.remove("SuppressionXpathSingleFilter");
1908             styleChecks.remove("TreeWalker");
1909             styleChecks.remove("Checker");
1910             styleChecks.remove("SuppressWithNearbyCommentFilter");
1911             styleChecks.remove("SuppressionCommentFilter");
1912             styleChecks.remove("SuppressWarningsFilter");
1913             styleChecks.remove("SuppressWarningsHolder");
1914             styleChecks.remove("SuppressWithNearbyTextFilter");
1915             styleChecks.remove("SuppressWithPlainTextCommentFilter");
1916             assertWithMessage(
1917                     fileName + " requires the following check(s) to appear: " + styleChecks)
1918                 .that(styleChecks)
1919                 .isEmpty();
1920         }
1921     }
1922 
1923     private static String[] validateRuleNameOrder(String fileName, String lastRuleName,
1924                                                   String[] lastRuleNumberParts, String ruleName) {
1925         final String[] ruleNumberParts = ruleName.split(" ", 2)[0].split("\\.");
1926 
1927         if (lastRuleName != null) {
1928             final int ruleNumberPartsAmount = ruleNumberParts.length;
1929             final int lastRuleNumberPartsAmount = lastRuleNumberParts.length;
1930             final String outOfOrderReason = fileName + " rule '" + ruleName
1931                     + "' is out of order compared to '" + lastRuleName + "'";
1932             boolean lastRuleNumberPartWasEqual = false;
1933             int partIndex;
1934             for (partIndex = 0; partIndex < ruleNumberPartsAmount; partIndex++) {
1935                 if (lastRuleNumberPartsAmount <= partIndex) {
1936                     // equal up to here and last rule has fewer parts,
1937                     // thus order is correct, stop comparing
1938                     break;
1939                 }
1940 
1941                 final String ruleNumberPart = ruleNumberParts[partIndex];
1942                 final String lastRuleNumberPart = lastRuleNumberParts[partIndex];
1943                 final boolean ruleNumberPartsAreNumeric = IntStream.concat(
1944                         ruleNumberPart.chars(),
1945                         lastRuleNumberPart.chars()
1946                 ).allMatch(Character::isDigit);
1947 
1948                 if (ruleNumberPartsAreNumeric) {
1949                     final int numericRuleNumberPart = parseInt(ruleNumberPart);
1950                     final int numericLastRuleNumberPart = parseInt(lastRuleNumberPart);
1951                     assertWithMessage(outOfOrderReason)
1952                         .that(numericRuleNumberPart)
1953                         .isAtLeast(numericLastRuleNumberPart);
1954                 }
1955                 else {
1956                     assertWithMessage(outOfOrderReason)
1957                         .that(ruleNumberPart.compareToIgnoreCase(lastRuleNumberPart))
1958                         .isAtLeast(0);
1959                 }
1960                 lastRuleNumberPartWasEqual = ruleNumberPart.equalsIgnoreCase(lastRuleNumberPart);
1961                 if (!lastRuleNumberPartWasEqual) {
1962                     // number part is not equal but properly ordered,
1963                     // thus order is correct, stop comparing
1964                     break;
1965                 }
1966             }
1967             if (ruleNumberPartsAmount == partIndex && lastRuleNumberPartWasEqual) {
1968                 if (lastRuleNumberPartsAmount == partIndex) {
1969                     assertWithMessage(fileName + " rule '" + ruleName + "' and rule '"
1970                             + lastRuleName + "' have the same rule number").fail();
1971                 }
1972                 else {
1973                     assertWithMessage(outOfOrderReason).fail();
1974                 }
1975             }
1976         }
1977 
1978         return ruleNumberParts;
1979     }
1980 
1981     private static void validateStyleAnchors(Set<Node> anchors, String fileName, String ruleName) {
1982         assertWithMessage(fileName + " rule '" + ruleName + "' must have two row anchors")
1983             .that(anchors)
1984             .hasSize(2);
1985 
1986         final int space = ruleName.indexOf(' ');
1987         assertWithMessage(fileName + " rule '" + ruleName
1988                 + "' must have have a space between the rule's number and the rule's name")
1989             .that(space)
1990             .isNotEqualTo(-1);
1991 
1992         final String ruleNumber = ruleName.substring(0, space);
1993 
1994         int position = 1;
1995 
1996         for (Node anchor : anchors) {
1997             final String actualUrl;
1998             final String expectedUrl;
1999 
2000             if (position == 1) {
2001                 actualUrl = XmlUtil.getNameAttributeOfNode(anchor);
2002                 expectedUrl = "a" + ruleNumber;
2003             }
2004             else {
2005                 actualUrl = anchor.getAttributes().getNamedItem("href").getTextContent();
2006                 expectedUrl = "#" + ruleNumber;
2007             }
2008 
2009             assertWithMessage(fileName + " rule '" + ruleName + "' anchor "
2010                     + position + " should have matching name/url")
2011                 .that(actualUrl)
2012                 .isEqualTo(expectedUrl);
2013 
2014             position++;
2015         }
2016     }
2017 
2018     private static void validateStyleModules(Set<Node> checks, Set<Node> configs,
2019             Set<String> styleChecks, String styleName, String ruleName) {
2020         final Iterator<Node> itrChecks = checks.iterator();
2021         final Iterator<Node> itrConfigs = configs.iterator();
2022         final boolean isGoogleDocumentation = "google".equals(styleName);
2023 
2024         if (isGoogleDocumentation) {
2025             validateChapterWiseTesting(itrChecks, itrConfigs, styleChecks, styleName, ruleName);
2026         }
2027         else {
2028             validateModuleWiseTesting(itrChecks, itrConfigs, styleChecks, styleName, ruleName);
2029         }
2030 
2031         assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' has too many configs")
2032                 .that(itrConfigs.hasNext())
2033                 .isFalse();
2034     }
2035 
2036     private static void validateModuleWiseTesting(Iterator<Node> itrChecks,
2037           Iterator<Node> itrConfigs, Set<String> styleChecks, String styleName, String ruleName) {
2038         while (itrChecks.hasNext()) {
2039             final Node module = itrChecks.next();
2040             final String moduleName = module.getTextContent().trim();
2041             final String href = module.getAttributes().getNamedItem("href").getTextContent();
2042             final boolean moduleIsCheck = href.startsWith("checks/");
2043 
2044             if (!moduleIsCheck) {
2045                 continue;
2046             }
2047 
2048             assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '" + moduleName
2049                         + "' shouldn't end with 'Check'")
2050                     .that(moduleName.endsWith("Check"))
2051                     .isFalse();
2052 
2053             styleChecks.remove(moduleName);
2054 
2055             for (String configName : new String[] {"config", "test"}) {
2056                 Node config = null;
2057 
2058                 try {
2059                     config = itrConfigs.next();
2060                 }
2061                 catch (NoSuchElementException ignore) {
2062                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2063                             + moduleName + "' is missing the config link: " + configName).fail();
2064                 }
2065 
2066                 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2067                                 + moduleName + "' has mismatched config/test links")
2068                     .that(config.getTextContent().trim())
2069                     .isEqualTo(configName);
2070 
2071                 final String configUrl = config.getAttributes().getNamedItem("href")
2072                         .getTextContent();
2073 
2074                 if ("config".equals(configName)) {
2075                     final String expectedUrl = "https://github.com/search?q="
2076                             + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2F" + styleName
2077                             + "_checks.xml+repo%3Acheckstyle%2Fcheckstyle+" + moduleName;
2078 
2079                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2080                                     + moduleName + "' should have matching " + configName + " url")
2081                         .that(configUrl)
2082                         .isEqualTo(expectedUrl);
2083                 }
2084                 else if ("test".equals(configName)) {
2085                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2086                                 + moduleName + "' should have matching " + configName + " url")
2087                             .that(configUrl)
2088                             .startsWith("https://github.com/checkstyle/checkstyle/"
2089                                     + "blob/master/src/it/java/com/" + styleName
2090                                     + "/checkstyle/test/");
2091                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2092                                 + moduleName + "' should have matching " + configName + " url")
2093                             .that(configUrl)
2094                             .endsWith("/" + moduleName + "Test.java");
2095 
2096                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2097                                 + moduleName + "' should have a test that exists")
2098                             .that(new File(configUrl.substring(53).replace('/',
2099                                             File.separatorChar)).exists())
2100                             .isTrue();
2101                 }
2102             }
2103         }
2104     }
2105 
2106     private static void validateChapterWiseTesting(Iterator<Node> itrChecks,
2107           Iterator<Node> itrSample, Set<String> styleChecks, String styleName, String ruleName) {
2108         boolean hasChecks = false;
2109         final Set<String> usedModules = new HashSet<>();
2110 
2111         while (itrChecks.hasNext()) {
2112             final Node module = itrChecks.next();
2113             final String moduleName = module.getTextContent().trim();
2114             final String href = module.getAttributes().getNamedItem("href").getTextContent();
2115             final boolean moduleIsCheck = href.startsWith("checks/");
2116 
2117             final String partialConfigUrl = "https://github.com/search?q="
2118                     + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2F" + styleName;
2119 
2120             if (!moduleIsCheck) {
2121                 if (href.startsWith(partialConfigUrl)) {
2122                     assertWithMessage("google_style.xml rule '" + ruleName + "' module '"
2123                             + moduleName + "' has too many config links").fail();
2124                 }
2125                 continue;
2126             }
2127 
2128             hasChecks = true;
2129 
2130             assertWithMessage("The module '" + moduleName + "' in the rule '" + ruleName
2131                     + "' of the style guide '" + styleName
2132                     + "_style.xml' should not appear more than once in the section.")
2133                     .that(usedModules)
2134                     .doesNotContain(moduleName);
2135 
2136             usedModules.add(moduleName);
2137 
2138             assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2139                     + moduleName + "' shouldn't end with 'Check'")
2140                     .that(moduleName.endsWith("Check"))
2141                     .isFalse();
2142 
2143             styleChecks.remove(moduleName);
2144 
2145             if (itrChecks.hasNext()) {
2146                 final Node config = itrChecks.next();
2147 
2148                 final String configUrl = config.getAttributes()
2149                                        .getNamedItem("href").getTextContent();
2150 
2151                 final String expectedUrl =
2152                     partialConfigUrl + "_checks.xml+repo%3Acheckstyle%2Fcheckstyle+" + moduleName;
2153 
2154                 assertWithMessage(
2155                         "google_style.xml rule '" + ruleName + "' module '" + moduleName
2156                             + "' should have matching config url")
2157                     .that(configUrl)
2158                     .isEqualTo(expectedUrl);
2159             }
2160             else {
2161                 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2162                         + moduleName + "' is missing the config link").fail();
2163             }
2164         }
2165 
2166         if (itrSample.hasNext()) {
2167             assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' should have checks"
2168                     + " if it has sample links")
2169                     .that(hasChecks)
2170                     .isTrue();
2171 
2172             final Node sample = itrSample.next();
2173             final String inputFolderUrl = sample.getAttributes().getNamedItem("href")
2174                     .getTextContent();
2175             final String extractedChapterNumber = getExtractedChapterNumber(ruleName);
2176             final String extractedSectionNumber = getExtractedSectionNumber(ruleName);
2177 
2178             assertWithMessage("google_style.xml rule '" + ruleName + "' rule '"
2179                     + "' should have matching sample url")
2180                     .that(inputFolderUrl)
2181                     .startsWith("https://github.com/checkstyle/checkstyle/"
2182                             + "tree/master/src/it/resources/com/google/checkstyle/test/");
2183 
2184             assertWithMessage("google_style.xml rule '" + ruleName
2185                     + "' should have matching sample url")
2186                 .that(inputFolderUrl)
2187                 .containsMatch(
2188                     "/chapter" + extractedChapterNumber
2189                           + "\\D[^/]+/rule" + extractedSectionNumber + "\\D");
2190 
2191             assertWithMessage("google_style.xml rule '" + ruleName
2192                     + "' should have a inputs test folder that exists")
2193                     .that(new File(inputFolderUrl.substring(53).replace('/',
2194                             File.separatorChar)).exists())
2195                     .isTrue();
2196 
2197             assertWithMessage(styleName + "_style.xml rule '" + ruleName
2198                     + "' has too many samples link")
2199                     .that(itrSample.hasNext())
2200                     .isFalse();
2201         }
2202         else {
2203             assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' is missing"
2204                  + " sample link")
2205                 .that(hasChecks)
2206                 .isFalse();
2207         }
2208     }
2209 
2210     private static String getExtractedChapterNumber(String ruleName) {
2211         final Pattern pattern = Pattern.compile("^\\d+");
2212         final Matcher matcher = pattern.matcher(ruleName);
2213         matcher.find();
2214         return matcher.group();
2215     }
2216 
2217     private static String getExtractedSectionNumber(String ruleName) {
2218         final Pattern pattern = Pattern.compile("^\\d+(\\.\\d+)*");
2219         final Matcher matcher = pattern.matcher(ruleName);
2220         matcher.find();
2221         return matcher.group().replaceAll("\\.", "");
2222     }
2223 
2224     @Test
2225     public void testAllExampleMacrosHaveParagraphWithIdBeforeThem() throws Exception {
2226         for (Path path : XdocUtil.getXdocsTemplatesFilePaths()) {
2227             final String fileName = path.getFileName().toString();
2228             final NodeList sources = getTagSourcesNode(path, "macro");
2229 
2230             for (int position = 0; position < sources.getLength(); position++) {
2231                 final Node macro = sources.item(position);
2232                 final String macroName = macro.getAttributes()
2233                         .getNamedItem("name").getTextContent();
2234 
2235                 if (!"example".equals(macroName)) {
2236                     continue;
2237                 }
2238 
2239                 final Node precedingParagraph = getPrecedingParagraph(macro);
2240                 assertWithMessage(fileName
2241                         + ": paragraph before example macro should have an id attribute")
2242                         .that(precedingParagraph.hasAttributes())
2243                         .isTrue();
2244 
2245                 final Node idAttribute = precedingParagraph.getAttributes().getNamedItem("id");
2246                 assertWithMessage(fileName
2247                         + ": paragraph before example macro should have an id attribute")
2248                         .that(idAttribute)
2249                         .isNotNull();
2250 
2251                 validatePrecedingParagraphId(macro, fileName, idAttribute);
2252             }
2253         }
2254     }
2255 
2256     private static void validatePrecedingParagraphId(
2257             Node macro, String fileName, Node idAttribute) {
2258         String exampleName = "";
2259         String exampleType = "";
2260         final NodeList params = macro.getChildNodes();
2261         for (int paramPosition = 0; paramPosition < params.getLength(); paramPosition++) {
2262             final Node item = params.item(paramPosition);
2263 
2264             if (!"param".equals(item.getNodeName())) {
2265                 continue;
2266             }
2267 
2268             final String paramName = item.getAttributes()
2269                     .getNamedItem("name").getTextContent();
2270             final String paramValue = item.getAttributes()
2271                     .getNamedItem("value").getTextContent();
2272             if ("path".equals(paramName)) {
2273                 exampleName = paramValue.substring(paramValue.lastIndexOf('/') + 1,
2274                         paramValue.lastIndexOf('.'));
2275             }
2276             else if ("type".equals(paramName)) {
2277                 exampleType = paramValue;
2278             }
2279         }
2280 
2281         final String id = idAttribute.getTextContent();
2282         final String expectedId = String.format(Locale.ROOT, "%s-%s", exampleName,
2283                 exampleType);
2284         if (expectedId.startsWith("package-info")) {
2285             assertWithMessage(fileName
2286                 + ": paragraph before example macro should have the expected id value")
2287                 .that(id)
2288                 .endsWith(expectedId);
2289         }
2290         else {
2291             assertWithMessage(fileName
2292                 + ": paragraph before example macro should have the expected id value")
2293                 .that(id)
2294                 .isEqualTo(expectedId);
2295         }
2296     }
2297 
2298     private static Node getPrecedingParagraph(Node macro) {
2299         Node precedingNode = macro.getPreviousSibling();
2300         while (!"p".equals(precedingNode.getNodeName())) {
2301             precedingNode = precedingNode.getPreviousSibling();
2302         }
2303         return precedingNode;
2304     }
2305 
2306     @Test
2307     public void validateExampleSectionSeparation() throws Exception {
2308         final List<Path> templates = collectAllXmlTemplatesUnderSrcSite();
2309 
2310         for (final Path template : templates) {
2311             final Document doc = parseXmlToDomDocument(template);
2312             final NodeList subsectionList = doc.getElementsByTagName("subsection");
2313 
2314             for (int index = 0; index < subsectionList.getLength(); index++) {
2315                 final Element subsection = (Element) subsectionList.item(index);
2316                 if (!"Examples".equals(subsection.getAttribute("name"))) {
2317                     continue;
2318                 }
2319 
2320                 final NodeList children = subsection.getChildNodes();
2321                 String lastExampleIdPrefix = null;
2322                 boolean separatorSeen = false;
2323 
2324                 for (int childIndex = 0; childIndex < children.getLength(); childIndex++) {
2325                     final Node child = children.item(childIndex);
2326                     if (child.getNodeType() != Node.ELEMENT_NODE) {
2327                         continue;
2328                     }
2329 
2330                     final Element element = (Element) child;
2331                     if ("hr".equals(element.getTagName())
2332                             && "example-separator".equals(element.getAttribute("class"))) {
2333                         separatorSeen = true;
2334                         continue;
2335                     }
2336 
2337                     final String currentId = element.getAttribute("id");
2338                     if (currentId != null && currentId.startsWith("Example")) {
2339                         final String currentExPrefix = getExamplePrefix(currentId);
2340                         if (lastExampleIdPrefix != null
2341                                 && !lastExampleIdPrefix.equals(currentExPrefix)) {
2342                             assertWithMessage("Missing <hr class=\"example-separator\"/> "
2343                                     + "between " + lastExampleIdPrefix + " and " + currentExPrefix
2344                                     + " in file: " + template)
2345                                     .that(separatorSeen)
2346                                     .isTrue();
2347                             separatorSeen = false;
2348                         }
2349                         lastExampleIdPrefix = currentExPrefix;
2350                     }
2351                 }
2352             }
2353         }
2354     }
2355 
2356     private static List<Path> collectAllXmlTemplatesUnderSrcSite() throws IOException {
2357         final Path root = Path.of("src/site/xdoc");
2358         try (Stream<Path> walk = Files.walk(root)) {
2359             return walk
2360                     .filter(path -> path.getFileName().toString().endsWith(".xml.template"))
2361                     .collect(toImmutableList());
2362         }
2363     }
2364 
2365     private static Document parseXmlToDomDocument(Path template) throws Exception {
2366         final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
2367         dbFactory.setNamespaceAware(true);
2368         final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
2369         final Document doc = dBuilder.parse(template.toFile());
2370         doc.getDocumentElement().normalize();
2371         return doc;
2372     }
2373 
2374     private static String getExamplePrefix(String id) {
2375         final int dash = id.indexOf('-');
2376         final String result;
2377         if (dash == -1) {
2378             result = id;
2379         }
2380         else {
2381             result = id.substring(0, dash);
2382         }
2383         return result;
2384     }
2385 
2386     @Test
2387     public void testAllOldReleaseNotesHaveRedirectInCheckstyleJs() throws Exception {
2388         final String checkstyleJsContent = Files.readString(CHECKSTYLE_JS_PATH);
2389         for (Path path : XdocUtil.getXdocsFilePaths()) {
2390             if (!path.toString().contains("releasenotes_old_")) {
2391                 continue;
2392             }
2393             final String fileNameWithoutExtension =
2394                     path.getFileName().toString().replace(".xml", "");
2395             final String expectedRedirect = String.format(Locale.ROOT,
2396                     "window.location.replace(`./%s.html", fileNameWithoutExtension);
2397             assertWithMessage(String.format(
2398                         Locale.ROOT,
2399                         "Missing redirect for %s: expected '%s...' in %s",
2400                         fileNameWithoutExtension,
2401                         expectedRedirect,
2402                         CHECKSTYLE_JS_PATH))
2403                     .that(checkstyleJsContent)
2404                     .contains(expectedRedirect);
2405         }
2406     }
2407 
2408     @Test
2409     public void testAllXdocsModulesTemplatesHaveSinceMacroAtTheBeginning() throws Exception {
2410         for (Path path : XdocUtil.getXdocsTemplatesFilePaths()) {
2411             final String fileName = path.getFileName().toString();
2412 
2413             if (isNonModulePage(fileName.replace(".template", ""))) {
2414                 continue;
2415             }
2416 
2417             final NodeList sources = getTagSourcesNode(path, "section");
2418             final Node section = sources.item(0);
2419             final String sectionName = section.getNodeName();
2420             final Node firstChild = XmlUtil.getFirstChildElement(section);
2421             assertWithMessage(
2422                 fileName + " first child of section " + sectionName + " should be a <macro> tag")
2423                 .that(firstChild.getNodeName())
2424                 .isEqualTo("macro");
2425             assertWithMessage(
2426                 fileName + " first child of section " + sectionName
2427                         + " should be a <macro> tag with name 'since'")
2428                 .that(firstChild.getAttributes().getNamedItem("name").getTextContent())
2429                 .isEqualTo("since");
2430         }
2431     }
2432 
2433     @FunctionalInterface
2434     private interface PredicateProcess {
2435         boolean hasFit(Path path);
2436     }
2437 }