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