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