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 java.io.File; 023import java.io.Reader; 024import java.io.StringReader; 025import java.util.AbstractMap.SimpleEntry; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.List; 029import java.util.Locale; 030import java.util.Map.Entry; 031import java.util.Set; 032 033import antlr.CommonHiddenStreamToken; 034import antlr.RecognitionException; 035import antlr.Token; 036import antlr.TokenStreamException; 037import antlr.TokenStreamHiddenTokenFilter; 038import antlr.TokenStreamRecognitionException; 039import com.google.common.collect.HashMultimap; 040import com.google.common.collect.Multimap; 041import com.google.common.collect.Sets; 042import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 043import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 044import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 045import com.puppycrawl.tools.checkstyle.api.Configuration; 046import com.puppycrawl.tools.checkstyle.api.Context; 047import com.puppycrawl.tools.checkstyle.api.DetailAST; 048import com.puppycrawl.tools.checkstyle.api.FileContents; 049import com.puppycrawl.tools.checkstyle.api.FileText; 050import com.puppycrawl.tools.checkstyle.api.TokenTypes; 051import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaLexer; 052import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaRecognizer; 053import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 054import com.puppycrawl.tools.checkstyle.utils.TokenUtils; 055 056/** 057 * Responsible for walking an abstract syntax tree and notifying interested 058 * checks at each each node. 059 * 060 * @author Oliver Burn 061 */ 062public final class TreeWalker 063 extends AbstractFileSetCheck { 064 065 /** Default distance between tab stops. */ 066 private static final int DEFAULT_TAB_WIDTH = 8; 067 068 /** Maps from token name to ordinary checks. */ 069 private final Multimap<String, AbstractCheck> tokenToOrdinaryChecks = 070 HashMultimap.create(); 071 072 /** Maps from token name to comment checks. */ 073 private final Multimap<String, AbstractCheck> tokenToCommentChecks = 074 HashMultimap.create(); 075 076 /** Registered ordinary checks, that don't use comment nodes. */ 077 private final Set<AbstractCheck> ordinaryChecks = Sets.newHashSet(); 078 079 /** Registered comment checks. */ 080 private final Set<AbstractCheck> commentChecks = Sets.newHashSet(); 081 082 /** The distance between tab stops. */ 083 private int tabWidth = DEFAULT_TAB_WIDTH; 084 085 /** Class loader to resolve classes with. **/ 086 private ClassLoader classLoader; 087 088 /** Context of child components. */ 089 private Context childContext; 090 091 /** A factory for creating submodules (i.e. the Checks) */ 092 private ModuleFactory moduleFactory; 093 094 /** 095 * Creates a new {@code TreeWalker} instance. 096 */ 097 public TreeWalker() { 098 setFileExtensions("java"); 099 } 100 101 /** 102 * Sets tab width. 103 * @param tabWidth the distance between tab stops 104 */ 105 public void setTabWidth(int tabWidth) { 106 this.tabWidth = tabWidth; 107 } 108 109 /** 110 * Sets cache file. 111 * @deprecated Use {@link Checker#setCacheFile} instead. It does not do anything now. We just 112 * keep the setter for transition period to the same option in Checker. The 113 * method will be completely removed in Checkstyle 8.0. See 114 * <a href="https://github.com/checkstyle/checkstyle/issues/2883">issue#2883</a> 115 * @param fileName the cache file 116 */ 117 @Deprecated 118 public void setCacheFile(String fileName) { 119 // Deprecated 120 } 121 122 /** 123 * @param classLoader class loader to resolve classes with. 124 */ 125 public void setClassLoader(ClassLoader classLoader) { 126 this.classLoader = classLoader; 127 } 128 129 /** 130 * Sets the module factory for creating child modules (Checks). 131 * @param moduleFactory the factory 132 */ 133 public void setModuleFactory(ModuleFactory moduleFactory) { 134 this.moduleFactory = moduleFactory; 135 } 136 137 @Override 138 public void finishLocalSetup() { 139 final DefaultContext checkContext = new DefaultContext(); 140 checkContext.add("classLoader", classLoader); 141 checkContext.add("messages", getMessageCollector()); 142 checkContext.add("severity", getSeverity()); 143 checkContext.add("tabWidth", String.valueOf(tabWidth)); 144 145 childContext = checkContext; 146 } 147 148 @Override 149 public void setupChild(Configuration childConf) 150 throws CheckstyleException { 151 final String name = childConf.getName(); 152 final Object module = moduleFactory.createModule(name); 153 if (!(module instanceof AbstractCheck)) { 154 throw new CheckstyleException( 155 "TreeWalker is not allowed as a parent of " + name); 156 } 157 final AbstractCheck check = (AbstractCheck) module; 158 check.contextualize(childContext); 159 check.configure(childConf); 160 check.init(); 161 162 registerCheck(check); 163 } 164 165 @Override 166 protected void processFiltered(File file, List<String> lines) throws CheckstyleException { 167 // check if already checked and passed the file 168 if (!CommonUtils.matchesFileExtension(file, getFileExtensions())) { 169 return; 170 } 171 172 final String msg = "%s occurred during the analysis of file %s."; 173 final String fileName = file.getPath(); 174 try { 175 final FileText text = FileText.fromLines(file, lines); 176 final FileContents contents = new FileContents(text); 177 final DetailAST rootAST = parse(contents); 178 179 getMessageCollector().reset(); 180 181 walk(rootAST, contents, AstState.ORDINARY); 182 183 final DetailAST astWithComments = appendHiddenCommentNodes(rootAST); 184 185 walk(astWithComments, contents, AstState.WITH_COMMENTS); 186 } 187 catch (final TokenStreamRecognitionException tre) { 188 final String exceptionMsg = String.format(Locale.ROOT, msg, 189 "TokenStreamRecognitionException", fileName); 190 throw new CheckstyleException(exceptionMsg, tre); 191 } 192 catch (RecognitionException | TokenStreamException ex) { 193 final String exceptionMsg = String.format(Locale.ROOT, msg, 194 ex.getClass().getSimpleName(), fileName); 195 throw new CheckstyleException(exceptionMsg, ex); 196 } 197 } 198 199 /** 200 * Register a check for a given configuration. 201 * @param check the check to register 202 * @throws CheckstyleException if an error occurs 203 */ 204 private void registerCheck(AbstractCheck check) 205 throws CheckstyleException { 206 validateDefaultTokens(check); 207 final int[] tokens; 208 final Set<String> checkTokens = check.getTokenNames(); 209 if (checkTokens.isEmpty()) { 210 tokens = check.getDefaultTokens(); 211 } 212 else { 213 tokens = check.getRequiredTokens(); 214 215 //register configured tokens 216 final int[] acceptableTokens = check.getAcceptableTokens(); 217 Arrays.sort(acceptableTokens); 218 for (String token : checkTokens) { 219 final int tokenId = TokenUtils.getTokenId(token); 220 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) { 221 registerCheck(token, check); 222 } 223 else { 224 final String message = String.format(Locale.ROOT, "Token \"%s\" was " 225 + "not found in Acceptable tokens list in check %s", 226 token, check.getClass().getName()); 227 throw new CheckstyleException(message); 228 } 229 } 230 } 231 for (int element : tokens) { 232 registerCheck(element, check); 233 } 234 if (check.isCommentNodesRequired()) { 235 commentChecks.add(check); 236 } 237 else { 238 ordinaryChecks.add(check); 239 } 240 } 241 242 /** 243 * Register a check for a specified token id. 244 * @param tokenId the id of the token 245 * @param check the check to register 246 * @throws CheckstyleException if Check is misconfigured 247 */ 248 private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException { 249 registerCheck(TokenUtils.getTokenName(tokenId), check); 250 } 251 252 /** 253 * Register a check for a specified token name. 254 * @param token the name of the token 255 * @param check the check to register 256 * @throws CheckstyleException if Check is misconfigured 257 */ 258 private void registerCheck(String token, AbstractCheck check) throws CheckstyleException { 259 if (check.isCommentNodesRequired()) { 260 tokenToCommentChecks.put(token, check); 261 } 262 else if (TokenUtils.isCommentType(token)) { 263 final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type " 264 + "token ('%s') and should override 'isCommentNodesRequired()' " 265 + "method to return 'true'", check.getClass().getName(), token); 266 throw new CheckstyleException(message); 267 } 268 else { 269 tokenToOrdinaryChecks.put(token, check); 270 } 271 } 272 273 /** 274 * Validates that check's required tokens are subset of default tokens. 275 * @param check to validate 276 * @throws CheckstyleException when validation of default tokens fails 277 */ 278 private static void validateDefaultTokens(AbstractCheck check) throws CheckstyleException { 279 if (check.getRequiredTokens().length != 0) { 280 final int[] defaultTokens = check.getDefaultTokens(); 281 Arrays.sort(defaultTokens); 282 for (final int token : check.getRequiredTokens()) { 283 if (Arrays.binarySearch(defaultTokens, token) < 0) { 284 final String message = String.format(Locale.ROOT, "Token \"%s\" from required " 285 + "tokens was not found in default tokens list in check %s", 286 token, check.getClass().getName()); 287 throw new CheckstyleException(message); 288 } 289 } 290 } 291 } 292 293 /** 294 * Initiates the walk of an AST. 295 * @param ast the root AST 296 * @param contents the contents of the file the AST was generated from. 297 * @param astState state of AST. 298 */ 299 private void walk(DetailAST ast, FileContents contents, 300 AstState astState) { 301 notifyBegin(ast, contents, astState); 302 303 // empty files are not flagged by javac, will yield ast == null 304 if (ast != null) { 305 processIter(ast, astState); 306 } 307 notifyEnd(ast, astState); 308 } 309 310 /** 311 * Notify checks that we are about to begin walking a tree. 312 * @param rootAST the root of the tree. 313 * @param contents the contents of the file the AST was generated from. 314 * @param astState state of AST. 315 */ 316 private void notifyBegin(DetailAST rootAST, FileContents contents, 317 AstState astState) { 318 final Set<AbstractCheck> checks; 319 320 if (astState == AstState.WITH_COMMENTS) { 321 checks = commentChecks; 322 } 323 else { 324 checks = ordinaryChecks; 325 } 326 327 for (AbstractCheck check : checks) { 328 check.setFileContents(contents); 329 check.beginTree(rootAST); 330 } 331 } 332 333 /** 334 * Notify checks that we have finished walking a tree. 335 * @param rootAST the root of the tree. 336 * @param astState state of AST. 337 */ 338 private void notifyEnd(DetailAST rootAST, AstState astState) { 339 final Set<AbstractCheck> checks; 340 341 if (astState == AstState.WITH_COMMENTS) { 342 checks = commentChecks; 343 } 344 else { 345 checks = ordinaryChecks; 346 } 347 348 for (AbstractCheck check : checks) { 349 check.finishTree(rootAST); 350 } 351 } 352 353 /** 354 * Notify checks that visiting a node. 355 * @param ast the node to notify for. 356 * @param astState state of AST. 357 */ 358 private void notifyVisit(DetailAST ast, AstState astState) { 359 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState); 360 361 if (visitors != null) { 362 for (AbstractCheck check : visitors) { 363 check.visitToken(ast); 364 } 365 } 366 } 367 368 /** 369 * Notify checks that leaving a node. 370 * @param ast 371 * the node to notify for 372 * @param astState state of AST. 373 */ 374 private void notifyLeave(DetailAST ast, AstState astState) { 375 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState); 376 377 if (visitors != null) { 378 for (AbstractCheck check : visitors) { 379 check.leaveToken(ast); 380 } 381 } 382 } 383 384 /** 385 * Method returns list of checks 386 * 387 * @param ast 388 * the node to notify for 389 * @param astState 390 * state of AST. 391 * @return list of visitors 392 */ 393 private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) { 394 Collection<AbstractCheck> visitors = null; 395 final String tokenType = TokenUtils.getTokenName(ast.getType()); 396 397 if (astState == AstState.WITH_COMMENTS) { 398 if (tokenToCommentChecks.containsKey(tokenType)) { 399 visitors = tokenToCommentChecks.get(tokenType); 400 } 401 } 402 else { 403 if (tokenToOrdinaryChecks.containsKey(tokenType)) { 404 visitors = tokenToOrdinaryChecks.get(tokenType); 405 } 406 } 407 return visitors; 408 } 409 410 /** 411 * Static helper method to parses a Java source file. 412 * 413 * @param contents 414 * contains the contents of the file 415 * @return the root of the AST 416 * @throws TokenStreamException 417 * if lexing failed 418 * @throws RecognitionException 419 * if parsing failed 420 */ 421 public static DetailAST parse(FileContents contents) 422 throws RecognitionException, TokenStreamException { 423 final String fullText = contents.getText().getFullText().toString(); 424 final Reader reader = new StringReader(fullText); 425 final GeneratedJavaLexer lexer = new GeneratedJavaLexer(reader); 426 lexer.setFilename(contents.getFileName()); 427 lexer.setCommentListener(contents); 428 lexer.setTreatAssertAsKeyword(true); 429 lexer.setTreatEnumAsKeyword(true); 430 lexer.setTokenObjectClass("antlr.CommonHiddenStreamToken"); 431 432 final TokenStreamHiddenTokenFilter filter = 433 new TokenStreamHiddenTokenFilter(lexer); 434 filter.hide(TokenTypes.SINGLE_LINE_COMMENT); 435 filter.hide(TokenTypes.BLOCK_COMMENT_BEGIN); 436 437 final GeneratedJavaRecognizer parser = 438 new GeneratedJavaRecognizer(filter); 439 parser.setFilename(contents.getFileName()); 440 parser.setASTNodeClass(DetailAST.class.getName()); 441 parser.compilationUnit(); 442 443 return (DetailAST) parser.getAST(); 444 } 445 446 /** 447 * Parses Java source file. Result AST contains comment nodes. 448 * @param contents source file content 449 * @return DetailAST tree 450 * @throws RecognitionException if parser failed 451 * @throws TokenStreamException if lexer failed 452 */ 453 public static DetailAST parseWithComments(FileContents contents) 454 throws RecognitionException, TokenStreamException { 455 return appendHiddenCommentNodes(parse(contents)); 456 } 457 458 @Override 459 public void destroy() { 460 for (AbstractCheck check : ordinaryChecks) { 461 check.destroy(); 462 } 463 for (AbstractCheck check : commentChecks) { 464 check.destroy(); 465 } 466 super.destroy(); 467 } 468 469 /** 470 * Processes a node calling interested checks at each node. 471 * Uses iterative algorithm. 472 * @param root the root of tree for process 473 * @param astState state of AST. 474 */ 475 private void processIter(DetailAST root, AstState astState) { 476 DetailAST curNode = root; 477 while (curNode != null) { 478 notifyVisit(curNode, astState); 479 DetailAST toVisit = curNode.getFirstChild(); 480 while (curNode != null && toVisit == null) { 481 notifyLeave(curNode, astState); 482 toVisit = curNode.getNextSibling(); 483 if (toVisit == null) { 484 curNode = curNode.getParent(); 485 } 486 } 487 curNode = toVisit; 488 } 489 } 490 491 /** 492 * Appends comment nodes to existing AST. 493 * It traverses each node in AST, looks for hidden comment tokens 494 * and appends found comment tokens as nodes in AST. 495 * @param root 496 * root of AST. 497 * @return root of AST with comment nodes. 498 */ 499 private static DetailAST appendHiddenCommentNodes(DetailAST root) { 500 DetailAST result = root; 501 DetailAST curNode = root; 502 DetailAST lastNode = root; 503 504 while (curNode != null) { 505 if (isPositionGreater(curNode, lastNode)) { 506 lastNode = curNode; 507 } 508 509 CommonHiddenStreamToken tokenBefore = curNode.getHiddenBefore(); 510 DetailAST currentSibling = curNode; 511 while (tokenBefore != null) { 512 final DetailAST newCommentNode = 513 createCommentAstFromToken(tokenBefore); 514 515 currentSibling.addPreviousSibling(newCommentNode); 516 517 if (currentSibling == result) { 518 result = newCommentNode; 519 } 520 521 currentSibling = newCommentNode; 522 tokenBefore = tokenBefore.getHiddenBefore(); 523 } 524 525 DetailAST toVisit = curNode.getFirstChild(); 526 while (curNode != null && toVisit == null) { 527 toVisit = curNode.getNextSibling(); 528 if (toVisit == null) { 529 curNode = curNode.getParent(); 530 } 531 } 532 curNode = toVisit; 533 } 534 if (lastNode != null) { 535 CommonHiddenStreamToken tokenAfter = lastNode.getHiddenAfter(); 536 DetailAST currentSibling = lastNode; 537 while (tokenAfter != null) { 538 final DetailAST newCommentNode = 539 createCommentAstFromToken(tokenAfter); 540 541 currentSibling.addNextSibling(newCommentNode); 542 543 currentSibling = newCommentNode; 544 tokenAfter = tokenAfter.getHiddenAfter(); 545 } 546 } 547 return result; 548 } 549 550 /** 551 * Checks if position of first DetailAST is greater than position of 552 * second DetailAST. Position is line number and column number in source 553 * file. 554 * @param ast1 555 * first DetailAST node. 556 * @param ast2 557 * second DetailAST node. 558 * @return true if position of ast1 is greater than position of ast2. 559 */ 560 private static boolean isPositionGreater(DetailAST ast1, DetailAST ast2) { 561 if (ast1.getLineNo() == ast2.getLineNo()) { 562 return ast1.getColumnNo() > ast2.getColumnNo(); 563 } 564 else { 565 return ast1.getLineNo() > ast2.getLineNo(); 566 } 567 } 568 569 /** 570 * Create comment AST from token. Depending on token type 571 * SINGLE_LINE_COMMENT or BLOCK_COMMENT_BEGIN is created. 572 * @param token 573 * Token object. 574 * @return DetailAST of comment node. 575 */ 576 private static DetailAST createCommentAstFromToken(Token token) { 577 if (token.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 578 return createSlCommentNode(token); 579 } 580 else { 581 return createBlockCommentNode(token); 582 } 583 } 584 585 /** 586 * Create single-line comment from token. 587 * @param token 588 * Token object. 589 * @return DetailAST with SINGLE_LINE_COMMENT type. 590 */ 591 private static DetailAST createSlCommentNode(Token token) { 592 final DetailAST slComment = new DetailAST(); 593 slComment.setType(TokenTypes.SINGLE_LINE_COMMENT); 594 slComment.setText("//"); 595 596 // column counting begins from 0 597 slComment.setColumnNo(token.getColumn() - 1); 598 slComment.setLineNo(token.getLine()); 599 600 final DetailAST slCommentContent = new DetailAST(); 601 slCommentContent.initialize(token); 602 slCommentContent.setType(TokenTypes.COMMENT_CONTENT); 603 604 // column counting begins from 0 605 // plus length of '//' 606 slCommentContent.setColumnNo(token.getColumn() - 1 + 2); 607 slCommentContent.setLineNo(token.getLine()); 608 slCommentContent.setText(token.getText()); 609 610 slComment.addChild(slCommentContent); 611 return slComment; 612 } 613 614 /** 615 * Create block comment from token. 616 * @param token 617 * Token object. 618 * @return DetailAST with BLOCK_COMMENT type. 619 */ 620 private static DetailAST createBlockCommentNode(Token token) { 621 final DetailAST blockComment = new DetailAST(); 622 blockComment.initialize(TokenTypes.BLOCK_COMMENT_BEGIN, "/*"); 623 624 // column counting begins from 0 625 blockComment.setColumnNo(token.getColumn() - 1); 626 blockComment.setLineNo(token.getLine()); 627 628 final DetailAST blockCommentContent = new DetailAST(); 629 blockCommentContent.initialize(token); 630 blockCommentContent.setType(TokenTypes.COMMENT_CONTENT); 631 632 // column counting begins from 0 633 // plus length of '/*' 634 blockCommentContent.setColumnNo(token.getColumn() - 1 + 2); 635 blockCommentContent.setLineNo(token.getLine()); 636 blockCommentContent.setText(token.getText()); 637 638 final DetailAST blockCommentClose = new DetailAST(); 639 blockCommentClose.initialize(TokenTypes.BLOCK_COMMENT_END, "*/"); 640 641 final Entry<Integer, Integer> linesColumns = countLinesColumns( 642 token.getText(), token.getLine(), token.getColumn()); 643 blockCommentClose.setLineNo(linesColumns.getKey()); 644 blockCommentClose.setColumnNo(linesColumns.getValue()); 645 646 blockComment.addChild(blockCommentContent); 647 blockComment.addChild(blockCommentClose); 648 return blockComment; 649 } 650 651 /** 652 * Count lines and columns (in last line) in text. 653 * @param text 654 * String. 655 * @param initialLinesCnt 656 * initial value of lines counter. 657 * @param initialColumnsCnt 658 * initial value of columns counter. 659 * @return entry(pair), first element is lines counter, second - columns 660 * counter. 661 */ 662 private static Entry<Integer, Integer> countLinesColumns( 663 String text, int initialLinesCnt, int initialColumnsCnt) { 664 int lines = initialLinesCnt; 665 int columns = initialColumnsCnt; 666 for (char c : text.toCharArray()) { 667 if (c == '\n') { 668 lines++; 669 columns = 0; 670 } 671 else { 672 columns++; 673 } 674 } 675 return new SimpleEntry<>(lines, columns); 676 } 677 678 /** 679 * State of AST. 680 * Indicates whether tree contains certain nodes. 681 */ 682 private enum AstState { 683 /** 684 * Ordinary tree. 685 */ 686 ORDINARY, 687 688 /** 689 * AST contains comment nodes. 690 */ 691 WITH_COMMENTS 692 } 693}