1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.internal;
21  
22  import static java.nio.charset.StandardCharsets.UTF_8;
23  
24  import java.io.File;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.regex.Pattern;
33  
34  import javax.xml.parsers.ParserConfigurationException;
35  
36  import org.junit.Assert;
37  import org.junit.Before;
38  import org.junit.Test;
39  import org.w3c.dom.Document;
40  import org.w3c.dom.NamedNodeMap;
41  import org.w3c.dom.Node;
42  import org.w3c.dom.NodeList;
43  
44  import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
45  import com.puppycrawl.tools.checkstyle.Checker;
46  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
47  import com.puppycrawl.tools.checkstyle.ModuleFactory;
48  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
49  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
50  import com.puppycrawl.tools.checkstyle.api.DetailAST;
51  import com.puppycrawl.tools.checkstyle.api.Scope;
52  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
53  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
54  import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
55  import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
56  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
57  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
58  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
59  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
60  
61  public class XdocsJavaDocsTest extends AbstractModuleTestSupport {
62      private static final List<List<Node>> CHECK_PROPERTIES = new ArrayList<>();
63      private static final Map<String, String> CHECK_PROPERTY_DOC = new HashMap<>();
64      private static final Map<String, String> CHECK_TEXT = new HashMap<>();
65  
66      /**
67       * The list of checks that are compatible with this rule.
68       * When the list becomes large, it should be replaced by a suppression list.
69       */
70      private static final String[] COMPATIBLE_CHECKS = {
71          "AbbreviationAsWordInName",
72          "AbstractClassName",
73          "AnnotationLocation",
74          "AnnotationOnSameLine",
75          "AnnotationUseStyle",
76          "ArrayTrailingComma",
77          "AtclauseOrder",
78          "AvoidNestedBlocks",
79          "AvoidInlineConditionals",
80          "CatchParameterName",
81          "ClassMemberImpliedModifier",
82          "ClassTypeParameterName",
83          "ConstantName",
84          "CovariantEquals",
85          "CustomImportOrder",
86          "DeclarationOrder",
87          "DefaultComesLast",
88          "EmptyBlock",
89          "EmptyCatchBlock",
90          "EmptyStatement",
91          "EqualsAvoidNull",
92          "EqualsHashCode",
93          "FallThrough",
94          "FinalLocalVariable",
95          "IllegalInstantiation",
96          "IllegalThrows",
97          "IllegalTokenText",
98          "ImportOrder",
99          "InnerAssignment",
100         "InterfaceMemberImpliedModifier",
101         "InterfaceTypeParameterName",
102         "LambdaParameterName",
103         "LeftCurly",
104         "LocalFinalVariableName",
105         "LocalVariableName",
106         "MagicNumber",
107         "MemberName",
108         "MethodName",
109         "MethodTypeParameterName",
110         "MissingCtor",
111         "MissingDeprecated",
112         "MissingJavadocType",
113         "MissingOverride",
114         "MissingSwitchDefault",
115         "NeedBraces",
116         "NoClone",
117         "PackageAnnotation",
118         "PackageDeclaration",
119         "PackageName",
120         "ParameterName",
121         "RequireThis",
122         "RightCurly",
123         "SimplifyBooleanExpression",
124         "StaticVariableName",
125         "StringLiteralEquality",
126         "SuppressWarnings",
127         "SuppressWarningsHolder",
128         "TypeName",
129         "UnnecessaryParentheses",
130         "VariableDeclarationUsageDistance",
131     };
132 
133     private static Checker checker;
134 
135     private static String checkName;
136 
137     static {
138         Arrays.sort(COMPATIBLE_CHECKS);
139     }
140 
141     @Override
142     protected String getPackageLocation() {
143         return "com.puppycrawl.tools.checkstyle.internal";
144     }
145 
146     @Before
147     public void setUp() throws Exception {
148         final DefaultConfiguration checkConfig = new DefaultConfiguration(
149                 JavaDocCapture.class.getName());
150         checker = createChecker(checkConfig);
151     }
152 
153     /**
154      * Test contains asserts in callstack, but idea does not see them.
155      * @noinspection JUnitTestMethodWithNoAssertions
156      */
157     @Test
158     public void testAllCheckSectionJavaDocs() throws Exception {
159         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
160 
161         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
162             final File file = path.toFile();
163             final String fileName = file.getName();
164 
165             if ("config_reporting.xml".equals(fileName)) {
166                 continue;
167             }
168 
169             final String input = new String(Files.readAllBytes(path), UTF_8);
170             final Document document = XmlUtil.getRawXml(fileName, input, input);
171             final NodeList sources = document.getElementsByTagName("section");
172 
173             for (int position = 0; position < sources.getLength(); position++) {
174                 final Node section = sources.item(position);
175                 final String sectionName = section.getAttributes().getNamedItem("name")
176                         .getNodeValue();
177 
178                 if ("Content".equals(sectionName) || "Overview".equals(sectionName)
179                         || Arrays.binarySearch(COMPATIBLE_CHECKS, sectionName) < 0) {
180                     continue;
181                 }
182 
183                 examineCheckSection(moduleFactory, fileName, sectionName, section);
184             }
185         }
186     }
187 
188     private static void examineCheckSection(ModuleFactory moduleFactory, String fileName,
189             String sectionName, Node section) throws Exception {
190         final Object instance;
191 
192         try {
193             instance = moduleFactory.createModule(sectionName);
194         }
195         catch (CheckstyleException ex) {
196             throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, ex);
197         }
198 
199         CHECK_TEXT.clear();
200         CHECK_PROPERTIES.clear();
201         CHECK_PROPERTY_DOC.clear();
202         checkName = sectionName;
203 
204         examineCheckSectionChildren(section);
205 
206         final List<File> files = new ArrayList<>();
207         files.add(new File("src/main/java/" + instance.getClass().getName().replace(".", "/")
208                 + ".java"));
209 
210         checker.process(files);
211     }
212 
213     private static void examineCheckSectionChildren(Node section) {
214         for (Node subSection : XmlUtil.getChildrenElements(section)) {
215             if (!"subsection".equals(subSection.getNodeName())) {
216                 final String text = getNodeText(subSection, false);
217                 if (text.startsWith("Since Checkstyle")) {
218                     CHECK_TEXT.put("since", text.substring(17));
219                 }
220                 continue;
221             }
222 
223             final String subSectionName = subSection.getAttributes().getNamedItem("name")
224                     .getNodeValue();
225 
226             examineCheckSubSection(subSection, subSectionName);
227         }
228     }
229 
230     private static void examineCheckSubSection(Node subSection, String subSectionName) {
231         switch (subSectionName) {
232             case "Description":
233             case "Examples":
234             case "Notes":
235             case "Rule Description":
236                 CHECK_TEXT.put(subSectionName, getNodeText(subSection, true).replace("\r", ""));
237                 break;
238             case "Properties":
239                 populateProperties(subSection);
240                 CHECK_TEXT.put(subSectionName, createPropertiesText());
241                 break;
242             case "Example of Usage":
243                 break;
244             case "Error Messages":
245                 break;
246             case "Package":
247                 break;
248             case "Parent Module":
249                 break;
250             default:
251                 break;
252         }
253     }
254 
255     private static void populateProperties(Node subSection) {
256         boolean skip = true;
257 
258         for (Node row : XmlUtil.getChildrenElements(XmlUtil.getFirstChildElement(subSection))) {
259             if (skip) {
260                 skip = false;
261                 continue;
262             }
263 
264             CHECK_PROPERTIES.add(new ArrayList<>(XmlUtil.getChildrenElements(row)));
265         }
266     }
267 
268     private static String createPropertiesText() {
269         final StringBuilder result = new StringBuilder(100);
270 
271         result.append("\n<ul>");
272 
273         for (List<Node> property : CHECK_PROPERTIES) {
274             final String propertyName = getNodeText(property.get(0), true);
275 
276             result.append("\n<li>\nProperty {@code ");
277             result.append(propertyName);
278             result.append("} - ");
279 
280             final String temp = getNodeText(property.get(1), true);
281 
282             result.append(temp);
283             CHECK_PROPERTY_DOC.put(propertyName, temp);
284 
285             if (propertyName.endsWith("token") || propertyName.endsWith("tokens")) {
286                 result.append(" Default value is: ");
287             }
288             else {
289                 result.append(" Default value is ");
290             }
291 
292             result.append(getNodeText(property.get(3), true));
293 
294             if (result.charAt(result.length() - 1) != '.') {
295                 result.append('.');
296             }
297 
298             result.append("\n</li>");
299         }
300 
301         result.append("\n</ul>");
302 
303         return result.toString();
304     }
305 
306     private static String getNodeText(Node node, boolean fixLinks) {
307         final StringBuilder result = new StringBuilder(20);
308 
309         for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
310             if (child.getNodeType() == Node.TEXT_NODE) {
311                 for (String temp : child.getTextContent().split("\n")) {
312                     final String text = temp.trim();
313 
314                     if (!text.isEmpty()) {
315                         if (shouldAppendSpace(result, text.charAt(0))) {
316                             result.append(' ');
317                         }
318 
319                         result.append(text);
320                     }
321                 }
322             }
323             else {
324                 appendNodeText(result, child, fixLinks);
325             }
326         }
327 
328         return result.toString();
329     }
330 
331     // -@cs[CyclomaticComplexity] No simple way to split this apart.
332     private static void appendNodeText(StringBuilder result, Node node, boolean fixLinks) {
333         final String name = transformXmlToJavaDocName(node.getNodeName());
334         final boolean list = "ol".equals(name) || "ul".equals(name);
335         final boolean newLineOpenBefore = list || "p".equals(name) || "pre".equals(name)
336                 || "li".equals(name);
337         final boolean newLineOpenAfter = newLineOpenBefore && !list;
338         final boolean newLineClose = newLineOpenAfter || list;
339         final boolean sanitize = "pre".equals(name);
340         final boolean changeToTag = "code".equals(name);
341 
342         if (newLineOpenBefore) {
343             result.append('\n');
344         }
345         else if (shouldAppendSpace(result, '<')) {
346             result.append(' ');
347         }
348 
349         if (changeToTag) {
350             result.append("{@");
351             result.append(name);
352             result.append(' ');
353         }
354         else {
355             result.append('<');
356             result.append(name);
357             result.append(getAttributeText(name, node.getAttributes(), fixLinks));
358             result.append('>');
359         }
360 
361         if (newLineOpenAfter) {
362             result.append('\n');
363         }
364 
365         if (sanitize) {
366             result.append(sanitizeXml(node.getTextContent()));
367         }
368         else {
369             result.append(getNodeText(node, fixLinks));
370         }
371 
372         if (newLineClose) {
373             result.append('\n');
374         }
375 
376         if (changeToTag) {
377             result.append('}');
378         }
379         else {
380             result.append("</");
381             result.append(name);
382             result.append('>');
383         }
384     }
385 
386     private static boolean shouldAppendSpace(StringBuilder text, char firstCharToAppend) {
387         final boolean result;
388 
389         if (text.length() == 0) {
390             result = false;
391         }
392         else {
393             final char last = text.charAt(text.length() - 1);
394 
395             result = (firstCharToAppend == '@'
396                     || Character.getType(last) == Character.OTHER_PUNCTUATION
397                     || Character.isAlphabetic(last)
398                     || Character.isAlphabetic(firstCharToAppend)) && !Character.isWhitespace(last);
399         }
400 
401         return result;
402     }
403 
404     private static String transformXmlToJavaDocName(String name) {
405         final String result;
406 
407         if ("source".equals(name)) {
408             result = "pre";
409         }
410         else {
411             result = name;
412         }
413 
414         return result;
415     }
416 
417     private static String getAttributeText(String nodeName, NamedNodeMap attributes,
418             boolean fixLinks) {
419         final StringBuilder result = new StringBuilder(20);
420 
421         for (int i = 0; i < attributes.getLength(); i++) {
422             result.append(' ');
423 
424             final Node attribute = attributes.item(i);
425             final String attrName = attribute.getNodeName();
426             final String attrValue;
427 
428             if (fixLinks && "a".equals(nodeName) && "href".equals(attrName)
429                     && attribute.getNodeValue().startsWith("apidocs/")) {
430                 attrValue = "https://checkstyle.org/" + attribute.getNodeValue();
431             }
432             else {
433                 attrValue = attribute.getNodeValue();
434             }
435 
436             result.append(attrName);
437             result.append("=\"");
438             result.append(attrValue);
439             result.append('"');
440         }
441 
442         return result.toString();
443     }
444 
445     private static String sanitizeXml(String nodeValue) {
446         return nodeValue.replaceAll("^[\\r\\n\\s]+", "").replaceAll("[\\r\\n\\s]+$", "")
447                 .replace("<", "&lt;").replace(">", "&gt;");
448     }
449 
450     private static class JavaDocCapture extends AbstractCheck {
451         private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*");
452 
453         @Override
454         public boolean isCommentNodesRequired() {
455             return true;
456         }
457 
458         @Override
459         public int[] getRequiredTokens() {
460             return new int[] {
461                 TokenTypes.BLOCK_COMMENT_BEGIN,
462             };
463         }
464 
465         @Override
466         public int[] getDefaultTokens() {
467             return getRequiredTokens();
468         }
469 
470         @Override
471         public int[] getAcceptableTokens() {
472             return getRequiredTokens();
473         }
474 
475         @Override
476         public void visitToken(DetailAST ast) {
477             if (JavadocUtil.isJavadocComment(ast)) {
478                 final DetailAST node = getParent(ast);
479 
480                 switch (node.getType()) {
481                     case TokenTypes.CLASS_DEF:
482                         visitClass(ast);
483                         break;
484                     case TokenTypes.METHOD_DEF:
485                         visitMethod(ast, node);
486                         break;
487                     case TokenTypes.VARIABLE_DEF:
488                         final String propertyName = node.findFirstToken(TokenTypes.IDENT).getText();
489                         final String propertyDoc = CHECK_PROPERTY_DOC.get(propertyName);
490 
491                         if (propertyDoc != null) {
492                             Assert.assertEquals(checkName + "'s class field-level JavaDoc for "
493                                     + propertyName, makeFirstUpper(propertyDoc),
494                                     getJavaDocText(ast));
495                         }
496                         break;
497                     case TokenTypes.CTOR_DEF:
498                     case TokenTypes.ENUM_DEF:
499                     case TokenTypes.ENUM_CONSTANT_DEF:
500                         // ignore
501                         break;
502                     default:
503                         Assert.fail("Unknown token '" + TokenUtil.getTokenName(node.getType())
504                                 + "': " + ast.getLineNo());
505                         break;
506                 }
507             }
508         }
509 
510         private static DetailAST getParent(DetailAST node) {
511             DetailAST result = node.getParent();
512             int type = result.getType();
513 
514             while (type == TokenTypes.MODIFIERS || type == TokenTypes.ANNOTATION) {
515                 result = result.getParent();
516                 type = result.getType();
517             }
518 
519             return result;
520         }
521 
522         private static void visitClass(DetailAST ast) {
523             if (ScopeUtil.isInScope(ast, Scope.PUBLIC)) {
524                 Assert.assertEquals(
525                         checkName + "'s class-level JavaDoc",
526                         CHECK_TEXT.get("Description")
527                                 + CHECK_TEXT.computeIfAbsent("Rule Description", unused -> "")
528                                 + CHECK_TEXT.computeIfAbsent("Notes", unused -> "")
529                                 + CHECK_TEXT.computeIfAbsent("Properties", unused -> "")
530                                 + CHECK_TEXT.get("Examples") + " @since "
531                                 + CHECK_TEXT.get("since"), getJavaDocText(ast));
532             }
533         }
534 
535         private static void visitMethod(DetailAST ast, DetailAST node) {
536             if (ScopeUtil.isInScope(ast, Scope.PUBLIC) && isSetterMethod(node)) {
537                 final String propertyUpper = node.findFirstToken(TokenTypes.IDENT)
538                         .getText().substring(3);
539                 final String propertyName = makeFirstLower(propertyUpper);
540                 final String propertyDoc = CHECK_PROPERTY_DOC.get(propertyName);
541 
542                 if (propertyDoc != null) {
543                     final String javaDoc = getJavaDocText(ast);
544 
545                     Assert.assertEquals(checkName + "'s class method-level JavaDoc for "
546                             + propertyName,
547                             "Setter to " + makeFirstLower(propertyDoc),
548                             javaDoc.substring(0, javaDoc.indexOf(" @param")));
549                 }
550             }
551         }
552 
553         /**
554          * Returns whether an AST represents a setter method. This is similar to
555          * {@link CheckUtil#isSetterMethod(DetailAST)} except this doesn't care
556          * about the number of children in the method.
557          * @param ast the AST to check with.
558          * @return whether the AST represents a setter method.
559          */
560         private static boolean isSetterMethod(DetailAST ast) {
561             boolean setterMethod = false;
562 
563             if (ast.getType() == TokenTypes.METHOD_DEF) {
564                 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
565                 final String name = type.getNextSibling().getText();
566                 final boolean matchesSetterFormat = SETTER_PATTERN.matcher(name).matches();
567                 final boolean voidReturnType = type.findFirstToken(TokenTypes.LITERAL_VOID) != null;
568 
569                 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
570                 final boolean singleParam = params.getChildCount(TokenTypes.PARAMETER_DEF) == 1;
571 
572                 if (matchesSetterFormat && voidReturnType && singleParam) {
573                     final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
574 
575                     setterMethod = slist != null;
576                 }
577             }
578             return setterMethod;
579         }
580 
581         private static String getJavaDocText(DetailAST node) {
582             final String text = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document>\n"
583                     + node.getFirstChild().getText().replaceAll("(^|\\r?\\n)\\s*\\* ?", "\n")
584                             .replaceAll("\\n@noinspection.*\\r?\\n", "\n")
585                             .trim() + "\n</document>";
586             String result = null;
587 
588             try {
589                 result = getNodeText(XmlUtil.getRawXml(checkName, text, text).getFirstChild(),
590                         false).replace("\r", "");
591             }
592             catch (ParserConfigurationException ex) {
593                 Assert.fail("Exception: " + ex.getClass() + " - " + ex.getMessage());
594             }
595 
596             return result;
597         }
598 
599         private static String makeFirstUpper(String str) {
600             final char ch = str.charAt(0);
601             final String result;
602 
603             if (Character.isLowerCase(ch)) {
604                 result = Character.toUpperCase(ch) + str.substring(1);
605             }
606             else {
607                 result = str;
608             }
609 
610             return result;
611         }
612 
613         private static String makeFirstLower(String str) {
614             return Character.toLowerCase(str.charAt(0)) + str.substring(1);
615         }
616     }
617 }