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