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.nio.file.Paths;
37  import java.util.ArrayList;
38  import java.util.Arrays;
39  import java.util.BitSet;
40  import java.util.Collection;
41  import java.util.Collections;
42  import java.util.HashMap;
43  import java.util.HashSet;
44  import java.util.Iterator;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.Map;
48  import java.util.NoSuchElementException;
49  import java.util.Optional;
50  import java.util.Properties;
51  import java.util.Set;
52  import java.util.TreeSet;
53  import java.util.regex.Matcher;
54  import java.util.regex.Pattern;
55  import java.util.stream.Collectors;
56  import java.util.stream.IntStream;
57  import java.util.stream.Stream;
58  
59  import javax.xml.parsers.DocumentBuilder;
60  import javax.xml.parsers.DocumentBuilderFactory;
61  
62  import org.apache.commons.beanutils.PropertyUtils;
63  import org.junit.jupiter.api.BeforeAll;
64  import org.junit.jupiter.api.Test;
65  import org.junit.jupiter.api.io.TempDir;
66  import org.w3c.dom.Document;
67  import org.w3c.dom.Element;
68  import org.w3c.dom.Node;
69  import org.w3c.dom.NodeList;
70  import org.xml.sax.InputSource;
71  
72  import com.puppycrawl.tools.checkstyle.Checker;
73  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
74  import com.puppycrawl.tools.checkstyle.ConfigurationLoader.IgnoredModulesOptions;
75  import com.puppycrawl.tools.checkstyle.ModuleFactory;
76  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
77  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
78  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
79  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
80  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
81  import com.puppycrawl.tools.checkstyle.api.Configuration;
82  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
83  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
84  import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
85  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
86  import com.puppycrawl.tools.checkstyle.internal.utils.XdocGenerator;
87  import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
88  import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
89  import com.puppycrawl.tools.checkstyle.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 !"JavadocMetadataScraper".equals(checkName)
293                     && !"ClassAndPropertiesSettersJavadocScraper".equals(checkName);
294             })
295             .forEach(checkName -> {
296                 if (!isPresent(availableChecks, checkName)) {
297                     assertWithMessage(
298                             checkName + " is not correctly listed on Available Checks page"
299                                     + " - add it to " + AVAILABLE_CHECKS_PATH).fail();
300                 }
301             });
302     }
303 
304     private static boolean isPresent(String availableChecks, String checkName) {
305         final String linkPattern = String.format(Locale.ROOT, LINK_TEMPLATE, checkName);
306         return availableChecks.matches(linkPattern);
307     }
308 
309     @Test
310     public void testAllConfigsHaveLinkInSite() throws Exception {
311         final String siteContent = Files.readString(SITE_PATH);
312 
313         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
314             final String expectedFile = path.toString()
315                     .replace(".xml", ".html")
316                     .replaceAll("\\\\", "/")
317                     .replaceAll("src[\\\\/]site[\\\\/]xdoc[\\\\/]", "");
318             final boolean isConfigHtmlFile = Pattern.matches("config_[a-z]+.html", expectedFile);
319             final boolean isChecksIndexHtmlFile = "checks/index.html".equals(expectedFile);
320             final boolean isOldReleaseNotes = path.toString().contains("releasenotes_");
321             final boolean isInnerPage = "report_issue.html".equals(expectedFile);
322 
323             if (!isConfigHtmlFile && !isChecksIndexHtmlFile
324                 && !isOldReleaseNotes && !isInnerPage) {
325                 final String expectedLink = String.format(Locale.ROOT, "href=\"%s\"", expectedFile);
326                 assertWithMessage("Expected to find link to '" + expectedLink + "' in " + SITE_PATH)
327                         .that(siteContent)
328                         .contains(expectedLink);
329             }
330         }
331     }
332 
333     @Test
334     public void testAllModulesPageInSyncWithModuleSummaries() throws Exception {
335         validateModulesSyncWithTheirSummaries(AVAILABLE_CHECKS_PATH,
336             (Path path) -> {
337                 final String fileName = path.getFileName().toString();
338                 return isNonModulePage(fileName) || !path.toString().contains("checks");
339             });
340 
341         validateModulesSyncWithTheirSummaries(AVAILABLE_FILTERS_PATH,
342             (Path path) -> {
343                 final String fileName = path.getFileName().toString();
344                 return isNonModulePage(fileName)
345                     || path.toString().contains("checks")
346                     || path.toString().contains("filefilters");
347             });
348 
349         validateModulesSyncWithTheirSummaries(AVAILABLE_FILE_FILTERS_PATH,
350             (Path path) -> {
351                 final String fileName = path.getFileName().toString();
352                 return isNonModulePage(fileName) || !path.toString().contains("filefilters");
353             });
354     }
355 
356     private static void validateModulesSyncWithTheirSummaries(Path availablePagePath,
357                                                               PredicateProcess skipPredicate)
358             throws Exception {
359         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
360             if (skipPredicate.hasFit(path)) {
361                 continue;
362             }
363 
364             final String fileName = path.getFileName().toString();
365             final Map<String, String> summaries = readSummaries(availablePagePath);
366             final NodeList subsectionSources = getTagSourcesNode(path, "subsection");
367 
368             for (int position = 0; position < subsectionSources.getLength(); position++) {
369                 final Node subsection = subsectionSources.item(position);
370                 final String subsectionName = XmlUtil.getNameAttributeOfNode(subsection);
371                 if (!"Description".equals(subsectionName)) {
372                     continue;
373                 }
374 
375                 final String moduleName = XmlUtil.getNameAttributeOfNode(
376                     subsection.getParentNode());
377                 final Matcher matcher = END_OF_SENTENCE.matcher(subsection.getTextContent());
378                 assertWithMessage(
379                     "The first sentence of the \"Description\" subsection for the module "
380                         + moduleName + " in the file \"" + fileName + "\" should end with a period")
381                     .that(matcher.find())
382                     .isTrue();
383 
384                 final String firstSentence = XmlUtil.sanitizeXml(matcher.group(1));
385 
386                 assertWithMessage("The summary for module " + moduleName
387                         + " in the file \"" + availablePagePath + "\""
388                         + " should match the first sentence of the \"Description\" subsection"
389                         + " for this module in the file \"" + fileName + "\"")
390                     .that(summaries.get(moduleName))
391                     .isEqualTo(firstSentence);
392             }
393         }
394     }
395 
396     @Test
397     public void testCategoryIndexPageTableInSyncWithAllChecksPageTable() throws Exception {
398         final Map<String, String> summaries = readSummaries(AVAILABLE_CHECKS_PATH);
399         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
400             final String fileName = path.getFileName().toString();
401             if (!"index.xml".equals(fileName)
402                     // Filters are excluded because they are not included in the main checks.xml
403                     // file and have their own separate validation in
404                     // testAllFiltersIndexPageTable()
405                     || path.getParent().toString().contains("filters")) {
406                 continue;
407             }
408 
409             final NodeList sources = getTagSourcesNode(path, "tr");
410 
411             for (int position = 0; position < sources.getLength(); position++) {
412                 final Node tableRow = sources.item(position);
413                 final Iterator<Node> cells = XmlUtil
414                         .findChildElementsByTag(tableRow, "td").iterator();
415                 final String checkName = XmlUtil.sanitizeXml(cells.next().getTextContent());
416                 final String description = XmlUtil.sanitizeXml(cells.next().getTextContent());
417                 assertWithMessage("The summary for check " + checkName
418                         + " in the file \"" + path + "\""
419                         + " should match the summary"
420                         + " for this check in the file \"" + AVAILABLE_CHECKS_PATH + "\"")
421                     .that(description)
422                     .isEqualTo(summaries.get(checkName));
423             }
424         }
425     }
426 
427     @Test
428     public void testAllFiltersIndexPageTable() throws Exception {
429         validateFilterTypeIndexPage(AVAILABLE_FILTERS_PATH);
430         validateFilterTypeIndexPage(AVAILABLE_FILE_FILTERS_PATH);
431     }
432 
433     private static void validateFilterTypeIndexPage(Path availablePath)
434             throws Exception {
435         final NodeList tableRowSources = getTagSourcesNode(availablePath, "tr");
436 
437         for (int position = 0; position < tableRowSources.getLength(); position++) {
438             final Node tableRow = tableRowSources.item(position);
439             final Iterator<Node> tdCells = XmlUtil
440                 .findChildElementsByTag(tableRow, "td").iterator();
441 
442             assertWithMessage("Filter name cell at row " + (position + 1)
443                 + " in " + availablePath + " should exist")
444                 .that(tdCells.hasNext())
445                 .isTrue();
446             final Node nameCell = tdCells.next();
447             final String filterName = XmlUtil.sanitizeXml(nameCell.getTextContent().trim());
448 
449             assertWithMessage("Description cell for " + filterName
450                 + " in index.xml should exist")
451                 .that(tdCells.hasNext())
452                 .isTrue();
453 
454             assertWithMessage("Filter name at row " + (position + 1) + " in " + availablePath
455                     + " should not be empty")
456                 .that(filterName)
457                 .isNotEmpty();
458 
459             final Node descriptionCell = tdCells.next();
460             final String description = XmlUtil.sanitizeXml(
461                 descriptionCell.getTextContent().trim());
462 
463             assertWithMessage("Filter description for " + filterName
464                 + " in " + availablePath + " should not be empty")
465                 .that(description)
466                 .isNotEmpty();
467 
468             assertWithMessage("Filter description for " + filterName
469                 + " in " + availablePath + " should end with a period")
470                 .that(description.charAt(description.length() - 1))
471                 .isEqualTo('.');
472         }
473     }
474 
475     private static NodeList getTagSourcesNode(Path availablePath, String tagName)
476             throws Exception {
477         final String input = Files.readString(availablePath);
478         final Document document = XmlUtil.getRawXml(
479             availablePath.toString(), input, input);
480 
481         return document.getElementsByTagName(tagName);
482     }
483 
484     @Test
485     public void testAlphabetOrderInNames() throws Exception {
486         final NodeList nodes = getTagSourcesNode(SITE_PATH, "item");
487 
488         for (int nodeIndex = 0; nodeIndex < nodes.getLength(); nodeIndex++) {
489             final Node current = nodes.item(nodeIndex);
490 
491             if ("Checks".equals(XmlUtil.getNameAttributeOfNode(current))) {
492                 final List<String> groupNames = getNames(current);
493                 final List<String> groupNamesSorted = groupNames.stream()
494                         .sorted()
495                         .toList();
496 
497                 assertWithMessage("Group" + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
498                         .that(groupNames)
499                         .containsExactlyElementsIn(groupNamesSorted)
500                         .inOrder();
501 
502                 Node groupNode = current.getFirstChild();
503                 int index = 0;
504                 final int totalGroups = XmlUtil.getChildrenElements(current).size();
505                 while (index < totalGroups) {
506                     if ("item".equals(groupNode.getNodeName())) {
507                         final List<String> checkNames = getNames(groupNode);
508                         final List<String> checkNamesSorted = checkNames.stream()
509                                 .sorted()
510                                 .toList();
511                         assertWithMessage("Check" + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
512                                 .that(checkNames)
513                                 .containsExactlyElementsIn(checkNamesSorted)
514                                 .inOrder();
515                         index++;
516                     }
517                     groupNode = groupNode.getNextSibling();
518                 }
519             }
520             if ("Filters".equals(XmlUtil.getNameAttributeOfNode(current))) {
521                 final List<String> filterNames = getNames(current);
522                 final List<String> filterNamesSorted = filterNames.stream()
523                         .sorted()
524                         .toList();
525                 assertWithMessage("Filter" + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
526                         .that(filterNames)
527                         .containsExactlyElementsIn(filterNamesSorted)
528                         .inOrder();
529             }
530             if ("File Filters".equals(XmlUtil.getNameAttributeOfNode(current))) {
531                 final List<String> fileFilterNames = getNames(current);
532                 final List<String> fileFilterNamesSorted = fileFilterNames.stream()
533                         .sorted()
534                         .toList();
535                 assertWithMessage("File Filter" + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
536                         .that(fileFilterNames)
537                         .containsExactlyElementsIn(fileFilterNamesSorted)
538                         .inOrder();
539             }
540         }
541     }
542 
543     @Test
544     public void testAlphabetOrderAtIndexPages() throws Exception {
545         final Path allChecks = Paths.get("src/site/xdoc/checks.xml");
546         validateOrder(allChecks, "Check");
547 
548         final String[] groupNames = {"annotation", "blocks", "design",
549             "coding", "header", "imports", "javadoc", "metrics",
550             "misc", "modifier", "naming", "regexp", "sizes", "whitespace"};
551         for (String name : groupNames) {
552             final Path checks = Paths.get("src/site/xdoc/checks/" + name + "/index.xml");
553             validateOrder(checks, "Check");
554         }
555         validateOrder(AVAILABLE_FILTERS_PATH, "Filter");
556 
557         final Path fileFilters = Paths.get("src/site/xdoc/filefilters/index.xml");
558         validateOrder(fileFilters, "File Filter");
559     }
560 
561     public static void validateOrder(Path path, String name) throws Exception {
562         final NodeList nodes = getTagSourcesNode(path, "div");
563 
564         for (int nodeIndex = 0; nodeIndex < nodes.getLength(); nodeIndex++) {
565             final Node current = nodes.item(nodeIndex);
566             final List<String> names = getNamesFromIndexPage(current);
567             final List<String> namesSorted = names.stream()
568                     .sorted()
569                     .toList();
570 
571             assertWithMessage(name + NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH + path)
572                     .that(names)
573                     .containsExactlyElementsIn(namesSorted)
574                     .inOrder();
575         }
576     }
577 
578     private static List<String> getNamesFromIndexPage(Node node) {
579         final List<String> result = new ArrayList<>();
580         final Set<Node> children = XmlUtil.findChildElementsByTag(node, "a");
581 
582         Node current = node.getFirstChild();
583         Node treeNode = current;
584         boolean getFirstChild = false;
585         int index = 0;
586         while (current != null && index < children.size()) {
587             if ("tr".equals(current.getNodeName())) {
588                 treeNode = current.getNextSibling();
589             }
590             if ("a".equals(current.getNodeName())) {
591                 final String name = current.getFirstChild().getTextContent()
592                     .replace(" ", "").replace("\n", "");
593                 result.add(name);
594                 current = treeNode;
595                 getFirstChild = false;
596                 index++;
597             }
598             else if (getFirstChild) {
599                 current = current.getFirstChild();
600                 getFirstChild = false;
601             }
602             else {
603                 current = current.getNextSibling();
604                 getFirstChild = true;
605             }
606         }
607         return result;
608     }
609 
610     private static List<String> getNames(Node node) {
611         final Set<Node> children = XmlUtil.getChildrenElements(node);
612         final List<String> result = new ArrayList<>();
613         Node current = node.getFirstChild();
614         int index = 0;
615         while (index < children.size()) {
616             if ("item".equals(current.getNodeName())) {
617                 final String name = XmlUtil.getNameAttributeOfNode(current);
618                 result.add(name);
619                 index++;
620             }
621             current = current.getNextSibling();
622         }
623         return result;
624     }
625 
626     private static Map<String, String> readSummaries(Path availablePath) throws Exception {
627         final NodeList rows = getTagSourcesNode(availablePath, "tr");
628         final Map<String, String> result = new HashMap<>();
629 
630         for (int position = 0; position < rows.getLength(); position++) {
631             final Node row = rows.item(position);
632             final Iterator<Node> cells = XmlUtil.findChildElementsByTag(row, "td").iterator();
633             final String name = XmlUtil.sanitizeXml(cells.next().getTextContent());
634             final String summary = XmlUtil.sanitizeXml(cells.next().getTextContent());
635 
636             result.put(name, summary);
637         }
638 
639         return result;
640     }
641 
642     @Test
643     public void testAllSubSections() throws Exception {
644         for (Path path : XdocUtil.getXdocsFilePaths()) {
645             final String fileName = path.getFileName().toString();
646             final NodeList subSections = getTagSourcesNode(path, "subsection");
647 
648             for (int position = 0; position < subSections.getLength(); position++) {
649                 final Node subSection = subSections.item(position);
650                 final Node name = subSection.getAttributes().getNamedItem("name");
651 
652                 assertWithMessage("All sub-sections in '" + fileName + "' must have a name")
653                     .that(name)
654                     .isNotNull();
655 
656                 final Node id = subSection.getAttributes().getNamedItem("id");
657 
658                 assertWithMessage("All sub-sections in '" + fileName + "' must have an id")
659                     .that(id)
660                     .isNotNull();
661 
662                 // Checks and filters have their own xdocs files, so the section name
663                 // is the same as the section id by default.
664                 String sectionName = XmlUtil.getNameAttributeOfNode(subSection.getParentNode());
665                 final String nameString = name.getNodeValue();
666                 final String subsectionId = id.getNodeValue();
667                 final String expectedId;
668 
669                 if ("google_style.xml".equals(fileName)) {
670                     sectionName = "Google";
671                     expectedId = (sectionName + "_" + nameString).replace(' ', '_');
672                 }
673                 else if ("sun_style.xml".equals(fileName)) {
674                     sectionName = "Sun";
675                     expectedId = (sectionName + "_" + nameString).replace(' ', '_');
676                 }
677                 else if ((path.toString().contains("filters")
678                         || path.toString().contains("checks"))
679                         && !subsectionId.startsWith(sectionName)) {
680                     expectedId = nameString.replace(' ', '_');
681                 }
682                 else {
683                     expectedId = (sectionName + "_" + nameString).replace(' ', '_');
684                 }
685 
686                 assertWithMessage(fileName + " sub-section " + nameString + " for section "
687                         + sectionName + " must match")
688                     .that(subsectionId)
689                     .isEqualTo(expectedId);
690             }
691         }
692     }
693 
694     @Test
695     public void testAllXmlExamples() throws Exception {
696         for (Path path : XdocUtil.getXdocsFilePaths()) {
697             final String fileName = path.getFileName().toString();
698             final NodeList sources = getTagSourcesNode(path, "source");
699 
700             for (int position = 0; position < sources.getLength(); position++) {
701                 final String unserializedSource = sources.item(position).getTextContent()
702                         .replace("...", "").trim();
703 
704                 if (unserializedSource.length() > 1 && (unserializedSource.charAt(0) != '<'
705                         || unserializedSource.charAt(unserializedSource.length() - 1) != '>'
706                         // no dtd testing yet
707                         || unserializedSource.contains("<!"))) {
708                     continue;
709                 }
710 
711                 final String code = buildXml(unserializedSource);
712                 // validate only
713                 XmlUtil.getRawXml(fileName, code, unserializedSource);
714 
715                 // can't test ant structure, or old and outdated checks
716                 assertWithMessage("Xml is invalid, old or has outdated structure")
717                         .that(fileName.startsWith("anttask")
718                                 || fileName.startsWith("releasenotes")
719                                 || fileName.startsWith("writingjavadocchecks")
720                                 || isValidCheckstyleXml(fileName, code, unserializedSource))
721                         .isTrue();
722             }
723         }
724     }
725 
726     private static String buildXml(String unserializedSource) throws IOException {
727         // not all examples come with the full xml structure
728         String code = unserializedSource
729             // don't corrupt our own cachefile
730             .replace("target/cachefile", "target/cachefile-test");
731 
732         if (!hasFileSetClass(code)) {
733             code = "<module name=\"TreeWalker\">\n" + code + "\n</module>";
734         }
735         if (!code.contains("name=\"Checker\"")) {
736             code = "<module name=\"Checker\">\n" + code + "\n</module>";
737         }
738         if (!code.startsWith("<?xml")) {
739             final String dtdPath = new File(
740                     "src/main/resources/com/puppycrawl/tools/checkstyle/configuration_1_3.dtd")
741                     .getCanonicalPath();
742 
743             code = "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC "
744                     + "\"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\" \"" + dtdPath
745                     + "\">\n" + code;
746         }
747         return code;
748     }
749 
750     private static boolean hasFileSetClass(String xml) {
751         boolean found = false;
752 
753         for (String find : XML_FILESET_LIST) {
754             if (xml.contains(find)) {
755                 found = true;
756                 break;
757             }
758         }
759 
760         return found;
761     }
762 
763     private static boolean isValidCheckstyleXml(String fileName, String code,
764                                                 String unserializedSource)
765             throws IOException, CheckstyleException {
766         // can't process non-existent examples, or out of context snippets
767         if (!code.contains("com.mycompany") && !code.contains("checkstyle-packages")
768                 && !code.contains("MethodLimit") && !code.contains("<suppress ")
769                 && !code.contains("<suppress-xpath ")
770                 && !code.contains("<import-control ")
771                 && !unserializedSource.startsWith("<property ")
772                 && !unserializedSource.startsWith("<taskdef ")) {
773             // validate checkstyle structure and contents
774             try {
775                 final Properties properties = new Properties();
776 
777                 properties.setProperty("checkstyle.header.file",
778                         new File("config/java.header").getCanonicalPath());
779                 properties.setProperty("config.folder",
780                         new File("config").getCanonicalPath());
781 
782                 final PropertiesExpander expander = new PropertiesExpander(properties);
783                 final Configuration config = ConfigurationLoader.loadConfiguration(new InputSource(
784                         new StringReader(code)), expander, IgnoredModulesOptions.EXECUTE);
785                 final Checker checker = new Checker();
786 
787                 try {
788                     final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
789                     checker.setModuleClassLoader(moduleClassLoader);
790                     checker.configure(config);
791                 }
792                 finally {
793                     checker.destroy();
794                 }
795             }
796             catch (CheckstyleException exc) {
797                 throw new CheckstyleException(fileName + " has invalid Checkstyle xml ("
798                         + exc.getMessage() + "): " + unserializedSource, exc);
799             }
800         }
801         return true;
802     }
803 
804     @Test
805     public void testAllCheckSections() throws Exception {
806         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
807 
808         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
809             final String fileName = path.getFileName().toString();
810 
811             if (isNonModulePage(fileName)) {
812                 continue;
813             }
814 
815             final NodeList sources = getTagSourcesNode(path, "section");
816             String lastSectionName = null;
817 
818             for (int position = 0; position < sources.getLength(); position++) {
819                 final Node section = sources.item(position);
820                 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
821 
822                 if ("Content".equals(sectionName) || "Overview".equals(sectionName)) {
823                     assertWithMessage(fileName + " section '" + sectionName + "' should be first")
824                         .that(lastSectionName)
825                         .isNull();
826                     continue;
827                 }
828 
829                 assertWithMessage(
830                         fileName + " section '" + sectionName + "' shouldn't end with 'Check'")
831                                 .that(sectionName.endsWith("Check"))
832                                 .isFalse();
833                 if (lastSectionName != null) {
834                     assertWithMessage(fileName + " section '" + sectionName
835                             + "' is out of order compared to '" + lastSectionName + "'")
836                                     .that(sectionName.toLowerCase(Locale.ENGLISH).compareTo(
837                                             lastSectionName.toLowerCase(Locale.ENGLISH)) >= 0)
838                                     .isTrue();
839                 }
840 
841                 validateCheckSection(moduleFactory, fileName, sectionName, section);
842 
843                 lastSectionName = sectionName;
844             }
845         }
846     }
847 
848     public static boolean isNonModulePage(String fileName) {
849         return NON_MODULE_XDOC.contains(fileName)
850             || fileName.startsWith("releasenotes")
851             || Pattern.matches("config_[a-z]+.xml", fileName);
852     }
853 
854     @Test
855     public void testAllCheckSectionsEx() throws Exception {
856         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
857 
858         final Path path = Path.of(XdocUtil.DIRECTORY_PATH + "/config.xml");
859         final String fileName = path.getFileName().toString();
860 
861         final NodeList sources = getTagSourcesNode(path, "section");
862 
863         for (int position = 0; position < sources.getLength(); position++) {
864             final Node section = sources.item(position);
865             final String sectionName = XmlUtil.getNameAttributeOfNode(section);
866 
867             if (!"Checker".equals(sectionName) && !"TreeWalker".equals(sectionName)) {
868                 continue;
869             }
870 
871             validateCheckSection(moduleFactory, fileName, sectionName, section);
872         }
873     }
874 
875     private static void validateCheckSection(ModuleFactory moduleFactory, String fileName,
876             String sectionName, Node section) throws Exception {
877         final Object instance;
878 
879         try {
880             instance = moduleFactory.createModule(sectionName);
881         }
882         catch (CheckstyleException exc) {
883             throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, exc);
884         }
885 
886         int subSectionPos = 0;
887         for (Node subSection : XmlUtil.getChildrenElements(section)) {
888             if (subSectionPos == 0 && "p".equals(subSection.getNodeName())) {
889                 validateSinceDescriptionSection(fileName, sectionName, subSection);
890                 continue;
891             }
892 
893             final String subSectionName = XmlUtil.getNameAttributeOfNode(subSection);
894 
895             // can be in different orders, and completely optional
896             if ("Notes".equals(subSectionName)
897                     || "Rule Description".equals(subSectionName)
898                     || "Metadata".equals(subSectionName)) {
899                 continue;
900             }
901 
902             // optional sections that can be skipped if they have nothing to report
903             if (subSectionPos == 1 && !"Properties".equals(subSectionName)) {
904                 validatePropertySection(fileName, sectionName, null, instance);
905                 subSectionPos++;
906             }
907             if (subSectionPos == 4 && !"Violation Messages".equals(subSectionName)) {
908                 validateViolationSection(fileName, sectionName, null, instance);
909                 subSectionPos++;
910             }
911 
912             assertWithMessage(fileName + " section '" + sectionName + "' should be in order")
913                 .that(subSectionName)
914                 .isEqualTo(getSubSectionName(subSectionPos));
915 
916             switch (subSectionPos) {
917                 case 0 -> validateDescriptionSection(fileName, sectionName, subSection);
918                 case 1 -> validatePropertySection(fileName, sectionName, subSection, instance);
919                 case 3 -> validateUsageExample(fileName, sectionName, subSection);
920                 case 4 -> validateViolationSection(fileName, sectionName, subSection, instance);
921                 case 5 -> validatePackageSection(fileName, sectionName, subSection, instance);
922                 case 6 -> validateParentSection(fileName, sectionName, subSection);
923                 default -> {
924                     // no code by design
925                 }
926             }
927 
928             subSectionPos++;
929         }
930 
931         if ("Checker".equals(sectionName)) {
932             assertWithMessage(fileName + " section '" + sectionName
933                     + "' should contain up to 'Package' sub-section")
934                     .that(subSectionPos)
935                     .isGreaterThan(5);
936         }
937         else {
938             assertWithMessage(fileName + " section '" + sectionName
939                     + "' should contain up to 'Parent' sub-section")
940                     .that(subSectionPos)
941                     .isGreaterThan(6);
942         }
943     }
944 
945     private static void validateSinceDescriptionSection(String fileName, String sectionName,
946             Node subSection) {
947         assertWithMessage(fileName + " section '" + sectionName
948                     + "' should have a valid version at the start of the description like:\n"
949                     + DESCRIPTION_VERSION.pattern())
950                 .that(DESCRIPTION_VERSION.matcher(subSection.getTextContent().trim()).find())
951                 .isTrue();
952     }
953 
954     private static Object getSubSectionName(int subSectionPos) {
955         return switch (subSectionPos) {
956             case 0 -> "Description";
957             case 1 -> "Properties";
958             case 2 -> "Examples";
959             case 3 -> "Example of Usage";
960             case 4 -> "Violation Messages";
961             case 5 -> "Package";
962             case 6 -> "Parent Module";
963             default -> null;
964         };
965     }
966 
967     private static void validateDescriptionSection(String fileName, String sectionName,
968             Node subSection) {
969         if ("config_filters.xml".equals(fileName) && "SuppressionXpathFilter".equals(sectionName)) {
970             validateListOfSuppressionXpathFilterIncompatibleChecks(subSection);
971         }
972     }
973 
974     private static void validateListOfSuppressionXpathFilterIncompatibleChecks(Node subSection) {
975         assertWithMessage(
976             "Incompatible check list should match XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES")
977             .that(getListById(subSection, "SuppressionXpathFilter_IncompatibleChecks"))
978             .isEqualTo(XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES);
979         final Set<String> suppressionXpathFilterJavadocChecks = getListById(subSection,
980                 "SuppressionXpathFilter_JavadocChecks");
981         assertWithMessage(
982             "Javadoc check list should match XpathRegressionTest.INCOMPATIBLE_JAVADOC_CHECK_NAMES")
983             .that(suppressionXpathFilterJavadocChecks)
984             .isEqualTo(XpathRegressionTest.INCOMPATIBLE_JAVADOC_CHECK_NAMES);
985     }
986 
987     private static void validatePropertySection(String fileName, String sectionName,
988             Node subSection, Object instance) throws Exception {
989         final Set<String> properties = getProperties(instance.getClass());
990         final Class<?> clss = instance.getClass();
991 
992         fixCapturedProperties(sectionName, instance, clss, properties);
993 
994         if (subSection != null) {
995             assertWithMessage(fileName + " section '" + sectionName
996                     + "' should have no properties to show")
997                 .that(properties)
998                 .isNotEmpty();
999 
1000             final Set<Node> nodes = XmlUtil.getChildrenElements(subSection);
1001             assertWithMessage(fileName + " section '" + sectionName
1002                     + "' subsection 'Properties' should have one child node")
1003                 .that(nodes)
1004                 .hasSize(1);
1005 
1006             final Node div = nodes.iterator().next();
1007             assertWithMessage(fileName + " section '" + sectionName
1008                         + "' subsection 'Properties' has unexpected child node")
1009                 .that(div.getNodeName())
1010                 .isEqualTo("div");
1011             final String wrapperMessage = fileName + " section '" + sectionName
1012                     + "' subsection 'Properties' wrapping div for table needs the"
1013                     + " class 'wrapper'";
1014             assertWithMessage(wrapperMessage)
1015                     .that(div.hasAttributes())
1016                     .isTrue();
1017             assertWithMessage(wrapperMessage)
1018                 .that(div.getAttributes().getNamedItem("class").getNodeValue())
1019                 .isNotNull();
1020             assertWithMessage(wrapperMessage)
1021                     .that(div.getAttributes().getNamedItem("class").getNodeValue())
1022                     .contains("wrapper");
1023 
1024             final Node table = XmlUtil.getFirstChildElement(div);
1025             assertWithMessage(fileName + " section '" + sectionName
1026                     + "' subsection 'Properties' has unexpected child node")
1027                 .that(table.getNodeName())
1028                 .isEqualTo("table");
1029 
1030             validatePropertySectionPropertiesOrder(fileName, sectionName, table, properties);
1031 
1032             validatePropertySectionProperties(fileName, sectionName, table, instance,
1033                     properties);
1034         }
1035 
1036         assertWithMessage(
1037                 fileName + " section '" + sectionName + "' should show properties: " + properties)
1038             .that(properties)
1039             .isEmpty();
1040     }
1041 
1042     private static void validatePropertySectionPropertiesOrder(String fileName, String sectionName,
1043                                                                Node table, Set<String> properties) {
1044         final Set<Node> rows = XmlUtil.getChildrenElements(table);
1045         final List<String> orderedPropertyNames = new ArrayList<>(properties);
1046         final List<String> tablePropertyNames = new ArrayList<>();
1047 
1048         // javadocTokens and tokens should be last
1049         if (orderedPropertyNames.contains("javadocTokens")) {
1050             orderedPropertyNames.remove("javadocTokens");
1051             orderedPropertyNames.add("javadocTokens");
1052         }
1053         if (orderedPropertyNames.contains("tokens")) {
1054             orderedPropertyNames.remove("tokens");
1055             orderedPropertyNames.add("tokens");
1056         }
1057 
1058         rows
1059             .stream()
1060             // First row is header row
1061             .skip(1)
1062             .forEach(row -> {
1063                 final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
1064                 assertWithMessage(fileName + " section '" + sectionName
1065                         + "' should have the requested columns")
1066                     .that(columns)
1067                     .hasSize(5);
1068 
1069                 final String propertyName = columns.get(0).getTextContent();
1070                 tablePropertyNames.add(propertyName);
1071             });
1072 
1073         assertWithMessage(fileName + " section '" + sectionName
1074                 + "' should have properties in the requested order")
1075             .that(tablePropertyNames)
1076             .isEqualTo(orderedPropertyNames);
1077     }
1078 
1079     private static void fixCapturedProperties(String sectionName, Object instance, Class<?> clss,
1080             Set<String> properties) {
1081         // remove global properties that don't need documentation
1082         if (hasParentModule(sectionName)) {
1083             if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
1084                 properties.removeAll(JAVADOC_CHECK_PROPERTIES);
1085 
1086                 // override
1087                 properties.add("violateExecutionOnNonTightHtml");
1088             }
1089             else if (AbstractCheck.class.isAssignableFrom(clss)) {
1090                 properties.removeAll(CHECK_PROPERTIES);
1091             }
1092         }
1093         if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
1094             properties.removeAll(FILESET_PROPERTIES);
1095 
1096             // override
1097             properties.add("fileExtensions");
1098         }
1099 
1100         // remove undocumented properties
1101         new HashSet<>(properties).stream()
1102             .filter(prop -> UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + "." + prop))
1103             .forEach(properties::remove);
1104 
1105         if (AbstractCheck.class.isAssignableFrom(clss)) {
1106             final AbstractCheck check = (AbstractCheck) instance;
1107 
1108             final int[] acceptableTokens = check.getAcceptableTokens();
1109             Arrays.sort(acceptableTokens);
1110             final int[] defaultTokens = check.getDefaultTokens();
1111             Arrays.sort(defaultTokens);
1112             final int[] requiredTokens = check.getRequiredTokens();
1113             Arrays.sort(requiredTokens);
1114 
1115             if (!Arrays.equals(acceptableTokens, defaultTokens)
1116                     || !Arrays.equals(acceptableTokens, requiredTokens)) {
1117                 properties.add("tokens");
1118             }
1119         }
1120 
1121         if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
1122             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
1123 
1124             final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
1125             Arrays.sort(acceptableJavadocTokens);
1126             final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
1127             Arrays.sort(defaultJavadocTokens);
1128             final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
1129             Arrays.sort(requiredJavadocTokens);
1130 
1131             if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
1132                     || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
1133                 properties.add("javadocTokens");
1134             }
1135         }
1136     }
1137 
1138     private static void validatePropertySectionProperties(String fileName, String sectionName,
1139             Node table, Object instance, Set<String> properties) throws Exception {
1140         boolean skip = true;
1141         boolean didJavadocTokens = false;
1142         boolean didTokens = false;
1143 
1144         for (Node row : XmlUtil.getChildrenElements(table)) {
1145             final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
1146 
1147             assertWithMessage(fileName + " section '" + sectionName
1148                     + "' should have the requested columns")
1149                 .that(columns)
1150                 .hasSize(5);
1151 
1152             if (skip) {
1153                 assertWithMessage(fileName + " section '" + sectionName
1154                                 + "' should have the specific title")
1155                     .that(columns.get(0).getTextContent())
1156                     .isEqualTo("name");
1157                 assertWithMessage(fileName + " section '" + sectionName
1158                                 + "' should have the specific title")
1159                     .that(columns.get(1).getTextContent())
1160                     .isEqualTo("description");
1161                 assertWithMessage(fileName + " section '" + sectionName
1162                                 + "' should have the specific title")
1163                     .that(columns.get(2).getTextContent())
1164                     .isEqualTo("type");
1165                 assertWithMessage(fileName + " section '" + sectionName
1166                                 + "' should have the specific title")
1167                     .that(columns.get(3).getTextContent())
1168                     .isEqualTo("default value");
1169                 assertWithMessage(fileName + " section '" + sectionName
1170                                 + "' should have the specific title")
1171                     .that(columns.get(4).getTextContent())
1172                     .isEqualTo("since");
1173 
1174                 skip = false;
1175                 continue;
1176             }
1177 
1178             assertWithMessage(fileName + " section '" + sectionName
1179                         + "' should have token properties last")
1180                     .that(didTokens)
1181                     .isFalse();
1182 
1183             final String propertyName = columns.get(0).getTextContent();
1184             assertWithMessage(fileName + " section '" + sectionName
1185                         + "' should not contain the property: " + propertyName)
1186                     .that(properties.remove(propertyName))
1187                     .isTrue();
1188 
1189             if ("tokens".equals(propertyName)) {
1190                 final AbstractCheck check = (AbstractCheck) instance;
1191                 validatePropertySectionPropertyTokens(fileName, sectionName, check, columns);
1192                 didTokens = true;
1193             }
1194             else if ("javadocTokens".equals(propertyName)) {
1195                 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
1196                 validatePropertySectionPropertyJavadocTokens(fileName, sectionName, check, columns);
1197                 didJavadocTokens = true;
1198             }
1199             else {
1200                 assertWithMessage(fileName + " section '" + sectionName
1201                         + "' should have javadoc token properties next to last, before tokens")
1202                                 .that(didJavadocTokens)
1203                                 .isFalse();
1204 
1205                 validatePropertySectionPropertyEx(fileName, sectionName, instance, columns,
1206                         propertyName);
1207             }
1208 
1209             assertWithMessage("%s section '%s' should have a version for %s",
1210                             fileName, sectionName, propertyName)
1211                     .that(columns.get(4).getTextContent().trim())
1212                     .isNotEmpty();
1213             assertWithMessage("%s section '%s' should have a valid version for %s",
1214                             fileName, sectionName, propertyName)
1215                     .that(columns.get(4).getTextContent().trim())
1216                     .matches(VERSION);
1217         }
1218     }
1219 
1220     private static void validatePropertySectionPropertyEx(String fileName, String sectionName,
1221             Object instance, List<Node> columns, String propertyName) throws Exception {
1222         assertWithMessage("%s section '%s' should have a description for %s",
1223                         fileName, sectionName, propertyName)
1224                 .that(columns.get(1).getTextContent().trim())
1225                 .isNotEmpty();
1226         assertWithMessage("%s section '%s' should have a description for %s"
1227                         + " that starts with uppercase character",
1228                         fileName, sectionName, propertyName)
1229                 .that(Character.isUpperCase(columns.get(1).getTextContent().trim().charAt(0)))
1230                 .isTrue();
1231 
1232         final String actualTypeName = columns.get(2).getTextContent().replace("\n", "")
1233                 .replace("\r", "").replaceAll(" +", " ").trim();
1234 
1235         assertWithMessage(
1236                 fileName + " section '" + sectionName + "' should have a type for " + propertyName)
1237                         .that(actualTypeName)
1238                         .isNotEmpty();
1239 
1240         final Field field = getField(instance.getClass(), propertyName);
1241         final Class<?> fieldClass = getFieldClass(fileName, sectionName, instance, field,
1242                 propertyName);
1243 
1244         final String expectedTypeName = Optional.ofNullable(field)
1245                 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
1246                 .map(propertyType -> propertyType.value().getDescription())
1247                 .orElse(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) {
1546             stream = ((BitSet) value).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("All messages can be customized if the default message doesn't "
1664                     + "suit you.\nPlease see the documentation to learn how to.");
1665         }
1666 
1667         if (subSection == null) {
1668             assertWithMessage(fileName + " section '" + sectionName
1669                     + "' should have the expected error keys")
1670                 .that(expectedText.toString())
1671                 .isEqualTo("");
1672         }
1673         else {
1674             final String subsectionTextContent = subSection.getTextContent()
1675                     .replaceAll("\n\\s+", "\n")
1676                     .replaceAll("\\s+", " ")
1677                     .trim();
1678             assertWithMessage(fileName + " section '" + sectionName
1679                             + "' should have the expected error keys")
1680                 .that(subsectionTextContent)
1681                 .isEqualTo(expectedText.toString().replaceAll("\n", " ").trim());
1682 
1683             for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1684                 final String url = node.getAttributes().getNamedItem("href").getTextContent();
1685                 final String linkText = node.getTextContent().trim();
1686                 final String expectedUrl;
1687 
1688                 if ("see the documentation".equals(linkText)) {
1689                     expectedUrl = "../../config.html#Custom_messages";
1690                 }
1691                 else {
1692                     expectedUrl = "https://github.com/search?q="
1693                             + "path%3Asrc%2Fmain%2Fresources%2F"
1694                             + clss.getPackage().getName().replace(".", "%2F")
1695                             + "%20path%3A**%2Fmessages*.properties+repo%3Acheckstyle%2F"
1696                             + "checkstyle+%22" + linkText + "%22";
1697                 }
1698 
1699                 assertWithMessage(fileName + " section '" + sectionName
1700                         + "' should have matching url for '" + linkText + "'")
1701                     .that(url)
1702                     .isEqualTo(expectedUrl);
1703             }
1704         }
1705     }
1706 
1707     private static void validateUsageExample(String fileName, String sectionName, Node subSection) {
1708         final String text = subSection.getTextContent()
1709             .replace("Checkstyle Style", "")
1710             .replace("Google Style", "")
1711             .replace("Sun Style", "")
1712             .replace("Checkstyle's Import Control Config", "")
1713             .trim();
1714 
1715         assertWithMessage(fileName + " section '" + sectionName
1716                 + "' has unknown text in 'Example of Usage': " + text)
1717             .that(text)
1718             .isEmpty();
1719 
1720         boolean hasCheckstyle = false;
1721         boolean hasGoogle = false;
1722         boolean hasSun = false;
1723 
1724         for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1725             final String url = node.getAttributes().getNamedItem("href").getTextContent();
1726             final String linkText = node.getTextContent().trim();
1727             String expectedUrl = null;
1728 
1729             if ("Checkstyle Style".equals(linkText)) {
1730                 hasCheckstyle = true;
1731                 expectedUrl = "https://github.com/search?q="
1732                         + "path%3Aconfig%20path%3A**%2Fcheckstyle-checks.xml+"
1733                         + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName;
1734             }
1735             else if ("Google Style".equals(linkText)) {
1736                 hasGoogle = true;
1737                 expectedUrl = "https://github.com/search?q="
1738                         + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2Fgoogle_checks.xml+"
1739                         + "repo%3Acheckstyle%2Fcheckstyle+"
1740                         + sectionName;
1741 
1742                 assertWithMessage(fileName + " section '" + sectionName
1743                             + "' should be in google_checks.xml or not reference 'Google Style'")
1744                         .that(GOOGLE_MODULES)
1745                         .contains(sectionName);
1746             }
1747             else if ("Sun Style".equals(linkText)) {
1748                 hasSun = true;
1749                 expectedUrl = "https://github.com/search?q="
1750                         + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2Fsun_checks.xml+"
1751                         + "repo%3Acheckstyle%2Fcheckstyle+"
1752                         + sectionName;
1753 
1754                 assertWithMessage(fileName + " section '" + sectionName
1755                             + "' should be in sun_checks.xml or not reference 'Sun Style'")
1756                         .that(SUN_MODULES)
1757                         .contains(sectionName);
1758             }
1759             else if ("Checkstyle's Import Control Config".equals(linkText)) {
1760                 expectedUrl = "https://github.com/checkstyle/checkstyle/blob/master/config/"
1761                     + "import-control.xml";
1762             }
1763 
1764             assertWithMessage(fileName + " section '" + sectionName
1765                     + "' should have matching url")
1766                 .that(url)
1767                 .isEqualTo(expectedUrl);
1768         }
1769 
1770         assertWithMessage(fileName + " section '" + sectionName
1771                     + "' should have a checkstyle section")
1772                 .that(hasCheckstyle)
1773                 .isTrue();
1774         assertWithMessage(fileName + " section '" + sectionName
1775                     + "' should have a google section since it is in it's config")
1776                 .that(hasGoogle || !GOOGLE_MODULES.contains(sectionName))
1777                 .isTrue();
1778         assertWithMessage(fileName + " section '" + sectionName
1779                     + "' should have a sun section since it is in it's config")
1780                 .that(hasSun || !SUN_MODULES.contains(sectionName))
1781                 .isTrue();
1782     }
1783 
1784     private static void validatePackageSection(String fileName, String sectionName,
1785             Node subSection, Object instance) {
1786         assertWithMessage(fileName + " section '" + sectionName
1787                         + "' should have matching package")
1788             .that(subSection.getTextContent().trim())
1789             .isEqualTo(instance.getClass().getPackage().getName());
1790     }
1791 
1792     private static void validateParentSection(String fileName, String sectionName,
1793             Node subSection) {
1794         final String expected;
1795 
1796         if (!"TreeWalker".equals(sectionName) && hasParentModule(sectionName)) {
1797             expected = "TreeWalker";
1798         }
1799         else {
1800             expected = "Checker";
1801         }
1802 
1803         assertWithMessage(fileName + " section '" + sectionName + "' should have matching parent")
1804             .that(subSection.getTextContent().trim())
1805             .isEqualTo(expected);
1806     }
1807 
1808     private static boolean hasParentModule(String sectionName) {
1809         final String search = "\"" + sectionName + "\"";
1810         boolean result = true;
1811 
1812         for (String find : XML_FILESET_LIST) {
1813             if (find.contains(search)) {
1814                 result = false;
1815                 break;
1816             }
1817         }
1818 
1819         return result;
1820     }
1821 
1822     private static Set<String> getProperties(Class<?> clss) {
1823         final Set<String> result = new TreeSet<>();
1824         final PropertyDescriptor[] map = PropertyUtils.getPropertyDescriptors(clss);
1825 
1826         for (PropertyDescriptor p : map) {
1827             if (p.getWriteMethod() != null) {
1828                 result.add(p.getName());
1829             }
1830         }
1831 
1832         return result;
1833     }
1834 
1835     @Test
1836     public void testAllStyleRules() throws Exception {
1837         for (Path path : XdocUtil.getXdocsStyleFilePaths(XdocUtil.getXdocsFilePaths())) {
1838             final String fileName = path.getFileName().toString();
1839             final String styleName = fileName.substring(0, fileName.lastIndexOf('_'));
1840             final NodeList sources = getTagSourcesNode(path, "tr");
1841 
1842             final Set<String> styleChecks = switch (styleName) {
1843                 case "google" -> new HashSet<>(GOOGLE_MODULES);
1844                 case "sun" -> {
1845                     final Set<String> checks = new HashSet<>(SUN_MODULES);
1846                     checks.removeAll(IGNORED_SUN_MODULES);
1847                     yield checks;
1848                 }
1849                 default -> {
1850                     assertWithMessage("Missing modules list for style file '" + fileName + "'")
1851                             .fail();
1852                     yield null;
1853                 }
1854             };
1855 
1856             String lastRuleName = null;
1857             String[] lastRuleNumberParts = null;
1858 
1859             for (int position = 0; position < sources.getLength(); position++) {
1860                 final Node row = sources.item(position);
1861                 final List<Node> columns = new ArrayList<>(
1862                         XmlUtil.findChildElementsByTag(row, "td"));
1863 
1864                 if (columns.isEmpty()) {
1865                     continue;
1866                 }
1867 
1868                 final String ruleName = columns.get(1).getTextContent().trim();
1869                 lastRuleNumberParts = validateRuleNameOrder(
1870                         fileName, lastRuleName, lastRuleNumberParts, ruleName);
1871 
1872                 if (!"--".equals(ruleName)) {
1873                     validateStyleAnchors(XmlUtil.findChildElementsByTag(columns.get(0), "a"),
1874                             fileName, ruleName);
1875                 }
1876 
1877                 validateStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"),
1878                         XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, styleName,
1879                         ruleName);
1880 
1881                 lastRuleName = ruleName;
1882             }
1883 
1884             // these modules aren't documented, but are added to the config
1885             styleChecks.remove("BeforeExecutionExclusionFileFilter");
1886             styleChecks.remove("SuppressionFilter");
1887             styleChecks.remove("SuppressionXpathFilter");
1888             styleChecks.remove("SuppressionXpathSingleFilter");
1889             styleChecks.remove("TreeWalker");
1890             styleChecks.remove("Checker");
1891             styleChecks.remove("SuppressWithNearbyCommentFilter");
1892             styleChecks.remove("SuppressionCommentFilter");
1893             styleChecks.remove("SuppressWarningsFilter");
1894             styleChecks.remove("SuppressWarningsHolder");
1895             styleChecks.remove("SuppressWithNearbyTextFilter");
1896 
1897             assertWithMessage(
1898                     fileName + " requires the following check(s) to appear: " + styleChecks)
1899                 .that(styleChecks)
1900                 .isEmpty();
1901         }
1902     }
1903 
1904     private static String[] validateRuleNameOrder(String fileName, String lastRuleName,
1905                                                   String[] lastRuleNumberParts, String ruleName) {
1906         final String[] ruleNumberParts = ruleName.split(" ", 2)[0].split("\\.");
1907 
1908         if (lastRuleName != null) {
1909             final int ruleNumberPartsAmount = ruleNumberParts.length;
1910             final int lastRuleNumberPartsAmount = lastRuleNumberParts.length;
1911             final String outOfOrderReason = fileName + " rule '" + ruleName
1912                     + "' is out of order compared to '" + lastRuleName + "'";
1913             boolean lastRuleNumberPartWasEqual = false;
1914             int partIndex;
1915             for (partIndex = 0; partIndex < ruleNumberPartsAmount; partIndex++) {
1916                 if (lastRuleNumberPartsAmount <= partIndex) {
1917                     // equal up to here and last rule has fewer parts,
1918                     // thus order is correct, stop comparing
1919                     break;
1920                 }
1921 
1922                 final String ruleNumberPart = ruleNumberParts[partIndex];
1923                 final String lastRuleNumberPart = lastRuleNumberParts[partIndex];
1924                 final boolean ruleNumberPartsAreNumeric = IntStream.concat(
1925                         ruleNumberPart.chars(),
1926                         lastRuleNumberPart.chars()
1927                 ).allMatch(Character::isDigit);
1928 
1929                 if (ruleNumberPartsAreNumeric) {
1930                     final int numericRuleNumberPart = parseInt(ruleNumberPart);
1931                     final int numericLastRuleNumberPart = parseInt(lastRuleNumberPart);
1932                     assertWithMessage(outOfOrderReason)
1933                         .that(numericRuleNumberPart)
1934                         .isAtLeast(numericLastRuleNumberPart);
1935                 }
1936                 else {
1937                     assertWithMessage(outOfOrderReason)
1938                         .that(ruleNumberPart.compareToIgnoreCase(lastRuleNumberPart))
1939                         .isAtLeast(0);
1940                 }
1941                 lastRuleNumberPartWasEqual = ruleNumberPart.equalsIgnoreCase(lastRuleNumberPart);
1942                 if (!lastRuleNumberPartWasEqual) {
1943                     // number part is not equal but properly ordered,
1944                     // thus order is correct, stop comparing
1945                     break;
1946                 }
1947             }
1948             if (ruleNumberPartsAmount == partIndex && lastRuleNumberPartWasEqual) {
1949                 if (lastRuleNumberPartsAmount == partIndex) {
1950                     assertWithMessage(fileName + " rule '" + ruleName + "' and rule '"
1951                             + lastRuleName + "' have the same rule number").fail();
1952                 }
1953                 else {
1954                     assertWithMessage(outOfOrderReason).fail();
1955                 }
1956             }
1957         }
1958 
1959         return ruleNumberParts;
1960     }
1961 
1962     private static void validateStyleAnchors(Set<Node> anchors, String fileName, String ruleName) {
1963         assertWithMessage(fileName + " rule '" + ruleName + "' must have two row anchors")
1964             .that(anchors)
1965             .hasSize(2);
1966 
1967         final int space = ruleName.indexOf(' ');
1968         assertWithMessage(fileName + " rule '" + ruleName
1969                 + "' must have have a space between the rule's number and the rule's name")
1970             .that(space)
1971             .isNotEqualTo(-1);
1972 
1973         final String ruleNumber = ruleName.substring(0, space);
1974 
1975         int position = 1;
1976 
1977         for (Node anchor : anchors) {
1978             final String actualUrl;
1979             final String expectedUrl;
1980 
1981             if (position == 1) {
1982                 actualUrl = XmlUtil.getNameAttributeOfNode(anchor);
1983                 expectedUrl = "a" + ruleNumber;
1984             }
1985             else {
1986                 actualUrl = anchor.getAttributes().getNamedItem("href").getTextContent();
1987                 expectedUrl = "#" + ruleNumber;
1988             }
1989 
1990             assertWithMessage(fileName + " rule '" + ruleName + "' anchor "
1991                     + position + " should have matching name/url")
1992                 .that(actualUrl)
1993                 .isEqualTo(expectedUrl);
1994 
1995             position++;
1996         }
1997     }
1998 
1999     private static void validateStyleModules(Set<Node> checks, Set<Node> configs,
2000             Set<String> styleChecks, String styleName, String ruleName) {
2001         final Iterator<Node> itrChecks = checks.iterator();
2002         final Iterator<Node> itrConfigs = configs.iterator();
2003         final boolean isGoogleDocumentation = "google".equals(styleName);
2004 
2005         if (isGoogleDocumentation) {
2006             validateChapterWiseTesting(itrChecks, itrConfigs, styleChecks, styleName, ruleName);
2007         }
2008         else {
2009             validateModuleWiseTesting(itrChecks, itrConfigs, styleChecks, styleName, ruleName);
2010         }
2011 
2012         assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' has too many configs")
2013                 .that(itrConfigs.hasNext())
2014                 .isFalse();
2015     }
2016 
2017     private static void validateModuleWiseTesting(Iterator<Node> itrChecks,
2018           Iterator<Node> itrConfigs, Set<String> styleChecks, String styleName, String ruleName) {
2019         while (itrChecks.hasNext()) {
2020             final Node module = itrChecks.next();
2021             final String moduleName = module.getTextContent().trim();
2022             final String href = module.getAttributes().getNamedItem("href").getTextContent();
2023             final boolean moduleIsCheck = href.startsWith("checks/");
2024 
2025             if (!moduleIsCheck) {
2026                 continue;
2027             }
2028 
2029             assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '" + moduleName
2030                         + "' shouldn't end with 'Check'")
2031                     .that(moduleName.endsWith("Check"))
2032                     .isFalse();
2033 
2034             styleChecks.remove(moduleName);
2035 
2036             for (String configName : new String[] {"config", "test"}) {
2037                 Node config = null;
2038 
2039                 try {
2040                     config = itrConfigs.next();
2041                 }
2042                 catch (NoSuchElementException ignore) {
2043                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2044                             + moduleName + "' is missing the config link: " + configName).fail();
2045                 }
2046 
2047                 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2048                                 + moduleName + "' has mismatched config/test links")
2049                     .that(config.getTextContent().trim())
2050                     .isEqualTo(configName);
2051 
2052                 final String configUrl = config.getAttributes().getNamedItem("href")
2053                         .getTextContent();
2054 
2055                 if ("config".equals(configName)) {
2056                     final String expectedUrl = "https://github.com/search?q="
2057                             + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2F" + styleName
2058                             + "_checks.xml+repo%3Acheckstyle%2Fcheckstyle+" + moduleName;
2059 
2060                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2061                                     + moduleName + "' should have matching " + configName + " url")
2062                         .that(configUrl)
2063                         .isEqualTo(expectedUrl);
2064                 }
2065                 else if ("test".equals(configName)) {
2066                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2067                                 + moduleName + "' should have matching " + configName + " url")
2068                             .that(configUrl)
2069                             .startsWith("https://github.com/checkstyle/checkstyle/"
2070                                     + "blob/master/src/it/java/com/" + styleName
2071                                     + "/checkstyle/test/");
2072                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2073                                 + moduleName + "' should have matching " + configName + " url")
2074                             .that(configUrl)
2075                             .endsWith("/" + moduleName + "Test.java");
2076 
2077                     assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2078                                 + moduleName + "' should have a test that exists")
2079                             .that(new File(configUrl.substring(53).replace('/',
2080                                             File.separatorChar)).exists())
2081                             .isTrue();
2082                 }
2083             }
2084         }
2085     }
2086 
2087     private static void validateChapterWiseTesting(Iterator<Node> itrChecks,
2088           Iterator<Node> itrSample, Set<String> styleChecks, String styleName, String ruleName) {
2089         boolean hasChecks = false;
2090         final Set<String> usedModules = new HashSet<>();
2091 
2092         while (itrChecks.hasNext()) {
2093             final Node module = itrChecks.next();
2094             final String moduleName = module.getTextContent().trim();
2095             final String href = module.getAttributes().getNamedItem("href").getTextContent();
2096             final boolean moduleIsCheck = href.startsWith("checks/");
2097 
2098             final String partialConfigUrl = "https://github.com/search?q="
2099                     + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2F" + styleName;
2100 
2101             if (!moduleIsCheck) {
2102                 if (href.startsWith(partialConfigUrl)) {
2103                     assertWithMessage("google_style.xml rule '" + ruleName + "' module '"
2104                             + moduleName + "' has too many config links").fail();
2105                 }
2106                 continue;
2107             }
2108 
2109             hasChecks = true;
2110 
2111             assertWithMessage("The module '" + moduleName + "' in the rule '" + ruleName
2112                     + "' of the style guide '" + styleName
2113                     + "_style.xml' should not appear more than once in the section.")
2114                     .that(usedModules)
2115                     .doesNotContain(moduleName);
2116 
2117             usedModules.add(moduleName);
2118 
2119             assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2120                     + moduleName + "' shouldn't end with 'Check'")
2121                     .that(moduleName.endsWith("Check"))
2122                     .isFalse();
2123 
2124             styleChecks.remove(moduleName);
2125 
2126             if (itrChecks.hasNext()) {
2127                 final Node config = itrChecks.next();
2128 
2129                 final String configUrl = config.getAttributes()
2130                                        .getNamedItem("href").getTextContent();
2131 
2132                 final String expectedUrl =
2133                     partialConfigUrl + "_checks.xml+repo%3Acheckstyle%2Fcheckstyle+" + moduleName;
2134 
2135                 assertWithMessage(
2136                         "google_style.xml rule '" + ruleName + "' module '" + moduleName
2137                             + "' should have matching config url")
2138                     .that(configUrl)
2139                     .isEqualTo(expectedUrl);
2140             }
2141             else {
2142                 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
2143                         + moduleName + "' is missing the config link").fail();
2144             }
2145         }
2146 
2147         if (itrSample.hasNext()) {
2148             assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' should have checks"
2149                     + " if it has sample links")
2150                     .that(hasChecks)
2151                     .isTrue();
2152 
2153             final Node sample = itrSample.next();
2154             final String inputFolderUrl = sample.getAttributes().getNamedItem("href")
2155                     .getTextContent();
2156             final String extractedChapterNumber = getExtractedChapterNumber(ruleName);
2157             final String extractedSectionNumber = getExtractedSectionNumber(ruleName);
2158 
2159             assertWithMessage("google_style.xml rule '" + ruleName + "' rule '"
2160                     + "' should have matching sample url")
2161                     .that(inputFolderUrl)
2162                     .startsWith("https://github.com/checkstyle/checkstyle/"
2163                             + "tree/master/src/it/resources/com/google/checkstyle/test/");
2164 
2165             assertWithMessage("google_style.xml rule '" + ruleName
2166                     + "' should have matching sample url")
2167                 .that(inputFolderUrl)
2168                 .containsMatch(
2169                     "/chapter" + extractedChapterNumber
2170                           + "\\D[^/]+/rule" + extractedSectionNumber + "\\D");
2171 
2172             assertWithMessage("google_style.xml rule '" + ruleName
2173                     + "' should have a inputs test folder that exists")
2174                     .that(new File(inputFolderUrl.substring(53).replace('/',
2175                             File.separatorChar)).exists())
2176                     .isTrue();
2177 
2178             assertWithMessage(styleName + "_style.xml rule '" + ruleName
2179                     + "' has too many samples link")
2180                     .that(itrSample.hasNext())
2181                     .isFalse();
2182         }
2183         else {
2184             assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' is missing"
2185                  + " sample link")
2186                 .that(hasChecks)
2187                 .isFalse();
2188         }
2189     }
2190 
2191     private static String getExtractedChapterNumber(String ruleName) {
2192         final Pattern pattern = Pattern.compile("^\\d+");
2193         final Matcher matcher = pattern.matcher(ruleName);
2194         matcher.find();
2195         return matcher.group();
2196     }
2197 
2198     private static String getExtractedSectionNumber(String ruleName) {
2199         final Pattern pattern = Pattern.compile("^\\d+(\\.\\d+)*");
2200         final Matcher matcher = pattern.matcher(ruleName);
2201         matcher.find();
2202         return matcher.group().replaceAll("\\.", "");
2203     }
2204 
2205     @Test
2206     public void testAllExampleMacrosHaveParagraphWithIdBeforeThem() throws Exception {
2207         for (Path path : XdocUtil.getXdocsTemplatesFilePaths()) {
2208             final String fileName = path.getFileName().toString();
2209             final NodeList sources = getTagSourcesNode(path, "macro");
2210 
2211             for (int position = 0; position < sources.getLength(); position++) {
2212                 final Node macro = sources.item(position);
2213                 final String macroName = macro.getAttributes()
2214                         .getNamedItem("name").getTextContent();
2215 
2216                 if (!"example".equals(macroName)) {
2217                     continue;
2218                 }
2219 
2220                 final Node precedingParagraph = getPrecedingParagraph(macro);
2221                 assertWithMessage(fileName
2222                         + ": paragraph before example macro should have an id attribute")
2223                         .that(precedingParagraph.hasAttributes())
2224                         .isTrue();
2225 
2226                 final Node idAttribute = precedingParagraph.getAttributes().getNamedItem("id");
2227                 assertWithMessage(fileName
2228                         + ": paragraph before example macro should have an id attribute")
2229                         .that(idAttribute)
2230                         .isNotNull();
2231 
2232                 validatePrecedingParagraphId(macro, fileName, idAttribute);
2233             }
2234         }
2235     }
2236 
2237     private static void validatePrecedingParagraphId(
2238             Node macro, String fileName, Node idAttribute) {
2239         String exampleName = "";
2240         String exampleType = "";
2241         final NodeList params = macro.getChildNodes();
2242         for (int paramPosition = 0; paramPosition < params.getLength(); paramPosition++) {
2243             final Node item = params.item(paramPosition);
2244 
2245             if (!"param".equals(item.getNodeName())) {
2246                 continue;
2247             }
2248 
2249             final String paramName = item.getAttributes()
2250                     .getNamedItem("name").getTextContent();
2251             final String paramValue = item.getAttributes()
2252                     .getNamedItem("value").getTextContent();
2253             if ("path".equals(paramName)) {
2254                 exampleName = paramValue.substring(paramValue.lastIndexOf('/') + 1,
2255                         paramValue.lastIndexOf('.'));
2256             }
2257             else if ("type".equals(paramName)) {
2258                 exampleType = paramValue;
2259             }
2260         }
2261 
2262         final String id = idAttribute.getTextContent();
2263         final String expectedId = String.format(Locale.ROOT, "%s-%s", exampleName,
2264                 exampleType);
2265         if (expectedId.startsWith("package-info")) {
2266             assertWithMessage(fileName
2267                 + ": paragraph before example macro should have the expected id value")
2268                 .that(id)
2269                 .endsWith(expectedId);
2270         }
2271         else {
2272             assertWithMessage(fileName
2273                 + ": paragraph before example macro should have the expected id value")
2274                 .that(id)
2275                 .isEqualTo(expectedId);
2276         }
2277     }
2278 
2279     private static Node getPrecedingParagraph(Node macro) {
2280         Node precedingNode = macro.getPreviousSibling();
2281         while (!"p".equals(precedingNode.getNodeName())) {
2282             precedingNode = precedingNode.getPreviousSibling();
2283         }
2284         return precedingNode;
2285     }
2286 
2287     @Test
2288     public void validateExampleSectionSeparation() throws Exception {
2289         final List<Path> templates = collectAllXmlTemplatesUnderSrcSite();
2290 
2291         for (final Path template : templates) {
2292             final Document doc = parseXmlToDomDocument(template);
2293             final NodeList subsectionList = doc.getElementsByTagName("subsection");
2294 
2295             for (int index = 0; index < subsectionList.getLength(); index++) {
2296                 final Element subsection = (Element) subsectionList.item(index);
2297                 if (!"Examples".equals(subsection.getAttribute("name"))) {
2298                     continue;
2299                 }
2300 
2301                 final NodeList children = subsection.getChildNodes();
2302                 String lastExampleIdPrefix = null;
2303                 boolean separatorSeen = false;
2304 
2305                 for (int childIndex = 0; childIndex < children.getLength(); childIndex++) {
2306                     final Node child = children.item(childIndex);
2307                     if (child.getNodeType() != Node.ELEMENT_NODE) {
2308                         continue;
2309                     }
2310 
2311                     final Element element = (Element) child;
2312                     if ("hr".equals(element.getTagName())
2313                             && "example-separator".equals(element.getAttribute("class"))) {
2314                         separatorSeen = true;
2315                         continue;
2316                     }
2317 
2318                     final String currentId = element.getAttribute("id");
2319                     if (currentId != null && currentId.startsWith("Example")) {
2320                         final String currentExPrefix = getExamplePrefix(currentId);
2321                         if (lastExampleIdPrefix != null
2322                                 && !lastExampleIdPrefix.equals(currentExPrefix)) {
2323                             assertWithMessage("Missing <hr class=\"example-separator\"/> "
2324                                     + "between " + lastExampleIdPrefix + " and " + currentExPrefix
2325                                     + " in file: " + template)
2326                                     .that(separatorSeen)
2327                                     .isTrue();
2328                             separatorSeen = false;
2329                         }
2330                         lastExampleIdPrefix = currentExPrefix;
2331                     }
2332                 }
2333             }
2334         }
2335     }
2336 
2337     private static List<Path> collectAllXmlTemplatesUnderSrcSite() throws IOException {
2338         final Path root = Paths.get("src/site/xdoc");
2339         try (Stream<Path> walk = Files.walk(root)) {
2340             return walk
2341                     .filter(path -> path.getFileName().toString().endsWith(".xml.template"))
2342                     .collect(toImmutableList());
2343         }
2344     }
2345 
2346     private static Document parseXmlToDomDocument(Path template) throws Exception {
2347         final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
2348         dbFactory.setNamespaceAware(true);
2349         final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
2350         final Document doc = dBuilder.parse(template.toFile());
2351         doc.getDocumentElement().normalize();
2352         return doc;
2353     }
2354 
2355     private static String getExamplePrefix(String id) {
2356         final int dash = id.indexOf('-');
2357         final String result;
2358         if (dash == -1) {
2359             result = id;
2360         }
2361         else {
2362             result = id.substring(0, dash);
2363         }
2364         return result;
2365     }
2366 
2367     @FunctionalInterface
2368     private interface PredicateProcess {
2369         boolean hasFit(Path path);
2370     }
2371 }