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