001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2014 Oliver Burn 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019package com.puppycrawl.tools.checkstyle.checks.javadoc; 020 021import com.google.common.collect.ImmutableSortedSet; 022import com.puppycrawl.tools.checkstyle.api.Check; 023import com.puppycrawl.tools.checkstyle.api.DetailAST; 024import com.puppycrawl.tools.checkstyle.api.FastStack; 025import com.puppycrawl.tools.checkstyle.api.FileContents; 026import com.puppycrawl.tools.checkstyle.api.JavadocTagInfo; 027import com.puppycrawl.tools.checkstyle.api.Scope; 028import com.puppycrawl.tools.checkstyle.api.ScopeUtils; 029import com.puppycrawl.tools.checkstyle.api.TextBlock; 030import com.puppycrawl.tools.checkstyle.api.TokenTypes; 031import com.puppycrawl.tools.checkstyle.checks.CheckUtils; 032import java.util.List; 033import java.util.Set; 034import java.util.regex.Pattern; 035 036/** 037 * Custom Checkstyle Check to validate Javadoc. 038 * 039 * @author Chris Stillwell 040 * @author Daniel Grenner 041 * @author Travis Schneeberger 042 * @version 1.2 043 */ 044public class JavadocStyleCheck 045 extends Check 046{ 047 /** Message property key for the Unclosed HTML message. */ 048 private static final String UNCLOSED_HTML = "javadoc.unclosedhtml"; 049 050 /** Message property key for the Extra HTML message. */ 051 private static final String EXTRA_HTML = "javadoc.extrahtml"; 052 053 /** HTML tags that do not require a close tag. */ 054 private static final Set<String> SINGLE_TAGS = ImmutableSortedSet.of( 055 "br", "li", "dt", "dd", "hr", "img", "p", "td", "tr", "th"); 056 057 /** HTML tags that are allowed in java docs. 058 * From http://www.w3schools.com/tags/default.asp 059 * The froms and structure tags are not allowed 060 */ 061 private static final Set<String> ALLOWED_TAGS = ImmutableSortedSet.of( 062 "a", "abbr", "acronym", "address", "area", "b", "bdo", "big", 063 "blockquote", "br", "caption", "cite", "code", "colgroup", "dd", 064 "del", "div", "dfn", "dl", "dt", "em", "fieldset", "font", "h1", 065 "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd", 066 "li", "ol", "p", "pre", "q", "samp", "small", "span", "strong", 067 "style", "sub", "sup", "table", "tbody", "td", "tfoot", "th", 068 "thead", "tr", "tt", "u", "ul"); 069 070 /** The scope to check. */ 071 private Scope scope = Scope.PRIVATE; 072 073 /** the visibility scope where Javadoc comments shouldn't be checked **/ 074 private Scope excludeScope; 075 076 /** Format for matching the end of a sentence. */ 077 private String endOfSentenceFormat = "([.?!][ \t\n\r\f<])|([.?!]$)"; 078 079 /** Regular expression for matching the end of a sentence. */ 080 private Pattern endOfSentencePattern; 081 082 /** 083 * Indicates if the first sentence should be checked for proper end of 084 * sentence punctuation. 085 */ 086 private boolean checkFirstSentence = true; 087 088 /** 089 * Indicates if the HTML within the comment should be checked. 090 */ 091 private boolean checkHtml = true; 092 093 /** 094 * Indicates if empty javadoc statements should be checked. 095 */ 096 private boolean checkEmptyJavadoc; 097 098 @Override 099 public int[] getDefaultTokens() 100 { 101 return new int[] { 102 TokenTypes.INTERFACE_DEF, 103 TokenTypes.CLASS_DEF, 104 TokenTypes.ANNOTATION_DEF, 105 TokenTypes.ENUM_DEF, 106 TokenTypes.METHOD_DEF, 107 TokenTypes.CTOR_DEF, 108 TokenTypes.VARIABLE_DEF, 109 TokenTypes.ENUM_CONSTANT_DEF, 110 TokenTypes.ANNOTATION_FIELD_DEF, 111 TokenTypes.PACKAGE_DEF, 112 }; 113 } 114 115 @Override 116 public void visitToken(DetailAST ast) 117 { 118 if (shouldCheck(ast)) { 119 final FileContents contents = getFileContents(); 120 // Need to start searching for the comment before the annotations 121 // that may exist. Even if annotations are not defined on the 122 // package, the ANNOTATIONS AST is defined. 123 final TextBlock cmt = 124 contents.getJavadocBefore(ast.getFirstChild().getLineNo()); 125 126 checkComment(ast, cmt); 127 } 128 } 129 130 /** 131 * Whether we should check this node. 132 * @param ast a given node. 133 * @return whether we should check a given node. 134 */ 135 private boolean shouldCheck(final DetailAST ast) 136 { 137 if (ast.getType() == TokenTypes.PACKAGE_DEF) { 138 return getFileContents().inPackageInfo(); 139 } 140 141 if (ScopeUtils.inCodeBlock(ast)) { 142 return false; 143 } 144 145 final Scope declaredScope; 146 if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) { 147 declaredScope = Scope.PUBLIC; 148 } 149 else { 150 declaredScope = ScopeUtils.getScopeFromMods( 151 ast.findFirstToken(TokenTypes.MODIFIERS)); 152 } 153 154 final Scope scope = 155 ScopeUtils.inInterfaceOrAnnotationBlock(ast) 156 ? Scope.PUBLIC : declaredScope; 157 final Scope surroundingScope = ScopeUtils.getSurroundingScope(ast); 158 159 return scope.isIn(this.scope) 160 && ((surroundingScope == null) || surroundingScope.isIn(this.scope)) 161 && ((excludeScope == null) 162 || !scope.isIn(excludeScope) 163 || ((surroundingScope != null) 164 && !surroundingScope.isIn(excludeScope))); 165 } 166 167 /** 168 * Performs the various checks agains the Javadoc comment. 169 * 170 * @param ast the AST of the element being documented 171 * @param comment the source lines that make up the Javadoc comment. 172 * 173 * @see #checkFirstSentence(DetailAST, TextBlock) 174 * @see #checkHtml(DetailAST, TextBlock) 175 */ 176 private void checkComment(final DetailAST ast, final TextBlock comment) 177 { 178 if (comment == null) { 179 /*checking for missing docs in JavadocStyleCheck is not consistent 180 with the rest of CheckStyle... Even though, I didn't think it 181 made sense to make another csheck just to ensure that the 182 package-info.java file actually contains package Javadocs.*/ 183 if (getFileContents().inPackageInfo()) { 184 log(ast.getLineNo(), "javadoc.missing"); 185 } 186 return; 187 } 188 189 if (checkFirstSentence) { 190 checkFirstSentence(ast, comment); 191 } 192 193 if (checkHtml) { 194 checkHtml(ast, comment); 195 } 196 197 if (checkEmptyJavadoc) { 198 checkEmptyJavadoc(comment); 199 } 200 } 201 202 /** 203 * Checks that the first sentence ends with proper punctuation. This method 204 * uses a regular expression that checks for the presence of a period, 205 * question mark, or exclamation mark followed either by whitespace, an 206 * HTML element, or the end of string. This method ignores {_AT_inheritDoc} 207 * comments for TokenTypes that are valid for {_AT_inheritDoc}. 208 * 209 * @param ast the current node 210 * @param comment the source lines that make up the Javadoc comment. 211 */ 212 private void checkFirstSentence(final DetailAST ast, TextBlock comment) 213 { 214 final String commentText = getCommentText(comment.getText()); 215 216 if ((commentText.length() != 0) 217 && !getEndOfSentencePattern().matcher(commentText).find() 218 && !("{@inheritDoc}".equals(commentText) 219 && JavadocTagInfo.INHERIT_DOC.isValidOn(ast))) 220 { 221 log(comment.getStartLineNo(), "javadoc.noperiod"); 222 } 223 } 224 225 /** 226 * Checks that the Javadoc is not empty. 227 * 228 * @param comment the source lines that make up the Javadoc comment. 229 */ 230 private void checkEmptyJavadoc(TextBlock comment) 231 { 232 final String commentText = getCommentText(comment.getText()); 233 234 if (commentText.length() == 0) { 235 log(comment.getStartLineNo(), "javadoc.empty"); 236 } 237 } 238 239 /** 240 * Returns the comment text from the Javadoc. 241 * @param comments the lines of Javadoc. 242 * @return a comment text String. 243 */ 244 private String getCommentText(String[] comments) 245 { 246 final StringBuffer buffer = new StringBuffer(); 247 for (final String line : comments) { 248 final int textStart = findTextStart(line); 249 250 if (textStart != -1) { 251 if (line.charAt(textStart) == '@') { 252 //we have found the tag section 253 break; 254 } 255 buffer.append(line.substring(textStart)); 256 trimTail(buffer); 257 buffer.append('\n'); 258 } 259 } 260 261 return buffer.toString().trim(); 262 } 263 264 /** 265 * Finds the index of the first non-whitespace character ignoring the 266 * Javadoc comment start and end strings (/** and */) as well as any 267 * leading asterisk. 268 * @param line the Javadoc comment line of text to scan. 269 * @return the int index relative to 0 for the start of text 270 * or -1 if not found. 271 */ 272 private int findTextStart(String line) 273 { 274 int textStart = -1; 275 for (int i = 0; i < line.length(); i++) { 276 if (!Character.isWhitespace(line.charAt(i))) { 277 if (line.regionMatches(i, "/**", 0, "/**".length())) { 278 i += 2; 279 } 280 else if (line.regionMatches(i, "*/", 0, 2)) { 281 i++; 282 } 283 else if (line.charAt(i) != '*') { 284 textStart = i; 285 break; 286 } 287 } 288 } 289 return textStart; 290 } 291 292 /** 293 * Trims any trailing whitespace or the end of Javadoc comment string. 294 * @param buffer the StringBuffer to trim. 295 */ 296 private void trimTail(StringBuffer buffer) 297 { 298 for (int i = buffer.length() - 1; i >= 0; i--) { 299 if (Character.isWhitespace(buffer.charAt(i))) { 300 buffer.deleteCharAt(i); 301 } 302 else if ((i > 0) 303 && (buffer.charAt(i - 1) == '*') 304 && (buffer.charAt(i) == '/')) 305 { 306 buffer.deleteCharAt(i); 307 buffer.deleteCharAt(i - 1); 308 i--; 309 while (buffer.charAt(i - 1) == '*') { 310 buffer.deleteCharAt(i - 1); 311 i--; 312 } 313 } 314 else { 315 break; 316 } 317 } 318 } 319 320 /** 321 * Checks the comment for HTML tags that do not have a corresponding close 322 * tag or a close tag that has no previous open tag. This code was 323 * primarily copied from the DocCheck checkHtml method. 324 * 325 * @param ast the node with the Javadoc 326 * @param comment the <code>TextBlock</code> which represents 327 * the Javadoc comment. 328 */ 329 private void checkHtml(final DetailAST ast, final TextBlock comment) 330 { 331 final int lineno = comment.getStartLineNo(); 332 final FastStack<HtmlTag> htmlStack = FastStack.newInstance(); 333 final String[] text = comment.getText(); 334 final List<String> typeParameters = 335 CheckUtils.getTypeParameterNames(ast); 336 337 TagParser parser = null; 338 parser = new TagParser(text, lineno); 339 340 while (parser.hasNextTag()) { 341 final HtmlTag tag = parser.nextTag(); 342 343 if (tag.isIncompleteTag()) { 344 log(tag.getLineno(), "javadoc.incompleteTag", 345 text[tag.getLineno() - lineno]); 346 return; 347 } 348 if (tag.isClosedTag()) { 349 //do nothing 350 continue; 351 } 352 if (!tag.isCloseTag()) { 353 //We only push html tags that are allowed 354 if (isAllowedTag(tag)) { 355 htmlStack.push(tag); 356 } 357 } 358 else { 359 // We have found a close tag. 360 if (isExtraHtml(tag.getId(), htmlStack)) { 361 // No corresponding open tag was found on the stack. 362 log(tag.getLineno(), 363 tag.getPosition(), 364 EXTRA_HTML, 365 tag); 366 } 367 else { 368 // See if there are any unclosed tags that were opened 369 // after this one. 370 checkUnclosedTags(htmlStack, tag.getId()); 371 } 372 } 373 } 374 375 // Identify any tags left on the stack. 376 String lastFound = ""; // Skip multiples, like <b>...<b> 377 for (final HtmlTag htag : htmlStack) { 378 if (!isSingleTag(htag) 379 && !htag.getId().equals(lastFound) 380 && !typeParameters.contains(htag.getId())) 381 { 382 log(htag.getLineno(), htag.getPosition(), UNCLOSED_HTML, htag); 383 lastFound = htag.getId(); 384 } 385 } 386 } 387 388 /** 389 * Checks to see if there are any unclosed tags on the stack. The token 390 * represents a html tag that has been closed and has a corresponding open 391 * tag on the stack. Any tags, except single tags, that were opened 392 * (pushed on the stack) after the token are missing a close. 393 * 394 * @param htmlStack the stack of opened HTML tags. 395 * @param token the current HTML tag name that has been closed. 396 */ 397 private void checkUnclosedTags(FastStack<HtmlTag> htmlStack, String token) 398 { 399 final FastStack<HtmlTag> unclosedTags = FastStack.newInstance(); 400 HtmlTag lastOpenTag = htmlStack.pop(); 401 while (!token.equalsIgnoreCase(lastOpenTag.getId())) { 402 // Find unclosed elements. Put them on a stack so the 403 // output order won't be back-to-front. 404 if (isSingleTag(lastOpenTag)) { 405 lastOpenTag = htmlStack.pop(); 406 } 407 else { 408 unclosedTags.push(lastOpenTag); 409 lastOpenTag = htmlStack.pop(); 410 } 411 } 412 413 // Output the unterminated tags, if any 414 String lastFound = ""; // Skip multiples, like <b>..<b> 415 for (final HtmlTag htag : unclosedTags) { 416 lastOpenTag = htag; 417 if (lastOpenTag.getId().equals(lastFound)) { 418 continue; 419 } 420 lastFound = lastOpenTag.getId(); 421 log(lastOpenTag.getLineno(), 422 lastOpenTag.getPosition(), 423 UNCLOSED_HTML, 424 lastOpenTag); 425 } 426 } 427 428 /** 429 * Determines if the HtmlTag is one which does not require a close tag. 430 * 431 * @param tag the HtmlTag to check. 432 * @return <code>true</code> if the HtmlTag is a single tag. 433 */ 434 private boolean isSingleTag(HtmlTag tag) 435 { 436 // If its a singleton tag (<p>, <br>, etc.), ignore it 437 // Can't simply not put them on the stack, since singletons 438 // like <dt> and <dd> (unhappily) may either be terminated 439 // or not terminated. Both options are legal. 440 return SINGLE_TAGS.contains(tag.getId().toLowerCase()); 441 } 442 443 /** 444 * Determines if the HtmlTag is one which is allowed in a javadoc. 445 * 446 * @param tag the HtmlTag to check. 447 * @return <code>true</code> if the HtmlTag is an allowed html tag. 448 */ 449 private boolean isAllowedTag(HtmlTag tag) 450 { 451 return ALLOWED_TAGS.contains(tag.getId().toLowerCase()); 452 } 453 454 /** 455 * Determines if the given token is an extra HTML tag. This indicates that 456 * a close tag was found that does not have a corresponding open tag. 457 * 458 * @param token an HTML tag id for which a close was found. 459 * @param htmlStack a Stack of previous open HTML tags. 460 * @return <code>false</code> if a previous open tag was found 461 * for the token. 462 */ 463 private boolean isExtraHtml(String token, FastStack<HtmlTag> htmlStack) 464 { 465 boolean isExtra = true; 466 for (final HtmlTag td : htmlStack) { 467 // Loop, looking for tags that are closed. 468 // The loop is needed in case there are unclosed 469 // tags on the stack. In that case, the stack would 470 // not be empty, but this tag would still be extra. 471 if (token.equalsIgnoreCase(td.getId())) { 472 isExtra = false; 473 break; 474 } 475 } 476 477 return isExtra; 478 } 479 480 /** 481 * Sets the scope to check. 482 * @param from string to get the scope from 483 */ 484 public void setScope(String from) 485 { 486 scope = Scope.getInstance(from); 487 } 488 489 /** 490 * Set the excludeScope. 491 * @param scope a <code>String</code> value 492 */ 493 public void setExcludeScope(String scope) 494 { 495 excludeScope = Scope.getInstance(scope); 496 } 497 498 /** 499 * Set the format for matching the end of a sentence. 500 * @param format format for matching the end of a sentence. 501 */ 502 public void setEndOfSentenceFormat(String format) 503 { 504 endOfSentenceFormat = format; 505 } 506 507 /** 508 * Returns a regular expression for matching the end of a sentence. 509 * 510 * @return a regular expression for matching the end of a sentence. 511 */ 512 private Pattern getEndOfSentencePattern() 513 { 514 if (endOfSentencePattern == null) { 515 endOfSentencePattern = Pattern.compile(endOfSentenceFormat); 516 } 517 return endOfSentencePattern; 518 } 519 520 /** 521 * Sets the flag that determines if the first sentence is checked for 522 * proper end of sentence punctuation. 523 * @param flag <code>true</code> if the first sentence is to be checked 524 */ 525 public void setCheckFirstSentence(boolean flag) 526 { 527 checkFirstSentence = flag; 528 } 529 530 /** 531 * Sets the flag that determines if HTML checking is to be performed. 532 * @param flag <code>true</code> if HTML checking is to be performed. 533 */ 534 public void setCheckHtml(boolean flag) 535 { 536 checkHtml = flag; 537 } 538 539 /** 540 * Sets the flag that determines if empty Javadoc checking should be done. 541 * @param flag <code>true</code> if empty Javadoc checking should be done. 542 */ 543 public void setCheckEmptyJavadoc(boolean flag) 544 { 545 checkEmptyJavadoc = flag; 546 } 547}