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