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}