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