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