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