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