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