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