001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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; 021 022import org.antlr.v4.runtime.ANTLRInputStream; 023import org.antlr.v4.runtime.BailErrorStrategy; 024import org.antlr.v4.runtime.BaseErrorListener; 025import org.antlr.v4.runtime.CommonTokenStream; 026import org.antlr.v4.runtime.ParserRuleContext; 027import org.antlr.v4.runtime.RecognitionException; 028import org.antlr.v4.runtime.Recognizer; 029import org.antlr.v4.runtime.Token; 030import org.antlr.v4.runtime.misc.ParseCancellationException; 031import org.antlr.v4.runtime.tree.ParseTree; 032import org.antlr.v4.runtime.tree.TerminalNode; 033 034import com.google.common.base.CaseFormat; 035import com.puppycrawl.tools.checkstyle.api.DetailAST; 036import com.puppycrawl.tools.checkstyle.api.DetailNode; 037import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 038import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocNodeImpl; 039import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocLexer; 040import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocParser; 041import com.puppycrawl.tools.checkstyle.utils.JavadocUtils; 042 043/** 044 * Used for parsing Javadoc comment as DetailNode tree. 045 * @author bizmailov 046 * 047 */ 048public class JavadocDetailNodeParser { 049 050 /** 051 * Message key of error message. Missed close HTML tag breaks structure 052 * of parse tree, so parser stops parsing and generates such error 053 * message. This case is special because parser prints error like 054 * {@code "no viable alternative at input 'b \n *\n'"} and it is not 055 * clear that error is about missed close HTML tag. 056 */ 057 public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close"; 058 059 /** 060 * Message key of error message. 061 */ 062 public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG = 063 "javadoc.wrong.singleton.html.tag"; 064 065 /** 066 * Parse error while rule recognition. 067 */ 068 public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error"; 069 070 /** 071 * Error message key for common javadoc errors. 072 */ 073 public static final String MSG_KEY_PARSE_ERROR = "javadoc.parse.error"; 074 075 /** 076 * Unrecognized error from antlr parser. 077 */ 078 public static final String MSG_KEY_UNRECOGNIZED_ANTLR_ERROR = 079 "javadoc.unrecognized.antlr.error"; 080 081 /** Symbols with which javadoc starts. */ 082 private static final String JAVADOC_START = "/**"; 083 084 /** 085 * Line number of the Block comment AST that is being parsed. 086 */ 087 private int blockCommentLineNumber; 088 089 /** 090 * Custom error listener. 091 */ 092 private DescriptiveErrorListener errorListener; 093 094 /** 095 * Parses Javadoc comment as DetailNode tree. 096 * @param javadocCommentAst 097 * DetailAST of Javadoc comment 098 * @return DetailNode tree of Javadoc comment 099 */ 100 public ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst) { 101 blockCommentLineNumber = javadocCommentAst.getLineNo(); 102 103 final String javadocComment = JavadocUtils.getJavadocCommentContent(javadocCommentAst); 104 105 // Use a new error listener each time to be able to use 106 // one check instance for multiple files to be checked 107 // without getting side effects. 108 errorListener = new DescriptiveErrorListener(); 109 110 // Log messages should have line number in scope of file, 111 // not in scope of Javadoc comment. 112 // Offset is line number of beginning of Javadoc comment. 113 errorListener.setOffset(javadocCommentAst.getLineNo() - 1); 114 115 final ParseStatus result = new ParseStatus(); 116 117 try { 118 final ParseTree parseTree = parseJavadocAsParseTree(javadocComment); 119 120 final DetailNode tree = convertParseTreeToDetailNode(parseTree); 121 // adjust first line to indent of /** 122 adjustFirstLineToJavadocIndent(tree, 123 javadocCommentAst.getColumnNo() 124 + JAVADOC_START.length()); 125 result.setTree(tree); 126 } 127 catch (ParseCancellationException | IllegalArgumentException ex) { 128 // If syntax error occurs then message is printed by error listener 129 // and parser throws this runtime exception to stop parsing. 130 // Just stop processing current Javadoc comment. 131 ParseErrorMessage parseErrorMessage = errorListener.getErrorMessage(); 132 133 // There are cases when antlr error listener does not handle syntax error 134 if (parseErrorMessage == null) { 135 parseErrorMessage = new ParseErrorMessage(javadocCommentAst.getLineNo(), 136 MSG_KEY_UNRECOGNIZED_ANTLR_ERROR, 137 javadocCommentAst.getColumnNo(), ex.getMessage()); 138 } 139 140 result.setParseErrorMessage(parseErrorMessage); 141 } 142 143 return result; 144 } 145 146 /** 147 * Parses block comment content as javadoc comment. 148 * @param blockComment 149 * block comment content. 150 * @return parse tree 151 */ 152 private ParseTree parseJavadocAsParseTree(String blockComment) { 153 final ANTLRInputStream input = new ANTLRInputStream(blockComment); 154 155 final JavadocLexer lexer = new JavadocLexer(input); 156 157 // remove default error listeners 158 lexer.removeErrorListeners(); 159 160 // add custom error listener that logs parsing errors 161 lexer.addErrorListener(errorListener); 162 163 final CommonTokenStream tokens = new CommonTokenStream(lexer); 164 165 final JavadocParser parser = new JavadocParser(tokens); 166 167 // remove default error listeners 168 parser.removeErrorListeners(); 169 170 // add custom error listener that logs syntax errors 171 parser.addErrorListener(errorListener); 172 173 // This strategy stops parsing when parser error occurs. 174 // By default it uses Error Recover Strategy which is slow and useless. 175 parser.setErrorHandler(new BailErrorStrategy()); 176 177 return parser.javadoc(); 178 } 179 180 /** 181 * Converts ParseTree (that is generated by ANTLRv4) to DetailNode tree. 182 * 183 * @param parseTreeNode root node of ParseTree 184 * @return root of DetailNode tree 185 */ 186 private DetailNode convertParseTreeToDetailNode(ParseTree parseTreeNode) { 187 final JavadocNodeImpl rootJavadocNode = createRootJavadocNode(parseTreeNode); 188 189 JavadocNodeImpl currentJavadocParent = rootJavadocNode; 190 ParseTree parseTreeParent = parseTreeNode; 191 192 while (currentJavadocParent != null) { 193 // remove unnecessary children tokens 194 if (currentJavadocParent.getType() == JavadocTokenTypes.TEXT) { 195 currentJavadocParent 196 .setChildren((DetailNode[]) JavadocNodeImpl.EMPTY_DETAIL_NODE_ARRAY); 197 } 198 199 final JavadocNodeImpl[] children = 200 (JavadocNodeImpl[]) currentJavadocParent.getChildren(); 201 202 insertChildrenNodes(children, parseTreeParent); 203 204 if (children.length > 0) { 205 currentJavadocParent = children[0]; 206 parseTreeParent = parseTreeParent.getChild(0); 207 } 208 else { 209 JavadocNodeImpl nextJavadocSibling = (JavadocNodeImpl) JavadocUtils 210 .getNextSibling(currentJavadocParent); 211 212 ParseTree nextParseTreeSibling = getNextSibling(parseTreeParent); 213 214 if (nextJavadocSibling == null) { 215 JavadocNodeImpl tempJavadocParent = 216 (JavadocNodeImpl) currentJavadocParent.getParent(); 217 218 ParseTree tempParseTreeParent = parseTreeParent.getParent(); 219 220 while (nextJavadocSibling == null && tempJavadocParent != null) { 221 222 nextJavadocSibling = (JavadocNodeImpl) JavadocUtils 223 .getNextSibling(tempJavadocParent); 224 225 nextParseTreeSibling = getNextSibling(tempParseTreeParent); 226 227 tempJavadocParent = (JavadocNodeImpl) tempJavadocParent.getParent(); 228 tempParseTreeParent = tempParseTreeParent.getParent(); 229 } 230 } 231 currentJavadocParent = nextJavadocSibling; 232 parseTreeParent = nextParseTreeSibling; 233 } 234 } 235 236 return rootJavadocNode; 237 } 238 239 /** 240 * Creates child nodes for each node from 'nodes' array. 241 * @param parseTreeParent original ParseTree parent node 242 * @param nodes array of JavadocNodeImpl nodes 243 */ 244 private void insertChildrenNodes(final JavadocNodeImpl[] nodes, ParseTree parseTreeParent) { 245 for (int i = 0; i < nodes.length; i++) { 246 final JavadocNodeImpl currentJavadocNode = nodes[i]; 247 final ParseTree currentParseTreeNodeChild = parseTreeParent.getChild(i); 248 final JavadocNodeImpl[] subChildren = 249 createChildrenNodes(currentJavadocNode, currentParseTreeNodeChild); 250 currentJavadocNode.setChildren((DetailNode[]) subChildren); 251 } 252 } 253 254 /** 255 * Creates children Javadoc nodes base on ParseTree node's children. 256 * @param parentJavadocNode node that will be parent for created children 257 * @param parseTreeNode original ParseTree node 258 * @return array of Javadoc nodes 259 */ 260 private JavadocNodeImpl[] 261 createChildrenNodes(JavadocNodeImpl parentJavadocNode, ParseTree parseTreeNode) { 262 final JavadocNodeImpl[] children = 263 new JavadocNodeImpl[parseTreeNode.getChildCount()]; 264 265 for (int j = 0; j < children.length; j++) { 266 final JavadocNodeImpl child = 267 createJavadocNode(parseTreeNode.getChild(j), parentJavadocNode, j); 268 269 children[j] = child; 270 } 271 return children; 272 } 273 274 /** 275 * Creates root JavadocNodeImpl node base on ParseTree root node. 276 * @param parseTreeNode ParseTree root node 277 * @return root Javadoc node 278 */ 279 private JavadocNodeImpl createRootJavadocNode(ParseTree parseTreeNode) { 280 final JavadocNodeImpl rootJavadocNode = createJavadocNode(parseTreeNode, null, -1); 281 282 final int childCount = parseTreeNode.getChildCount(); 283 final JavadocNodeImpl[] children = new JavadocNodeImpl[childCount]; 284 285 for (int i = 0; i < childCount; i++) { 286 final JavadocNodeImpl child = createJavadocNode(parseTreeNode.getChild(i), 287 rootJavadocNode, i); 288 children[i] = child; 289 } 290 rootJavadocNode.setChildren((DetailNode[]) children); 291 return rootJavadocNode; 292 } 293 294 /** 295 * Creates JavadocNodeImpl node on base of ParseTree node. 296 * 297 * @param parseTree ParseTree node 298 * @param parent DetailNode that will be parent of new node 299 * @param index child index that has new node 300 * @return JavadocNodeImpl node on base of ParseTree node. 301 */ 302 private JavadocNodeImpl createJavadocNode(ParseTree parseTree, DetailNode parent, int index) { 303 final JavadocNodeImpl node = new JavadocNodeImpl(); 304 if (parseTree.getChildCount() == 0 305 || "Text".equals(getNodeClassNameWithoutContext(parseTree))) { 306 node.setText(parseTree.getText()); 307 } 308 else { 309 node.setText(getFormattedNodeClassNameWithoutContext(parseTree)); 310 } 311 node.setColumnNumber(getColumn(parseTree)); 312 node.setLineNumber(getLine(parseTree) + blockCommentLineNumber); 313 node.setIndex(index); 314 node.setType(getTokenType(parseTree)); 315 node.setParent(parent); 316 node.setChildren((DetailNode[]) new JavadocNodeImpl[parseTree.getChildCount()]); 317 return node; 318 } 319 320 /** 321 * Adjust first line nodes to javadoc indent. 322 * @param tree DetailNode tree root 323 * @param javadocColumnNumber javadoc indent 324 */ 325 private void adjustFirstLineToJavadocIndent(DetailNode tree, int javadocColumnNumber) { 326 if (tree.getLineNumber() == blockCommentLineNumber) { 327 ((JavadocNodeImpl) tree).setColumnNumber(tree.getColumnNumber() + javadocColumnNumber); 328 final DetailNode[] children = tree.getChildren(); 329 for (DetailNode child : children) { 330 adjustFirstLineToJavadocIndent(child, javadocColumnNumber); 331 } 332 } 333 } 334 335 /** 336 * Gets line number from ParseTree node. 337 * @param tree 338 * ParseTree node 339 * @return line number 340 */ 341 private static int getLine(ParseTree tree) { 342 final int line; 343 if (tree instanceof TerminalNode) { 344 line = ((TerminalNode) tree).getSymbol().getLine() - 1; 345 } 346 else { 347 final ParserRuleContext rule = (ParserRuleContext) tree; 348 line = rule.start.getLine() - 1; 349 } 350 return line; 351 } 352 353 /** 354 * Gets column number from ParseTree node. 355 * @param tree 356 * ParseTree node 357 * @return column number 358 */ 359 private static int getColumn(ParseTree tree) { 360 final int column; 361 if (tree instanceof TerminalNode) { 362 column = ((TerminalNode) tree).getSymbol().getCharPositionInLine(); 363 } 364 else { 365 final ParserRuleContext rule = (ParserRuleContext) tree; 366 column = rule.start.getCharPositionInLine(); 367 } 368 return column; 369 } 370 371 /** 372 * Gets next sibling of ParseTree node. 373 * @param node ParseTree node 374 * @return next sibling of ParseTree node. 375 */ 376 private static ParseTree getNextSibling(ParseTree node) { 377 ParseTree nextSibling = null; 378 379 if (node.getParent() != null) { 380 final ParseTree parent = node.getParent(); 381 final int childCount = parent.getChildCount(); 382 383 int index = 0; 384 while (true) { 385 final ParseTree currentNode = parent.getChild(index); 386 if (currentNode.equals(node)) { 387 if (index != childCount - 1) { 388 nextSibling = parent.getChild(index + 1); 389 } 390 break; 391 } 392 index++; 393 } 394 } 395 return nextSibling; 396 } 397 398 /** 399 * Gets token type of ParseTree node from JavadocTokenTypes class. 400 * @param node ParseTree node. 401 * @return token type from JavadocTokenTypes 402 */ 403 private static int getTokenType(ParseTree node) { 404 final int tokenType; 405 406 if (node.getChildCount() == 0) { 407 tokenType = ((TerminalNode) node).getSymbol().getType(); 408 } 409 else { 410 final String className = getNodeClassNameWithoutContext(node); 411 final String typeName = 412 CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, className); 413 tokenType = JavadocUtils.getTokenId(typeName); 414 } 415 416 return tokenType; 417 } 418 419 /** 420 * Gets class name of ParseTree node and removes 'Context' postfix at the 421 * end and formats it. 422 * @param node {@code ParseTree} node whose class name is to be formatted and returned 423 * @return uppercased class name without the word 'Context' and with appropriately 424 * inserted underscores 425 */ 426 private static String getFormattedNodeClassNameWithoutContext(ParseTree node) { 427 final String classNameWithoutContext = getNodeClassNameWithoutContext(node); 428 return CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, classNameWithoutContext); 429 } 430 431 /** 432 * Gets class name of ParseTree node and removes 'Context' postfix at the 433 * end. 434 * @param node 435 * ParseTree node. 436 * @return class name without 'Context' 437 */ 438 private static String getNodeClassNameWithoutContext(ParseTree node) { 439 final String className = node.getClass().getSimpleName(); 440 // remove 'Context' at the end 441 final int contextLength = 7; 442 return className.substring(0, className.length() - contextLength); 443 } 444 445 /** 446 * Custom error listener for JavadocParser that prints user readable errors. 447 */ 448 private static class DescriptiveErrorListener extends BaseErrorListener { 449 450 /** 451 * Offset is line number of beginning of the Javadoc comment. Log 452 * messages should have line number in scope of file, not in scope of 453 * Javadoc comment. 454 */ 455 private int offset; 456 457 /** 458 * Error message that appeared while parsing. 459 */ 460 private ParseErrorMessage errorMessage; 461 462 /** 463 * Getter for error message during parsing. 464 * @return Error message during parsing. 465 */ 466 private ParseErrorMessage getErrorMessage() { 467 return errorMessage; 468 } 469 470 /** 471 * Sets offset. Offset is line number of beginning of the Javadoc 472 * comment. Log messages should have line number in scope of file, not 473 * in scope of Javadoc comment. 474 * @param offset 475 * offset line number 476 */ 477 public void setOffset(int offset) { 478 this.offset = offset; 479 } 480 481 /** 482 * Logs parser errors in Checkstyle manner. Parser can generate error 483 * messages. There is special error that parser can generate. It is 484 * missed close HTML tag. This case is special because parser prints 485 * error like {@code "no viable alternative at input 'b \n *\n'"} and it 486 * is not clear that error is about missed close HTML tag. Other error 487 * messages are not special and logged simply as "Parse Error...". 488 * 489 * <p>{@inheritDoc} 490 */ 491 @Override 492 public void syntaxError( 493 Recognizer<?, ?> recognizer, Object offendingSymbol, 494 int line, int charPositionInLine, 495 String msg, RecognitionException ex) { 496 final int lineNumber = offset + line; 497 final Token token = (Token) offendingSymbol; 498 499 if (MSG_JAVADOC_MISSED_HTML_CLOSE.equals(msg)) { 500 errorMessage = new ParseErrorMessage(lineNumber, 501 MSG_JAVADOC_MISSED_HTML_CLOSE, charPositionInLine, token.getText()); 502 503 throw new IllegalArgumentException(msg); 504 } 505 else if (MSG_JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) { 506 errorMessage = new ParseErrorMessage(lineNumber, 507 MSG_JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine, token.getText()); 508 509 throw new IllegalArgumentException(msg); 510 } 511 else { 512 final int ruleIndex = ex.getCtx().getRuleIndex(); 513 final String ruleName = recognizer.getRuleNames()[ruleIndex]; 514 final String upperCaseRuleName = CaseFormat.UPPER_CAMEL.to( 515 CaseFormat.UPPER_UNDERSCORE, ruleName); 516 517 errorMessage = new ParseErrorMessage(lineNumber, 518 MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName); 519 } 520 } 521 } 522 523 /** 524 * Contains result of parsing javadoc comment: DetailNode tree and parse 525 * error message. 526 */ 527 public static class ParseStatus { 528 /** 529 * DetailNode tree (is null if parsing fails). 530 */ 531 private DetailNode tree; 532 533 /** 534 * Parse error message (is null if parsing is successful). 535 */ 536 private ParseErrorMessage parseErrorMessage; 537 538 /** 539 * Getter for DetailNode tree. 540 * @return DetailNode tree if parsing was successful, null otherwise. 541 */ 542 public DetailNode getTree() { 543 return tree; 544 } 545 546 /** 547 * Sets DetailNode tree. 548 * @param tree DetailNode tree. 549 */ 550 public void setTree(DetailNode tree) { 551 this.tree = tree; 552 } 553 554 /** 555 * Getter for error message during parsing. 556 * @return Error message if parsing was unsuccessful, null otherwise. 557 */ 558 public ParseErrorMessage getParseErrorMessage() { 559 return parseErrorMessage; 560 } 561 562 /** 563 * Sets parse error message. 564 * @param parseErrorMessage Parse error message. 565 */ 566 public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) { 567 this.parseErrorMessage = parseErrorMessage; 568 } 569 570 } 571 572 /** 573 * Contains information about parse error message. 574 */ 575 public static class ParseErrorMessage { 576 /** 577 * Line number where parse error occurred. 578 */ 579 private final int lineNumber; 580 581 /** 582 * Key for error message. 583 */ 584 private final String messageKey; 585 586 /** 587 * Error message arguments. 588 */ 589 private final Object[] messageArguments; 590 591 /** 592 * Initializes parse error message. 593 * 594 * @param lineNumber line number 595 * @param messageKey message key 596 * @param messageArguments message arguments 597 */ 598 ParseErrorMessage(int lineNumber, String messageKey, Object... messageArguments) { 599 this.lineNumber = lineNumber; 600 this.messageKey = messageKey; 601 this.messageArguments = messageArguments.clone(); 602 } 603 604 /** 605 * Getter for line number where parse error occurred. 606 * @return Line number where parse error occurred. 607 */ 608 public int getLineNumber() { 609 return lineNumber; 610 } 611 612 /** 613 * Getter for key for error message. 614 * @return Key for error message. 615 */ 616 public String getMessageKey() { 617 return messageKey; 618 } 619 620 /** 621 * Getter for error message arguments. 622 * @return Array of error message arguments. 623 */ 624 public Object[] getMessageArguments() { 625 return messageArguments.clone(); 626 } 627 } 628 629}