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