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