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