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}