001: ////////////////////////////////////////////////////////////////////////////////
002: // checkstyle: Checks Java source code for adherence to a set of rules.
003: // Copyright (C) 2001-2007 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: ////////////////////////////////////////////////////////////////////////////////
019: package com.puppycrawl.tools.checkstyle;
020:
021: import java.io.File;
022: import java.io.FileNotFoundException;
023: import java.io.IOException;
024: import java.io.Reader;
025: import java.util.ArrayList;
026: import java.util.Arrays;
027: import java.util.HashMap;
028: import java.util.HashSet;
029: import java.util.Iterator;
030: import java.util.Map;
031: import java.util.Set;
032:
033: import antlr.RecognitionException;
034: import antlr.TokenStreamException;
035: import antlr.TokenStreamRecognitionException;
036: import antlr.TokenStream;
037:
038: import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
039: import com.puppycrawl.tools.checkstyle.api.Check;
040: import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
041: import com.puppycrawl.tools.checkstyle.api.Configuration;
042: import com.puppycrawl.tools.checkstyle.api.Context;
043: import com.puppycrawl.tools.checkstyle.api.DetailAST;
044: import com.puppycrawl.tools.checkstyle.api.FileContents;
045: import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
046: import com.puppycrawl.tools.checkstyle.api.TokenTypes;
047: import com.puppycrawl.tools.checkstyle.api.Utils;
048: import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaRecognizer;
049: import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaLexer;
050:
051: import org.apache.commons.logging.Log;
052: import org.apache.commons.logging.LogFactory;
053:
054: /**
055: * Responsible for walking an abstract syntax tree and notifying interested
056: * checks at each each node.
057: *
058: * @author Oliver Burn
059: * @version 1.0
060: */
061: public final class TreeWalker extends AbstractFileSetCheck {
062: /**
063: * Overrides ANTLR error reporting so we completely control
064: * checkstyle's output during parsing. This is important because
065: * we try parsing with several grammers (with/without support for
066: * <code>assert</code>). We must not write any error messages when
067: * parsing fails because with the next grammar it might succeed
068: * and the user will be confused.
069: */
070: private static final class SilentJavaRecognizer extends
071: GeneratedJavaRecognizer {
072: /**
073: * Creates a new <code>SilentJavaRecognizer</code> instance.
074: *
075: * @param aLexer the tokenstream the recognizer operates on.
076: */
077: public SilentJavaRecognizer(TokenStream aLexer) {
078: super (aLexer);
079: }
080:
081: /**
082: * Parser error-reporting function, does nothing.
083: * @param aRex the exception to be reported
084: */
085: public void reportError(RecognitionException aRex) {
086: }
087:
088: /**
089: * Parser error-reporting function, does nothing.
090: * @param aMsg the error message
091: */
092: public void reportError(String aMsg) {
093: }
094:
095: /**
096: * Parser warning-reporting function, does nothing.
097: * @param aMsg the error message
098: */
099: public void reportWarning(String aMsg) {
100: }
101: }
102:
103: /** default distance between tab stops */
104: private static final int DEFAULT_TAB_WIDTH = 8;
105:
106: /** maps from token name to checks */
107: private final Map mTokenToChecks = new HashMap();
108: /** all the registered checks */
109: private final Set mAllChecks = new HashSet();
110: /** the distance between tab stops */
111: private int mTabWidth = DEFAULT_TAB_WIDTH;
112: /** cache file **/
113: private PropertyCacheFile mCache = new PropertyCacheFile(null, null);
114:
115: /** class loader to resolve classes with. **/
116: private ClassLoader mClassLoader;
117:
118: /** context of child components */
119: private Context mChildContext;
120:
121: /** a factory for creating submodules (i.e. the Checks) */
122: private ModuleFactory mModuleFactory;
123:
124: /** controls whether we should use recursive or iterative
125: * algorithm for tree processing.
126: */
127: private final boolean mRecursive;
128:
129: /** logger for debug purpose */
130: private static final Log LOG = LogFactory
131: .getLog("com.puppycrawl.tools.checkstyle.TreeWalker");
132:
133: /**
134: * Creates a new <code>TreeWalker</code> instance.
135: */
136: public TreeWalker() {
137: setFileExtensions(new String[] { "java" });
138: // Tree walker can use two possible algorithms for
139: // tree processing (iterative and recursive.
140: // Recursive is default for now.
141: final String recursive = System.getProperty(
142: "checkstyle.use.recursive.algorithm", "false");
143: mRecursive = "true".equals(recursive);
144: if (mRecursive) {
145: LOG.debug("TreeWalker uses recursive algorithm");
146: } else {
147: LOG.debug("TreeWalker uses iterative algorithm");
148: }
149: }
150:
151: /** @param aTabWidth the distance between tab stops */
152: public void setTabWidth(int aTabWidth) {
153: mTabWidth = aTabWidth;
154: }
155:
156: /** @param aFileName the cache file */
157: public void setCacheFile(String aFileName) {
158: final Configuration configuration = getConfiguration();
159: mCache = new PropertyCacheFile(configuration, aFileName);
160: }
161:
162: /** @param aClassLoader class loader to resolve classes with. */
163: public void setClassLoader(ClassLoader aClassLoader) {
164: mClassLoader = aClassLoader;
165: }
166:
167: /**
168: * Sets the module factory for creating child modules (Checks).
169: * @param aModuleFactory the factory
170: */
171: public void setModuleFactory(ModuleFactory aModuleFactory) {
172: mModuleFactory = aModuleFactory;
173: }
174:
175: /** @see com.puppycrawl.tools.checkstyle.api.Configurable */
176: public void finishLocalSetup() {
177: final DefaultContext checkContext = new DefaultContext();
178: checkContext.add("classLoader", mClassLoader);
179: checkContext.add("messages", getMessageCollector());
180: checkContext.add("severity", getSeverity());
181: // TODO: hmmm.. this looks less than elegant
182: // we have just parsed the string,
183: // now we're recreating it only to parse it again a few moments later
184: checkContext.add("tabWidth", String.valueOf(mTabWidth));
185:
186: mChildContext = checkContext;
187: }
188:
189: /**
190: * Instantiates, configures and registers a Check that is specified
191: * in the provided configuration.
192: * {@inheritDoc}
193: */
194: public void setupChild(Configuration aChildConf)
195: throws CheckstyleException {
196: // TODO: improve the error handing
197: final String name = aChildConf.getName();
198: final Object module = mModuleFactory.createModule(name);
199: if (!(module instanceof Check)) {
200: throw new CheckstyleException(
201: "TreeWalker is not allowed as a parent of " + name);
202: }
203: final Check c = (Check) module;
204: c.contextualize(mChildContext);
205: c.configure(aChildConf);
206: c.init();
207:
208: registerCheck(c);
209: }
210:
211: /**
212: * Processes a specified file and reports all errors found.
213: * @param aFile the file to process
214: **/
215: private void process(File aFile) {
216: // check if already checked and passed the file
217: final String fileName = aFile.getPath();
218: final long timestamp = aFile.lastModified();
219: if (mCache.alreadyChecked(fileName, timestamp)) {
220: return;
221: }
222:
223: try {
224: getMessageDispatcher().fireFileStarted(fileName);
225: final String[] lines = Utils.getLines(fileName,
226: getCharset());
227: final FileContents contents = new FileContents(fileName,
228: lines);
229: final DetailAST rootAST = TreeWalker.parse(contents);
230: walk(rootAST, contents);
231: } catch (final FileNotFoundException fnfe) {
232: Utils.getExceptionLogger().debug(
233: "FileNotFoundException occured.", fnfe);
234: getMessageCollector().add(
235: new LocalizedMessage(0, Defn.CHECKSTYLE_BUNDLE,
236: "general.fileNotFound", null, getId(), this
237: .getClass()));
238: } catch (final IOException ioe) {
239: Utils.getExceptionLogger().debug("IOException occured.",
240: ioe);
241: getMessageCollector().add(
242: new LocalizedMessage(0, Defn.CHECKSTYLE_BUNDLE,
243: "general.exception", new String[] { ioe
244: .getMessage() }, getId(), this
245: .getClass()));
246: } catch (final RecognitionException re) {
247: Utils.getExceptionLogger().debug(
248: "RecognitionException occured.", re);
249: getMessageCollector().add(
250: new LocalizedMessage(re.getLine(), re.getColumn(),
251: Defn.CHECKSTYLE_BUNDLE,
252: "general.exception", new String[] { re
253: .getMessage() }, getId(), this
254: .getClass()));
255: } catch (final TokenStreamRecognitionException tre) {
256: Utils.getExceptionLogger().debug(
257: "TokenStreamRecognitionException occured.", tre);
258: final RecognitionException re = tre.recog;
259: if (re != null) {
260: getMessageCollector().add(
261: new LocalizedMessage(re.getLine(), re
262: .getColumn(), Defn.CHECKSTYLE_BUNDLE,
263: "general.exception", new String[] { re
264: .getMessage() }, getId(), this
265: .getClass()));
266: } else {
267: getMessageCollector()
268: .add(
269: new LocalizedMessage(
270: 0,
271: Defn.CHECKSTYLE_BUNDLE,
272: "general.exception",
273: new String[] { "TokenStreamRecognitionException occured." },
274: getId(), this .getClass()));
275: }
276: } catch (final TokenStreamException te) {
277: Utils.getExceptionLogger().debug(
278: "TokenStreamException occured.", te);
279: getMessageCollector().add(
280: new LocalizedMessage(0, Defn.CHECKSTYLE_BUNDLE,
281: "general.exception", new String[] { te
282: .getMessage() }, getId(), this
283: .getClass()));
284: } catch (final Throwable err) {
285: Utils.getExceptionLogger().debug("Throwable occured.", err);
286: getMessageCollector().add(
287: new LocalizedMessage(0, Defn.CHECKSTYLE_BUNDLE,
288: "general.exception", new String[] { ""
289: + err }, getId(), this .getClass()));
290: }
291:
292: if (getMessageCollector().size() == 0) {
293: mCache.checkedOk(fileName, timestamp);
294: } else {
295: fireErrors(fileName);
296: }
297:
298: getMessageDispatcher().fireFileFinished(fileName);
299: }
300:
301: /**
302: * Register a check for a given configuration.
303: * @param aCheck the check to register
304: * @throws CheckstyleException if an error occurs
305: */
306: private void registerCheck(Check aCheck) throws CheckstyleException {
307: int[] tokens = new int[] {}; //safety initialization
308: final Set checkTokens = aCheck.getTokenNames();
309: if (!checkTokens.isEmpty()) {
310: tokens = aCheck.getRequiredTokens();
311:
312: //register configured tokens
313: final int acceptableTokens[] = aCheck.getAcceptableTokens();
314: Arrays.sort(acceptableTokens);
315: final Iterator it = checkTokens.iterator();
316: while (it.hasNext()) {
317: final String token = (String) it.next();
318: try {
319: final int tokenId = TokenTypes.getTokenId(token);
320: if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
321: registerCheck(token, aCheck);
322: }
323: // TODO: else log warning
324: } catch (final IllegalArgumentException ex) {
325: throw new CheckstyleException("illegal token \""
326: + token + "\" in check " + aCheck, ex);
327: }
328: }
329: } else {
330: tokens = aCheck.getDefaultTokens();
331: }
332: for (int i = 0; i < tokens.length; i++) {
333: registerCheck(tokens[i], aCheck);
334: }
335: mAllChecks.add(aCheck);
336: }
337:
338: /**
339: * Register a check for a specified token id.
340: * @param aTokenID the id of the token
341: * @param aCheck the check to register
342: */
343: private void registerCheck(int aTokenID, Check aCheck) {
344: registerCheck(TokenTypes.getTokenName(aTokenID), aCheck);
345: }
346:
347: /**
348: * Register a check for a specified token name
349: * @param aToken the name of the token
350: * @param aCheck the check to register
351: */
352: private void registerCheck(String aToken, Check aCheck) {
353: ArrayList visitors = (ArrayList) mTokenToChecks.get(aToken);
354: if (visitors == null) {
355: visitors = new ArrayList();
356: mTokenToChecks.put(aToken, visitors);
357: }
358:
359: visitors.add(aCheck);
360: }
361:
362: /**
363: * Initiates the walk of an AST.
364: * @param aAST the root AST
365: * @param aContents the contents of the file the AST was generated from
366: */
367: private void walk(DetailAST aAST, FileContents aContents) {
368: getMessageCollector().reset();
369: notifyBegin(aAST, aContents);
370:
371: // empty files are not flagged by javac, will yield aAST == null
372: if (aAST != null) {
373: if (useRecursiveAlgorithm()) {
374: processRec(aAST);
375: } else {
376: processIter(aAST);
377: }
378: }
379:
380: notifyEnd(aAST);
381: }
382:
383: /**
384: * Notify interested checks that about to begin walking a tree.
385: * @param aRootAST the root of the tree
386: * @param aContents the contents of the file the AST was generated from
387: */
388: private void notifyBegin(DetailAST aRootAST, FileContents aContents) {
389: final Iterator it = mAllChecks.iterator();
390: while (it.hasNext()) {
391: final Check check = (Check) it.next();
392: check.setFileContents(aContents);
393: check.beginTree(aRootAST);
394: }
395: }
396:
397: /**
398: * Notify checks that finished walking a tree.
399: * @param aRootAST the root of the tree
400: */
401: private void notifyEnd(DetailAST aRootAST) {
402: final Iterator it = mAllChecks.iterator();
403: while (it.hasNext()) {
404: final Check check = (Check) it.next();
405: check.finishTree(aRootAST);
406: }
407: }
408:
409: /**
410: * Recursively processes a node calling interested checks at each node.
411: * Uses recursive algorithm.
412: * @param aAST the node to start from
413: */
414: private void processRec(DetailAST aAST) {
415: if (aAST == null) {
416: return;
417: }
418:
419: notifyVisit(aAST);
420:
421: final DetailAST child = (DetailAST) aAST.getFirstChild();
422: if (child != null) {
423: processRec(child);
424: }
425:
426: notifyLeave(aAST);
427:
428: final DetailAST sibling = (DetailAST) aAST.getNextSibling();
429: if (sibling != null) {
430: processRec(sibling);
431: }
432: }
433:
434: /**
435: * Notify interested checks that visiting a node.
436: * @param aAST the node to notify for
437: */
438: private void notifyVisit(DetailAST aAST) {
439: final ArrayList visitors = (ArrayList) mTokenToChecks
440: .get(TokenTypes.getTokenName(aAST.getType()));
441: if (visitors != null) {
442: for (int i = 0; i < visitors.size(); i++) {
443: final Check check = (Check) visitors.get(i);
444: check.visitToken(aAST);
445: }
446: }
447: }
448:
449: /**
450: * Notify interested checks that leaving a node.
451: * @param aAST the node to notify for
452: */
453: private void notifyLeave(DetailAST aAST) {
454: final ArrayList visitors = (ArrayList) mTokenToChecks
455: .get(TokenTypes.getTokenName(aAST.getType()));
456: if (visitors != null) {
457: for (int i = 0; i < visitors.size(); i++) {
458: final Check check = (Check) visitors.get(i);
459: check.leaveToken(aAST);
460: }
461: }
462: }
463:
464: /**
465: * Static helper method to parses a Java source file.
466: * @param aContents contains the contents of the file
467: * @throws TokenStreamException if lexing failed
468: * @throws RecognitionException if parsing failed
469: * @return the root of the AST
470: */
471: public static DetailAST parse(FileContents aContents)
472: throws RecognitionException, TokenStreamException {
473: DetailAST rootAST = null;
474:
475: try {
476: rootAST = parse(aContents, true, true, true);
477: } catch (final RecognitionException exception) {
478: try {
479: rootAST = parse(aContents, true, true, false);
480: } catch (final RecognitionException exception2) {
481: rootAST = parse(aContents, false, false, false);
482: }
483: }
484: return rootAST;
485: }
486:
487: /**
488: * Static helper method to parses a Java source file with a given
489: * lexer class and parser class.
490: * @param aContents contains the contents of the file
491: * @param aSilentlyConsumeErrors flag to output errors to stdout or not
492: * @param aTreatAssertAsKeyword flag to treat 'assert' as a keyowrd
493: * @param aTreatEnumAsKeyword flag to treat 'enum' as a keyowrd
494: * @throws TokenStreamException if lexing failed
495: * @throws RecognitionException if parsing failed
496: * @return the root of the AST
497: */
498: private static DetailAST parse(FileContents aContents,
499: boolean aSilentlyConsumeErrors,
500: boolean aTreatAssertAsKeyword, boolean aTreatEnumAsKeyword)
501: throws RecognitionException, TokenStreamException {
502: final Reader sar = new StringArrayReader(aContents.getLines());
503: final GeneratedJavaLexer lexer = new GeneratedJavaLexer(sar);
504: lexer.setFilename(aContents.getFilename());
505: lexer.setCommentListener(aContents);
506: lexer.setTreatAssertAsKeyword(aTreatAssertAsKeyword);
507: lexer.setTreatEnumAsKeyword(aTreatEnumAsKeyword);
508:
509: final GeneratedJavaRecognizer parser = aSilentlyConsumeErrors ? new SilentJavaRecognizer(
510: lexer)
511: : new GeneratedJavaRecognizer(lexer);
512: parser.setFilename(aContents.getFilename());
513: parser.setASTNodeClass(DetailAST.class.getName());
514: parser.compilationUnit();
515:
516: return (DetailAST) parser.getAST();
517: }
518:
519: /** {@inheritDoc} */
520: public void process(File[] aFiles) {
521: final File[] javaFiles = filter(aFiles);
522:
523: for (int i = 0; i < javaFiles.length; i++) {
524: process(javaFiles[i]);
525: }
526: }
527:
528: /**
529: * @see com.puppycrawl.tools.checkstyle.api.FileSetCheck
530: */
531: public void destroy() {
532: for (final Iterator it = mAllChecks.iterator(); it.hasNext();) {
533: final Check c = (Check) it.next();
534: c.destroy();
535: }
536: mCache.destroy();
537: super .destroy();
538: }
539:
540: /**
541: * @return true if we should use recursive algorithm
542: * for tree processing, false for iterative one.
543: */
544: private boolean useRecursiveAlgorithm() {
545: return mRecursive;
546: }
547:
548: /**
549: * Processes a node calling interested checks at each node.
550: * Uses iterative algorithm.
551: * @param aRoot the root of tree for process
552: */
553: private void processIter(DetailAST aRoot) {
554: DetailAST curNode = aRoot;
555: while (curNode != null) {
556: notifyVisit(curNode);
557: DetailAST toVisit = (DetailAST) curNode.getFirstChild();
558: while ((curNode != null) && (toVisit == null)) {
559: notifyLeave(curNode);
560: toVisit = (DetailAST) curNode.getNextSibling();
561: if (toVisit == null) {
562: curNode = curNode.getParent();
563: }
564: }
565: curNode = toVisit;
566: }
567: }
568: }
|