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