View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd;
5   
6   import java.io.File;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.net.URISyntaxException;
10  import java.sql.SQLException;
11  import java.util.ArrayList;
12  import java.util.Collection;
13  import java.util.Collections;
14  import java.util.Comparator;
15  import java.util.HashSet;
16  import java.util.LinkedList;
17  import java.util.List;
18  import java.util.Properties;
19  import java.util.Set;
20  import java.util.concurrent.atomic.AtomicInteger;
21  import java.util.logging.Handler;
22  import java.util.logging.Level;
23  import java.util.logging.Logger;
24  
25  import net.sourceforge.pmd.benchmark.Benchmark;
26  import net.sourceforge.pmd.benchmark.Benchmarker;
27  import net.sourceforge.pmd.benchmark.TextReport;
28  import net.sourceforge.pmd.cli.PMDCommandLineInterface;
29  import net.sourceforge.pmd.cli.PMDParameters;
30  import net.sourceforge.pmd.lang.*;
31  import net.sourceforge.pmd.processor.MonoThreadProcessor;
32  import net.sourceforge.pmd.processor.MultiThreadProcessor;
33  import net.sourceforge.pmd.renderers.Renderer;
34  import net.sourceforge.pmd.stat.Metric;
35  import net.sourceforge.pmd.util.FileUtil;
36  import net.sourceforge.pmd.util.IOUtil;
37  import net.sourceforge.pmd.util.SystemUtils;
38  import net.sourceforge.pmd.util.database.DBMSMetadata;
39  import net.sourceforge.pmd.util.database.DBURI;
40  import net.sourceforge.pmd.util.database.SourceObject;
41  import net.sourceforge.pmd.util.datasource.DataSource;
42  import net.sourceforge.pmd.util.datasource.ReaderDataSource;
43  import net.sourceforge.pmd.util.log.ConsoleLogHandler;
44  import net.sourceforge.pmd.util.log.ScopedLogHandlersManager;
45  
46  /**
47   * This is the main class for interacting with PMD. The primary flow of all Rule
48   * process is controlled via interactions with this class. A command line
49   * interface is supported, as well as a programmatic API for integrating PMD
50   * with other software such as IDEs and Ant.
51   */
52  public class PMD {
53  
54      private static final Logger LOG = Logger.getLogger(PMD.class.getName());
55  
56      /** The line delimiter used by PMD in outputs. Usually the platform specific line separator. */
57      public static final String EOL = System.getProperty("line.separator", "\n");
58  
59      /** The default suppress marker string. */
60      public static final String SUPPRESS_MARKER = "NOPMD";
61  
62      /**
63       * Parses the given string as a database uri and returns a list of datasources.
64       * @param uriString the URI to parse
65       * @return list of data sources
66       * @throws PMDException if the URI couldn't be parsed
67       * @see DBURI
68       */
69      public static List<DataSource> getURIDataSources(String uriString) throws PMDException {
70          List<DataSource> dataSources = new ArrayList<>();
71  
72          try {
73              DBURI dbUri = new DBURI(uriString);
74              DBMSMetadata dbmsMetadata = new DBMSMetadata(dbUri);
75              LOG.log(Level.FINE, "DBMSMetadata retrieved");
76              List<SourceObject> sourceObjectList = dbmsMetadata.getSourceObjectList();
77              LOG.log(Level.FINE, "Located {0} database source objects", sourceObjectList.size());
78              for (SourceObject sourceObject : sourceObjectList) {
79                  String falseFilePath = sourceObject.getPseudoFileName();
80                  LOG.log(Level.FINEST, "Adding database source object {0}", falseFilePath);
81  
82                  try {
83                      dataSources.add(new ReaderDataSource(dbmsMetadata.getSourceCode(sourceObject), falseFilePath));
84                  } catch (SQLException ex) {
85                      if (LOG.isLoggable(Level.WARNING)) {
86                          LOG.log(Level.WARNING, "Cannot get SourceCode for " + falseFilePath + "  - skipping ...", ex);
87                      }
88                  }
89              }
90          } catch (URISyntaxException e) {
91              throw new PMDException("Cannot get DataSources from DBURI - \"" + uriString + "\"", e);
92          } catch (SQLException e) {
93              throw new PMDException("Cannot get DataSources from DBURI, couldn't access the database - \"" + uriString
94                      + "\"", e);
95          } catch (ClassNotFoundException e) {
96              throw new PMDException("Cannot get DataSources from DBURI, probably missing database jdbc driver - \""
97                      + uriString + "\"", e);
98          } catch (Exception e) {
99              throw new PMDException("Encountered unexpected problem with URI \""
100                     + uriString + "\"", e);
101         }
102         return dataSources;
103     }
104 
105     /** Contains the configuration with which this PMD instance has been created. */
106     protected final PMDConfiguration configuration;
107 
108     private final SourceCodeProcessor rulesetsFileProcessor;
109 
110     /**
111      * Helper method to get a configured parser for the requested language. The parser is
112      * configured based on the given {@link PMDConfiguration}.
113      * @param languageVersion the requested language
114      * @param configuration the given configuration
115      * @return the pre-configured parser
116      */
117     public static Parser parserFor(LanguageVersion languageVersion, PMDConfiguration configuration) {
118 
119         // TODO Handle Rules having different parser options.
120         LanguageVersionHandler languageVersionHandler = languageVersion.getLanguageVersionHandler();
121         ParserOptions options = languageVersionHandler.getDefaultParserOptions();
122         if (configuration != null) {
123             options.setSuppressMarker(configuration.getSuppressMarker());
124         }
125         return languageVersionHandler.getParser(options);
126     }
127 
128     /**
129      * Create a report, filter out any defective rules, and keep a record of
130      * them.
131      * 
132      * @param rs the rules
133      * @param ctx the rule context
134      * @param fileName the filename of the source file, which should appear in the report
135      * @return the Report
136      */
137     public static Report setupReport(RuleSets rs, RuleContext ctx, String fileName) {
138 
139         Set<Rule> brokenRules = removeBrokenRules(rs);
140         Report report = Report.createReport(ctx, fileName);
141 
142         for (Rule rule : brokenRules) {
143             report.addConfigError(new Report.RuleConfigurationError(rule, rule.dysfunctionReason()));
144         }
145 
146         return report;
147     }
148 
149     /**
150      * Remove and return the misconfigured rules from the rulesets and log them
151      * for good measure.
152      * 
153      * @param ruleSets
154      *            RuleSets
155      * @return Set<Rule>
156      */
157     private static Set<Rule> removeBrokenRules(RuleSets ruleSets) {
158 
159         Set<Rule> brokenRules = new HashSet<>();
160         ruleSets.removeDysfunctionalRules(brokenRules);
161 
162         for (Rule rule : brokenRules) {
163             if (LOG.isLoggable(Level.WARNING)) {
164                 LOG.log(Level.WARNING,
165                     "Removed misconfigured rule: " + rule.getName() + "  cause: " + rule.dysfunctionReason());
166             }
167         }
168 
169         return brokenRules;
170     }
171 
172     /**
173      * Create a PMD instance using a default Configuration. Changes to the
174      * configuration may be required.
175      */
176     public PMD() {
177         this(new PMDConfiguration());
178     }
179 
180     /**
181      * Create a PMD instance using the specified Configuration.
182      * 
183      * @param configuration
184      *            The runtime Configuration of PMD to use.
185      */
186     public PMD(PMDConfiguration configuration) {
187         this.configuration = configuration;
188         this.rulesetsFileProcessor = new SourceCodeProcessor(configuration);
189     }
190 
191     /**
192      * Get the runtime configuration. The configuration can be modified to
193      * affect how PMD behaves.
194      * 
195      * @return The configuration.
196      * @see PMDConfiguration
197      */
198     public PMDConfiguration getConfiguration() {
199         return configuration;
200     }
201 
202     /**
203      * Gets the source code processor.
204      * @return SourceCodeProcessor
205      */
206     public SourceCodeProcessor getSourceCodeProcessor() {
207         return rulesetsFileProcessor;
208     }
209 
210     /**
211      * This method is the main entry point for command line usage.
212      * 
213      * @param configuration the configure to use
214      * @return number of violations found.
215      */
216     public static int doPMD(PMDConfiguration configuration) {
217 
218         // Load the RuleSets
219         RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.getRulesetFactory(configuration);
220         RuleSets ruleSets = RulesetsFactoryUtils.getRuleSetsWithBenchmark(configuration.getRuleSets(), configuration.getPmdRuleSets(), ruleSetFactory);
221         configuration.setPmdRuleSets(ruleSets);
222         if (ruleSets == null) {
223             return 0;
224         }
225 
226         Set<Language> languages = getApplicableLanguages(configuration, ruleSets);
227         List<DataSource> files = getApplicableFiles(configuration, languages);
228 
229         long reportStart = System.nanoTime();
230         try {
231             Renderer renderer = configuration.createRenderer();
232             List<Renderer> renderers = new LinkedList<>();
233             renderers.add(renderer);
234 
235             renderer.setWriter(IOUtil.createWriter(configuration.getReportFile()));
236             renderer.start();
237 
238             Benchmarker.mark(Benchmark.Reporting, System.nanoTime() - reportStart, 0);
239 
240             RuleContext ctx = new RuleContext();
241             final AtomicInteger violations = new AtomicInteger(0);
242             ctx.getReport().addListener(new ReportListener() {
243                 @Override
244                 public void ruleViolationAdded(RuleViolation ruleViolation) {
245                     violations.incrementAndGet();
246                 }
247                 @Override
248                 public void metricAdded(Metric metric) {
249                 }
250             });
251 
252             processFiles(configuration, ruleSetFactory, files, ctx, renderers);
253 
254             reportStart = System.nanoTime();
255             renderer.end();
256             renderer.flush();
257             return violations.get();
258         } catch (Exception e) {
259             String message = e.getMessage();
260             if (message != null) {
261                 LOG.severe(message);
262             } else {
263                 LOG.log(Level.SEVERE, "Exception during processing", e);
264             }
265             LOG.log(Level.FINE, "Exception during processing", e);
266             LOG.info(PMDCommandLineInterface.buildUsageText());
267             return 0;
268         } finally {
269             Benchmarker.mark(Benchmark.Reporting, System.nanoTime() - reportStart, 0);
270         }
271     }
272 
273     /**
274      * Creates a new rule context, initialized with a new, empty report.
275      *
276      * @param sourceCodeFilename the source code filename
277      * @param sourceCodeFile the source code file
278      * @return the rule context
279      */
280     public static RuleContext newRuleContext(String sourceCodeFilename, File sourceCodeFile) {
281 
282         RuleContext context = new RuleContext();
283         context.setSourceCodeFile(sourceCodeFile);
284         context.setSourceCodeFilename(sourceCodeFilename);
285         context.setReport(new Report());
286         return context;
287     }
288 
289     /**
290      * A callback that would be implemented by IDEs keeping track of PMD's
291      * progress as it evaluates a set of files.
292      * 
293      * @author Brian Remedios
294      */
295     public interface ProgressMonitor {
296         /**
297          * A status update reporting on current progress. Implementers will
298          * return true if it is to continue, false otherwise.
299          * 
300          * @param total total number of files to be analyzed
301          * @param totalDone number of files, that have been done analyzing.
302          * @return <code>true</code> if the execution of PMD should continue, <code>false</code> if the execution
303          * should be cancelled/terminated.
304          */
305         boolean status(int total, int totalDone);
306     }
307 
308     /**
309      * An entry point that would typically be used by IDEs intent on providing
310      * ongoing feedback and the ability to terminate it at will.
311      * 
312      * @param configuration the PMD configuration to use
313      * @param ruleSetFactory ruleset factory
314      * @param files the files to analyze
315      * @param ctx the rule context to use for the execution
316      * @param monitor PMD informs about the progress through this progress monitor. It provides also
317      * the ability to terminate/cancel the execution.
318      */
319     public static void processFiles(PMDConfiguration configuration, RuleSetFactory ruleSetFactory,
320             Collection<File> files, RuleContext ctx, ProgressMonitor monitor) {
321 
322         // TODO
323         // call the main processFiles with just the new monitor and a single
324         // logRenderer
325     }
326 
327     /**
328      * Run PMD on a list of files using multiple threads - if more than one is
329      * available
330      * 
331      * @param configuration
332      *            Configuration
333      * @param ruleSetFactory
334      *            RuleSetFactory
335      * @param files
336      *            List<DataSource>
337      * @param ctx
338      *            RuleContext
339      * @param renderers
340      *            List<Renderer>
341      */
342     public static void processFiles(final PMDConfiguration configuration, final RuleSetFactory ruleSetFactory,
343             final List<DataSource> files, final RuleContext ctx, final List<Renderer> renderers) {
344 
345         sortFiles(configuration, files);
346 
347         /*
348          * Check if multithreaded support is available. ExecutorService can also
349          * be disabled if threadCount is not positive, e.g. using the
350          * "-threads 0" command line option.
351          */
352         if (SystemUtils.MT_SUPPORTED && configuration.getThreads() > 0) {
353             new MultiThreadProcessor(configuration).processFiles(ruleSetFactory, files, ctx, renderers);
354         } else {
355             new MonoThreadProcessor(configuration).processFiles(ruleSetFactory, files, ctx, renderers);
356         }
357     }
358 
359     private static void sortFiles(final PMDConfiguration configuration, final List<DataSource> files) {
360         if (configuration.isStressTest()) {
361             // randomize processing order
362             Collections.shuffle(files);
363         } else {
364             final boolean useShortNames = configuration.isReportShortNames();
365             final String inputPaths = configuration.getInputPaths();
366             Collections.sort(files, new Comparator<DataSource>() {
367                 public int compare(DataSource left, DataSource right) {
368                     String leftString = left.getNiceFileName(useShortNames, inputPaths);
369                     String rightString = right.getNiceFileName(useShortNames, inputPaths);
370                     return leftString.compareTo(rightString);
371                 }
372             });
373         }
374     }
375 
376     /**
377      * Determines all the files, that should be analyzed by PMD.
378      * @param configuration contains either the file path or the DB URI, from where to load the files
379      * @param languages used to filter by file extension
380      * @return List<DataSource> of files
381      */
382     public static List<DataSource> getApplicableFiles(PMDConfiguration configuration, Set<Language> languages) {
383         long startFiles = System.nanoTime();
384         List<DataSource> files = internalGetApplicableFiles(configuration, languages);
385         long endFiles = System.nanoTime();
386         Benchmarker.mark(Benchmark.CollectFiles, endFiles - startFiles, 0);
387         return files;
388     }
389 
390     private static List<DataSource> internalGetApplicableFiles(PMDConfiguration configuration, Set<Language> languages) {
391         LanguageFilenameFilter fileSelector = new LanguageFilenameFilter(languages);
392         List<DataSource> files = new ArrayList<>();
393 
394         if (null != configuration.getInputPaths()) {
395             files.addAll(FileUtil.collectFiles(configuration.getInputPaths(), fileSelector));
396         }
397 
398         if (null != configuration.getInputUri()) {
399             String uriString = configuration.getInputUri();
400             try {
401                 List<DataSource> dataSources = getURIDataSources(uriString);
402 
403                 files.addAll(dataSources);
404             } catch (PMDException ex) {
405                 LOG.log(Level.SEVERE, "Problem with Input URI", ex);
406                 throw new RuntimeException("Problem with DBURI: " + uriString, ex);
407             }
408         }
409         return files;
410     }
411 
412     private static Set<Language> getApplicableLanguages(PMDConfiguration configuration, RuleSets ruleSets) {
413         Set<Language> languages = new HashSet<>();
414         LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer();
415 
416         for (Rule rule : ruleSets.getAllRules()) {
417             Language language = rule.getLanguage();
418             if (languages.contains(language)) {
419                 continue;
420             }
421             LanguageVersion version = discoverer.getDefaultLanguageVersion(language);
422             if (RuleSet.applies(rule, version)) {
423                 languages.add(language);
424                 if (LOG.isLoggable(Level.FINE)) {
425                     LOG.fine("Using " + language.getShortName() + " version: " + version.getShortName());
426                 }
427             }
428         }
429         return languages;
430     }
431 
432     /**
433      * Entry to invoke PMD as command line tool
434      * 
435      * @param args command line arguments
436      */
437     public static void main(String[] args) {
438         PMDCommandLineInterface.run(args);
439     }
440 
441     /**
442      * Parses the command line arguments and executes PMD.
443      * @param args command line arguments
444      * @return the exit code, where <code>0</code> means successful execution, <code>1</code> means error,
445      * <code>4</code> means there have been violations found.
446      */
447     public static int run(String[] args) {
448         int status = 0;
449         long start = System.nanoTime();
450         final PMDParameters params = PMDCommandLineInterface.extractParameters(new PMDParameters(), args, "pmd");
451         final PMDConfiguration configuration = PMDParameters.transformParametersIntoConfiguration(params);
452 
453         final Level logLevel = params.isDebug() ? Level.FINER : Level.INFO;
454         final Handler logHandler = new ConsoleLogHandler();
455         final ScopedLogHandlersManager logHandlerManager = new ScopedLogHandlersManager(logLevel, logHandler);
456         final Level oldLogLevel = LOG.getLevel();
457         LOG.setLevel(logLevel); // Need to do this, since the static logger has
458                                 // already been initialized at this point
459         try {
460             int violations = PMD.doPMD(configuration);
461             if (violations > 0 && configuration.isFailOnViolation()) {
462                 status = PMDCommandLineInterface.VIOLATIONS_FOUND;
463             } else {
464                 status = 0;
465             }
466         } catch (Exception e) {
467             System.out.println(PMDCommandLineInterface.buildUsageText());
468             System.out.println();
469             System.err.println(e.getMessage());
470             status = PMDCommandLineInterface.ERROR_STATUS;
471         } finally {
472             logHandlerManager.close();
473             LOG.setLevel(oldLogLevel);
474             if (params.isBenchmark()) {
475                 long end = System.nanoTime();
476                 Benchmarker.mark(Benchmark.TotalPMD, end - start, 0);
477 
478                 TextReport report = new TextReport(); // TODO get specified
479                                                       // report format from
480                                                       // config
481                 report.generate(Benchmarker.values(), System.err);
482             }
483         }
484         return status;
485     }
486 
487     /**
488      * Constant that contains always the current version of PMD.
489      */
490     public static final String VERSION;
491     /**
492      * Determines the version from maven's generated pom.properties file.
493      */
494     static {
495         String pmdVersion = null;
496         InputStream stream = PMD.class.getResourceAsStream("/META-INF/maven/net.sourceforge.pmd/pmd-core/pom.properties");
497         if (stream != null) {
498             try {
499                 Properties properties = new Properties();
500                 properties.load(stream);
501                 pmdVersion = properties.getProperty("version");
502             } catch (IOException e) {
503                 LOG.log(Level.FINE, "Couldn't determine version of PMD", e);
504             }
505         }
506         if (pmdVersion == null) {
507             pmdVersion = "unknown";
508         }
509         VERSION = pmdVersion;
510     }
511 }