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