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