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