/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; import net.sourceforge.pmd.benchmark.Benchmark; import net.sourceforge.pmd.benchmark.Benchmarker; import net.sourceforge.pmd.benchmark.TextReport; import net.sourceforge.pmd.cli.PMDCommandLineInterface; import net.sourceforge.pmd.cli.PMDParameters; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageFilenameFilter; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.LanguageVersionDiscoverer; import net.sourceforge.pmd.lang.LanguageVersionHandler; import net.sourceforge.pmd.lang.Parser; import net.sourceforge.pmd.lang.ParserOptions; import net.sourceforge.pmd.processor.MonoThreadProcessor; import net.sourceforge.pmd.processor.MultiThreadProcessor; import net.sourceforge.pmd.renderers.Renderer; import net.sourceforge.pmd.stat.Metric; import net.sourceforge.pmd.util.ClasspathClassLoader; import net.sourceforge.pmd.util.FileUtil; import net.sourceforge.pmd.util.IOUtil; import net.sourceforge.pmd.util.database.DBMSMetadata; import net.sourceforge.pmd.util.database.DBURI; import net.sourceforge.pmd.util.database.SourceObject; import net.sourceforge.pmd.util.datasource.DataSource; import net.sourceforge.pmd.util.datasource.ReaderDataSource; import net.sourceforge.pmd.util.log.ConsoleLogHandler; import net.sourceforge.pmd.util.log.ScopedLogHandlersManager; /** * This is the main class for interacting with PMD. The primary flow of all Rule * process is controlled via interactions with this class. A command line * interface is supported, as well as a programmatic API for integrating PMD * with other software such as IDEs and Ant. */ public class PMD { private static final Logger LOG = Logger.getLogger(PMD.class.getName()); /** * The line delimiter used by PMD in outputs. Usually the platform specific * line separator. */ public static final String EOL = System.getProperty("line.separator", "\n"); /** The default suppress marker string. */ public static final String SUPPRESS_MARKER = "NOPMD"; /** * Contains the configuration with which this PMD instance has been created. */ protected final PMDConfiguration configuration; private final SourceCodeProcessor rulesetsFileProcessor; /** * Constant that contains always the current version of PMD. */ public static final String VERSION; /** * Create a PMD instance using a default Configuration. Changes to the * configuration may be required. */ public PMD() { this(new PMDConfiguration()); } /** * Create a PMD instance using the specified Configuration. * * @param configuration * The runtime Configuration of PMD to use. */ public PMD(PMDConfiguration configuration) { this.configuration = configuration; this.rulesetsFileProcessor = new SourceCodeProcessor(configuration); } /** * Parses the given string as a database uri and returns a list of * datasources. * * @param uriString * the URI to parse * @return list of data sources * @throws PMDException * if the URI couldn't be parsed * @see DBURI */ public static List<DataSource> getURIDataSources(String uriString) throws PMDException { List<DataSource> dataSources = new ArrayList<>(); try { DBURI dbUri = new DBURI(uriString); DBMSMetadata dbmsMetadata = new DBMSMetadata(dbUri); LOG.log(Level.FINE, "DBMSMetadata retrieved"); List<SourceObject> sourceObjectList = dbmsMetadata.getSourceObjectList(); LOG.log(Level.FINE, "Located {0} database source objects", sourceObjectList.size()); for (SourceObject sourceObject : sourceObjectList) { String falseFilePath = sourceObject.getPseudoFileName(); LOG.log(Level.FINEST, "Adding database source object {0}", falseFilePath); try { dataSources.add(new ReaderDataSource(dbmsMetadata.getSourceCode(sourceObject), falseFilePath)); } catch (SQLException ex) { if (LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Cannot get SourceCode for " + falseFilePath + " - skipping ...", ex); } } } } catch (URISyntaxException e) { throw new PMDException("Cannot get DataSources from DBURI - \"" + uriString + "\"", e); } catch (SQLException e) { throw new PMDException( "Cannot get DataSources from DBURI, couldn't access the database - \"" + uriString + "\"", e); } catch (ClassNotFoundException e) { throw new PMDException( "Cannot get DataSources from DBURI, probably missing database jdbc driver - \"" + uriString + "\"", e); } catch (Exception e) { throw new PMDException("Encountered unexpected problem with URI \"" + uriString + "\"", e); } return dataSources; } /** * Helper method to get a configured parser for the requested language. The * parser is configured based on the given {@link PMDConfiguration}. * * @param languageVersion * the requested language * @param configuration * the given configuration * @return the pre-configured parser */ public static Parser parserFor(LanguageVersion languageVersion, PMDConfiguration configuration) { // TODO Handle Rules having different parser options. LanguageVersionHandler languageVersionHandler = languageVersion.getLanguageVersionHandler(); ParserOptions options = languageVersionHandler.getDefaultParserOptions(); if (configuration != null) { options.setSuppressMarker(configuration.getSuppressMarker()); } return languageVersionHandler.getParser(options); } /** * Create a report, filter out any defective rules, and keep a record of * them. * * @param rs * the rules * @param ctx * the rule context * @param fileName * the filename of the source file, which should appear in the * report * @return the Report */ public static Report setupReport(RuleSets rs, RuleContext ctx, String fileName) { Set<Rule> brokenRules = removeBrokenRules(rs); Report report = Report.createReport(ctx, fileName); for (Rule rule : brokenRules) { report.addConfigError(new Report.RuleConfigurationError(rule, rule.dysfunctionReason())); } return report; } /** * Remove and return the misconfigured rules from the rulesets and log them * for good measure. * * @param ruleSets * RuleSets * @return Set<Rule> */ private static Set<Rule> removeBrokenRules(RuleSets ruleSets) { Set<Rule> brokenRules = new HashSet<>(); ruleSets.removeDysfunctionalRules(brokenRules); for (Rule rule : brokenRules) { if (LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Removed misconfigured rule: " + rule.getName() + " cause: " + rule.dysfunctionReason()); } } return brokenRules; } /** * Get the runtime configuration. The configuration can be modified to * affect how PMD behaves. * * @return The configuration. * @see PMDConfiguration */ public PMDConfiguration getConfiguration() { return configuration; } /** * Gets the source code processor. * * @return SourceCodeProcessor */ public SourceCodeProcessor getSourceCodeProcessor() { return rulesetsFileProcessor; } /** * This method is the main entry point for command line usage. * * @param configuration * the configure to use * @return number of violations found. */ public static int doPMD(PMDConfiguration configuration) { // Load the RuleSets RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.getRulesetFactory(configuration); RuleSets ruleSets = RulesetsFactoryUtils.getRuleSetsWithBenchmark(configuration.getRuleSets(), ruleSetFactory); if (ruleSets == null) { return 0; } Set<Language> languages = getApplicableLanguages(configuration, ruleSets); List<DataSource> files = getApplicableFiles(configuration, languages); long reportStart = System.nanoTime(); try { Renderer renderer = configuration.createRenderer(); List<Renderer> renderers = Collections.singletonList(renderer); renderer.setWriter(IOUtil.createWriter(configuration.getReportFile())); renderer.start(); Benchmarker.mark(Benchmark.Reporting, System.nanoTime() - reportStart, 0); RuleContext ctx = new RuleContext(); final AtomicInteger violations = new AtomicInteger(0); ctx.getReport().addListener(new ThreadSafeReportListener() { @Override public void ruleViolationAdded(RuleViolation ruleViolation) { violations.incrementAndGet(); } @Override public void metricAdded(Metric metric) { } }); processFiles(configuration, ruleSetFactory, files, ctx, renderers); reportStart = System.nanoTime(); renderer.end(); renderer.flush(); return violations.get(); } catch (Exception e) { String message = e.getMessage(); if (message != null) { LOG.severe(message); } else { LOG.log(Level.SEVERE, "Exception during processing", e); } LOG.log(Level.FINE, "Exception during processing", e); LOG.info(PMDCommandLineInterface.buildUsageText()); return 0; } finally { Benchmarker.mark(Benchmark.Reporting, System.nanoTime() - reportStart, 0); /* * Make sure it's our own classloader before attempting to close it.... * Maven + Jacoco provide us with a cloaseable classloader that if closed * will throw a ClassNotFoundException. */ if (configuration.getClassLoader() instanceof ClasspathClassLoader) { IOUtil.tryCloseClassLoader(configuration.getClassLoader()); } } } /** * Creates a new rule context, initialized with a new, empty report. * * @param sourceCodeFilename * the source code filename * @param sourceCodeFile * the source code file * @return the rule context */ public static RuleContext newRuleContext(String sourceCodeFilename, File sourceCodeFile) { RuleContext context = new RuleContext(); context.setSourceCodeFile(sourceCodeFile); context.setSourceCodeFilename(sourceCodeFilename); context.setReport(new Report()); return context; } /** * A callback that would be implemented by IDEs keeping track of PMD's * progress as it evaluates a set of files. * * @author Brian Remedios */ public interface ProgressMonitor { /** * A status update reporting on current progress. Implementers will * return true if it is to continue, false otherwise. * * @param total * total number of files to be analyzed * @param totalDone * number of files, that have been done analyzing. * @return <code>true</code> if the execution of PMD should continue, * <code>false</code> if the execution should be * cancelled/terminated. */ boolean status(int total, int totalDone); } /** * An entry point that would typically be used by IDEs intent on providing * ongoing feedback and the ability to terminate it at will. * * @param configuration * the PMD configuration to use * @param ruleSetFactory * ruleset factory * @param files * the files to analyze * @param ctx * the rule context to use for the execution * @param monitor * PMD informs about the progress through this progress monitor. * It provides also the ability to terminate/cancel the * execution. */ public static void processFiles(PMDConfiguration configuration, RuleSetFactory ruleSetFactory, Collection<File> files, RuleContext ctx, ProgressMonitor monitor) { // TODO // call the main processFiles with just the new monitor and a single // logRenderer } /** * Run PMD on a list of files using multiple threads - if more than one is * available * * @param configuration * Configuration * @param ruleSetFactory * RuleSetFactory * @param files * List of {@link DataSource}s * @param ctx * RuleContext * @param renderers * List of {@link Renderer}s */ public static void processFiles(final PMDConfiguration configuration, final RuleSetFactory ruleSetFactory, final List<DataSource> files, final RuleContext ctx, final List<Renderer> renderers) { sortFiles(configuration, files); // Make sure the cache is listening for analysis results ctx.getReport().addListener(configuration.getAnalysisCache()); final RuleSetFactory silentFactoy = new RuleSetFactory(ruleSetFactory, false); /* * Check if multithreaded support is available. ExecutorService can also * be disabled if threadCount is not positive, e.g. using the * "-threads 0" command line option. */ if (configuration.getThreads() > 0) { new MultiThreadProcessor(configuration).processFiles(silentFactoy, files, ctx, renderers); } else { new MonoThreadProcessor(configuration).processFiles(silentFactoy, files, ctx, renderers); } // Persist the analysis cache configuration.getAnalysisCache().persist(); } private static void sortFiles(final PMDConfiguration configuration, final List<DataSource> files) { if (configuration.isStressTest()) { // randomize processing order Collections.shuffle(files); } else { final boolean useShortNames = configuration.isReportShortNames(); final String inputPaths = configuration.getInputPaths(); Collections.sort(files, new Comparator<DataSource>() { @Override public int compare(DataSource left, DataSource right) { String leftString = left.getNiceFileName(useShortNames, inputPaths); String rightString = right.getNiceFileName(useShortNames, inputPaths); return leftString.compareTo(rightString); } }); } } /** * Determines all the files, that should be analyzed by PMD. * * @param configuration * contains either the file path or the DB URI, from where to * load the files * @param languages * used to filter by file extension * @return List of {@link DataSource} of files */ public static List<DataSource> getApplicableFiles(PMDConfiguration configuration, Set<Language> languages) { long startFiles = System.nanoTime(); List<DataSource> files = internalGetApplicableFiles(configuration, languages); long endFiles = System.nanoTime(); Benchmarker.mark(Benchmark.CollectFiles, endFiles - startFiles, 0); return files; } private static List<DataSource> internalGetApplicableFiles(PMDConfiguration configuration, Set<Language> languages) { LanguageFilenameFilter fileSelector = new LanguageFilenameFilter(languages); List<DataSource> files = new ArrayList<>(); if (null != configuration.getInputPaths()) { files.addAll(FileUtil.collectFiles(configuration.getInputPaths(), fileSelector)); } if (null != configuration.getInputUri()) { String uriString = configuration.getInputUri(); try { List<DataSource> dataSources = getURIDataSources(uriString); files.addAll(dataSources); } catch (PMDException ex) { LOG.log(Level.SEVERE, "Problem with Input URI", ex); throw new RuntimeException("Problem with DBURI: " + uriString, ex); } } if (null != configuration.getInputFilePath()) { String inputFilePath = configuration.getInputFilePath(); File file = new File(inputFilePath); try { if (!file.exists()) { LOG.log(Level.SEVERE, "Problem with Input File Path", inputFilePath); throw new RuntimeException("Problem with Input File Path: " + inputFilePath); } else { String filePaths = FileUtil.readFilelist(new File(inputFilePath)); files.addAll(FileUtil.collectFiles(filePaths, fileSelector)); } } catch (IOException ex) { LOG.log(Level.SEVERE, "Problem with Input File", ex); throw new RuntimeException("Problem with Input File Path: " + inputFilePath, ex); } } return files; } private static Set<Language> getApplicableLanguages(PMDConfiguration configuration, RuleSets ruleSets) { Set<Language> languages = new HashSet<>(); LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer(); for (Rule rule : ruleSets.getAllRules()) { Language language = rule.getLanguage(); if (languages.contains(language)) { continue; } LanguageVersion version = discoverer.getDefaultLanguageVersion(language); if (RuleSet.applies(rule, version)) { languages.add(language); if (LOG.isLoggable(Level.FINE)) { LOG.fine("Using " + language.getShortName() + " version: " + version.getShortName()); } } } return languages; } /** * Entry to invoke PMD as command line tool * * @param args * command line arguments */ public static void main(String[] args) { PMDCommandLineInterface.run(args); } /** * Parses the command line arguments and executes PMD. * * @param args * command line arguments * @return the exit code, where <code>0</code> means successful execution, * <code>1</code> means error, <code>4</code> means there have been * violations found. */ public static int run(String[] args) { int status = 0; long start = System.nanoTime(); final PMDParameters params = PMDCommandLineInterface.extractParameters(new PMDParameters(), args, "pmd"); final PMDConfiguration configuration = PMDParameters.transformParametersIntoConfiguration(params); final Level logLevel = params.isDebug() ? Level.FINER : Level.INFO; final Handler logHandler = new ConsoleLogHandler(); final ScopedLogHandlersManager logHandlerManager = new ScopedLogHandlersManager(logLevel, logHandler); final Level oldLogLevel = LOG.getLevel(); // Need to do this, since the static logger has already been initialized // at this point LOG.setLevel(logLevel); try { int violations = PMD.doPMD(configuration); if (violations > 0 && configuration.isFailOnViolation()) { status = PMDCommandLineInterface.VIOLATIONS_FOUND; } else { status = 0; } } catch (Exception e) { System.out.println(PMDCommandLineInterface.buildUsageText()); System.out.println(); System.err.println(e.getMessage()); status = PMDCommandLineInterface.ERROR_STATUS; } finally { logHandlerManager.close(); LOG.setLevel(oldLogLevel); if (params.isBenchmark()) { long end = System.nanoTime(); Benchmarker.mark(Benchmark.TotalPMD, end - start, 0); // TODO get specified report format from config TextReport report = new TextReport(); report.generate(Benchmarker.values(), System.err); } } return status; } /** * Determines the version from maven's generated pom.properties file. */ static { String pmdVersion = null; InputStream stream = PMD.class .getResourceAsStream("/META-INF/maven/net.sourceforge.pmd/pmd-core/pom.properties"); if (stream != null) { try { Properties properties = new Properties(); properties.load(stream); pmdVersion = properties.getProperty("version"); } catch (IOException e) { LOG.log(Level.FINE, "Couldn't determine version of PMD", e); } finally { IOUtils.closeQuietly(stream); } } if (pmdVersion == null) { pmdVersion = "unknown"; } VERSION = pmdVersion; } }