1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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 java.lang.Integer.parseInt;
23  import static java.nio.charset.StandardCharsets.UTF_8;
24  import static org.hamcrest.CoreMatchers.describedAs;
25  import static org.hamcrest.CoreMatchers.is;
26  
27  import java.beans.PropertyDescriptor;
28  import java.io.File;
29  import java.io.IOException;
30  import java.io.StringReader;
31  import java.lang.reflect.Array;
32  import java.lang.reflect.Field;
33  import java.lang.reflect.ParameterizedType;
34  import java.net.URI;
35  import java.nio.file.Files;
36  import java.nio.file.Path;
37  import java.nio.file.Paths;
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.HashSet;
44  import java.util.Iterator;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.NoSuchElementException;
48  import java.util.Properties;
49  import java.util.Set;
50  import java.util.TreeSet;
51  import java.util.regex.Pattern;
52  import java.util.stream.IntStream;
53  
54  import org.apache.commons.beanutils.PropertyUtils;
55  import org.junit.Assert;
56  import org.junit.Test;
57  import org.w3c.dom.Document;
58  import org.w3c.dom.Node;
59  import org.w3c.dom.NodeList;
60  import org.xml.sax.InputSource;
61  
62  import com.puppycrawl.tools.checkstyle.Checker;
63  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
64  import com.puppycrawl.tools.checkstyle.ModuleFactory;
65  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
66  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
67  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
68  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
69  import com.puppycrawl.tools.checkstyle.api.Configuration;
70  import com.puppycrawl.tools.checkstyle.api.Scope;
71  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
72  import com.puppycrawl.tools.checkstyle.checks.LineSeparatorOption;
73  import com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.ClosingParens;
74  import com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.ElementStyle;
75  import com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck.TrailingArrayComma;
76  import com.puppycrawl.tools.checkstyle.checks.blocks.BlockOption;
77  import com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyOption;
78  import com.puppycrawl.tools.checkstyle.checks.blocks.RightCurlyOption;
79  import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderOption;
80  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
81  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifier;
82  import com.puppycrawl.tools.checkstyle.checks.whitespace.PadOption;
83  import com.puppycrawl.tools.checkstyle.checks.whitespace.WrapOption;
84  import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
85  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
86  import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
87  import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
88  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
89  
90  public class XdocsPagesTest {
91  
92      private static final Path AVAILABLE_CHECKS_PATH = Paths.get("src/xdocs/checks.xml");
93      private static final String LINK_TEMPLATE =
94              "(?s).*<a href=\"config_\\w+\\.html#%1$s\">(\\s)*%1$s</a>.*";
95  
96      private static final Pattern VERSION = Pattern.compile("\\d+\\.\\d+(\\.\\d+)?");
97  
98      private static final Pattern DESCRIPTION_VERSION = Pattern
99              .compile("^Since Checkstyle \\d+\\.\\d+(\\.\\d+)?");
100 
101     private static final List<String> XML_FILESET_LIST = Arrays.asList(
102             "TreeWalker",
103             "name=\"Checker\"",
104             "name=\"Header\"",
105             "name=\"Translation\"",
106             "name=\"SeverityMatchFilter\"",
107             "name=\"SuppressWithPlainTextCommentFilter\"",
108             "name=\"SuppressionFilter\"",
109             "name=\"SuppressWarningsFilter\"",
110             "name=\"BeforeExecutionExclusionFileFilter\"",
111             "name=\"RegexpHeader\"",
112             "name=\"RegexpOnFilename\"",
113             "name=\"RegexpSingleline\"",
114             "name=\"RegexpMultiline\"",
115             "name=\"JavadocPackage\"",
116             "name=\"NewlineAtEndOfFile\"",
117             "name=\"UniqueProperties\"",
118             "name=\"FileLength\"",
119             "name=\"FileTabCharacter\""
120     );
121 
122     private static final Set<String> CHECK_PROPERTIES = getProperties(AbstractCheck.class);
123     private static final Set<String> JAVADOC_CHECK_PROPERTIES =
124             getProperties(AbstractJavadocCheck.class);
125     private static final Set<String> FILESET_PROPERTIES = getProperties(AbstractFileSetCheck.class);
126 
127     private static final List<String> UNDOCUMENTED_PROPERTIES = Arrays.asList(
128             "Checker.classLoader",
129             "Checker.classloader",
130             "Checker.moduleClassLoader",
131             "Checker.moduleFactory",
132             "TreeWalker.classLoader",
133             "TreeWalker.moduleFactory",
134             "TreeWalker.cacheFile",
135             "TreeWalker.upChild",
136             "SuppressWithNearbyCommentFilter.fileContents",
137             "SuppressionCommentFilter.fileContents"
138     );
139 
140     private static final List<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Arrays.asList(
141             // static field (all upper case)
142             "SuppressWarningsHolder.aliasList",
143             // loads string into memory similar to file
144             "Header.header",
145             "RegexpHeader.header"
146     );
147 
148     private static final Set<String> SUN_MODULES = Collections.unmodifiableSet(
149         new HashSet<>(CheckUtil.getConfigSunStyleModules()));
150     // ignore the not yet properly covered modules while testing newly added ones
151     // add proper sections to the coverage report and integration tests
152     // and then remove this list eventually
153     private static final List<String> IGNORED_SUN_MODULES = Arrays.asList(
154             "ArrayTypeStyle",
155             "AvoidNestedBlocks",
156             "AvoidStarImport",
157             "ConstantName",
158             "DesignForExtension",
159             "EmptyBlock",
160             "EmptyForIteratorPad",
161             "EmptyStatement",
162             "EqualsHashCode",
163             "FileLength",
164             "FileTabCharacter",
165             "FinalClass",
166             "FinalParameters",
167             "GenericWhitespace",
168             "HiddenField",
169             "HideUtilityClassConstructor",
170             "IllegalImport",
171             "IllegalInstantiation",
172             "InnerAssignment",
173             "InterfaceIsType",
174             "JavadocMethod",
175             "JavadocPackage",
176             "JavadocStyle",
177             "JavadocType",
178             "JavadocVariable",
179             "LeftCurly",
180             "LineLength",
181             "LocalFinalVariableName",
182             "LocalVariableName",
183             "MagicNumber",
184             "MemberName",
185             "MethodLength",
186             "MethodName",
187             "MethodParamPad",
188             "MissingSwitchDefault",
189             "ModifierOrder",
190             "NeedBraces",
191             "NewlineAtEndOfFile",
192             "NoWhitespaceAfter",
193             "NoWhitespaceBefore",
194             "OperatorWrap",
195             "PackageName",
196             "ParameterName",
197             "ParameterNumber",
198             "ParenPad",
199             "RedundantImport",
200             "RedundantModifier",
201             "RegexpSingleline",
202             "RightCurly",
203             "SimplifyBooleanExpression",
204             "SimplifyBooleanReturn",
205             "StaticVariableName",
206             "TodoComment",
207             "Translation",
208             "TypecastParenPad",
209             "TypeName",
210             "UnusedImports",
211             "UpperEll",
212             "VisibilityModifier",
213             "WhitespaceAfter",
214             "WhitespaceAround"
215     );
216     private static final Set<String> GOOGLE_MODULES = Collections.unmodifiableSet(
217         new HashSet<>(CheckUtil.getConfigGoogleStyleModules()));
218 
219     @Test
220     public void testAllChecksPresentOnAvailableChecksPage() throws Exception {
221         final String availableChecks = new String(Files.readAllBytes(AVAILABLE_CHECKS_PATH), UTF_8);
222 
223         CheckUtil.getSimpleNames(CheckUtil.getCheckstyleChecks())
224             .forEach(checkName -> {
225                 if (!isPresent(availableChecks, checkName)) {
226                     Assert.fail(checkName + " is not correctly listed on Available Checks page"
227                         + " - add it to " + AVAILABLE_CHECKS_PATH);
228                 }
229             });
230     }
231 
232     private static boolean isPresent(String availableChecks, String checkName) {
233         final String linkPattern = String.format(Locale.ROOT, LINK_TEMPLATE, checkName);
234         return availableChecks.matches(linkPattern);
235     }
236 
237     @Test
238     public void testAllSubSections() throws Exception {
239         for (Path path : XdocUtil.getXdocsFilePaths()) {
240             final String input = new String(Files.readAllBytes(path), UTF_8);
241             final String fileName = path.getFileName().toString();
242 
243             final Document document = XmlUtil.getRawXml(fileName, input, input);
244             final NodeList subSections = document.getElementsByTagName("subsection");
245 
246             for (int position = 0; position < subSections.getLength(); position++) {
247                 final Node subSection = subSections.item(position);
248                 final Node name = subSection.getAttributes().getNamedItem("name");
249 
250                 Assert.assertNotNull("All sub-sections in '" + fileName + "' must have a name",
251                         name);
252 
253                 final Node id = subSection.getAttributes().getNamedItem("id");
254 
255                 Assert.assertNotNull("All sub-sections in '" + fileName + "' must have an id", id);
256 
257                 final String sectionName;
258 
259                 if ("google_style.xml".equals(fileName)) {
260                     sectionName = "Google";
261                 }
262                 else if ("sun_style.xml".equals(fileName)) {
263                     sectionName = "Sun";
264                 }
265                 else {
266                     sectionName = subSection.getParentNode().getAttributes()
267                             .getNamedItem("name").getTextContent();
268                 }
269 
270                 final String nameString = name.getNodeValue();
271                 final String idString = id.getNodeValue();
272 
273                 Assert.assertEquals(fileName + " sub-section " + nameString + " for section "
274                         + sectionName + " must match",
275                         (sectionName + " " + nameString).replace(' ', '_'), idString);
276             }
277         }
278     }
279 
280     @Test
281     public void testAllXmlExamples() throws Exception {
282         for (Path path : XdocUtil.getXdocsFilePaths()) {
283             final String input = new String(Files.readAllBytes(path), UTF_8);
284             final String fileName = path.getFileName().toString();
285 
286             final Document document = XmlUtil.getRawXml(fileName, input, input);
287             final NodeList sources = document.getElementsByTagName("source");
288 
289             for (int position = 0; position < sources.getLength(); position++) {
290                 final String unserializedSource = sources.item(position).getTextContent()
291                         .replace("...", "").trim();
292 
293                 if (unserializedSource.charAt(0) != '<'
294                         || unserializedSource.charAt(unserializedSource.length() - 1) != '>'
295                         // no dtd testing yet
296                         || unserializedSource.contains("<!")) {
297                     continue;
298                 }
299 
300                 final String code = buildXml(unserializedSource);
301                 // validate only
302                 XmlUtil.getRawXml(fileName, code, unserializedSource);
303 
304                 // can't test ant structure, or old and outdated checks
305                 Assert.assertTrue("Xml is invalid, old or has outdated structure",
306                         fileName.startsWith("anttask")
307                         || fileName.startsWith("releasenotes")
308                         || isValidCheckstyleXml(fileName, code, unserializedSource));
309             }
310         }
311     }
312 
313     private static String buildXml(String unserializedSource) throws IOException {
314         // not all examples come with the full xml structure
315         String code = unserializedSource
316             // don't corrupt our own cachefile
317             .replace("target/cachefile", "target/cachefile-test");
318 
319         if (!hasFileSetClass(code)) {
320             code = "<module name=\"TreeWalker\">\n" + code + "\n</module>";
321         }
322         if (!code.contains("name=\"Checker\"")) {
323             code = "<module name=\"Checker\">\n" + code + "\n</module>";
324         }
325         if (!code.startsWith("<?xml")) {
326             final String dtdPath = new File(
327                     "src/main/resources/com/puppycrawl/tools/checkstyle/configuration_1_3.dtd")
328                     .getCanonicalPath();
329 
330             code = "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC "
331                     + "\"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\" \"" + dtdPath
332                     + "\">\n" + code;
333         }
334         return code;
335     }
336 
337     private static boolean hasFileSetClass(String xml) {
338         boolean found = false;
339 
340         for (String find : XML_FILESET_LIST) {
341             if (xml.contains(find)) {
342                 found = true;
343                 break;
344             }
345         }
346 
347         return found;
348     }
349 
350     private static boolean isValidCheckstyleXml(String fileName, String code,
351                                                 String unserializedSource)
352             throws IOException, CheckstyleException {
353         // can't process non-existent examples, or out of context snippets
354         if (!code.contains("com.mycompany") && !code.contains("checkstyle-packages")
355                 && !code.contains("MethodLimit") && !code.contains("<suppress ")
356                 && !code.contains("<suppress-xpath ")
357                 && !code.contains("<import-control ")
358                 && !unserializedSource.startsWith("<property ")
359                 && !unserializedSource.startsWith("<taskdef ")) {
360             // validate checkstyle structure and contents
361             try {
362                 final Properties properties = new Properties();
363 
364                 properties.setProperty("checkstyle.header.file",
365                         new File("config/java.header").getCanonicalPath());
366 
367                 final PropertiesExpander expander = new PropertiesExpander(properties);
368                 final Configuration config = ConfigurationLoader.loadConfiguration(new InputSource(
369                         new StringReader(code)), expander, false);
370                 final Checker checker = new Checker();
371 
372                 try {
373                     final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
374                     checker.setModuleClassLoader(moduleClassLoader);
375                     checker.configure(config);
376                 }
377                 finally {
378                     checker.destroy();
379                 }
380             }
381             catch (CheckstyleException ex) {
382                 throw new CheckstyleException(fileName + " has invalid Checkstyle xml ("
383                         + ex.getMessage() + "): " + unserializedSource, ex);
384             }
385         }
386         return true;
387     }
388 
389     @Test
390     public void testAllCheckSections() throws Exception {
391         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
392 
393         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
394             final String fileName = path.getFileName().toString();
395 
396             if ("config_reporting.xml".equals(fileName)) {
397                 continue;
398             }
399 
400             final String input = new String(Files.readAllBytes(path), UTF_8);
401             final Document document = XmlUtil.getRawXml(fileName, input, input);
402             final NodeList sources = document.getElementsByTagName("section");
403             String lastSectionName = null;
404 
405             for (int position = 0; position < sources.getLength(); position++) {
406                 final Node section = sources.item(position);
407                 final String sectionName = section.getAttributes().getNamedItem("name")
408                         .getNodeValue();
409 
410                 if ("Content".equals(sectionName) || "Overview".equals(sectionName)) {
411                     Assert.assertNull(fileName + " section '" + sectionName + "' should be first",
412                             lastSectionName);
413                     continue;
414                 }
415 
416                 Assert.assertTrue(fileName + " section '" + sectionName
417                         + "' shouldn't end with 'Check'", !sectionName.endsWith("Check"));
418                 if (lastSectionName != null) {
419                     Assert.assertTrue(
420                             fileName + " section '" + sectionName
421                                     + "' is out of order compared to '" + lastSectionName + "'",
422                             sectionName.toLowerCase(Locale.ENGLISH).compareTo(
423                                     lastSectionName.toLowerCase(Locale.ENGLISH)) >= 0);
424                 }
425 
426                 validateCheckSection(moduleFactory, fileName, sectionName, section);
427 
428                 lastSectionName = sectionName;
429             }
430         }
431     }
432 
433     /**
434      * Test contains asserts in callstack, but idea does not see them.
435      * @noinspection JUnitTestMethodWithNoAssertions
436      */
437     @Test
438     public void testAllCheckSectionsEx() throws Exception {
439         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
440 
441         final Path path = Paths.get(XdocUtil.DIRECTORY_PATH + "/config.xml");
442         final String fileName = path.getFileName().toString();
443 
444         final String input = new String(Files.readAllBytes(path), UTF_8);
445         final Document document = XmlUtil.getRawXml(fileName, input, input);
446         final NodeList sources = document.getElementsByTagName("section");
447 
448         for (int position = 0; position < sources.getLength(); position++) {
449             final Node section = sources.item(position);
450             final String sectionName = section.getAttributes().getNamedItem("name")
451                     .getNodeValue();
452 
453             if (!"Checker".equals(sectionName) && !"TreeWalker".equals(sectionName)) {
454                 continue;
455             }
456 
457             validateCheckSection(moduleFactory, fileName, sectionName, section);
458         }
459     }
460 
461     private static void validateCheckSection(ModuleFactory moduleFactory, String fileName,
462             String sectionName, Node section) throws Exception {
463         final Object instance;
464 
465         try {
466             instance = moduleFactory.createModule(sectionName);
467         }
468         catch (CheckstyleException ex) {
469             throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, ex);
470         }
471 
472         int subSectionPos = 0;
473         for (Node subSection : XmlUtil.getChildrenElements(section)) {
474             if (subSectionPos == 0 && "p".equals(subSection.getNodeName())) {
475                 validateSinceDescriptionSection(fileName, sectionName, subSection);
476                 continue;
477             }
478 
479             final String subSectionName = subSection.getAttributes().getNamedItem("name")
480                     .getNodeValue();
481 
482             // can be in different orders, and completely optional
483             if ("Notes".equals(subSectionName)
484                     || "Rule Description".equals(subSectionName)
485                     || "Metadata".equals(subSectionName)) {
486                 continue;
487             }
488 
489             // optional sections that can be skipped if they have nothing to report
490             if (subSectionPos == 1 && !"Properties".equals(subSectionName)) {
491                 validatePropertySection(fileName, sectionName, null, instance);
492                 subSectionPos++;
493             }
494             if (subSectionPos == 4 && !"Error Messages".equals(subSectionName)) {
495                 validateErrorSection(fileName, sectionName, null, instance);
496                 subSectionPos++;
497             }
498 
499             Assert.assertEquals(fileName + " section '" + sectionName
500                     + "' should be in order", getSubSectionName(subSectionPos),
501                     subSectionName);
502 
503             switch (subSectionPos) {
504                 case 0:
505                     break;
506                 case 1:
507                     validatePropertySection(fileName, sectionName, subSection, instance);
508                     break;
509                 case 2:
510                     break;
511                 case 3:
512                     validateUsageExample(fileName, sectionName, subSection);
513                     break;
514                 case 4:
515                     validateErrorSection(fileName, sectionName, subSection, instance);
516                     break;
517                 case 5:
518                     validatePackageSection(fileName, sectionName, subSection, instance);
519                     break;
520                 case 6:
521                     validateParentSection(fileName, sectionName, subSection);
522                     break;
523                 default:
524                     break;
525             }
526 
527             subSectionPos++;
528         }
529 
530         if ("Checker".equals(sectionName)) {
531             Assert.assertTrue(fileName + " section '" + sectionName
532                     + "' should contain up to 'Package' sub-section", subSectionPos >= 6);
533         }
534         else {
535             Assert.assertTrue(fileName + " section '" + sectionName
536                     + "' should contain up to 'Parent' sub-section", subSectionPos >= 7);
537         }
538     }
539 
540     private static void validateSinceDescriptionSection(String fileName, String sectionName,
541             Node subSection) {
542         Assert.assertTrue(fileName + " section '" + sectionName
543                 + "' should have a valid version at the start of the description like:\n"
544                 + DESCRIPTION_VERSION.pattern(),
545                 DESCRIPTION_VERSION.matcher(subSection.getTextContent().trim()).find());
546     }
547 
548     private static Object getSubSectionName(int subSectionPos) {
549         final String result;
550 
551         switch (subSectionPos) {
552             case 0:
553                 result = "Description";
554                 break;
555             case 1:
556                 result = "Properties";
557                 break;
558             case 2:
559                 result = "Examples";
560                 break;
561             case 3:
562                 result = "Example of Usage";
563                 break;
564             case 4:
565                 result = "Error Messages";
566                 break;
567             case 5:
568                 result = "Package";
569                 break;
570             case 6:
571                 result = "Parent Module";
572                 break;
573             default:
574                 result = null;
575                 break;
576         }
577 
578         return result;
579     }
580 
581     private static void validatePropertySection(String fileName, String sectionName,
582             Node subSection, Object instance) throws Exception {
583         final Set<String> properties = getProperties(instance.getClass());
584         final Class<?> clss = instance.getClass();
585 
586         fixCapturedProperties(sectionName, instance, clss, properties);
587 
588         if (subSection != null) {
589             Assert.assertTrue(fileName + " section '" + sectionName
590                     + "' should have no properties to show", !properties.isEmpty());
591 
592             validatePropertySectionProperties(fileName, sectionName, subSection, instance,
593                     properties);
594         }
595 
596         Assert.assertTrue(fileName + " section '" + sectionName + "' should show properties: "
597                 + properties, properties.isEmpty());
598     }
599 
600     private static void fixCapturedProperties(String sectionName, Object instance, Class<?> clss,
601             Set<String> properties) {
602         // remove global properties that don't need documentation
603         if (hasParentModule(sectionName)) {
604             if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
605                 properties.removeAll(JAVADOC_CHECK_PROPERTIES);
606 
607                 // override
608                 properties.add("violateExecutionOnNonTightHtml");
609             }
610             else if (AbstractCheck.class.isAssignableFrom(clss)) {
611                 properties.removeAll(CHECK_PROPERTIES);
612             }
613         }
614         if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
615             properties.removeAll(FILESET_PROPERTIES);
616 
617             // override
618             properties.add("fileExtensions");
619         }
620 
621         // remove undocumented properties
622         new HashSet<>(properties).stream()
623             .filter(prop -> UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + "." + prop))
624             .forEach(properties::remove);
625 
626         if (AbstractCheck.class.isAssignableFrom(clss)) {
627             final AbstractCheck check = (AbstractCheck) instance;
628 
629             final int[] acceptableTokens = check.getAcceptableTokens();
630             Arrays.sort(acceptableTokens);
631             final int[] defaultTokens = check.getDefaultTokens();
632             Arrays.sort(defaultTokens);
633             final int[] requiredTokens = check.getRequiredTokens();
634             Arrays.sort(requiredTokens);
635 
636             if (!Arrays.equals(acceptableTokens, defaultTokens)
637                     || !Arrays.equals(acceptableTokens, requiredTokens)) {
638                 properties.add("tokens");
639             }
640         }
641 
642         if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
643             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
644 
645             final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
646             Arrays.sort(acceptableJavadocTokens);
647             final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
648             Arrays.sort(defaultJavadocTokens);
649             final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
650             Arrays.sort(requiredJavadocTokens);
651 
652             if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
653                     || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
654                 properties.add("javadocTokens");
655             }
656         }
657     }
658 
659     private static void validatePropertySectionProperties(String fileName, String sectionName,
660             Node subSection, Object instance, Set<String> properties) throws Exception {
661         boolean skip = true;
662         boolean didJavadocTokens = false;
663         boolean didTokens = false;
664 
665         for (Node row : XmlUtil.getChildrenElements(XmlUtil.getFirstChildElement(subSection))) {
666             final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
667 
668             Assert.assertEquals(fileName + " section '" + sectionName
669                     + "' should have the requested columns", 5, columns.size());
670 
671             if (skip) {
672                 Assert.assertEquals(fileName + " section '" + sectionName
673                         + "' should have the specific title", "name", columns.get(0)
674                         .getTextContent());
675                 Assert.assertEquals(fileName + " section '" + sectionName
676                         + "' should have the specific title", "description", columns.get(1)
677                         .getTextContent());
678                 Assert.assertEquals(fileName + " section '" + sectionName
679                         + "' should have the specific title", "type", columns.get(2)
680                         .getTextContent());
681                 Assert.assertEquals(fileName + " section '" + sectionName
682                         + "' should have the specific title", "default value", columns.get(3)
683                         .getTextContent());
684                 Assert.assertEquals(fileName + " section '" + sectionName
685                         + "' should have the specific title", "since", columns.get(4)
686                         .getTextContent());
687 
688                 skip = false;
689                 continue;
690             }
691 
692             Assert.assertFalse(fileName + " section '" + sectionName
693                     + "' should have token properties last", didTokens);
694 
695             final String propertyName = columns.get(0).getTextContent();
696             Assert.assertTrue(fileName + " section '" + sectionName
697                     + "' should not contain the property: " + propertyName,
698                     properties.remove(propertyName));
699 
700             if ("tokens".equals(propertyName)) {
701                 final AbstractCheck check = (AbstractCheck) instance;
702                 validatePropertySectionPropertyTokens(fileName, sectionName, check, columns);
703                 didTokens = true;
704             }
705             else if ("javadocTokens".equals(propertyName)) {
706                 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
707                 validatePropertySectionPropertyJavadocTokens(fileName, sectionName, check, columns);
708                 didJavadocTokens = true;
709             }
710             else {
711                 Assert.assertFalse(fileName + " section '" + sectionName
712                         + "' should have javadoc token properties next to last, before tokens",
713                         didJavadocTokens);
714 
715                 validatePropertySectionPropertyEx(fileName, sectionName, instance, columns,
716                         propertyName);
717             }
718 
719             Assert.assertFalse(fileName + " section '" + sectionName
720                     + "' should have a version for " + propertyName, columns.get(4)
721                     .getTextContent().trim().isEmpty());
722             Assert.assertTrue(fileName + " section '" + sectionName
723                     + "' should have a valid version for " + propertyName,
724                     VERSION.matcher(columns.get(4).getTextContent().trim()).matches());
725         }
726     }
727 
728     private static void validatePropertySectionPropertyEx(String fileName, String sectionName,
729             Object instance, List<Node> columns, String propertyName) throws Exception {
730         Assert.assertFalse(fileName + " section '" + sectionName
731                 + "' should have a description for " + propertyName, columns.get(1)
732                 .getTextContent().trim().isEmpty());
733 
734         final String actualTypeName = columns.get(2).getTextContent().replace("\n", "")
735                 .replace("\r", "").replaceAll(" +", " ").trim();
736 
737         Assert.assertFalse(fileName + " section '" + sectionName + "' should have a type for "
738                 + propertyName, actualTypeName.isEmpty());
739 
740         final Field field = getField(instance.getClass(), propertyName);
741         final Class<?> fieldClss = getFieldClass(fileName, sectionName, instance, field,
742                 propertyName);
743 
744         final String expectedTypeName = getModulePropertyExpectedTypeName(sectionName, fieldClss,
745                 instance, propertyName);
746         final String expectedValue = getModulePropertyExpectedValue(sectionName, propertyName,
747                 field, fieldClss, instance);
748 
749         Assert.assertEquals(fileName + " section '" + sectionName
750                 + "' should have the type for " + propertyName, expectedTypeName,
751                 actualTypeName);
752 
753         if (expectedValue != null) {
754             final String actualValue = columns.get(3).getTextContent().replace("\n", "")
755                     .replace("\r", "").replaceAll(" +", " ").trim();
756 
757             Assert.assertEquals(fileName + " section '" + sectionName
758                     + "' should have the value for " + propertyName, expectedValue,
759                     actualValue);
760         }
761     }
762 
763     private static void validatePropertySectionPropertyTokens(String fileName, String sectionName,
764             AbstractCheck check, List<Node> columns) {
765         Assert.assertEquals(fileName + " section '" + sectionName
766                 + "' should have the basic token description", "tokens to check", columns.get(1)
767                 .getTextContent());
768         Assert.assertEquals(
769                 fileName + " section '" + sectionName + "' should have all the acceptable tokens",
770                 "subset of tokens "
771                         + CheckUtil.getTokenText(check.getAcceptableTokens(),
772                                 check.getRequiredTokens()), columns.get(2).getTextContent()
773                         .replaceAll("\\s+", " ").trim());
774         Assert.assertEquals(fileName + " section '" + sectionName
775                 + "' should have all the default tokens",
776                 CheckUtil.getTokenText(check.getDefaultTokens(), check.getRequiredTokens()),
777                 columns.get(3).getTextContent().replaceAll("\\s+", " ").trim());
778     }
779 
780     private static void validatePropertySectionPropertyJavadocTokens(String fileName,
781             String sectionName, AbstractJavadocCheck check, List<Node> columns) {
782         Assert.assertEquals(fileName + " section '" + sectionName
783                 + "' should have the basic token javadoc description", "javadoc tokens to check",
784                 columns.get(1).getTextContent());
785         Assert.assertEquals(
786                 fileName + " section '" + sectionName
787                         + "' should have all the acceptable javadoc tokens",
788                 "subset of javadoc tokens "
789                         + CheckUtil.getJavadocTokenText(check.getAcceptableJavadocTokens(),
790                                 check.getRequiredJavadocTokens()), columns.get(2).getTextContent()
791                         .replaceAll("\\s+", " ").trim());
792         Assert.assertEquals(
793                 fileName + " section '" + sectionName
794                         + "' should have all the default javadoc tokens",
795                 CheckUtil.getJavadocTokenText(check.getDefaultJavadocTokens(),
796                         check.getRequiredJavadocTokens()), columns.get(3).getTextContent()
797                         .replaceAll("\\s+", " ").trim());
798     }
799 
800     /**
801      * Get's the name of the bean property's type for the class.
802      * @param sectionName The name of the section/module being worked on.
803      * @param fieldClass The bean property's type.
804      * @param instance The class instance to work with.
805      * @param propertyName The property name to work with.
806      * @return String form of property's type.
807      * @noinspection IfStatementWithTooManyBranches, OverlyComplexBooleanExpression
808      */
809     private static String getModulePropertyExpectedTypeName(String sectionName, Class<?> fieldClass,
810             Object instance, String propertyName) {
811         final String instanceName = instance.getClass().getSimpleName();
812         String result = null;
813 
814         if (("SuppressionCommentFilter".equals(sectionName)
815                 || "SuppressWithNearbyCommentFilter".equals(sectionName)
816                 || "SuppressWithPlainTextCommentFilter".equals(sectionName))
817                     && ("checkFormat".equals(propertyName)
818                         || "messageFormat".equals(propertyName)
819                         || "influenceFormat".equals(propertyName))
820                 || ("RegexpMultiline".equals(sectionName)
821                     || "RegexpSingleline".equals(sectionName)
822                     || "RegexpSinglelineJava".equals(sectionName))
823                     && "format".equals(propertyName)) {
824             // dynamic custom expression
825             result = "Regular Expression";
826         }
827         else if ("CustomImportOrder".equals(sectionName)
828                 && "customImportOrderRules".equals(propertyName)) {
829             // specially separated list
830             result = "String";
831         }
832         else if (fieldClass == boolean.class) {
833             result = "Boolean";
834         }
835         else if (fieldClass == int.class) {
836             result = "Integer";
837         }
838         else if (fieldClass == int[].class) {
839             if (isPropertyTokenType(sectionName, propertyName)) {
840                 result = "subset of tokens TokenTypes";
841             }
842             else {
843                 result = "Integer Set";
844             }
845         }
846         else if (fieldClass == double[].class) {
847             result = "Number Set";
848         }
849         else if (fieldClass == String.class) {
850             result = "String";
851 
852             if ("Checker".equals(sectionName) && "localeCountry".equals(propertyName)) {
853                 result += " (either the empty string or an uppercase ISO 3166 2-letter code)";
854             }
855             else if ("Checker".equals(sectionName) && "localeLanguage".equals(propertyName)) {
856                 result += " (either the empty string or a lowercase ISO 639 code)";
857             }
858         }
859         else if (fieldClass == String[].class) {
860             if (propertyName.endsWith("Tokens") || propertyName.endsWith("Token")
861                     || "AtclauseOrderCheck".equals(instanceName) && "target".equals(propertyName)
862                     || "MultipleStringLiteralsCheck".equals(instanceName)
863                             && "ignoreOccurrenceContext".equals(propertyName)) {
864                 result = "subset of tokens TokenTypes";
865             }
866             else {
867                 result = "String Set";
868             }
869         }
870         else if (fieldClass == URI.class) {
871             result = "URI";
872         }
873         else if (fieldClass == Pattern.class) {
874             result = "Regular Expression";
875         }
876         else if (fieldClass == Pattern[].class) {
877             result = "Regular Expressions";
878         }
879         else if (fieldClass == SeverityLevel.class) {
880             result = "Severity";
881         }
882         else if (fieldClass == Scope.class) {
883             result = "Scope";
884         }
885         else if (fieldClass == ElementStyle.class) {
886             result = "Element Style";
887         }
888         else if (fieldClass == ClosingParens.class) {
889             result = "Closing Parens";
890         }
891         else if (fieldClass == TrailingArrayComma.class) {
892             result = "Trailing Comma";
893         }
894         else if (fieldClass == PadOption.class) {
895             result = "Pad Policy";
896         }
897         else if (fieldClass == WrapOption.class) {
898             result = "Wrap Operator Policy";
899         }
900         else if (fieldClass == BlockOption.class) {
901             result = "Block Policy";
902         }
903         else if (fieldClass == LeftCurlyOption.class) {
904             result = "Left Curly Brace Policy";
905         }
906         else if (fieldClass == RightCurlyOption.class) {
907             result = "Right Curly Brace Policy";
908         }
909         else if (fieldClass == LineSeparatorOption.class) {
910             result = "Line Separator Policy";
911         }
912         else if (fieldClass == ImportOrderOption.class) {
913             result = "Import Order Policy";
914         }
915         else if (fieldClass == AccessModifier[].class) {
916             result = "Access Modifier Set";
917         }
918         else if ("PropertyCacheFile".equals(fieldClass.getSimpleName())) {
919             result = "File";
920         }
921         else {
922             Assert.fail("Unknown property type: " + fieldClass.getSimpleName());
923         }
924 
925         if ("SuppressWarningsHolder".equals(instanceName)) {
926             result = result + " in a format of comma separated attribute=value entries. The "
927                     + "attribute is the fully qualified name of the Check and value is its alias.";
928         }
929 
930         return result;
931     }
932 
933     /**
934      * Get's the name of the bean property's default value for the class.
935      * @param sectionName The name of the section/module being worked on.
936      * @param propertyName The property name to work with.
937      * @param field The bean property's field.
938      * @param fieldClass The bean property's type.
939      * @param instance The class instance to work with.
940      * @return String form of property's default value.
941      * @noinspection ReuseOfLocalVariable, OverlyNestedMethod
942      */
943     private static String getModulePropertyExpectedValue(String sectionName, String propertyName,
944             Field field, Class<?> fieldClass, Object instance) throws Exception {
945         String result = null;
946 
947         if (field != null) {
948             Object value = field.get(instance);
949 
950             // noinspection IfStatementWithTooManyBranches
951             if ("Checker".equals(sectionName) && "localeCountry".equals(propertyName)) {
952                 result = "default locale country for the Java Virtual Machine";
953             }
954             else if ("Checker".equals(sectionName) && "localeLanguage".equals(propertyName)) {
955                 result = "default locale language for the Java Virtual Machine";
956             }
957             else if ("Checker".equals(sectionName) && "charset".equals(propertyName)) {
958                 result = "System property \"file.encoding\"";
959             }
960             else if ("charset".equals(propertyName)) {
961                 result = "the charset property of the parent Checker module";
962             }
963             else if ("PropertyCacheFile".equals(fieldClass.getSimpleName())) {
964                 result = "null (no cache file)";
965             }
966             else if (fieldClass == boolean.class) {
967                 result = value.toString();
968             }
969             else if (fieldClass == int.class) {
970                 if (value.equals(Integer.MAX_VALUE)) {
971                     result = "java.lang.Integer.MAX_VALUE";
972                 }
973                 else {
974                     result = value.toString();
975                 }
976             }
977             else if (fieldClass == int[].class) {
978                 if (value instanceof Collection) {
979                     final Collection<?> collection = (Collection<?>) value;
980                     final int[] newArray = new int[collection.size()];
981                     final Iterator<?> iterator = collection.iterator();
982                     int index = 0;
983 
984                     while (iterator.hasNext()) {
985                         newArray[index] = (Integer) iterator.next();
986                         index++;
987                     }
988 
989                     value = newArray;
990                 }
991 
992                 if (isPropertyTokenType(sectionName, propertyName)) {
993                     result = "";
994                     boolean first = true;
995 
996                     if (value instanceof BitSet) {
997                         final BitSet list = (BitSet) value;
998                         final StringBuilder sb = new StringBuilder(20);
999 
1000                         for (int i = 0; i < list.size(); i++) {
1001                             if (list.get(i)) {
1002                                 if (first) {
1003                                     first = false;
1004                                 }
1005                                 else {
1006                                     sb.append(", ");
1007                                 }
1008 
1009                                 sb.append(TokenUtil.getTokenName(i));
1010                             }
1011                         }
1012 
1013                         result = sb.toString();
1014                     }
1015                     else if (value != null) {
1016                         final StringBuilder sb = new StringBuilder(20);
1017 
1018                         for (int i = 0; i < Array.getLength(value); i++) {
1019                             if (first) {
1020                                 first = false;
1021                             }
1022                             else {
1023                                 sb.append(", ");
1024                             }
1025 
1026                             sb.append(TokenUtil.getTokenName((int) Array.get(value, i)));
1027                         }
1028 
1029                         result = sb.toString();
1030                     }
1031                 }
1032                 else {
1033                     result = Arrays.toString((int[]) value).replace("[", "").replace("]", "");
1034 
1035                     if (result.isEmpty()) {
1036                         result = "{}";
1037                     }
1038                 }
1039             }
1040             else if (fieldClass == double[].class) {
1041                 result = Arrays.toString((double[]) value).replace("[", "").replace("]", "")
1042                         .replace(".0", "");
1043                 if (result.isEmpty()) {
1044                     result = "{}";
1045                 }
1046             }
1047             else if (fieldClass == String[].class) {
1048                 if (value instanceof Collection) {
1049                     final Collection<?> collection = (Collection<?>) value;
1050                     final String[] newArray = new String[collection.size()];
1051                     final Iterator<?> iterator = collection.iterator();
1052                     int index = 0;
1053 
1054                     while (iterator.hasNext()) {
1055                         final Object next = iterator.next();
1056                         newArray[index] = (String) next;
1057                         index++;
1058                     }
1059 
1060                     value = newArray;
1061                 }
1062 
1063                 if (value != null && Array.getLength(value) > 0) {
1064                     if (Array.get(value, 0) instanceof Number) {
1065                         final String[] newArray = new String[Array.getLength(value)];
1066 
1067                         for (int i = 0; i < newArray.length; i++) {
1068                             newArray[i] = TokenUtil.getTokenName(((Number) Array.get(value, i))
1069                                     .intValue());
1070                         }
1071 
1072                         value = newArray;
1073                     }
1074 
1075                     result = Arrays.toString((Object[]) value).replace("[", "")
1076                             .replace("]", "");
1077                 }
1078                 else {
1079                     result = "";
1080                 }
1081 
1082                 if (result.isEmpty()) {
1083                     if ("fileExtensions".equals(propertyName)) {
1084                         result = "all files";
1085                     }
1086                     else {
1087                         result = "{}";
1088                     }
1089                 }
1090             }
1091             else if (fieldClass == URI.class || fieldClass == String.class) {
1092                 if (value != null) {
1093                     result = '"' + value.toString() + '"';
1094                 }
1095             }
1096             else if (fieldClass == Pattern.class) {
1097                 if (value != null) {
1098                     result = '"' + value.toString().replace("\n", "\\n").replace("\t", "\\t")
1099                             .replace("\r", "\\r").replace("\f", "\\f") + '"';
1100 
1101                     if ("\"^$\"".equals(result)) {
1102                         result += " (empty)";
1103                     }
1104                 }
1105             }
1106             else if (fieldClass == Pattern[].class) {
1107                 if (value instanceof Collection) {
1108                     final Collection<?> collection = (Collection<?>) value;
1109                     final Pattern[] newArray = new Pattern[collection.size()];
1110                     final Iterator<?> iterator = collection.iterator();
1111                     int index = 0;
1112 
1113                     while (iterator.hasNext()) {
1114                         final Object next = iterator.next();
1115                         newArray[index] = (Pattern) next;
1116                         index++;
1117                     }
1118 
1119                     value = newArray;
1120                 }
1121 
1122                 if (value != null && Array.getLength(value) > 0) {
1123                     final String[] newArray = new String[Array.getLength(value)];
1124 
1125                     for (int i = 0; i < newArray.length; i++) {
1126                         newArray[i] = ((Pattern) Array.get(value, i)).pattern();
1127                     }
1128 
1129                     result = Arrays.toString(newArray).replace("[", "")
1130                             .replace("]", "");
1131                 }
1132                 else {
1133                     result = "";
1134                 }
1135 
1136                 if (result.isEmpty()) {
1137                     result = "{}";
1138                 }
1139             }
1140             else if (fieldClass.isEnum()) {
1141                 if (value != null) {
1142                     result = value.toString().toLowerCase(Locale.ENGLISH);
1143                 }
1144             }
1145             else if (fieldClass == AccessModifier[].class) {
1146                 result = Arrays.toString((Object[]) value).replace("[", "").replace("]", "");
1147             }
1148             else {
1149                 Assert.fail("Unknown property type: " + fieldClass.getSimpleName());
1150             }
1151 
1152             if (result == null) {
1153                 result = "null";
1154             }
1155         }
1156 
1157         return result;
1158     }
1159 
1160     /**
1161      * Checks if the given property is takes token names as a type.
1162      * @param sectionName The name of the section/module being worked on.
1163      * @param propertyName The property name to work with.
1164      * @return {@code true} if the property is takes token names as a type.
1165      * @noinspection OverlyComplexBooleanExpression
1166      */
1167     private static boolean isPropertyTokenType(String sectionName, String propertyName) {
1168         return "AtclauseOrder".equals(sectionName) && "target".equals(propertyName)
1169             || "IllegalType".equals(sectionName) && "memberModifiers".equals(propertyName)
1170             || "MagicNumber".equals(sectionName)
1171                     && "constantWaiverParentToken".equals(propertyName)
1172             || "MultipleStringLiterals".equals(sectionName)
1173                     && "ignoreOccurrenceContext".equals(propertyName)
1174             || "DescendantToken".equals(sectionName) && "limitedTokens".equals(propertyName);
1175     }
1176 
1177     private static Field getField(Class<?> clss, String propertyName) {
1178         Field result = null;
1179 
1180         if (clss != null) {
1181             try {
1182                 result = clss.getDeclaredField(propertyName);
1183                 result.setAccessible(true);
1184             }
1185             catch (NoSuchFieldException ignored) {
1186                 result = getField(clss.getSuperclass(), propertyName);
1187             }
1188         }
1189 
1190         return result;
1191     }
1192 
1193     private static Class<?> getFieldClass(String fileName, String sectionName, Object instance,
1194             Field field, String propertyName) throws Exception {
1195         Class<?> result = null;
1196 
1197         if (field != null) {
1198             result = field.getType();
1199         }
1200         if (result == null) {
1201             Assert.assertTrue(
1202                     fileName + " section '" + sectionName + "' could not find field "
1203                             + propertyName,
1204                     PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD.contains(sectionName + "."
1205                             + propertyName));
1206 
1207             final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1208                     propertyName);
1209             result = descriptor.getPropertyType();
1210         }
1211         if (result == List.class || result == Set.class) {
1212             final ParameterizedType type = (ParameterizedType) field.getGenericType();
1213             final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1214 
1215             if (parameterClass == Integer.class) {
1216                 result = int[].class;
1217             }
1218             else if (parameterClass == String.class) {
1219                 result = String[].class;
1220             }
1221             else if (parameterClass == Pattern.class) {
1222                 result = Pattern[].class;
1223             }
1224             else {
1225                 Assert.fail("Unknown parameterized type: " + parameterClass.getSimpleName());
1226             }
1227         }
1228         else if (result == BitSet.class) {
1229             result = int[].class;
1230         }
1231 
1232         return result;
1233     }
1234 
1235     private static void validateErrorSection(String fileName, String sectionName, Node subSection,
1236             Object instance) throws Exception {
1237         final Class<?> clss = instance.getClass();
1238         final Set<Field> fields = CheckUtil.getCheckMessages(clss);
1239         final Set<String> list = new TreeSet<>();
1240 
1241         for (Field field : fields) {
1242             // below is required for package/private classes
1243             if (!field.isAccessible()) {
1244                 field.setAccessible(true);
1245             }
1246 
1247             list.add(field.get(null).toString());
1248         }
1249 
1250         final StringBuilder expectedText = new StringBuilder(120);
1251 
1252         for (String s : list) {
1253             expectedText.append(s);
1254             expectedText.append('\n');
1255         }
1256 
1257         if (expectedText.length() > 0) {
1258             expectedText.append("All messages can be customized if the default message doesn't "
1259                     + "suit you.\nPlease see the documentation to learn how to.");
1260         }
1261 
1262         if (subSection == null) {
1263             Assert.assertEquals(fileName + " section '" + sectionName
1264                     + "' should have the expected error keys", "", expectedText.toString());
1265         }
1266         else {
1267             Assert.assertEquals(fileName + " section '" + sectionName
1268                     + "' should have the expected error keys", expectedText.toString().trim(),
1269                     subSection.getTextContent().replaceAll("\n\\s+", "\n").trim());
1270 
1271             for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1272                 final String url = node.getAttributes().getNamedItem("href").getTextContent();
1273                 final String linkText = node.getTextContent().trim();
1274                 final String expectedUrl;
1275 
1276                 if ("see the documentation".equals(linkText)) {
1277                     expectedUrl = "config.html#Custom_messages";
1278                 }
1279                 else {
1280                     expectedUrl = "https://github.com/search?q="
1281                             + "path%3Asrc%2Fmain%2Fresources%2F"
1282                             + clss.getPackage().getName().replace(".", "%2F")
1283                             + "+filename%3Amessages*.properties+repo%3Acheckstyle%2Fcheckstyle+%22"
1284                             + linkText + "%22";
1285                 }
1286 
1287                 Assert.assertEquals(fileName + " section '" + sectionName
1288                         + "' should have matching url for '" + linkText + "'", expectedUrl, url);
1289             }
1290         }
1291     }
1292 
1293     private static void validateUsageExample(String fileName, String sectionName, Node subSection) {
1294         final String text = subSection.getTextContent().replace("Checkstyle Style", "")
1295                 .replace("Google Style", "").replace("Sun Style", "").trim();
1296 
1297         Assert.assertTrue(fileName + " section '" + sectionName
1298                 + "' has unknown text in 'Example of Usage': " + text, text.isEmpty());
1299 
1300         boolean hasCheckstyle = false;
1301         boolean hasGoogle = false;
1302         boolean hasSun = false;
1303 
1304         for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1305             final String url = node.getAttributes().getNamedItem("href").getTextContent();
1306             final String linkText = node.getTextContent().trim();
1307             String expectedUrl = null;
1308 
1309             if ("Checkstyle Style".equals(linkText)) {
1310                 hasCheckstyle = true;
1311                 expectedUrl = "https://github.com/search?q="
1312                         + "path%3Aconfig+filename%3Acheckstyle_checks.xml+"
1313                         + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName;
1314             }
1315             else if ("Google Style".equals(linkText)) {
1316                 hasGoogle = true;
1317                 expectedUrl = "https://github.com/search?q="
1318                         + "path%3Asrc%2Fmain%2Fresources+filename%3Agoogle_checks.xml+"
1319                         + "repo%3Acheckstyle%2Fcheckstyle+"
1320                         + sectionName;
1321 
1322                 Assert.assertTrue(fileName + " section '" + sectionName
1323                         + "' should be in google_checks.xml or not reference 'Google Style'",
1324                         GOOGLE_MODULES.contains(sectionName));
1325             }
1326             else if ("Sun Style".equals(linkText)) {
1327                 hasSun = true;
1328                 expectedUrl = "https://github.com/search?q="
1329                         + "path%3Asrc%2Fmain%2Fresources+filename%3Asun_checks.xml+"
1330                         + "repo%3Acheckstyle%2Fcheckstyle+"
1331                         + sectionName;
1332 
1333                 Assert.assertTrue(fileName + " section '" + sectionName
1334                         + "' should be in sun_checks.xml or not reference 'Sun Style'",
1335                         SUN_MODULES.contains(sectionName));
1336             }
1337 
1338             Assert.assertEquals(fileName + " section '" + sectionName
1339                     + "' should have matching url", expectedUrl, url);
1340         }
1341 
1342         Assert.assertTrue(fileName + " section '" + sectionName
1343                 + "' should have a checkstyle section", hasCheckstyle);
1344         Assert.assertTrue(fileName + " section '" + sectionName
1345                 + "' should have a google section since it is in it's config", hasGoogle
1346                 || !GOOGLE_MODULES.contains(sectionName));
1347         Assert.assertTrue(fileName + " section '" + sectionName
1348                 + "' should have a sun section since it is in it's config",
1349                 hasSun || !SUN_MODULES.contains(sectionName));
1350     }
1351 
1352     private static void validatePackageSection(String fileName, String sectionName,
1353             Node subSection, Object instance) {
1354         Assert.assertEquals(fileName + " section '" + sectionName
1355                 + "' should have matching package", instance.getClass().getPackage().getName(),
1356                 subSection.getTextContent().trim());
1357     }
1358 
1359     private static void validateParentSection(String fileName, String sectionName,
1360             Node subSection) {
1361         final String expected;
1362 
1363         if (!"TreeWalker".equals(sectionName) && hasParentModule(sectionName)) {
1364             expected = "TreeWalker";
1365         }
1366         else {
1367             expected = "Checker";
1368         }
1369 
1370         Assert.assertEquals(
1371                 fileName + " section '" + sectionName + "' should have matching parent",
1372                 expected, subSection
1373                         .getTextContent().trim());
1374     }
1375 
1376     private static boolean hasParentModule(String sectionName) {
1377         final String search = "\"" + sectionName + "\"";
1378         boolean result = true;
1379 
1380         for (String find : XML_FILESET_LIST) {
1381             if (find.contains(search)) {
1382                 result = false;
1383                 break;
1384             }
1385         }
1386 
1387         return result;
1388     }
1389 
1390     private static Set<String> getProperties(Class<?> clss) {
1391         final Set<String> result = new TreeSet<>();
1392         final PropertyDescriptor[] map = PropertyUtils.getPropertyDescriptors(clss);
1393 
1394         for (PropertyDescriptor p : map) {
1395             if (p.getWriteMethod() != null) {
1396                 result.add(p.getName());
1397             }
1398         }
1399 
1400         return result;
1401     }
1402 
1403     @Test
1404     public void testAllStyleRules() throws Exception {
1405         for (Path path : XdocUtil.getXdocsStyleFilePaths(XdocUtil.getXdocsFilePaths())) {
1406             final String fileName = path.getFileName().toString();
1407             final String styleName = fileName.substring(0, fileName.lastIndexOf('_'));
1408             final String input = new String(Files.readAllBytes(path), UTF_8);
1409             final Document document = XmlUtil.getRawXml(fileName, input, input);
1410             final NodeList sources = document.getElementsByTagName("tr");
1411 
1412             final Set<String> styleChecks;
1413             switch (styleName) {
1414                 case "google":
1415                     styleChecks = new HashSet<>(GOOGLE_MODULES);
1416                     break;
1417 
1418                 case "sun":
1419                     styleChecks = new HashSet<>(SUN_MODULES);
1420                     styleChecks.removeAll(IGNORED_SUN_MODULES);
1421                     break;
1422 
1423                 default:
1424                     Assert.fail("Missing modules list for style file '" + fileName + "'");
1425                     styleChecks = null;
1426             }
1427 
1428             String lastRuleName = null;
1429             String[] lastRuleNumberParts = null;
1430 
1431             for (int position = 0; position < sources.getLength(); position++) {
1432                 final Node row = sources.item(position);
1433                 final List<Node> columns = new ArrayList<>(
1434                         XmlUtil.findChildElementsByTag(row, "td"));
1435 
1436                 if (columns.isEmpty()) {
1437                     continue;
1438                 }
1439 
1440                 final String ruleName = columns.get(1).getTextContent().trim();
1441                 lastRuleNumberParts = validateRuleNameOrder(
1442                         fileName, lastRuleName, lastRuleNumberParts, ruleName);
1443 
1444                 if (!"--".equals(ruleName)) {
1445                     validateStyleAnchors(XmlUtil.findChildElementsByTag(columns.get(0), "a"),
1446                             fileName, ruleName);
1447                 }
1448 
1449                 validateStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"),
1450                         XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, styleName,
1451                         ruleName);
1452 
1453                 lastRuleName = ruleName;
1454             }
1455 
1456             // these modules aren't documented, but are added to the config
1457             styleChecks.remove("BeforeExecutionExclusionFileFilter");
1458             styleChecks.remove("TreeWalker");
1459             styleChecks.remove("Checker");
1460 
1461             Assert.assertTrue(fileName + " requires the following check(s) to appear: "
1462                     + styleChecks, styleChecks.isEmpty());
1463         }
1464     }
1465 
1466     private static String[] validateRuleNameOrder(String fileName, String lastRuleName,
1467                                                   String[] lastRuleNumberParts, String ruleName) {
1468         final String[] ruleNumberParts = ruleName.split(" ", 2)[0].split("\\.");
1469 
1470         if (lastRuleName != null) {
1471             final int ruleNumberPartsAmount = ruleNumberParts.length;
1472             final int lastRuleNumberPartsAmount = lastRuleNumberParts.length;
1473             final String outOfOrderReason = fileName + " rule '" + ruleName
1474                     + "' is out of order compared to '" + lastRuleName + "'";
1475             boolean lastRuleNumberPartWasEqual = false;
1476             int partIndex;
1477             for (partIndex = 0; partIndex < ruleNumberPartsAmount; partIndex++) {
1478                 if (lastRuleNumberPartsAmount <= partIndex) {
1479                     // equal up to here and last rule has less parts,
1480                     // thus order is correct, stop comparing
1481                     break;
1482                 }
1483 
1484                 final String ruleNumberPart = ruleNumberParts[partIndex];
1485                 final String lastRuleNumberPart = lastRuleNumberParts[partIndex];
1486                 final boolean ruleNumberPartsAreNumeric = IntStream.concat(
1487                         ruleNumberPart.chars(),
1488                         lastRuleNumberPart.chars()
1489                 ).allMatch(Character::isDigit);
1490 
1491                 if (ruleNumberPartsAreNumeric) {
1492                     final int numericRuleNumberPart = parseInt(ruleNumberPart);
1493                     final int numericLastRuleNumberPart = parseInt(lastRuleNumberPart);
1494                     Assert.assertThat(
1495                             outOfOrderReason,
1496                             numericRuleNumberPart < numericLastRuleNumberPart,
1497                             describedAs("'%0' should not be less than '%1'",
1498                                     is(false),
1499                                     numericRuleNumberPart, numericLastRuleNumberPart));
1500                 }
1501                 else {
1502                     Assert.assertThat(
1503                             outOfOrderReason,
1504                             ruleNumberPart.compareToIgnoreCase(lastRuleNumberPart) < 0,
1505                             describedAs("'%0' should not be less than '%1'",
1506                                     is(false),
1507                                     ruleNumberPart, lastRuleNumberPart));
1508                 }
1509                 lastRuleNumberPartWasEqual = ruleNumberPart.equalsIgnoreCase(lastRuleNumberPart);
1510                 if (!lastRuleNumberPartWasEqual) {
1511                     // number part is not equal but properly ordered,
1512                     // thus order is correct, stop comparing
1513                     break;
1514                 }
1515             }
1516             if (ruleNumberPartsAmount == partIndex && lastRuleNumberPartWasEqual) {
1517                 if (lastRuleNumberPartsAmount == partIndex) {
1518                     Assert.fail(fileName + " rule '" + ruleName + "' and rule '"
1519                             + lastRuleName + "' have the same rule number");
1520                 }
1521                 else {
1522                     Assert.fail(outOfOrderReason);
1523                 }
1524             }
1525         }
1526 
1527         return ruleNumberParts;
1528     }
1529 
1530     private static void validateStyleAnchors(Set<Node> anchors, String fileName, String ruleName) {
1531         Assert.assertEquals(fileName + " rule '" + ruleName + "' must have two row anchors", 2,
1532                 anchors.size());
1533 
1534         final int space = ruleName.indexOf(' ');
1535         Assert.assertTrue(fileName + " rule '" + ruleName
1536                 + "' must have have a space between the rule's number and the rule's name",
1537                 space != -1);
1538 
1539         final String ruleNumber = ruleName.substring(0, space);
1540 
1541         int position = 1;
1542 
1543         for (Node anchor : anchors) {
1544             final String actualUrl;
1545             final String expectedUrl;
1546 
1547             if (position == 1) {
1548                 actualUrl = anchor.getAttributes().getNamedItem("name").getTextContent();
1549                 expectedUrl = ruleNumber;
1550             }
1551             else {
1552                 actualUrl = anchor.getAttributes().getNamedItem("href").getTextContent();
1553                 expectedUrl = "#" + ruleNumber;
1554             }
1555 
1556             Assert.assertEquals(fileName + " rule '" + ruleName + "' anchor " + position
1557                     + " should have matching name/url", expectedUrl, actualUrl);
1558 
1559             position++;
1560         }
1561     }
1562 
1563     private static void validateStyleModules(Set<Node> checks, Set<Node> configs,
1564             Set<String> styleChecks, String styleName, String ruleName) {
1565         final Iterator<Node> itrChecks = checks.iterator();
1566         final Iterator<Node> itrConfigs = configs.iterator();
1567 
1568         while (itrChecks.hasNext()) {
1569             final Node module = itrChecks.next();
1570             final String moduleName = module.getTextContent().trim();
1571 
1572             if (!module.getAttributes().getNamedItem("href").getTextContent()
1573                     .startsWith("config_")) {
1574                 continue;
1575             }
1576 
1577             Assert.assertTrue(styleName + "_style.xml rule '" + ruleName + "' module '" + moduleName
1578                     + "' shouldn't end with 'Check'", !moduleName.endsWith("Check"));
1579 
1580             styleChecks.remove(moduleName);
1581 
1582             for (String configName : new String[] {"config", "test"}) {
1583                 Node config = null;
1584 
1585                 try {
1586                     config = itrConfigs.next();
1587                 }
1588                 catch (NoSuchElementException ignore) {
1589                     Assert.fail(styleName + "_style.xml rule '" + ruleName + "' module '"
1590                             + moduleName + "' is missing the config link: " + configName);
1591                 }
1592 
1593                 Assert.assertEquals(styleName + "_style.xml rule '" + ruleName + "' module '"
1594                         + moduleName + "' has mismatched config/test links", configName,
1595                         config.getTextContent().trim());
1596 
1597                 final String configUrl = config.getAttributes().getNamedItem("href")
1598                         .getTextContent();
1599 
1600                 if ("config".equals(configName)) {
1601                     final String expectedUrl = "https://github.com/search?q="
1602                             + "path%3Asrc%2Fmain%2Fresources+filename%3A" + styleName
1603                             + "_checks.xml+repo%3Acheckstyle%2Fcheckstyle+" + moduleName;
1604 
1605                     Assert.assertEquals(styleName + "_style.xml rule '" + ruleName + "' module '"
1606                                     + moduleName + "' should have matching " + configName + " url",
1607                             expectedUrl, configUrl);
1608                 }
1609                 else if ("test".equals(configName)) {
1610                     Assert.assertTrue(styleName + "_style.xml rule '" + ruleName + "' module '"
1611                                     + moduleName + "' should have matching " + configName + " url",
1612                             configUrl.startsWith("https://github.com/checkstyle/checkstyle/"
1613                                     + "blob/master/src/it/java/com/" + styleName
1614                                     + "/checkstyle/test/"));
1615                     Assert.assertTrue(styleName + "_style.xml rule '" + ruleName + "' module '"
1616                                     + moduleName + "' should have matching " + configName + " url",
1617                             configUrl.endsWith("/" + moduleName + "Test.java"));
1618 
1619                     Assert.assertTrue(styleName + "_style.xml rule '" + ruleName + "' module '"
1620                             + moduleName + "' should have a test that exists",
1621                             new File(configUrl.substring(53)
1622                                     .replace('/', File.separatorChar)).exists());
1623                 }
1624             }
1625         }
1626 
1627         Assert.assertFalse(styleName + "_style.xml rule '" + ruleName + "' has too many configs",
1628                 itrConfigs.hasNext());
1629     }
1630 
1631 }