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