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