001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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.google.common.collect.Sets; 043import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 044import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 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.ExternalResourceHolder; 050import com.puppycrawl.tools.checkstyle.api.FileContents; 051import com.puppycrawl.tools.checkstyle.api.FileText; 052import com.puppycrawl.tools.checkstyle.api.TokenTypes; 053import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaLexer; 054import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaRecognizer; 055import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 056import com.puppycrawl.tools.checkstyle.utils.TokenUtils; 057 058/** 059 * Responsible for walking an abstract syntax tree and notifying interested 060 * checks at each each node. 061 * 062 * @author Oliver Burn 063 */ 064public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder { 065 066 /** Default distance between tab stops. */ 067 private static final int DEFAULT_TAB_WIDTH = 8; 068 069 /** Maps from token name to ordinary checks. */ 070 private final Multimap<String, AbstractCheck> tokenToOrdinaryChecks = 071 HashMultimap.create(); 072 073 /** Maps from token name to comment checks. */ 074 private final Multimap<String, AbstractCheck> tokenToCommentChecks = 075 HashMultimap.create(); 076 077 /** Registered ordinary checks, that don't use comment nodes. */ 078 private final Set<AbstractCheck> ordinaryChecks = Sets.newHashSet(); 079 080 /** Registered comment checks. */ 081 private final Set<AbstractCheck> commentChecks = Sets.newHashSet(); 082 083 /** The distance between tab stops. */ 084 private int tabWidth = DEFAULT_TAB_WIDTH; 085 086 /** Class loader to resolve classes with. **/ 087 private ClassLoader classLoader; 088 089 /** Context of child components. */ 090 private Context childContext; 091 092 /** A factory for creating submodules (i.e. the Checks) */ 093 private ModuleFactory moduleFactory; 094 095 /** 096 * Creates a new {@code TreeWalker} instance. 097 */ 098 public TreeWalker() { 099 setFileExtensions("java"); 100 } 101 102 /** 103 * Sets tab width. 104 * @param tabWidth the distance between tab stops 105 */ 106 public void setTabWidth(int tabWidth) { 107 this.tabWidth = tabWidth; 108 } 109 110 /** 111 * Sets cache file. 112 * @deprecated Use {@link Checker#setCacheFile} instead. It does not do anything now. We just 113 * keep the setter for transition period to the same option in Checker. The 114 * method will be completely removed in Checkstyle 8.0. See 115 * <a href="https://github.com/checkstyle/checkstyle/issues/2883">issue#2883</a> 116 * @param fileName the cache file 117 */ 118 @Deprecated 119 public void setCacheFile(String fileName) { 120 // Deprecated 121 } 122 123 /** 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 } 158 final AbstractCheck check = (AbstractCheck) module; 159 check.contextualize(childContext); 160 check.configure(childConf); 161 check.init(); 162 163 registerCheck(check); 164 } 165 166 @Override 167 protected void processFiltered(File file, List<String> lines) throws CheckstyleException { 168 // check if already checked and passed the file 169 if (CommonUtils.matchesFileExtension(file, getFileExtensions())) { 170 final String msg = "%s occurred during the analysis of file %s."; 171 final String fileName = file.getPath(); 172 try { 173 final FileText text = FileText.fromLines(file, lines); 174 final FileContents contents = new FileContents(text); 175 final DetailAST rootAST = parse(contents); 176 177 getMessageCollector().reset(); 178 179 walk(rootAST, contents, AstState.ORDINARY); 180 181 final DetailAST astWithComments = appendHiddenCommentNodes(rootAST); 182 183 walk(astWithComments, contents, AstState.WITH_COMMENTS); 184 } 185 catch (final TokenStreamRecognitionException tre) { 186 final String exceptionMsg = String.format(Locale.ROOT, msg, 187 "TokenStreamRecognitionException", fileName); 188 throw new CheckstyleException(exceptionMsg, tre); 189 } 190 catch (RecognitionException | TokenStreamException ex) { 191 final String exceptionMsg = String.format(Locale.ROOT, msg, 192 ex.getClass().getSimpleName(), fileName); 193 throw new CheckstyleException(exceptionMsg, ex); 194 } 195 } 196 } 197 198 /** 199 * Register a check for a given configuration. 200 * @param check the check to register 201 * @throws CheckstyleException if an error occurs 202 */ 203 private void registerCheck(AbstractCheck check) 204 throws CheckstyleException { 205 validateDefaultTokens(check); 206 final int[] tokens; 207 final Set<String> checkTokens = check.getTokenNames(); 208 if (checkTokens.isEmpty()) { 209 tokens = check.getDefaultTokens(); 210 } 211 else { 212 tokens = check.getRequiredTokens(); 213 214 //register configured tokens 215 final int[] acceptableTokens = check.getAcceptableTokens(); 216 Arrays.sort(acceptableTokens); 217 for (String token : checkTokens) { 218 final int tokenId = TokenUtils.getTokenId(token); 219 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) { 220 registerCheck(token, check); 221 } 222 else { 223 final String message = String.format(Locale.ROOT, "Token \"%s\" was " 224 + "not found in Acceptable tokens list in check %s", 225 token, check.getClass().getName()); 226 throw new CheckstyleException(message); 227 } 228 } 229 } 230 for (int element : tokens) { 231 registerCheck(element, check); 232 } 233 if (check.isCommentNodesRequired()) { 234 commentChecks.add(check); 235 } 236 else { 237 ordinaryChecks.add(check); 238 } 239 } 240 241 /** 242 * Register a check for a specified token id. 243 * @param tokenId the id of the token 244 * @param check the check to register 245 * @throws CheckstyleException if Check is misconfigured 246 */ 247 private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException { 248 registerCheck(TokenUtils.getTokenName(tokenId), check); 249 } 250 251 /** 252 * Register a check for a specified token name. 253 * @param token the name of the token 254 * @param check the check to register 255 * @throws CheckstyleException if Check is misconfigured 256 */ 257 private void registerCheck(String token, AbstractCheck check) throws CheckstyleException { 258 if (check.isCommentNodesRequired()) { 259 tokenToCommentChecks.put(token, check); 260 } 261 else if (TokenUtils.isCommentType(token)) { 262 final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type " 263 + "token ('%s') and should override 'isCommentNodesRequired()' " 264 + "method to return 'true'", check.getClass().getName(), token); 265 throw new CheckstyleException(message); 266 } 267 else { 268 tokenToOrdinaryChecks.put(token, check); 269 } 270 } 271 272 /** 273 * Validates that check's required tokens are subset of default tokens. 274 * @param check to validate 275 * @throws CheckstyleException when validation of default tokens fails 276 */ 277 private static void validateDefaultTokens(AbstractCheck check) throws CheckstyleException { 278 if (check.getRequiredTokens().length != 0) { 279 final int[] defaultTokens = check.getDefaultTokens(); 280 Arrays.sort(defaultTokens); 281 for (final int token : check.getRequiredTokens()) { 282 if (Arrays.binarySearch(defaultTokens, token) < 0) { 283 final String message = String.format(Locale.ROOT, "Token \"%s\" from required " 284 + "tokens was not found in default tokens list in check %s", 285 token, check.getClass().getName()); 286 throw new CheckstyleException(message); 287 } 288 } 289 } 290 } 291 292 /** 293 * Initiates the walk of an AST. 294 * @param ast the root AST 295 * @param contents the contents of the file the AST was generated from. 296 * @param astState state of AST. 297 */ 298 private void walk(DetailAST ast, FileContents contents, 299 AstState astState) { 300 notifyBegin(ast, contents, astState); 301 302 // empty files are not flagged by javac, will yield ast == null 303 if (ast != null) { 304 processIter(ast, astState); 305 } 306 notifyEnd(ast, astState); 307 } 308 309 /** 310 * Notify checks that we are about to begin walking a tree. 311 * @param rootAST the root of the tree. 312 * @param contents the contents of the file the AST was generated from. 313 * @param astState state of AST. 314 */ 315 private void notifyBegin(DetailAST rootAST, FileContents contents, 316 AstState astState) { 317 final Set<AbstractCheck> checks; 318 319 if (astState == AstState.WITH_COMMENTS) { 320 checks = commentChecks; 321 } 322 else { 323 checks = ordinaryChecks; 324 } 325 326 for (AbstractCheck check : checks) { 327 check.setFileContents(contents); 328 check.beginTree(rootAST); 329 } 330 } 331 332 /** 333 * Notify checks that we have finished walking a tree. 334 * @param rootAST the root of the tree. 335 * @param astState state of AST. 336 */ 337 private void notifyEnd(DetailAST rootAST, AstState astState) { 338 final Set<AbstractCheck> checks; 339 340 if (astState == AstState.WITH_COMMENTS) { 341 checks = commentChecks; 342 } 343 else { 344 checks = ordinaryChecks; 345 } 346 347 for (AbstractCheck check : checks) { 348 check.finishTree(rootAST); 349 } 350 } 351 352 /** 353 * Notify checks that visiting a node. 354 * @param ast the node to notify for. 355 * @param astState state of AST. 356 */ 357 private void notifyVisit(DetailAST ast, AstState astState) { 358 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState); 359 360 if (visitors != null) { 361 for (AbstractCheck check : visitors) { 362 check.visitToken(ast); 363 } 364 } 365 } 366 367 /** 368 * Notify checks that leaving a node. 369 * @param ast 370 * the node to notify for 371 * @param astState state of AST. 372 */ 373 private void notifyLeave(DetailAST ast, AstState astState) { 374 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState); 375 376 if (visitors != null) { 377 for (AbstractCheck check : visitors) { 378 check.leaveToken(ast); 379 } 380 } 381 } 382 383 /** 384 * Method returns list of checks 385 * 386 * @param ast 387 * the node to notify for 388 * @param astState 389 * state of AST. 390 * @return list of visitors 391 */ 392 private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) { 393 Collection<AbstractCheck> visitors = null; 394 final String tokenType = TokenUtils.getTokenName(ast.getType()); 395 396 if (astState == AstState.WITH_COMMENTS) { 397 if (tokenToCommentChecks.containsKey(tokenType)) { 398 visitors = tokenToCommentChecks.get(tokenType); 399 } 400 } 401 else { 402 if (tokenToOrdinaryChecks.containsKey(tokenType)) { 403 visitors = tokenToOrdinaryChecks.get(tokenType); 404 } 405 } 406 return visitors; 407 } 408 409 /** 410 * Static helper method to parses a Java source file. 411 * 412 * @param contents 413 * contains the contents of the file 414 * @return the root of the AST 415 * @throws TokenStreamException 416 * if lexing failed 417 * @throws RecognitionException 418 * if parsing failed 419 */ 420 public static DetailAST parse(FileContents contents) 421 throws RecognitionException, TokenStreamException { 422 final String fullText = contents.getText().getFullText().toString(); 423 final Reader reader = new StringReader(fullText); 424 final GeneratedJavaLexer lexer = new GeneratedJavaLexer(reader); 425 lexer.setFilename(contents.getFileName()); 426 lexer.setCommentListener(contents); 427 lexer.setTreatAssertAsKeyword(true); 428 lexer.setTreatEnumAsKeyword(true); 429 lexer.setTokenObjectClass("antlr.CommonHiddenStreamToken"); 430 431 final TokenStreamHiddenTokenFilter filter = 432 new TokenStreamHiddenTokenFilter(lexer); 433 filter.hide(TokenTypes.SINGLE_LINE_COMMENT); 434 filter.hide(TokenTypes.BLOCK_COMMENT_BEGIN); 435 436 final GeneratedJavaRecognizer parser = 437 new GeneratedJavaRecognizer(filter); 438 parser.setFilename(contents.getFileName()); 439 parser.setASTNodeClass(DetailAST.class.getName()); 440 parser.compilationUnit(); 441 442 return (DetailAST) parser.getAST(); 443 } 444 445 /** 446 * Parses Java source file. Result AST contains comment nodes. 447 * @param contents source file content 448 * @return DetailAST tree 449 * @throws RecognitionException if parser failed 450 * @throws TokenStreamException if lexer failed 451 */ 452 public static DetailAST parseWithComments(FileContents contents) 453 throws RecognitionException, TokenStreamException { 454 return appendHiddenCommentNodes(parse(contents)); 455 } 456 457 @Override 458 public void destroy() { 459 for (AbstractCheck check : ordinaryChecks) { 460 check.destroy(); 461 } 462 for (AbstractCheck check : commentChecks) { 463 check.destroy(); 464 } 465 super.destroy(); 466 } 467 468 @Override 469 public Set<String> getExternalResourceLocations() { 470 final Set<String> orinaryChecksResources = getExternalResourceLocations(ordinaryChecks); 471 final Set<String> commentChecksResources = getExternalResourceLocations(commentChecks); 472 final int resultListSize = orinaryChecksResources.size() + commentChecksResources.size(); 473 final Set<String> resourceLocations = new HashSet<>(resultListSize); 474 resourceLocations.addAll(orinaryChecksResources); 475 resourceLocations.addAll(commentChecksResources); 476 return resourceLocations; 477 } 478 479 /** 480 * Returns a set of external configuration resource locations which are used by the checks set. 481 * @param checks a set of checks. 482 * @return a set of external configuration resource locations which are used by the checks set. 483 */ 484 private Set<String> getExternalResourceLocations(Set<AbstractCheck> checks) { 485 final Set<String> externalConfigurationResources = Sets.newHashSet(); 486 for (AbstractCheck check : checks) { 487 if (check instanceof ExternalResourceHolder) { 488 final Set<String> checkExternalResources = 489 ((ExternalResourceHolder) check).getExternalResourceLocations(); 490 externalConfigurationResources.addAll(checkExternalResources); 491 } 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 if (ast1.getLineNo() == ast2.getLineNo()) { 589 return ast1.getColumnNo() > ast2.getColumnNo(); 590 } 591 else { 592 return ast1.getLineNo() > ast2.getLineNo(); 593 } 594 } 595 596 /** 597 * Create comment AST from token. Depending on token type 598 * SINGLE_LINE_COMMENT or BLOCK_COMMENT_BEGIN is created. 599 * @param token 600 * Token object. 601 * @return DetailAST of comment node. 602 */ 603 private static DetailAST createCommentAstFromToken(Token token) { 604 if (token.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 605 return createSlCommentNode(token); 606 } 607 else { 608 return createBlockCommentNode(token); 609 } 610 } 611 612 /** 613 * Create single-line comment from token. 614 * @param token 615 * Token object. 616 * @return DetailAST with SINGLE_LINE_COMMENT type. 617 */ 618 private static DetailAST createSlCommentNode(Token token) { 619 final DetailAST slComment = new DetailAST(); 620 slComment.setType(TokenTypes.SINGLE_LINE_COMMENT); 621 slComment.setText("//"); 622 623 // column counting begins from 0 624 slComment.setColumnNo(token.getColumn() - 1); 625 slComment.setLineNo(token.getLine()); 626 627 final DetailAST slCommentContent = new DetailAST(); 628 slCommentContent.initialize(token); 629 slCommentContent.setType(TokenTypes.COMMENT_CONTENT); 630 631 // column counting begins from 0 632 // plus length of '//' 633 slCommentContent.setColumnNo(token.getColumn() - 1 + 2); 634 slCommentContent.setLineNo(token.getLine()); 635 slCommentContent.setText(token.getText()); 636 637 slComment.addChild(slCommentContent); 638 return slComment; 639 } 640 641 /** 642 * Create block comment from token. 643 * @param token 644 * Token object. 645 * @return DetailAST with BLOCK_COMMENT type. 646 */ 647 private static DetailAST createBlockCommentNode(Token token) { 648 final DetailAST blockComment = new DetailAST(); 649 blockComment.initialize(TokenTypes.BLOCK_COMMENT_BEGIN, "/*"); 650 651 // column counting begins from 0 652 blockComment.setColumnNo(token.getColumn() - 1); 653 blockComment.setLineNo(token.getLine()); 654 655 final DetailAST blockCommentContent = new DetailAST(); 656 blockCommentContent.initialize(token); 657 blockCommentContent.setType(TokenTypes.COMMENT_CONTENT); 658 659 // column counting begins from 0 660 // plus length of '/*' 661 blockCommentContent.setColumnNo(token.getColumn() - 1 + 2); 662 blockCommentContent.setLineNo(token.getLine()); 663 blockCommentContent.setText(token.getText()); 664 665 final DetailAST blockCommentClose = new DetailAST(); 666 blockCommentClose.initialize(TokenTypes.BLOCK_COMMENT_END, "*/"); 667 668 final Entry<Integer, Integer> linesColumns = countLinesColumns( 669 token.getText(), token.getLine(), token.getColumn()); 670 blockCommentClose.setLineNo(linesColumns.getKey()); 671 blockCommentClose.setColumnNo(linesColumns.getValue()); 672 673 blockComment.addChild(blockCommentContent); 674 blockComment.addChild(blockCommentClose); 675 return blockComment; 676 } 677 678 /** 679 * Count lines and columns (in last line) in text. 680 * @param text 681 * String. 682 * @param initialLinesCnt 683 * initial value of lines counter. 684 * @param initialColumnsCnt 685 * initial value of columns counter. 686 * @return entry(pair), first element is lines counter, second - columns 687 * counter. 688 */ 689 private static Entry<Integer, Integer> countLinesColumns( 690 String text, int initialLinesCnt, int initialColumnsCnt) { 691 int lines = initialLinesCnt; 692 int columns = initialColumnsCnt; 693 boolean foundCr = false; 694 for (char c : text.toCharArray()) { 695 if (c == '\n') { 696 foundCr = false; 697 lines++; 698 columns = 0; 699 } 700 else { 701 if (foundCr) { 702 foundCr = false; 703 lines++; 704 columns = 0; 705 } 706 if (c == '\r') { 707 foundCr = true; 708 } 709 columns++; 710 } 711 } 712 if (foundCr) { 713 lines++; 714 columns = 0; 715 } 716 return new SimpleEntry<>(lines, columns); 717 } 718 719 /** 720 * State of AST. 721 * Indicates whether tree contains certain nodes. 722 */ 723 private enum AstState { 724 /** 725 * Ordinary tree. 726 */ 727 ORDINARY, 728 729 /** 730 * AST contains comment nodes. 731 */ 732 WITH_COMMENTS 733 } 734}