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