Coverage Report - com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocStyleCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
JavadocStyleCheck
100%
162/162
100%
102/102
0
 
 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.checks.javadoc;
 21  
 
 22  
 import java.util.ArrayDeque;
 23  
 import java.util.Arrays;
 24  
 import java.util.Collections;
 25  
 import java.util.Deque;
 26  
 import java.util.List;
 27  
 import java.util.Locale;
 28  
 import java.util.Set;
 29  
 import java.util.TreeSet;
 30  
 import java.util.regex.Pattern;
 31  
 import java.util.stream.Collectors;
 32  
 
 33  
 import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
 34  
 import com.puppycrawl.tools.checkstyle.StatelessCheck;
 35  
 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
 36  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 37  
 import com.puppycrawl.tools.checkstyle.api.FileContents;
 38  
 import com.puppycrawl.tools.checkstyle.api.Scope;
 39  
 import com.puppycrawl.tools.checkstyle.api.TextBlock;
 40  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 41  
 import com.puppycrawl.tools.checkstyle.utils.CheckUtils;
 42  
 import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
 43  
 import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
 44  
 
 45  
 /**
 46  
  * Custom Checkstyle Check to validate Javadoc.
 47  
  *
 48  
  * @author Chris Stillwell
 49  
  * @author Daniel Grenner
 50  
  * @author Travis Schneeberger
 51  
  */
 52  
 @StatelessCheck
 53  28
 public class JavadocStyleCheck
 54  
     extends AbstractCheck {
 55  
 
 56  
     /** Message property key for the Unclosed HTML message. */
 57  
     public static final String MSG_JAVADOC_MISSING = "javadoc.missing";
 58  
 
 59  
     /** Message property key for the Unclosed HTML message. */
 60  
     public static final String MSG_EMPTY = "javadoc.empty";
 61  
 
 62  
     /** Message property key for the Unclosed HTML message. */
 63  
     public static final String MSG_NO_PERIOD = "javadoc.noPeriod";
 64  
 
 65  
     /** Message property key for the Unclosed HTML message. */
 66  
     public static final String MSG_INCOMPLETE_TAG = "javadoc.incompleteTag";
 67  
 
 68  
     /** Message property key for the Unclosed HTML message. */
 69  
     public static final String MSG_UNCLOSED_HTML = JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
 70  
 
 71  
     /** Message property key for the Extra HTML message. */
 72  
     public static final String MSG_EXTRA_HTML = "javadoc.extraHtml";
 73  
 
 74  
     /** HTML tags that do not require a close tag. */
 75  2
     private static final Set<String> SINGLE_TAGS = Collections.unmodifiableSortedSet(
 76  1
         Arrays.stream(new String[] {"br", "li", "dt", "dd", "hr", "img", "p", "td", "tr", "th", })
 77  1
             .collect(Collectors.toCollection(TreeSet::new)));
 78  
 
 79  
     /** HTML tags that are allowed in java docs.
 80  
      * From https://www.w3schools.com/tags/default.asp
 81  
      * The forms and structure tags are not allowed
 82  
      */
 83  2
     private static final Set<String> ALLOWED_TAGS = Collections.unmodifiableSortedSet(
 84  1
         Arrays.stream(new String[] {
 85  
             "a", "abbr", "acronym", "address", "area", "b", "bdo", "big",
 86  
             "blockquote", "br", "caption", "cite", "code", "colgroup", "dd",
 87  
             "del", "div", "dfn", "dl", "dt", "em", "fieldset", "font", "h1",
 88  
             "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd",
 89  
             "li", "ol", "p", "pre", "q", "samp", "small", "span", "strong",
 90  
             "style", "sub", "sup", "table", "tbody", "td", "tfoot", "th",
 91  
             "thead", "tr", "tt", "u", "ul", "var", })
 92  1
         .collect(Collectors.toCollection(TreeSet::new)));
 93  
 
 94  
     /** The scope to check. */
 95  28
     private Scope scope = Scope.PRIVATE;
 96  
 
 97  
     /** The visibility scope where Javadoc comments shouldn't be checked. **/
 98  
     private Scope excludeScope;
 99  
 
 100  
     /** Format for matching the end of a sentence. */
 101  28
     private Pattern endOfSentenceFormat = Pattern.compile("([.?!][ \t\n\r\f<])|([.?!]$)");
 102  
 
 103  
     /**
 104  
      * Indicates if the first sentence should be checked for proper end of
 105  
      * sentence punctuation.
 106  
      */
 107  28
     private boolean checkFirstSentence = true;
 108  
 
 109  
     /**
 110  
      * Indicates if the HTML within the comment should be checked.
 111  
      */
 112  28
     private boolean checkHtml = true;
 113  
 
 114  
     /**
 115  
      * Indicates if empty javadoc statements should be checked.
 116  
      */
 117  
     private boolean checkEmptyJavadoc;
 118  
 
 119  
     @Override
 120  
     public int[] getDefaultTokens() {
 121  26
         return getAcceptableTokens();
 122  
     }
 123  
 
 124  
     @Override
 125  
     public int[] getAcceptableTokens() {
 126  33
         return new int[] {
 127  
             TokenTypes.ANNOTATION_DEF,
 128  
             TokenTypes.ANNOTATION_FIELD_DEF,
 129  
             TokenTypes.CLASS_DEF,
 130  
             TokenTypes.CTOR_DEF,
 131  
             TokenTypes.ENUM_CONSTANT_DEF,
 132  
             TokenTypes.ENUM_DEF,
 133  
             TokenTypes.INTERFACE_DEF,
 134  
             TokenTypes.METHOD_DEF,
 135  
             TokenTypes.PACKAGE_DEF,
 136  
             TokenTypes.VARIABLE_DEF,
 137  
         };
 138  
     }
 139  
 
 140  
     @Override
 141  
     public int[] getRequiredTokens() {
 142  29
         return CommonUtils.EMPTY_INT_ARRAY;
 143  
     }
 144  
 
 145  
     @Override
 146  
     public void visitToken(DetailAST ast) {
 147  625
         if (shouldCheck(ast)) {
 148  482
             final FileContents contents = getFileContents();
 149  
             // Need to start searching for the comment before the annotations
 150  
             // that may exist. Even if annotations are not defined on the
 151  
             // package, the ANNOTATIONS AST is defined.
 152  482
             final TextBlock textBlock =
 153  482
                 contents.getJavadocBefore(ast.getFirstChild().getLineNo());
 154  
 
 155  482
             checkComment(ast, textBlock);
 156  
         }
 157  625
     }
 158  
 
 159  
     /**
 160  
      * Whether we should check this node.
 161  
      * @param ast a given node.
 162  
      * @return whether we should check a given node.
 163  
      */
 164  
     private boolean shouldCheck(final DetailAST ast) {
 165  625
         boolean check = false;
 166  
 
 167  625
         if (ast.getType() == TokenTypes.PACKAGE_DEF) {
 168  17
             check = getFileContents().inPackageInfo();
 169  
         }
 170  608
         else if (!ScopeUtils.isInCodeBlock(ast)) {
 171  
             final Scope customScope;
 172  
 
 173  599
             if (ScopeUtils.isInInterfaceOrAnnotationBlock(ast)
 174  589
                     || ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
 175  28
                 customScope = Scope.PUBLIC;
 176  
             }
 177  
             else {
 178  571
                 customScope = ScopeUtils.getScopeFromMods(ast.findFirstToken(TokenTypes.MODIFIERS));
 179  
             }
 180  599
             final Scope surroundingScope = ScopeUtils.getSurroundingScope(ast);
 181  
 
 182  599
             check = customScope.isIn(scope)
 183  492
                     && (surroundingScope == null || surroundingScope.isIn(scope))
 184  
                     && (excludeScope == null
 185  52
                         || !customScope.isIn(excludeScope)
 186  
                         || surroundingScope != null
 187  25
                             && !surroundingScope.isIn(excludeScope));
 188  
         }
 189  625
         return check;
 190  
     }
 191  
 
 192  
     /**
 193  
      * Performs the various checks against the Javadoc comment.
 194  
      *
 195  
      * @param ast the AST of the element being documented
 196  
      * @param comment the source lines that make up the Javadoc comment.
 197  
      *
 198  
      * @see #checkFirstSentenceEnding(DetailAST, TextBlock)
 199  
      * @see #checkHtmlTags(DetailAST, TextBlock)
 200  
      */
 201  
     private void checkComment(final DetailAST ast, final TextBlock comment) {
 202  482
         if (comment == null) {
 203  
             // checking for missing docs in JavadocStyleCheck is not consistent
 204  
             // with the rest of CheckStyle...  Even though, I didn't think it
 205  
             // made sense to make another check just to ensure that the
 206  
             // package-info.java file actually contains package Javadocs.
 207  114
             if (getFileContents().inPackageInfo()) {
 208  1
                 log(ast.getLineNo(), MSG_JAVADOC_MISSING);
 209  
             }
 210  
         }
 211  
         else {
 212  368
             if (checkFirstSentence) {
 213  265
                 checkFirstSentenceEnding(ast, comment);
 214  
             }
 215  
 
 216  368
             if (checkHtml) {
 217  209
                 checkHtmlTags(ast, comment);
 218  
             }
 219  
 
 220  368
             if (checkEmptyJavadoc) {
 221  126
                 checkJavadocIsNotEmpty(comment);
 222  
             }
 223  
         }
 224  482
     }
 225  
 
 226  
     /**
 227  
      * Checks that the first sentence ends with proper punctuation.  This method
 228  
      * uses a regular expression that checks for the presence of a period,
 229  
      * question mark, or exclamation mark followed either by whitespace, an
 230  
      * HTML element, or the end of string. This method ignores {_AT_inheritDoc}
 231  
      * comments for TokenTypes that are valid for {_AT_inheritDoc}.
 232  
      *
 233  
      * @param ast the current node
 234  
      * @param comment the source lines that make up the Javadoc comment.
 235  
      */
 236  
     private void checkFirstSentenceEnding(final DetailAST ast, TextBlock comment) {
 237  265
         final String commentText = getCommentText(comment.getText());
 238  
 
 239  265
         if (!commentText.isEmpty()
 240  237
             && !endOfSentenceFormat.matcher(commentText).find()
 241  85
             && !(commentText.startsWith("{@inheritDoc}")
 242  17
             && JavadocTagInfo.INHERIT_DOC.isValidOn(ast))) {
 243  73
             log(comment.getStartLineNo(), MSG_NO_PERIOD);
 244  
         }
 245  265
     }
 246  
 
 247  
     /**
 248  
      * Checks that the Javadoc is not empty.
 249  
      *
 250  
      * @param comment the source lines that make up the Javadoc comment.
 251  
      */
 252  
     private void checkJavadocIsNotEmpty(TextBlock comment) {
 253  126
         final String commentText = getCommentText(comment.getText());
 254  
 
 255  126
         if (commentText.isEmpty()) {
 256  14
             log(comment.getStartLineNo(), MSG_EMPTY);
 257  
         }
 258  126
     }
 259  
 
 260  
     /**
 261  
      * Returns the comment text from the Javadoc.
 262  
      * @param comments the lines of Javadoc.
 263  
      * @return a comment text String.
 264  
      */
 265  
     private static String getCommentText(String... comments) {
 266  391
         final StringBuilder builder = new StringBuilder(1024);
 267  1827
         for (final String line : comments) {
 268  1595
             final int textStart = findTextStart(line);
 269  
 
 270  1595
             if (textStart != -1) {
 271  925
                 if (line.charAt(textStart) == '@') {
 272  
                     //we have found the tag section
 273  159
                     break;
 274  
                 }
 275  766
                 builder.append(line.substring(textStart));
 276  766
                 trimTail(builder);
 277  766
                 builder.append('\n');
 278  
             }
 279  
         }
 280  
 
 281  391
         return builder.toString().trim();
 282  
     }
 283  
 
 284  
     /**
 285  
      * Finds the index of the first non-whitespace character ignoring the
 286  
      * Javadoc comment start and end strings (&#47** and *&#47) as well as any
 287  
      * leading asterisk.
 288  
      * @param line the Javadoc comment line of text to scan.
 289  
      * @return the int index relative to 0 for the start of text
 290  
      *         or -1 if not found.
 291  
      */
 292  
     private static int findTextStart(String line) {
 293  1595
         int textStart = -1;
 294  1595
         int index = 0;
 295  13560
         while (index < line.length()) {
 296  12890
             if (!Character.isWhitespace(line.charAt(index))) {
 297  2531
                 if (line.regionMatches(index, "/**", 0, "/**".length())) {
 298  391
                     index += 2;
 299  
                 }
 300  2140
                 else if (line.regionMatches(index, "*/", 0, 2)) {
 301  206
                     index++;
 302  
                 }
 303  1934
                 else if (line.charAt(index) != '*') {
 304  925
                     textStart = index;
 305  925
                     break;
 306  
                 }
 307  
             }
 308  11965
             index++;
 309  
         }
 310  1595
         return textStart;
 311  
     }
 312  
 
 313  
     /**
 314  
      * Trims any trailing whitespace or the end of Javadoc comment string.
 315  
      * @param builder the StringBuilder to trim.
 316  
      */
 317  
     private static void trimTail(StringBuilder builder) {
 318  766
         int index = builder.length() - 1;
 319  
         while (true) {
 320  868
             if (Character.isWhitespace(builder.charAt(index))) {
 321  76
                 builder.deleteCharAt(index);
 322  
             }
 323  792
             else if (index > 0 && builder.charAt(index) == '/'
 324  33
                     && builder.charAt(index - 1) == '*') {
 325  26
                 builder.deleteCharAt(index);
 326  26
                 builder.deleteCharAt(index - 1);
 327  26
                 index--;
 328  37
                 while (builder.charAt(index - 1) == '*') {
 329  11
                     builder.deleteCharAt(index - 1);
 330  11
                     index--;
 331  
                 }
 332  
             }
 333  
             else {
 334  
                 break;
 335  
             }
 336  102
             index--;
 337  
         }
 338  766
     }
 339  
 
 340  
     /**
 341  
      * Checks the comment for HTML tags that do not have a corresponding close
 342  
      * tag or a close tag that has no previous open tag.  This code was
 343  
      * primarily copied from the DocCheck checkHtml method.
 344  
      *
 345  
      * @param ast the node with the Javadoc
 346  
      * @param comment the {@code TextBlock} which represents
 347  
      *                 the Javadoc comment.
 348  
      * @noinspection MethodWithMultipleReturnPoints
 349  
      */
 350  
     // -@cs[ReturnCount] Too complex to break apart.
 351  
     private void checkHtmlTags(final DetailAST ast, final TextBlock comment) {
 352  209
         final int lineNo = comment.getStartLineNo();
 353  209
         final Deque<HtmlTag> htmlStack = new ArrayDeque<>();
 354  209
         final String[] text = comment.getText();
 355  
 
 356  209
         final TagParser parser = new TagParser(text, lineNo);
 357  
 
 358  411
         while (parser.hasNextTag()) {
 359  207
             final HtmlTag tag = parser.nextTag();
 360  
 
 361  207
             if (tag.isIncompleteTag()) {
 362  10
                 log(tag.getLineNo(), MSG_INCOMPLETE_TAG,
 363  5
                     text[tag.getLineNo() - lineNo]);
 364  5
                 return;
 365  
             }
 366  202
             if (tag.isClosedTag()) {
 367  
                 //do nothing
 368  16
                 continue;
 369  
             }
 370  186
             if (tag.isCloseTag()) {
 371  
                 // We have found a close tag.
 372  51
                 if (isExtraHtml(tag.getId(), htmlStack)) {
 373  
                     // No corresponding open tag was found on the stack.
 374  32
                     log(tag.getLineNo(),
 375  16
                         tag.getPosition(),
 376  
                         MSG_EXTRA_HTML,
 377  16
                         tag.getText());
 378  
                 }
 379  
                 else {
 380  
                     // See if there are any unclosed tags that were opened
 381  
                     // after this one.
 382  35
                     checkUnclosedTags(htmlStack, tag.getId());
 383  
                 }
 384  
             }
 385  
             else {
 386  
                 //We only push html tags that are allowed
 387  135
                 if (isAllowedTag(tag)) {
 388  86
                     htmlStack.push(tag);
 389  
                 }
 390  
             }
 391  186
         }
 392  
 
 393  
         // Identify any tags left on the stack.
 394  
         // Skip multiples, like <b>...<b>
 395  204
         String lastFound = "";
 396  204
         final List<String> typeParameters = CheckUtils.getTypeParameterNames(ast);
 397  204
         for (final HtmlTag htmlTag : htmlStack) {
 398  34
             if (!isSingleTag(htmlTag)
 399  21
                 && !htmlTag.getId().equals(lastFound)
 400  18
                 && !typeParameters.contains(htmlTag.getId())) {
 401  34
                 log(htmlTag.getLineNo(), htmlTag.getPosition(),
 402  17
                         MSG_UNCLOSED_HTML, htmlTag.getText());
 403  17
                 lastFound = htmlTag.getId();
 404  
             }
 405  34
         }
 406  204
     }
 407  
 
 408  
     /**
 409  
      * Checks to see if there are any unclosed tags on the stack.  The token
 410  
      * represents a html tag that has been closed and has a corresponding open
 411  
      * tag on the stack.  Any tags, except single tags, that were opened
 412  
      * (pushed on the stack) after the token are missing a close.
 413  
      *
 414  
      * @param htmlStack the stack of opened HTML tags.
 415  
      * @param token the current HTML tag name that has been closed.
 416  
      */
 417  
     private void checkUnclosedTags(Deque<HtmlTag> htmlStack, String token) {
 418  35
         final Deque<HtmlTag> unclosedTags = new ArrayDeque<>();
 419  35
         HtmlTag lastOpenTag = htmlStack.pop();
 420  52
         while (!token.equalsIgnoreCase(lastOpenTag.getId())) {
 421  
             // Find unclosed elements. Put them on a stack so the
 422  
             // output order won't be back-to-front.
 423  17
             if (isSingleTag(lastOpenTag)) {
 424  8
                 lastOpenTag = htmlStack.pop();
 425  
             }
 426  
             else {
 427  9
                 unclosedTags.push(lastOpenTag);
 428  9
                 lastOpenTag = htmlStack.pop();
 429  
             }
 430  
         }
 431  
 
 432  
         // Output the unterminated tags, if any
 433  
         // Skip multiples, like <b>..<b>
 434  35
         String lastFound = "";
 435  35
         for (final HtmlTag htag : unclosedTags) {
 436  9
             lastOpenTag = htag;
 437  9
             if (lastOpenTag.getId().equals(lastFound)) {
 438  3
                 continue;
 439  
             }
 440  6
             lastFound = lastOpenTag.getId();
 441  12
             log(lastOpenTag.getLineNo(),
 442  6
                 lastOpenTag.getPosition(),
 443  
                 MSG_UNCLOSED_HTML,
 444  6
                 lastOpenTag.getText());
 445  6
         }
 446  35
     }
 447  
 
 448  
     /**
 449  
      * Determines if the HtmlTag is one which does not require a close tag.
 450  
      *
 451  
      * @param tag the HtmlTag to check.
 452  
      * @return {@code true} if the HtmlTag is a single tag.
 453  
      */
 454  
     private static boolean isSingleTag(HtmlTag tag) {
 455  
         // If its a singleton tag (<p>, <br>, etc.), ignore it
 456  
         // Can't simply not put them on the stack, since singletons
 457  
         // like <dt> and <dd> (unhappily) may either be terminated
 458  
         // or not terminated. Both options are legal.
 459  51
         return SINGLE_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
 460  
     }
 461  
 
 462  
     /**
 463  
      * Determines if the HtmlTag is one which is allowed in a javadoc.
 464  
      *
 465  
      * @param tag the HtmlTag to check.
 466  
      * @return {@code true} if the HtmlTag is an allowed html tag.
 467  
      */
 468  
     private static boolean isAllowedTag(HtmlTag tag) {
 469  135
         return ALLOWED_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
 470  
     }
 471  
 
 472  
     /**
 473  
      * Determines if the given token is an extra HTML tag. This indicates that
 474  
      * a close tag was found that does not have a corresponding open tag.
 475  
      *
 476  
      * @param token an HTML tag id for which a close was found.
 477  
      * @param htmlStack a Stack of previous open HTML tags.
 478  
      * @return {@code false} if a previous open tag was found
 479  
      *         for the token.
 480  
      */
 481  
     private static boolean isExtraHtml(String token, Deque<HtmlTag> htmlStack) {
 482  51
         boolean isExtra = true;
 483  51
         for (final HtmlTag tag : htmlStack) {
 484  
             // Loop, looking for tags that are closed.
 485  
             // The loop is needed in case there are unclosed
 486  
             // tags on the stack. In that case, the stack would
 487  
             // not be empty, but this tag would still be extra.
 488  52
             if (token.equalsIgnoreCase(tag.getId())) {
 489  35
                 isExtra = false;
 490  35
                 break;
 491  
             }
 492  17
         }
 493  
 
 494  51
         return isExtra;
 495  
     }
 496  
 
 497  
     /**
 498  
      * Sets the scope to check.
 499  
      * @param scope a scope.
 500  
      */
 501  
     public void setScope(Scope scope) {
 502  7
         this.scope = scope;
 503  7
     }
 504  
 
 505  
     /**
 506  
      * Set the excludeScope.
 507  
      * @param excludeScope a scope.
 508  
      */
 509  
     public void setExcludeScope(Scope excludeScope) {
 510  2
         this.excludeScope = excludeScope;
 511  2
     }
 512  
 
 513  
     /**
 514  
      * Set the format for matching the end of a sentence.
 515  
      * @param pattern a pattern.
 516  
      */
 517  
     public void setEndOfSentenceFormat(Pattern pattern) {
 518  1
         endOfSentenceFormat = pattern;
 519  1
     }
 520  
 
 521  
     /**
 522  
      * Sets the flag that determines if the first sentence is checked for
 523  
      * proper end of sentence punctuation.
 524  
      * @param flag {@code true} if the first sentence is to be checked
 525  
      */
 526  
     public void setCheckFirstSentence(boolean flag) {
 527  10
         checkFirstSentence = flag;
 528  10
     }
 529  
 
 530  
     /**
 531  
      * Sets the flag that determines if HTML checking is to be performed.
 532  
      * @param flag {@code true} if HTML checking is to be performed.
 533  
      */
 534  
     public void setCheckHtml(boolean flag) {
 535  9
         checkHtml = flag;
 536  9
     }
 537  
 
 538  
     /**
 539  
      * Sets the flag that determines if empty Javadoc checking should be done.
 540  
      * @param flag {@code true} if empty Javadoc checking should be done.
 541  
      */
 542  
     public void setCheckEmptyJavadoc(boolean flag) {
 543  5
         checkEmptyJavadoc = flag;
 544  5
     }
 545  
 }