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 java.io.ByteArrayInputStream; 022import java.io.IOException; 023import java.io.InputStream; 024import java.nio.charset.Charset; 025import java.util.HashMap; 026import java.util.Map; 027 028import org.antlr.v4.runtime.ANTLRInputStream; 029import org.antlr.v4.runtime.BailErrorStrategy; 030import org.antlr.v4.runtime.BaseErrorListener; 031import org.antlr.v4.runtime.CommonTokenStream; 032import org.antlr.v4.runtime.ParserRuleContext; 033import org.antlr.v4.runtime.RecognitionException; 034import org.antlr.v4.runtime.Recognizer; 035import org.antlr.v4.runtime.RuleContext; 036import org.antlr.v4.runtime.Token; 037import org.antlr.v4.runtime.misc.ParseCancellationException; 038import org.antlr.v4.runtime.tree.ParseTree; 039import org.antlr.v4.runtime.tree.TerminalNode; 040 041import com.google.common.base.CaseFormat; 042import com.google.common.primitives.Ints; 043import com.puppycrawl.tools.checkstyle.api.Check; 044import com.puppycrawl.tools.checkstyle.api.DetailAST; 045import com.puppycrawl.tools.checkstyle.api.DetailNode; 046import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 047import com.puppycrawl.tools.checkstyle.api.TokenTypes; 048import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocLexer; 049import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocParser; 050 051/** 052 * Base class for Checks that process Javadoc comments. 053 * @author Baratali Izmailov 054 */ 055public abstract class AbstractJavadocCheck extends Check 056{ 057 /** 058 * Error message key for common javadoc errors. 059 */ 060 private static final String PARSE_ERROR_MESSAGE_KEY = "javadoc.parse.error"; 061 062 /** 063 * Unrecognized error from antlr parser 064 */ 065 private static final String UNRECOGNIZED_ANTLR_ERROR_MESSAGE_KEY = 066 "javadoc.unrecognized.antlr.error"; 067 068 /** 069 * key is "line:column" 070 * value is DetailNode tree 071 */ 072 private static final Map<String, ParseStatus> TREE_CACHE = new HashMap<String, ParseStatus>(); 073 074 /** 075 * Custom error listener. 076 */ 077 private final DescriptiveErrorListener errorListener = 078 new DescriptiveErrorListener(); 079 080 /** 081 * DetailAST node of considered Javadoc comment that is just a block comment 082 * in Java language syntax tree. 083 */ 084 private DetailAST blockCommentAst; 085 086 /** 087 * Returns the default token types a check is interested in. 088 * @return the default token types 089 * @see JavadocTokenTypes 090 */ 091 public abstract int[] getDefaultJavadocTokens(); 092 093 /** 094 * Called before the starting to process a tree. 095 * @param rootAst 096 * the root of the tree 097 */ 098 public void beginJavadocTree(DetailNode rootAst) 099 { 100 } 101 102 /** 103 * Called after finished processing a tree. 104 * @param rootAst 105 * the root of the tree 106 */ 107 public void finishJavadocTree(DetailNode rootAst) 108 { 109 } 110 111 /** 112 * Called to process a Javadoc token. 113 * @param ast 114 * the token to process 115 */ 116 public void visitJavadocToken(DetailNode ast) 117 { 118 } 119 120 /** 121 * Called after all the child nodes have been process. 122 * @param ast 123 * the token leaving 124 */ 125 public void leaveJavadocToken(DetailNode ast) 126 { 127 } 128 129 /** 130 * Defined final to not allow JavadocChecks to change default tokens. 131 * @return default tokens 132 */ 133 @Override 134 public final int[] getDefaultTokens() 135 { 136 return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN }; 137 } 138 139 /** 140 * Defined final to not allow JavadocChecks to change acceptable tokens. 141 * @return acceptable tokens 142 */ 143 @Override 144 public final int[] getAcceptableTokens() 145 { 146 return super.getAcceptableTokens(); 147 } 148 149 /** 150 * Defined final to not allow JavadocChecks to change required tokens. 151 * @return required tokens 152 */ 153 @Override 154 public final int[] getRequiredTokens() 155 { 156 return super.getRequiredTokens(); 157 } 158 159 /** 160 * Defined final because all JavadocChecks require comment nodes. 161 * @return true 162 */ 163 @Override 164 public final boolean isCommentNodesRequired() 165 { 166 return true; 167 } 168 169 @Override 170 public final void beginTree(DetailAST rootAST) 171 { 172 TREE_CACHE.clear(); 173 } 174 175 @Override 176 public final void finishTree(DetailAST rootAST) 177 { 178 TREE_CACHE.clear(); 179 } 180 181 @Override 182 public final void leaveToken(DetailAST ast) 183 { 184 } 185 186 @Override 187 public final void visitToken(DetailAST blockCommentAst) 188 { 189 if (JavadocUtils.isJavadocComment(blockCommentAst)) { 190 this.blockCommentAst = blockCommentAst; 191 192 final String treeCacheKey = blockCommentAst.getLineNo() + ":" 193 + blockCommentAst.getColumnNo(); 194 195 ParseStatus ps; 196 197 if (TREE_CACHE.containsKey(treeCacheKey)) { 198 ps = TREE_CACHE.get(treeCacheKey); 199 } 200 else { 201 ps = parseJavadocAsDetailNode(blockCommentAst); 202 TREE_CACHE.put(treeCacheKey, ps); 203 } 204 205 if (ps.getParseErrorMessage() == null) { 206 processTree(ps.getTree()); 207 } 208 else { 209 final ParseErrorMessage parseErrorMessage = ps.getParseErrorMessage(); 210 log(parseErrorMessage.getLineNumber(), 211 parseErrorMessage.getMessageKey(), 212 parseErrorMessage.getMessageArguments()); 213 } 214 } 215 216 } 217 218 protected DetailAST getBlockCommentAst() 219 { 220 return blockCommentAst; 221 } 222 223 /** 224 * Parses Javadoc comment as DetailNode tree. 225 * @param javadocCommentAst 226 * DetailAST of Javadoc comment 227 * @return DetailNode tree of Javadoc comment 228 */ 229 private ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst) 230 { 231 final String javadocComment = JavadocUtils.getJavadocCommentContent(javadocCommentAst); 232 233 // Log messages should have line number in scope of file, 234 // not in scope of Javadoc comment. 235 // Offset is line number of beginning of Javadoc comment. 236 errorListener.setOffset(javadocCommentAst.getLineNo() - 1); 237 238 final ParseStatus result = new ParseStatus(); 239 ParseTree parseTree = null; 240 ParseErrorMessage parseErrorMessage = null; 241 242 try { 243 parseTree = parseJavadocAsParseTree(javadocComment); 244 } 245 catch (IOException e) { 246 // Antlr can not initiate its ANTLRInputStream 247 parseErrorMessage = new ParseErrorMessage(javadocCommentAst.getLineNo(), 248 PARSE_ERROR_MESSAGE_KEY, 249 javadocCommentAst.getColumnNo(), e.getMessage()); 250 } 251 catch (ParseCancellationException e) { 252 // If syntax error occurs then message is printed by error listener 253 // and parser throws this runtime exception to stop parsing. 254 // Just stop processing current Javadoc comment. 255 parseErrorMessage = errorListener.getErrorMessage(); 256 257 // There are cases when antlr error listener does not handle syntax error 258 if (parseErrorMessage == null) { 259 parseErrorMessage = new ParseErrorMessage(javadocCommentAst.getLineNo(), 260 UNRECOGNIZED_ANTLR_ERROR_MESSAGE_KEY, 261 javadocCommentAst.getColumnNo(), e.getMessage()); 262 } 263 } 264 265 if (parseErrorMessage == null) { 266 final DetailNode tree = convertParseTree2DetailNode(parseTree); 267 result.setTree(tree); 268 } 269 else { 270 result.setParseErrorMessage(parseErrorMessage); 271 } 272 273 return result; 274 } 275 276 /** 277 * Converts ParseTree (that is generated by ANTLRv4) to DetailNode tree. 278 * 279 * @param rootParseTree root node of ParseTree 280 * @return root of DetailNode tree 281 */ 282 private DetailNode convertParseTree2DetailNode(ParseTree rootParseTree) 283 { 284 final ParseTree currentParseTreeNode = rootParseTree; 285 final JavadocNodeImpl rootJavadocNode = createJavadocNode(currentParseTreeNode, null, -1); 286 287 int childCount = currentParseTreeNode.getChildCount(); 288 JavadocNodeImpl[] children = (JavadocNodeImpl[]) rootJavadocNode.getChildren(); 289 290 for (int i = 0; i < childCount; i++) { 291 final JavadocNodeImpl child = createJavadocNode(currentParseTreeNode.getChild(i) 292 , rootJavadocNode, i); 293 children[i] = child; 294 } 295 296 JavadocNodeImpl currentJavadocParent = rootJavadocNode; 297 ParseTree currentParseTreeParent = currentParseTreeNode; 298 299 while (currentJavadocParent != null) { 300 children = (JavadocNodeImpl[]) currentJavadocParent.getChildren(); 301 childCount = children.length; 302 303 for (int i = 0; i < childCount; i++) { 304 final JavadocNodeImpl currentJavadocNode = children[i]; 305 final ParseTree currentParseTreeNodeChild = currentParseTreeParent.getChild(i); 306 307 final JavadocNodeImpl[] subChildren = (JavadocNodeImpl[]) currentJavadocNode 308 .getChildren(); 309 310 for (int j = 0; j < subChildren.length; j++) { 311 final JavadocNodeImpl child = 312 createJavadocNode(currentParseTreeNodeChild.getChild(j) 313 , currentJavadocNode, j); 314 315 subChildren[j] = child; 316 } 317 } 318 319 if (childCount > 0) { 320 currentJavadocParent = children[0]; 321 currentParseTreeParent = currentParseTreeParent.getChild(0); 322 } 323 else { 324 JavadocNodeImpl nextJavadocSibling = (JavadocNodeImpl) JavadocUtils 325 .getNextSibling(currentJavadocParent); 326 327 ParseTree nextParseTreeSibling = getNextSibling(currentParseTreeParent); 328 329 if (nextJavadocSibling == null) { 330 JavadocNodeImpl tempJavadocParent = 331 (JavadocNodeImpl) currentJavadocParent.getParent(); 332 333 ParseTree tempParseTreeParent = currentParseTreeParent.getParent(); 334 335 while (nextJavadocSibling == null && tempJavadocParent != null) { 336 337 nextJavadocSibling = (JavadocNodeImpl) JavadocUtils 338 .getNextSibling(tempJavadocParent); 339 340 nextParseTreeSibling = getNextSibling(tempParseTreeParent); 341 342 tempJavadocParent = (JavadocNodeImpl) tempJavadocParent.getParent(); 343 tempParseTreeParent = tempParseTreeParent.getParent(); 344 } 345 } 346 currentJavadocParent = nextJavadocSibling; 347 currentParseTreeParent = nextParseTreeSibling; 348 } 349 } 350 351 return rootJavadocNode; 352 } 353 354 /** 355 * Creates JavadocNodeImpl node on base of ParseTree node. 356 * 357 * @param parseTree ParseTree node 358 * @param parent DetailNode that will be parent of new node 359 * @param index child index that has new node 360 * @return JavadocNodeImpl node on base of ParseTree node. 361 */ 362 private JavadocNodeImpl createJavadocNode(ParseTree parseTree, DetailNode parent, int index) 363 { 364 final JavadocNodeImpl node = new JavadocNodeImpl(); 365 node.setText(parseTree.getText()); 366 node.setColumnNumber(getColumn(parseTree)); 367 node.setLineNumber(getLine(parseTree) + blockCommentAst.getLineNo()); 368 node.setIndex(index); 369 node.setType(getTokenType(parseTree)); 370 node.setParent(parent); 371 node.setChildren(new JavadocNodeImpl[parseTree.getChildCount()]); 372 return node; 373 } 374 375 /** 376 * Gets next sibling of ParseTree node. 377 * @param node ParseTree node 378 * @return next sibling of ParseTree node. 379 */ 380 private static ParseTree getNextSibling(ParseTree node) 381 { 382 if (node.getParent() == null) { 383 return null; 384 } 385 386 final ParseTree parent = node.getParent(); 387 final int childCount = parent.getChildCount(); 388 389 for (int i = 0; i < childCount; i++) { 390 final ParseTree currentNode = parent.getChild(i); 391 if (currentNode.equals(node)) { 392 if (i == childCount - 1) { 393 return null; 394 } 395 return parent.getChild(i + 1); 396 } 397 } 398 return null; 399 } 400 401 /** 402 * Gets token type of ParseTree node from JavadocTokenTypes class. 403 * @param node ParseTree node. 404 * @return token type from JavadocTokenTypes 405 */ 406 private static int getTokenType(ParseTree node) 407 { 408 int tokenType = Integer.MIN_VALUE; 409 410 if (node.getChildCount() == 0) { 411 tokenType = ((TerminalNode) node).getSymbol().getType(); 412 } 413 else { 414 final String className = getNodeClassNameWithoutContext(node); 415 final String typeName = 416 CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, className); 417 tokenType = JavadocUtils.getTokenId(typeName); 418 } 419 420 return tokenType; 421 } 422 423 /** 424 * Gets class name of ParseTree node and removes 'Context' postfix at the 425 * end. 426 * @param node 427 * ParseTree node. 428 * @return class name without 'Context' 429 */ 430 private static String getNodeClassNameWithoutContext(ParseTree node) 431 { 432 final String className = node.getClass().getSimpleName(); 433 // remove 'Context' at the end 434 final int contextLength = 7; 435 return className.substring(0, className.length() - contextLength); 436 } 437 438 /** 439 * Gets line number from ParseTree node. 440 * @param tree 441 * ParseTree node 442 * @return line number 443 */ 444 private static int getLine(ParseTree tree) 445 { 446 if (tree instanceof TerminalNode) { 447 return ((TerminalNode) tree).getSymbol().getLine() - 1; 448 } 449 else { 450 final ParserRuleContext rule = (ParserRuleContext) tree; 451 return rule.start.getLine() - 1; 452 } 453 } 454 455 /** 456 * Gets column number from ParseTree node. 457 * @param tree 458 * ParseTree node 459 * @return column number 460 */ 461 private static int getColumn(ParseTree tree) 462 { 463 if (tree instanceof TerminalNode) { 464 return ((TerminalNode) tree).getSymbol().getCharPositionInLine(); 465 } 466 else { 467 final ParserRuleContext rule = (ParserRuleContext) tree; 468 return rule.start.getCharPositionInLine(); 469 } 470 } 471 472 /** 473 * Parses block comment content as javadoc comment. 474 * @param blockComment 475 * block comment content. 476 * @return parse tree 477 * @throws IOException 478 * errors in ANTLRInputStream 479 */ 480 private ParseTree parseJavadocAsParseTree(String blockComment) 481 throws IOException 482 { 483 final Charset utf8Charset = Charset.forName("UTF-8"); 484 final InputStream in = new ByteArrayInputStream(blockComment.getBytes(utf8Charset)); 485 486 final ANTLRInputStream input = new ANTLRInputStream(in); 487 488 final JavadocLexer lexer = new JavadocLexer(input); 489 490 // remove default error listeners 491 lexer.removeErrorListeners(); 492 493 // add custom error listener that logs parsing errors 494 lexer.addErrorListener(errorListener); 495 496 final CommonTokenStream tokens = new CommonTokenStream(lexer); 497 498 final JavadocParser parser = new JavadocParser(tokens); 499 500 // remove default error listeners 501 parser.removeErrorListeners(); 502 503 // add custom error listener that logs syntax errors 504 parser.addErrorListener(errorListener); 505 506 // This strategy stops parsing when parser error occurs. 507 // By default it uses Error Recover Strategy which is slow and useless. 508 parser.setErrorHandler(new BailErrorStrategy()); 509 510 return parser.javadoc(); 511 } 512 513 /** 514 * Processes JavadocAST tree notifying Check. 515 * @param root 516 * root of JavadocAST tree. 517 */ 518 private void processTree(DetailNode root) 519 { 520 beginJavadocTree(root); 521 walk(root); 522 finishJavadocTree(root); 523 } 524 525 /** 526 * Processes a node calling Check at interested nodes. 527 * @param root 528 * the root of tree for process 529 */ 530 private void walk(DetailNode root) 531 { 532 final int[] defaultTokenTypes = getDefaultJavadocTokens(); 533 534 if (defaultTokenTypes == null) { 535 return; 536 } 537 538 DetailNode curNode = root; 539 while (curNode != null) { 540 final boolean waitsFor = Ints.contains(defaultTokenTypes, curNode.getType()); 541 542 if (waitsFor) { 543 visitJavadocToken(curNode); 544 } 545 DetailNode toVisit = JavadocUtils.getFirstChild(curNode); 546 while ((curNode != null) && (toVisit == null)) { 547 548 if (waitsFor) { 549 leaveJavadocToken(curNode); 550 } 551 552 toVisit = JavadocUtils.getNextSibling(curNode); 553 if (toVisit == null) { 554 curNode = curNode.getParent(); 555 } 556 } 557 curNode = toVisit; 558 } 559 } 560 561 /** 562 * Custom error listener for JavadocParser that prints user readable errors. 563 */ 564 class DescriptiveErrorListener extends BaseErrorListener 565 { 566 /** 567 * Parse error while token recognition. 568 */ 569 private static final String JAVADOC_PARSE_TOKEN_ERROR = "javadoc.parse.token.error"; 570 571 /** 572 * Parse error while rule recognition. 573 */ 574 private static final String JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error"; 575 576 /** 577 * Message key of error message. Missed close HTML tag breaks structure 578 * of parse tree, so parser stops parsing and generates such error 579 * message. This case is special because parser prints error like 580 * {@code "no viable alternative at input 'b \n *\n'"} and it is not 581 * clear that error is about missed close HTML tag. 582 */ 583 private static final String JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close"; 584 585 /** 586 * Message key of error message. 587 */ 588 private static final String JAVADOC_WRONG_SINGLETON_TAG = 589 "javadoc.wrong.singleton.html.tag"; 590 591 /** 592 * Offset is line number of beginning of the Javadoc comment. Log 593 * messages should have line number in scope of file, not in scope of 594 * Javadoc comment. 595 */ 596 private int offset; 597 598 /** 599 * Error message that appeared while parsing. 600 */ 601 private ParseErrorMessage errorMessage; 602 603 public ParseErrorMessage getErrorMessage() 604 { 605 return errorMessage; 606 } 607 608 /** 609 * Sets offset. Offset is line number of beginning of the Javadoc 610 * comment. Log messages should have line number in scope of file, not 611 * in scope of Javadoc comment. 612 * @param offset 613 * offset line number 614 */ 615 public void setOffset(int offset) 616 { 617 this.offset = offset; 618 } 619 620 /** 621 * Logs parser errors in Checkstyle manner. Parser can generate error 622 * messages. There is special error that parser can generate. It is 623 * missed close HTML tag. This case is special because parser prints 624 * error like {@code "no viable alternative at input 'b \n *\n'"} and it 625 * is not clear that error is about missed close HTML tag. Other error 626 * messages are not special and logged simply as "Parse Error...". 627 * <p> 628 * {@inheritDoc} 629 */ 630 @Override 631 public void syntaxError( 632 Recognizer<?, ?> recognizer, Object offendingSymbol, 633 int line, int charPositionInLine, 634 String msg, RecognitionException ex) 635 { 636 final int lineNumber = offset + line; 637 final Token token = (Token) offendingSymbol; 638 639 if (JAVADOC_MISSED_HTML_CLOSE.equals(msg)) { 640 errorMessage = new ParseErrorMessage(lineNumber, 641 JAVADOC_MISSED_HTML_CLOSE, charPositionInLine, token.getText()); 642 643 throw new ParseCancellationException(); 644 } 645 else if (JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) { 646 errorMessage = new ParseErrorMessage(lineNumber, 647 JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine, token.getText()); 648 649 throw new ParseCancellationException(); 650 } 651 else { 652 final RuleContext ruleContext = ex.getCtx(); 653 if (ruleContext != null) { 654 final int ruleIndex = ex.getCtx().getRuleIndex(); 655 final String ruleName = recognizer.getRuleNames()[ruleIndex]; 656 final String upperCaseRuleName = CaseFormat.UPPER_CAMEL.to( 657 CaseFormat.UPPER_UNDERSCORE, ruleName); 658 659 errorMessage = new ParseErrorMessage(lineNumber, 660 JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName); 661 } 662 else { 663 errorMessage = new ParseErrorMessage(lineNumber, JAVADOC_PARSE_TOKEN_ERROR, 664 charPositionInLine, msg, charPositionInLine); 665 } 666 } 667 } 668 } 669 670 /** 671 * Contains result of parsing javadoc comment: DetailNode tree and parse 672 * error message. 673 */ 674 private static class ParseStatus 675 { 676 /** 677 * DetailNode tree (is null if parsing fails) 678 */ 679 private DetailNode tree; 680 681 /** 682 * Parse error message (is null if parsing is successful) 683 */ 684 private ParseErrorMessage parseErrorMessage; 685 686 public DetailNode getTree() 687 { 688 return tree; 689 } 690 691 public void setTree(DetailNode tree) 692 { 693 this.tree = tree; 694 } 695 696 public ParseErrorMessage getParseErrorMessage() 697 { 698 return parseErrorMessage; 699 } 700 701 public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) 702 { 703 this.parseErrorMessage = parseErrorMessage; 704 } 705 706 } 707 708 /** 709 * Contains information about parse error message. 710 */ 711 private static class ParseErrorMessage 712 { 713 /** 714 * Line number where parse error occurred. 715 */ 716 private int lineNumber; 717 718 /** 719 * Key for error message. 720 */ 721 private String messageKey; 722 723 /** 724 * Error message arguments. 725 */ 726 private Object[] messageArguments; 727 728 /** 729 * Initializes parse error message. 730 * 731 * @param lineNumber line number 732 * @param messageKey message key 733 * @param messageArguments message arguments 734 */ 735 public ParseErrorMessage(int lineNumber, String messageKey, Object ... messageArguments) 736 { 737 this.lineNumber = lineNumber; 738 this.messageKey = messageKey; 739 this.messageArguments = messageArguments; 740 } 741 742 public int getLineNumber() 743 { 744 return lineNumber; 745 } 746 747 public String getMessageKey() 748 { 749 return messageKey; 750 } 751 752 public Object[] getMessageArguments() 753 { 754 return messageArguments; 755 } 756 757 } 758 759}