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