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