View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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.Field;
29  import java.net.URI;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Collections;
36  import java.util.HashSet;
37  import java.util.Iterator;
38  import java.util.List;
39  import java.util.Locale;
40  import java.util.NoSuchElementException;
41  import java.util.Properties;
42  import java.util.Set;
43  import java.util.TreeSet;
44  import java.util.regex.Pattern;
45  
46  import org.apache.commons.beanutils.PropertyUtils;
47  import org.junit.Assert;
48  import org.junit.Test;
49  import org.w3c.dom.Document;
50  import org.w3c.dom.Node;
51  import org.w3c.dom.NodeList;
52  import org.xml.sax.InputSource;
53  
54  import com.puppycrawl.tools.checkstyle.Checker;
55  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
56  import com.puppycrawl.tools.checkstyle.ModuleFactory;
57  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
58  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
59  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
60  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
61  import com.puppycrawl.tools.checkstyle.api.Configuration;
62  import com.puppycrawl.tools.checkstyle.api.Scope;
63  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
64  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
65  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifier;
66  import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
67  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
68  import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
69  import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
70  
71  public class XdocsPagesTest {
72      private static final Path AVAILABLE_CHECKS_PATH = Paths.get("src/xdocs/checks.xml");
73      private static final String LINK_TEMPLATE =
74              "(?s).*<a href=\"config_\\w+\\.html#%1$s\">%1$s</a>.*";
75  
76      private static final Pattern VERSION = Pattern.compile("\\d+\\.\\d+(\\.\\d+)?");
77  
78      private static final Pattern DESCRIPTION_VERSION = Pattern
79              .compile("^Since Checkstyle \\d+\\.\\d+(\\.\\d+)?");
80  
81      private static final List<String> XML_FILESET_LIST = Arrays.asList(
82              "TreeWalker",
83              "name=\"Checker\"",
84              "name=\"Header\"",
85              "name=\"Translation\"",
86              "name=\"SeverityMatchFilter\"",
87              "name=\"SuppressWithPlainTextCommentFilter\"",
88              "name=\"SuppressionFilter\"",
89              "name=\"SuppressWarningsFilter\"",
90              "name=\"BeforeExecutionExclusionFileFilter\"",
91              "name=\"RegexpHeader\"",
92              "name=\"RegexpOnFilename\"",
93              "name=\"RegexpSingleline\"",
94              "name=\"RegexpMultiline\"",
95              "name=\"JavadocPackage\"",
96              "name=\"NewlineAtEndOfFile\"",
97              "name=\"UniqueProperties\"",
98              "name=\"FileLength\"",
99              "name=\"FileTabCharacter\""
100     );
101 
102     private static final Set<String> CHECK_PROPERTIES = getProperties(AbstractCheck.class);
103     private static final Set<String> JAVADOC_CHECK_PROPERTIES =
104             getProperties(AbstractJavadocCheck.class);
105     private static final Set<String> FILESET_PROPERTIES = getProperties(AbstractFileSetCheck.class);
106 
107     private static final List<String> UNDOCUMENTED_PROPERTIES = Arrays.asList(
108             "Checker.classLoader",
109             "Checker.classloader",
110             "Checker.moduleClassLoader",
111             "Checker.moduleFactory",
112             "TreeWalker.classLoader",
113             "TreeWalker.moduleFactory",
114             "TreeWalker.cacheFile",
115             "TreeWalker.upChild",
116             "SuppressWithNearbyCommentFilter.fileContents",
117             "SuppressionCommentFilter.fileContents"
118     );
119 
120     private static final Set<String> SUN_MODULES = Collections.unmodifiableSet(
121         new HashSet<>(CheckUtil.getConfigSunStyleModules()));
122     private static final Set<String> GOOGLE_MODULES = Collections.unmodifiableSet(
123         new HashSet<>(CheckUtil.getConfigGoogleStyleModules()));
124 
125     @Test
126     public void testAllChecksPresentOnAvailableChecksPage() throws Exception {
127         final String availableChecks = new String(Files.readAllBytes(AVAILABLE_CHECKS_PATH), UTF_8);
128 
129         CheckUtil.getSimpleNames(CheckUtil.getCheckstyleChecks())
130             .forEach(checkName -> {
131                 if (!isPresent(availableChecks, checkName)) {
132                     Assert.fail(checkName + " is not correctly listed on Available Checks page"
133                         + " - add it to " + AVAILABLE_CHECKS_PATH);
134                 }
135             });
136     }
137 
138     private static boolean isPresent(String availableChecks, String checkName) {
139         final String linkPattern = String.format(Locale.ROOT, LINK_TEMPLATE, checkName);
140         return availableChecks.matches(linkPattern);
141     }
142 
143     @Test
144     public void testAllXmlExamples() throws Exception {
145         for (Path path : XdocUtil.getXdocsFilePaths()) {
146             final String input = new String(Files.readAllBytes(path), UTF_8);
147             final String fileName = path.getFileName().toString();
148 
149             final Document document = XmlUtil.getRawXml(fileName, input, input);
150             final NodeList sources = document.getElementsByTagName("source");
151 
152             for (int position = 0; position < sources.getLength(); position++) {
153                 final String unserializedSource = sources.item(position).getTextContent()
154                         .replace("...", "").trim();
155 
156                 if (unserializedSource.charAt(0) != '<'
157                         || unserializedSource.charAt(unserializedSource.length() - 1) != '>'
158                         // no dtd testing yet
159                         || unserializedSource.contains("<!")) {
160                     continue;
161                 }
162 
163                 final String code = buildXml(unserializedSource);
164                 // validate only
165                 XmlUtil.getRawXml(fileName, code, unserializedSource);
166 
167                 // can't test ant structure, or old and outdated checks
168                 Assert.assertTrue("Xml is invalid, old or has outdated structure",
169                         fileName.startsWith("anttask")
170                         || fileName.startsWith("releasenotes")
171                         || isValidCheckstyleXml(fileName, code, unserializedSource));
172             }
173         }
174     }
175 
176     private static String buildXml(String unserializedSource) throws IOException {
177         // not all examples come with the full xml structure
178         String code = unserializedSource
179             // don't corrupt our own cachefile
180             .replace("target/cachefile", "target/cachefile-test");
181 
182         if (!hasFileSetClass(code)) {
183             code = "<module name=\"TreeWalker\">\n" + code + "\n</module>";
184         }
185         if (!code.contains("name=\"Checker\"")) {
186             code = "<module name=\"Checker\">\n" + code + "\n</module>";
187         }
188         if (!code.startsWith("<?xml")) {
189             final String dtdPath = new File(
190                     "src/main/resources/com/puppycrawl/tools/checkstyle/configuration_1_3.dtd")
191                     .getCanonicalPath();
192 
193             code = "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC "
194                     + "\"-//Puppy Crawl//DTD Check Configuration 1.3//EN\" \"" + dtdPath + "\">\n"
195                     + code;
196         }
197         return code;
198     }
199 
200     private static boolean hasFileSetClass(String xml) {
201         boolean found = false;
202 
203         for (String find : XML_FILESET_LIST) {
204             if (xml.contains(find)) {
205                 found = true;
206                 break;
207             }
208         }
209 
210         return found;
211     }
212 
213     private static boolean isValidCheckstyleXml(String fileName, String code,
214                                                 String unserializedSource)
215             throws IOException, CheckstyleException {
216         // can't process non-existent examples, or out of context snippets
217         if (!code.contains("com.mycompany") && !code.contains("checkstyle-packages")
218                 && !code.contains("MethodLimit") && !code.contains("<suppress ")
219                 && !code.contains("<suppress-xpath ")
220                 && !code.contains("<import-control ")
221                 && !unserializedSource.startsWith("<property ")
222                 && !unserializedSource.startsWith("<taskdef ")) {
223             // validate checkstyle structure and contents
224             try {
225                 final Properties properties = new Properties();
226 
227                 properties.setProperty("checkstyle.header.file",
228                         new File("config/java.header").getCanonicalPath());
229 
230                 final PropertiesExpander expander = new PropertiesExpander(properties);
231                 final Configuration config = ConfigurationLoader.loadConfiguration(new InputSource(
232                         new StringReader(code)), expander, false);
233                 final Checker checker = new Checker();
234 
235                 try {
236                     final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
237                     checker.setModuleClassLoader(moduleClassLoader);
238                     checker.configure(config);
239                 }
240                 finally {
241                     checker.destroy();
242                 }
243             }
244             catch (CheckstyleException ex) {
245                 throw new CheckstyleException(fileName + " has invalid Checkstyle xml ("
246                         + ex.getMessage() + "): " + unserializedSource, ex);
247             }
248         }
249         return true;
250     }
251 
252     @Test
253     public void testAllCheckSections() throws Exception {
254         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
255 
256         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
257             final String fileName = path.getFileName().toString();
258 
259             if ("config_reporting.xml".equals(fileName)) {
260                 continue;
261             }
262 
263             final String input = new String(Files.readAllBytes(path), UTF_8);
264             final Document document = XmlUtil.getRawXml(fileName, input, input);
265             final NodeList sources = document.getElementsByTagName("section");
266             String lastSectionName = null;
267 
268             for (int position = 0; position < sources.getLength(); position++) {
269                 final Node section = sources.item(position);
270                 final String sectionName = section.getAttributes().getNamedItem("name")
271                         .getNodeValue();
272 
273                 if ("Content".equals(sectionName) || "Overview".equals(sectionName)) {
274                     Assert.assertNull(fileName + " section '" + sectionName + "' should be first",
275                             lastSectionName);
276                     continue;
277                 }
278 
279                 Assert.assertTrue(fileName + " section '" + sectionName
280                         + "' shouldn't end with 'Check'", !sectionName.endsWith("Check"));
281                 if (lastSectionName != null) {
282                     Assert.assertTrue(
283                             fileName + " section '" + sectionName
284                                     + "' is out of order compared to '" + lastSectionName + "'",
285                             sectionName.toLowerCase(Locale.ENGLISH).compareTo(
286                                     lastSectionName.toLowerCase(Locale.ENGLISH)) >= 0);
287                 }
288 
289                 validateCheckSection(moduleFactory, fileName, sectionName, section);
290 
291                 lastSectionName = sectionName;
292             }
293         }
294     }
295 
296     /**
297      * Test contains asserts in callstack, but idea does not see them.
298      * @noinspection JUnitTestMethodWithNoAssertions
299      */
300     @Test
301     public void testAllCheckSectionsEx() throws Exception {
302         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
303 
304         final Path path = Paths.get(XdocUtil.DIRECTORY_PATH + "/config.xml");
305         final String fileName = path.getFileName().toString();
306 
307         final String input = new String(Files.readAllBytes(path), UTF_8);
308         final Document document = XmlUtil.getRawXml(fileName, input, input);
309         final NodeList sources = document.getElementsByTagName("section");
310 
311         for (int position = 0; position < sources.getLength(); position++) {
312             final Node section = sources.item(position);
313             final String sectionName = section.getAttributes().getNamedItem("name")
314                     .getNodeValue();
315 
316             if (!"Checker".equals(sectionName) && !"TreeWalker".equals(sectionName)) {
317                 continue;
318             }
319 
320             validateCheckSection(moduleFactory, fileName, sectionName, section);
321         }
322     }
323 
324     private static void validateCheckSection(ModuleFactory moduleFactory, String fileName,
325             String sectionName, Node section) throws Exception {
326         final Object instance;
327 
328         try {
329             instance = moduleFactory.createModule(sectionName);
330         }
331         catch (CheckstyleException ex) {
332             throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, ex);
333         }
334 
335         int subSectionPos = 0;
336         for (Node subSection : XmlUtil.getChildrenElements(section)) {
337             final String subSectionName = subSection.getAttributes().getNamedItem("name")
338                     .getNodeValue();
339 
340             // can be in different orders, and completely optional
341             if ("Notes".equals(subSectionName)
342                     || "Rule Description".equals(subSectionName)) {
343                 continue;
344             }
345 
346             // optional sections that can be skipped if they have nothing to report
347             if (subSectionPos == 1 && !"Properties".equals(subSectionName)) {
348                 validatePropertySection(fileName, sectionName, null, instance);
349                 subSectionPos++;
350             }
351             if (subSectionPos == 4 && !"Error Messages".equals(subSectionName)) {
352                 validateErrorSection(fileName, sectionName, null, instance);
353                 subSectionPos++;
354             }
355 
356             Assert.assertEquals(fileName + " section '" + sectionName
357                     + "' should be in order", getSubSectionName(subSectionPos),
358                     subSectionName);
359 
360             switch (subSectionPos) {
361                 case 0:
362                     validateSinceDescriptionSection(fileName, sectionName, subSection);
363                     break;
364                 case 1:
365                     validatePropertySection(fileName, sectionName, subSection, instance);
366                     break;
367                 case 2:
368                     break;
369                 case 3:
370                     validateUsageExample(fileName, sectionName, subSection);
371                     break;
372                 case 4:
373                     validateErrorSection(fileName, sectionName, subSection, instance);
374                     break;
375                 case 5:
376                     validatePackageSection(fileName, sectionName, subSection, instance);
377                     break;
378                 case 6:
379                     validateParentSection(fileName, sectionName, subSection);
380                     break;
381                 default:
382                     break;
383             }
384 
385             subSectionPos++;
386         }
387     }
388 
389     private static void validateSinceDescriptionSection(String fileName, String sectionName,
390             Node subSection) {
391         Assert.assertTrue(fileName + " section '" + sectionName
392                 + "' should have a valid version at the start of the description like:\n"
393                 + DESCRIPTION_VERSION.pattern(),
394                 DESCRIPTION_VERSION.matcher(subSection.getTextContent().trim()).find());
395     }
396 
397     private static Object getSubSectionName(int subSectionPos) {
398         final String result;
399 
400         switch (subSectionPos) {
401             case 0:
402                 result = "Description";
403                 break;
404             case 1:
405                 result = "Properties";
406                 break;
407             case 2:
408                 result = "Examples";
409                 break;
410             case 3:
411                 result = "Example of Usage";
412                 break;
413             case 4:
414                 result = "Error Messages";
415                 break;
416             case 5:
417                 result = "Package";
418                 break;
419             case 6:
420                 result = "Parent Module";
421                 break;
422             default:
423                 result = null;
424                 break;
425         }
426 
427         return result;
428     }
429 
430     private static void validatePropertySection(String fileName, String sectionName,
431             Node subSection, Object instance) throws Exception {
432         final Set<String> properties = getProperties(instance.getClass());
433         final Class<?> clss = instance.getClass();
434 
435         fixCapturedProperties(sectionName, instance, clss, properties);
436 
437         if (subSection != null) {
438             Assert.assertTrue(fileName + " section '" + sectionName
439                     + "' should have no properties to show", !properties.isEmpty());
440 
441             validatePropertySectionProperties(fileName, sectionName, subSection, instance,
442                     properties);
443         }
444 
445         Assert.assertTrue(fileName + " section '" + sectionName + "' should show properties: "
446                 + properties, properties.isEmpty());
447     }
448 
449     private static void fixCapturedProperties(String sectionName, Object instance, Class<?> clss,
450             Set<String> properties) {
451         // remove global properties that don't need documentation
452         if (hasParentModule(sectionName)) {
453             if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
454                 properties.removeAll(JAVADOC_CHECK_PROPERTIES);
455 
456                 // override
457                 properties.add("violateExecutionOnNonTightHtml");
458             }
459             else if (AbstractCheck.class.isAssignableFrom(clss)) {
460                 properties.removeAll(CHECK_PROPERTIES);
461             }
462         }
463         if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
464             properties.removeAll(FILESET_PROPERTIES);
465 
466             // override
467             properties.add("fileExtensions");
468         }
469 
470         // remove undocumented properties
471         new HashSet<>(properties).stream()
472             .filter(prop -> UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + "." + prop))
473             .forEach(properties::remove);
474 
475         if (AbstractCheck.class.isAssignableFrom(clss)) {
476             final AbstractCheck check = (AbstractCheck) instance;
477 
478             final int[] acceptableTokens = check.getAcceptableTokens();
479             Arrays.sort(acceptableTokens);
480             final int[] defaultTokens = check.getDefaultTokens();
481             Arrays.sort(defaultTokens);
482             final int[] requiredTokens = check.getRequiredTokens();
483             Arrays.sort(requiredTokens);
484 
485             if (!Arrays.equals(acceptableTokens, defaultTokens)
486                     || !Arrays.equals(acceptableTokens, requiredTokens)) {
487                 properties.add("tokens");
488             }
489         }
490 
491         if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
492             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
493 
494             final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
495             Arrays.sort(acceptableJavadocTokens);
496             final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
497             Arrays.sort(defaultJavadocTokens);
498             final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
499             Arrays.sort(requiredJavadocTokens);
500 
501             if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
502                     || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
503                 properties.add("javadocTokens");
504             }
505         }
506     }
507 
508     private static void validatePropertySectionProperties(String fileName, String sectionName,
509             Node subSection, Object instance, Set<String> properties) throws Exception {
510         boolean skip = true;
511         boolean didJavadocTokens = false;
512         boolean didTokens = false;
513 
514         for (Node row : XmlUtil.getChildrenElements(XmlUtil.getFirstChildElement(subSection))) {
515             final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
516 
517             Assert.assertEquals(fileName + " section '" + sectionName
518                     + "' should have the requested columns", 5, columns.size());
519 
520             if (skip) {
521                 Assert.assertEquals(fileName + " section '" + sectionName
522                         + "' should have the specific title", "name", columns.get(0)
523                         .getTextContent());
524                 Assert.assertEquals(fileName + " section '" + sectionName
525                         + "' should have the specific title", "description", columns.get(1)
526                         .getTextContent());
527                 Assert.assertEquals(fileName + " section '" + sectionName
528                         + "' should have the specific title", "type", columns.get(2)
529                         .getTextContent());
530                 Assert.assertEquals(fileName + " section '" + sectionName
531                         + "' should have the specific title", "default value", columns.get(3)
532                         .getTextContent());
533                 Assert.assertEquals(fileName + " section '" + sectionName
534                         + "' should have the specific title", "since", columns.get(4)
535                         .getTextContent());
536 
537                 skip = false;
538                 continue;
539             }
540 
541             Assert.assertFalse(fileName + " section '" + sectionName
542                     + "' should have token properties last", didTokens);
543 
544             final String propertyName = columns.get(0).getTextContent();
545             Assert.assertTrue(fileName + " section '" + sectionName
546                     + "' should not contain the property: " + propertyName,
547                     properties.remove(propertyName));
548 
549             if ("tokens".equals(propertyName)) {
550                 final AbstractCheck check = (AbstractCheck) instance;
551                 validatePropertySectionPropertyTokens(fileName, sectionName, check, columns);
552                 didTokens = true;
553             }
554             else if ("javadocTokens".equals(propertyName)) {
555                 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
556                 validatePropertySectionPropertyJavadocTokens(fileName, sectionName, check, columns);
557                 didJavadocTokens = true;
558             }
559             else {
560                 Assert.assertFalse(fileName + " section '" + sectionName
561                         + "' should have javadoc token properties next to last, before tokens",
562                         didJavadocTokens);
563 
564                 validatePropertySectionPropertyEx(fileName, sectionName, instance, columns,
565                         propertyName);
566             }
567 
568             Assert.assertFalse(fileName + " section '" + sectionName
569                     + "' should have a version for " + propertyName, columns.get(4)
570                     .getTextContent().trim().isEmpty());
571             Assert.assertTrue(fileName + " section '" + sectionName
572                     + "' should have a valid version for " + propertyName,
573                     VERSION.matcher(columns.get(4).getTextContent().trim()).matches());
574         }
575     }
576 
577     private static void validatePropertySectionPropertyEx(String fileName, String sectionName,
578             Object instance, List<Node> columns, String propertyName) throws Exception {
579         Assert.assertFalse(fileName + " section '" + sectionName
580                 + "' should have a description for " + propertyName, columns.get(1)
581                 .getTextContent().trim().isEmpty());
582 
583         final String actualTypeName = columns.get(2).getTextContent().replace("\n", "")
584                 .replace("\r", "").replaceAll(" +", " ").trim();
585 
586         Assert.assertFalse(fileName + " section '" + sectionName + "' should have a type for "
587                 + propertyName, actualTypeName.isEmpty());
588 
589         final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
590                 propertyName);
591         final Class<?> clss = descriptor.getPropertyType();
592         final String expectedTypeName = getModulePropertyExpectedTypeName(clss, instance,
593                 propertyName);
594 
595         if (expectedTypeName != null) {
596             final String expectedValue = getModulePropertyExpectedValue(clss, instance,
597                     propertyName);
598 
599             Assert.assertEquals(fileName + " section '" + sectionName
600                     + "' should have the type for " + propertyName, expectedTypeName,
601                     actualTypeName);
602 
603             if (expectedValue != null) {
604                 final String actualValue = columns.get(3).getTextContent().replace("\n", "")
605                         .replace("\r", "").replaceAll(" +", " ").trim();
606 
607                 Assert.assertEquals(fileName + " section '" + sectionName
608                         + "' should have the value for " + propertyName, expectedValue,
609                         actualValue);
610             }
611         }
612     }
613 
614     private static void validatePropertySectionPropertyTokens(String fileName, String sectionName,
615             AbstractCheck check, List<Node> columns) {
616         Assert.assertEquals(fileName + " section '" + sectionName
617                 + "' should have the basic token description", "tokens to check", columns.get(1)
618                 .getTextContent());
619         Assert.assertEquals(
620                 fileName + " section '" + sectionName + "' should have all the acceptable tokens",
621                 "subset of tokens "
622                         + CheckUtil.getTokenText(check.getAcceptableTokens(),
623                                 check.getRequiredTokens()), columns.get(2).getTextContent()
624                         .replaceAll("\\s+", " ").trim());
625         Assert.assertEquals(fileName + " section '" + sectionName
626                 + "' should have all the default tokens",
627                 CheckUtil.getTokenText(check.getDefaultTokens(), check.getRequiredTokens()),
628                 columns.get(3).getTextContent().replaceAll("\\s+", " ").trim());
629     }
630 
631     private static void validatePropertySectionPropertyJavadocTokens(String fileName,
632             String sectionName, AbstractJavadocCheck check, List<Node> columns) {
633         Assert.assertEquals(fileName + " section '" + sectionName
634                 + "' should have the basic token javadoc description", "javadoc tokens to check",
635                 columns.get(1).getTextContent());
636         Assert.assertEquals(
637                 fileName + " section '" + sectionName
638                         + "' should have all the acceptable javadoc tokens",
639                 "subset of javadoc tokens "
640                         + CheckUtil.getJavadocTokenText(check.getAcceptableJavadocTokens(),
641                                 check.getRequiredJavadocTokens()), columns.get(2).getTextContent()
642                         .replaceAll("\\s+", " ").trim());
643         Assert.assertEquals(
644                 fileName + " section '" + sectionName
645                         + "' should have all the default javadoc tokens",
646                 CheckUtil.getJavadocTokenText(check.getDefaultJavadocTokens(),
647                         check.getRequiredJavadocTokens()), columns.get(3).getTextContent()
648                         .replaceAll("\\s+", " ").trim());
649     }
650 
651     /**
652      * Get's the name of the bean property's type for the class.
653      * @param clss The bean property's defined type.
654      * @param instance The class instance to work with.
655      * @param propertyName The property name to work with.
656      * @return String form of property's type.
657      * @noinspection IfStatementWithTooManyBranches
658      */
659     private static String getModulePropertyExpectedTypeName(Class<?> clss, Object instance,
660             String propertyName) {
661         final String instanceName = instance.getClass().getSimpleName();
662         String result = null;
663 
664         if (clss == boolean.class) {
665             result = "Boolean";
666         }
667         else if (clss == int.class) {
668             result = "Integer";
669         }
670         else if (clss == int[].class) {
671             result = "Integer Set";
672         }
673         else if (clss == double[].class) {
674             result = "Number Set";
675         }
676         else if (clss == String[].class) {
677             if (propertyName.endsWith("Tokens") || propertyName.endsWith("Token")
678                     || "AtclauseOrderCheck".equals(instanceName) && "target".equals(propertyName)
679                     || "MultipleStringLiteralsCheck".equals(instanceName)
680                             && "ignoreOccurrenceContext".equals(propertyName)) {
681                 result = "subset of tokens TokenTypes";
682             }
683             else {
684                 result = "String Set";
685             }
686         }
687         else if (clss == URI.class) {
688             result = "URI";
689         }
690         else if (clss == Pattern.class) {
691             result = "Regular Expression";
692         }
693         else if (clss == SeverityLevel.class) {
694             result = "Severity";
695         }
696         else if (clss == Scope.class) {
697             result = "Scope";
698         }
699         else if (clss == AccessModifier[].class) {
700             result = "Access Modifier Set";
701         }
702         else if (clss != String.class) {
703             Assert.fail("Unknown property type: " + clss.getSimpleName());
704         }
705 
706         if ("SuppressWarningsHolder".equals(instanceName)) {
707             result = result + " in a format of comma separated attribute=value entries. The "
708                     + "attribute is the fully qualified name of the Check and value is its alias.";
709         }
710 
711         return result;
712     }
713 
714     private static String getModulePropertyExpectedValue(Class<?> clss, Object instance,
715             String propertyName) throws Exception {
716         final Field field = getField(instance.getClass(), propertyName);
717         String result = null;
718 
719         if (field != null) {
720             final Object value = field.get(instance);
721 
722             if (clss == boolean.class) {
723                 result = value.toString();
724             }
725             else if (clss == int.class) {
726                 if (value.equals(Integer.MAX_VALUE)) {
727                     result = "java.lang.Integer.MAX_VALUE";
728                 }
729                 else {
730                     result = value.toString();
731                 }
732             }
733             else if (clss == int[].class) {
734                 result = Arrays.toString((int[]) value).replace("[", "").replace("]", "");
735                 if (result.isEmpty()) {
736                     result = "{}";
737                 }
738             }
739             else if (clss == double[].class) {
740                 result = Arrays.toString((double[]) value).replace("[", "").replace("]", "")
741                         .replace(".0", "");
742                 if (result.isEmpty()) {
743                     result = "{}";
744                 }
745             }
746             else if (clss == URI.class) {
747                 if (value != null) {
748                     result = '"' + value.toString() + '"';
749                 }
750             }
751             else if (clss == Pattern.class) {
752                 if (value != null) {
753                     result = '"' + value.toString().replace("\n", "\\n").replace("\t", "\\t")
754                             .replace("\r", "\\r").replace("\f", "\\f") + '"';
755                 }
756 
757                 if ("\"$^\"".equals(result)) {
758                     result += " (empty)";
759                 }
760             }
761             else if (value != null && (clss == SeverityLevel.class || clss == Scope.class)) {
762                 result = value.toString().toLowerCase(Locale.ENGLISH);
763             }
764             else if (value != null && clss == AccessModifier[].class) {
765                 result = Arrays.toString((Object[]) value).replace("[", "").replace("]", "");
766             }
767 
768             if (clss != String.class && clss != String[].class && result == null) {
769                 result = "null";
770             }
771         }
772 
773         return result;
774     }
775 
776     private static Field getField(Class<?> clss, String propertyName) {
777         Field result = null;
778 
779         if (clss != null) {
780             try {
781                 result = clss.getDeclaredField(propertyName);
782                 result.setAccessible(true);
783             }
784             catch (NoSuchFieldException ignored) {
785                 result = getField(clss.getSuperclass(), propertyName);
786             }
787         }
788 
789         return result;
790     }
791 
792     private static void validateErrorSection(String fileName, String sectionName, Node subSection,
793             Object instance) throws Exception {
794         final Class<?> clss = instance.getClass();
795         final Set<Field> fields = CheckUtil.getCheckMessages(clss);
796         final Set<String> list = new TreeSet<>();
797 
798         for (Field field : fields) {
799             // below is required for package/private classes
800             if (!field.isAccessible()) {
801                 field.setAccessible(true);
802             }
803 
804             list.add(field.get(null).toString());
805         }
806 
807         final StringBuilder expectedText = new StringBuilder(120);
808 
809         for (String s : list) {
810             expectedText.append(s);
811             expectedText.append('\n');
812         }
813 
814         if (expectedText.length() > 0) {
815             expectedText.append("All messages can be customized if the default message doesn't "
816                     + "suit you.\nPlease see the documentation to learn how to.");
817         }
818 
819         if (subSection == null) {
820             Assert.assertEquals(fileName + " section '" + sectionName
821                     + "' should have the expected error keys", "", expectedText.toString());
822         }
823         else {
824             Assert.assertEquals(fileName + " section '" + sectionName
825                     + "' should have the expected error keys", expectedText.toString().trim(),
826                     subSection.getTextContent().replaceAll("\n\\s+", "\n").trim());
827 
828             for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
829                 final String url = node.getAttributes().getNamedItem("href").getTextContent();
830                 final String linkText = node.getTextContent().trim();
831                 final String expectedUrl;
832 
833                 if ("see the documentation".equals(linkText)) {
834                     expectedUrl = "config.html#Custom_messages";
835                 }
836                 else {
837                     expectedUrl = "https://github.com/search?q="
838                             + "path%3Asrc%2Fmain%2Fresources%2F"
839                             + clss.getPackage().getName().replace(".", "%2F")
840                             + "+filename%3Amessages*.properties+repo%3Acheckstyle%2Fcheckstyle+%22"
841                             + linkText + "%22";
842                 }
843 
844                 Assert.assertEquals(fileName + " section '" + sectionName
845                         + "' should have matching url for '" + linkText + "'", expectedUrl, url);
846             }
847         }
848     }
849 
850     private static void validateUsageExample(String fileName, String sectionName, Node subSection) {
851         final String text = subSection.getTextContent().replace("Checkstyle Style", "")
852                 .replace("Google Style", "").replace("Sun Style", "").trim();
853 
854         Assert.assertTrue(fileName + " section '" + sectionName
855                 + "' has unknown text in 'Example of Usage': " + text, text.isEmpty());
856 
857         boolean hasCheckstyle = false;
858         boolean hasGoogle = false;
859         boolean hasSun = false;
860 
861         for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
862             final String url = node.getAttributes().getNamedItem("href").getTextContent();
863             final String linkText = node.getTextContent().trim();
864             String expectedUrl = null;
865 
866             if ("Checkstyle Style".equals(linkText)) {
867                 hasCheckstyle = true;
868                 expectedUrl = "https://github.com/search?q="
869                         + "path%3Aconfig+filename%3Acheckstyle_checks.xml+"
870                         + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName;
871             }
872             else if ("Google Style".equals(linkText)) {
873                 hasGoogle = true;
874                 expectedUrl = "https://github.com/search?q="
875                         + "path%3Asrc%2Fmain%2Fresources+filename%3Agoogle_checks.xml+"
876                         + "repo%3Acheckstyle%2Fcheckstyle+"
877                         + sectionName;
878 
879                 Assert.assertTrue(fileName + " section '" + sectionName
880                         + "' should be in google_checks.xml or not reference 'Google Style'",
881                         GOOGLE_MODULES.contains(sectionName));
882             }
883             else if ("Sun Style".equals(linkText)) {
884                 hasSun = true;
885                 expectedUrl = "https://github.com/search?q="
886                         + "path%3Asrc%2Fmain%2Fresources+filename%3Asun_checks.xml+"
887                         + "repo%3Acheckstyle%2Fcheckstyle+"
888                         + sectionName;
889 
890                 Assert.assertTrue(fileName + " section '" + sectionName
891                         + "' should be in sun_checks.xml or not reference 'Sun Style'",
892                         SUN_MODULES.contains(sectionName));
893             }
894 
895             Assert.assertEquals(fileName + " section '" + sectionName
896                     + "' should have matching url", expectedUrl, url);
897         }
898 
899         Assert.assertTrue(fileName + " section '" + sectionName
900                 + "' should have a checkstyle section", hasCheckstyle);
901         Assert.assertTrue(fileName + " section '" + sectionName
902                 + "' should have a google section since it is in it's config", hasGoogle
903                 || !GOOGLE_MODULES.contains(sectionName));
904         Assert.assertTrue(fileName + " section '" + sectionName
905                 + "' should have a sun section since it is in it's config",
906                 hasSun || !SUN_MODULES.contains(sectionName));
907     }
908 
909     private static void validatePackageSection(String fileName, String sectionName,
910             Node subSection, Object instance) {
911         Assert.assertEquals(fileName + " section '" + sectionName
912                 + "' should have matching package", instance.getClass().getPackage().getName(),
913                 subSection.getTextContent().trim());
914     }
915 
916     private static void validateParentSection(String fileName, String sectionName,
917             Node subSection) {
918         final String expected;
919 
920         if (hasParentModule(sectionName)) {
921             expected = "TreeWalker";
922         }
923         else {
924             expected = "Checker";
925         }
926 
927         Assert.assertEquals(
928                 fileName + " section '" + sectionName + "' should have matching parent",
929                 expected, subSection
930                         .getTextContent().trim());
931     }
932 
933     private static boolean hasParentModule(String sectionName) {
934         final String search = "\"" + sectionName + "\"";
935         boolean result = true;
936 
937         for (String find : XML_FILESET_LIST) {
938             if (find.contains(search)) {
939                 result = false;
940                 break;
941             }
942         }
943 
944         return result;
945     }
946 
947     private static Set<String> getProperties(Class<?> clss) {
948         final Set<String> result = new TreeSet<>();
949         final PropertyDescriptor[] map = PropertyUtils.getPropertyDescriptors(clss);
950 
951         for (PropertyDescriptor p : map) {
952             if (p.getWriteMethod() != null) {
953                 result.add(p.getName());
954             }
955         }
956 
957         return result;
958     }
959 
960     @Test
961     public void testAllStyleRules() throws Exception {
962         for (Path path : XdocUtil.getXdocsStyleFilePaths(XdocUtil.getXdocsFilePaths())) {
963             final String fileName = path.getFileName().toString();
964             final String input = new String(Files.readAllBytes(path), UTF_8);
965             final Document document = XmlUtil.getRawXml(fileName, input, input);
966             final NodeList sources = document.getElementsByTagName("tr");
967             Set<String> styleChecks = null;
968 
969             if (path.toFile().getName().contains("google")) {
970                 styleChecks = new HashSet<>(GOOGLE_MODULES);
971             }
972             else if (path.toFile().getName().contains("sun")) {
973                 styleChecks = new HashSet<>();
974             }
975 
976             String lastRuleName = null;
977 
978             for (int position = 0; position < sources.getLength(); position++) {
979                 final Node row = sources.item(position);
980                 final List<Node> columns = new ArrayList<>(
981                         XmlUtil.findChildElementsByTag(row, "td"));
982 
983                 if (columns.isEmpty()) {
984                     continue;
985                 }
986 
987                 final String ruleName = columns.get(1).getTextContent().trim();
988 
989                 if (lastRuleName != null) {
990                     Assert.assertTrue(
991                             fileName + " rule '" + ruleName + "' is out of order compared to '"
992                                     + lastRuleName + "'",
993                             ruleName.toLowerCase(Locale.ENGLISH).compareTo(
994                                     lastRuleName.toLowerCase(Locale.ENGLISH)) >= 0);
995                 }
996 
997                 if (!"--".equals(ruleName)) {
998                     validateStyleAnchors(XmlUtil.findChildElementsByTag(columns.get(0), "a"),
999                             fileName, ruleName);
1000                 }
1001 
1002                 validateStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"),
1003                         XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, fileName,
1004                         ruleName);
1005 
1006                 lastRuleName = ruleName;
1007             }
1008 
1009             // these modules aren't documented, but are added to the config
1010             styleChecks.remove("TreeWalker");
1011             styleChecks.remove("Checker");
1012 
1013             Assert.assertTrue(fileName + " requires the following check(s) to appear: "
1014                     + styleChecks, styleChecks.isEmpty());
1015         }
1016     }
1017 
1018     private static void validateStyleAnchors(Set<Node> anchors, String fileName, String ruleName) {
1019         Assert.assertEquals(fileName + " rule '" + ruleName + "' must have two row anchors", 2,
1020                 anchors.size());
1021 
1022         final int space = ruleName.indexOf(' ');
1023         Assert.assertTrue(fileName + " rule '" + ruleName
1024                 + "' must have have a space between the rule's number and the rule's name",
1025                 space != -1);
1026 
1027         final String ruleNumber = ruleName.substring(0, space);
1028 
1029         int position = 1;
1030 
1031         for (Node anchor : anchors) {
1032             final String actualUrl;
1033             final String expectedUrl;
1034 
1035             if (position == 1) {
1036                 actualUrl = anchor.getAttributes().getNamedItem("name").getTextContent();
1037                 expectedUrl = ruleNumber;
1038             }
1039             else {
1040                 actualUrl = anchor.getAttributes().getNamedItem("href").getTextContent();
1041                 expectedUrl = "#" + ruleNumber;
1042             }
1043 
1044             Assert.assertEquals(fileName + " rule '" + ruleName + "' anchor " + position
1045                     + " should have matching name/url", expectedUrl, actualUrl);
1046 
1047             position++;
1048         }
1049     }
1050 
1051     private static void validateStyleModules(Set<Node> checks, Set<Node> configs,
1052             Set<String> styleChecks, String fileName, String ruleName) {
1053         final Iterator<Node> itrChecks = checks.iterator();
1054         final Iterator<Node> itrConfigs = configs.iterator();
1055 
1056         while (itrChecks.hasNext()) {
1057             final Node module = itrChecks.next();
1058             final String moduleName = module.getTextContent().trim();
1059 
1060             if (!module.getAttributes().getNamedItem("href").getTextContent()
1061                     .startsWith("config_")) {
1062                 continue;
1063             }
1064 
1065             Assert.assertTrue(fileName + " rule '" + ruleName + "' module '" + moduleName
1066                     + "' shouldn't end with 'Check'", !moduleName.endsWith("Check"));
1067 
1068             styleChecks.remove(moduleName);
1069 
1070             for (String configName : new String[] {"config", "test"}) {
1071                 Node config = null;
1072 
1073                 try {
1074                     config = itrConfigs.next();
1075                 }
1076                 catch (NoSuchElementException ignore) {
1077                     Assert.fail(fileName + " rule '" + ruleName + "' module '" + moduleName
1078                             + "' is missing the config link: " + configName);
1079                 }
1080 
1081                 Assert.assertEquals(fileName + " rule '" + ruleName + "' module '" + moduleName
1082                         + "' has mismatched config/test links", configName, config.getTextContent()
1083                         .trim());
1084 
1085                 final String configUrl = config.getAttributes().getNamedItem("href")
1086                         .getTextContent();
1087 
1088                 if ("config".equals(configName)) {
1089                     final String expectedUrl = "https://github.com/search?q="
1090                             + "path%3Asrc%2Fmain%2Fresources+filename%3Agoogle_checks.xml+"
1091                             + "repo%3Acheckstyle%2Fcheckstyle+" + moduleName;
1092 
1093                     Assert.assertEquals(fileName + " rule '" + ruleName + "' module '" + moduleName
1094                             + "' should have matching " + configName + " url", expectedUrl,
1095                             configUrl);
1096                 }
1097                 else if ("test".equals(configName)) {
1098                     Assert.assertTrue(fileName + " rule '" + ruleName + "' module '" + moduleName
1099                             + "' should have matching " + configName + " url",
1100                             configUrl.startsWith("https://github.com/checkstyle/checkstyle/"
1101                                     + "blob/master/src/it/java/com/google/checkstyle/test/"));
1102                     Assert.assertTrue(fileName + " rule '" + ruleName + "' module '" + moduleName
1103                             + "' should have matching " + configName + " url",
1104                             configUrl.endsWith("/" + moduleName + "Test.java"));
1105 
1106                     Assert.assertTrue(fileName + " rule '" + ruleName + "' module '" + moduleName
1107                             + "' should have a test that exists", new File(configUrl.substring(53)
1108                             .replace('/', File.separatorChar)).exists());
1109                 }
1110             }
1111         }
1112 
1113         Assert.assertFalse(fileName + " rule '" + ruleName + "' has too many configs",
1114                 itrConfigs.hasNext());
1115     }
1116 }