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