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