diff --git a/.all-contributorsrc b/.all-contributorsrc index 531090e31d..7d94f872d8 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -6539,6 +6539,15 @@ "code" ] }, + { + "login": "filiprafalowicz", + "name": "filiprafalowicz", + "avatar_url": "https://avatars.githubusercontent.com/u/24355557?v=4", + "profile": "https://github.com/filiprafalowicz", + "contributions": [ + "code" + ] + }, { "login": "JerritEic", "name": "JerritEic", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87417153a5..b4b787a283 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: os: [ ubuntu-latest, windows-latest, macos-latest ] if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 2 - uses: actions/cache@v2 diff --git a/.github/workflows/git-repo-sync.yml b/.github/workflows/git-repo-sync.yml index 2219f2330a..250dd00933 100644 --- a/.github/workflows/git-repo-sync.yml +++ b/.github/workflows/git-repo-sync.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 100 - name: Setup Environment diff --git a/.github/workflows/troubleshooting.yml b/.github/workflows/troubleshooting.yml index d58e0f2aa1..4554ddedf7 100644 --- a/.github/workflows/troubleshooting.yml +++ b/.github/workflows/troubleshooting.yml @@ -12,7 +12,7 @@ jobs: os: [ ubuntu-latest ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/cache@v2 with: path: | diff --git a/docs/pages/pmd/projectdocs/credits.md b/docs/pages/pmd/projectdocs/credits.md index c2d45320d9..fdac14b9c1 100644 --- a/docs/pages/pmd/projectdocs/credits.md +++ b/docs/pages/pmd/projectdocs/credits.md @@ -775,163 +775,164 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
accounts) {
@@ -21,6 +22,7 @@ public class Foo {
No describer options using Schema class
1
+ 3
accounts) {
@@ -89,4 +91,54 @@ public class Foo {
]]>
+
+ False positive with no describer options on SObjectField
+ 0
+
+
+
+
+
+ False positive on SObjectField with FieldDescribeOptions.FULL_DESCRIBE
+ 0
+
+
+
+
+ False positive on SObjectField with FieldDescribeOptions.DEFAULT
+ 0
+
+
+
+
+ False positive on SObjectField with FieldDescribeOptions.DEFAULT with noDefault=true
+ true
+ 0
+
+
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java b/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java
index eb8fcb2381..9c1b261c88 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java
@@ -6,21 +6,14 @@ package net.sourceforge.pmd;
import static net.sourceforge.pmd.util.CollectionUtil.listOf;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
-import java.net.URISyntaxException;
-import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
+import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
-import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,8 +22,6 @@ import org.slf4j.event.Level;
import net.sourceforge.pmd.Report.GlobalReportBuilderListener;
import net.sourceforge.pmd.benchmark.TextTimingReportRenderer;
import net.sourceforge.pmd.benchmark.TimeTracker;
-import net.sourceforge.pmd.benchmark.TimedOperation;
-import net.sourceforge.pmd.benchmark.TimedOperationCategory;
import net.sourceforge.pmd.benchmark.TimingReport;
import net.sourceforge.pmd.benchmark.TimingReportRenderer;
import net.sourceforge.pmd.cache.NoopAnalysisCache;
@@ -38,38 +29,19 @@ import net.sourceforge.pmd.cli.PMDCommandLineInterface;
import net.sourceforge.pmd.cli.PmdParametersParseResult;
import net.sourceforge.pmd.cli.internal.CliMessages;
import net.sourceforge.pmd.internal.Slf4jSimpleConfiguration;
-import net.sourceforge.pmd.internal.util.AssertionUtil;
-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.processor.AbstractPMDProcessor;
import net.sourceforge.pmd.renderers.Renderer;
-import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
-import net.sourceforge.pmd.reporting.GlobalAnalysisListener.ViolationCounterListener;
-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.reporting.ReportStats;
+import net.sourceforge.pmd.reporting.ReportStatsListener;
import net.sourceforge.pmd.util.datasource.DataSource;
-import net.sourceforge.pmd.util.datasource.ReaderDataSource;
+import net.sourceforge.pmd.util.log.MessageReporter;
+import net.sourceforge.pmd.util.log.internal.SimpleMessageReporter;
/**
- * 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.
- *
- * Main entrypoints are:
- *
- * - {@link #main(String[])} which exits the java process
- * - {@link #runPmd(String...)} which returns a {@link StatusCode}
- * - {@link #runPmd(PMDConfiguration)}
- * - {@link #processFiles(PMDConfiguration, List, Collection, List)}
- *
- *
+ * Entry point for PMD's CLI. Use {@link #runPmd(PMDConfiguration)}
+ * or {@link #runPmd(String...)} to mimic a CLI run. This class is
+ * not a supported programmatic API anymore, use {@link PmdAnalysis}
+ * for fine control over the PMD integration and execution.
+ *
* Warning: This class is not intended to be instantiated or subclassed. It will
* be made final in PMD7.
*/
@@ -81,158 +53,60 @@ public final class PMD {
/**
* The line delimiter used by PMD in outputs. Usually the platform specific
* line separator.
+ *
+ * @deprecated Use {@link System#lineSeparator()}
*/
- public static final String EOL = System.getProperty("line.separator", "\n");
+ @Deprecated
+ public static final String EOL = System.lineSeparator();
- /** The default suppress marker string. */
- public static final String SUPPRESS_MARKER = "NOPMD";
+ /**
+ * The default suppress marker string.
+ *
+ * @deprecated Use {@link PMDConfiguration#DEFAULT_SUPPRESS_MARKER}
+ */
+ @Deprecated
+ public static final String SUPPRESS_MARKER = PMDConfiguration.DEFAULT_SUPPRESS_MARKER;
private PMD() {
}
- /**
- * 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 IOException if the URI couldn't be parsed
- * @see DBURI
- *
- * @deprecated Will be hidden as part of the parsing of {@link PMD#getApplicableFiles(PMDConfiguration, Set)}
- */
- @Deprecated
- public static List getURIDataSources(String uriString) throws IOException {
- List dataSources = new ArrayList<>();
+ private static ReportStats runAndReturnStats(PmdAnalysis pmd) {
+ if (pmd.getRulesets().isEmpty()) {
+ return ReportStats.empty();
+ }
+
+ @SuppressWarnings("PMD.CloseResource")
+ ReportStatsListener listener = new ReportStatsListener();
+
+ pmd.addListener(listener);
try {
- DBURI dbUri = new DBURI(uriString);
- DBMSMetadata dbmsMetadata = new DBMSMetadata(dbUri);
- log.debug("DBMSMetadata retrieved");
- List sourceObjectList = dbmsMetadata.getSourceObjectList();
- log.debug("Located {} database source objects", sourceObjectList.size());
- for (SourceObject sourceObject : sourceObjectList) {
- String falseFilePath = sourceObject.getPseudoFileName();
- log.trace("Adding database source object {}", falseFilePath);
-
- try {
- dataSources.add(new ReaderDataSource(dbmsMetadata.getSourceCode(sourceObject), falseFilePath));
- } catch (SQLException ex) {
- log.warn("Cannot get SourceCode for {} - skipping ...", falseFilePath, ex);
- }
- }
- } catch (URISyntaxException e) {
- throw new IOException("Cannot get DataSources from DBURI - \"" + uriString + "\"", e);
- } catch (SQLException e) {
- throw new IOException("Cannot get DataSources from DBURI, couldn't access the database - \"" + uriString + "\"", e);
- } catch (ClassNotFoundException e) {
- throw new IOException("Cannot get DataSources from DBURI, probably missing database jdbc driver - \"" + uriString + "\"", e);
+ pmd.performAnalysis();
} catch (Exception e) {
- throw new IOException("Encountered unexpected problem with URI \"" + uriString + "\"", e);
+ pmd.getReporter().errorEx("Exception during processing", e);
+ ReportStats stats = listener.getResult();
+ printErrorDetected(1 + stats.getNumErrors());
+ return stats; // should have been closed
}
- return dataSources;
- }
+ ReportStats stats = listener.getResult();
- /**
- * This method is the main entry point for command line usage.
- *
- * @param configuration the configuration to use
- *
- * @return number of violations found.
- *
- * @deprecated Use {@link #runPmd(PMDConfiguration)}.
- */
- @Deprecated
- public static int doPMD(final PMDConfiguration configuration) {
-
- // Load the RuleSets
- final RuleSetLoader ruleSetFactory = RuleSetLoader.fromPmdConfig(configuration);
- final List ruleSets = getRuleSetsWithBenchmark(configuration.getRuleSetPaths(), ruleSetFactory);
-
- final Set languages = getApplicableLanguages(configuration, ruleSets);
-
- try {
-
- final List files = getApplicableFiles(configuration, languages);
- Renderer renderer = configuration.createRenderer(true);
-
- @SuppressWarnings("PMD.CloseResource")
- ViolationCounterListener violationCounter = new ViolationCounterListener();
- @SuppressWarnings("PMD.CloseResource")
- GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener();
-
- List allListeners = listOf(
- reportBuilder,
- renderer.newListener(),
- violationCounter);
- try (GlobalAnalysisListener listener = GlobalAnalysisListener.tee(allListeners)) {
- try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) {
- processFiles(configuration, ruleSets, files, listener);
- }
- }
-
- Report report = reportBuilder.getResult();
- if (!report.getProcessingErrors().isEmpty()) {
- printErrorDetected(report.getProcessingErrors().size());
- }
-
- return violationCounter.getResult();
- } catch (final Exception e) {
- log.error("Exception during processing: {}", e.toString()); // only exception without stacktrace
- log.debug("Exception during processing", e); // with stacktrace
- printErrorDetected(1);
- return PMDCommandLineInterface.NO_ERRORS_STATUS; // fixme?
- } finally {
- /*
- * 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());
- }
+ if (stats.getNumErrors() > 0) {
+ printErrorDetected(stats.getNumErrors());
}
+
+ return stats;
}
- private static List getRuleSetsWithBenchmark(List rulesetPaths, RuleSetLoader factory) {
- try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.LOAD_RULES)) {
- List ruleSets;
- try {
- ruleSets = factory.loadFromResources(rulesetPaths);
- printRuleNamesInDebug(ruleSets);
- if (isEmpty(ruleSets)) {
- String msg = "No rules found. Maybe you misspelled a rule name? ("
- + String.join(",", rulesetPaths) + ')';
- log.error(msg);
- throw new IllegalArgumentException(msg);
- }
- } catch (RuleSetLoadException rsnfe) {
- log.error("Ruleset not found", rsnfe);
- throw rsnfe;
- }
- return ruleSets;
- }
- }
- private static boolean isEmpty(List rsets) {
- return rsets.stream().noneMatch(it -> it.size() > 0);
- }
-
- /**
- * If in debug modus, print the names of the rules.
- *
- * @param rulesets the RuleSets to print
- */
- private static void printRuleNamesInDebug(List rulesets) {
- if (log.isDebugEnabled()) {
- for (RuleSet rset : rulesets) {
- for (Rule r : rset.getRules()) {
- log.debug("Loaded rule {}", r.getName());
- }
- }
+ static void encourageToUseIncrementalAnalysis(final PMDConfiguration configuration) {
+ if (!configuration.isIgnoreIncrementalAnalysis()
+ && configuration.getAnalysisCache() instanceof NoopAnalysisCache
+ && log.isWarnEnabled()) {
+ final String version =
+ PMDVersion.isUnknown() || PMDVersion.isSnapshot() ? "latest" : "pmd-" + PMDVersion.VERSION;
+ log.warn("This analysis could be faster, please consider using Incremental Analysis: "
+ + "https://pmd.github.io/{}/pmd_userdocs_incremental_analysis.html", version);
}
}
@@ -249,227 +123,25 @@ public final class PMD {
* @return Report in which violations are accumulated
*
* @throws Exception If there was a problem when opening or closing the renderers
+ *
+ * @deprecated Use {@link PmdAnalysis}
*/
- @SuppressWarnings("PMD.CloseResource")
+ @Deprecated
public static Report processFiles(PMDConfiguration configuration,
List ruleSets,
Collection extends DataSource> files,
List renderers) throws Exception {
-
- GlobalAnalysisListener rendererListeners = createComposedRendererListener(renderers);
- GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener();
-
- List allListeners = listOf(reportBuilder, rendererListeners);
-
- try (GlobalAnalysisListener listener = GlobalAnalysisListener.tee(allListeners)) {
- processFiles(configuration, ruleSets, new ArrayList<>(files), listener);
+ try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) {
+ pmd.addRuleSets(ruleSets);
+ pmd.addRenderers(renderers);
+ @SuppressWarnings("PMD.CloseResource")
+ GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener();
+ List sortedFiles = new ArrayList<>(files);
+ sortedFiles.sort(Comparator.comparing(ds -> ds.getNiceFileName(false, "")));
+ pmd.performAnalysisImpl(listOf(reportBuilder), sortedFiles);
+ return reportBuilder.getResult();
}
-
- return reportBuilder.getResult();
- }
-
- private static GlobalAnalysisListener createComposedRendererListener(List renderers) throws Exception {
- if (renderers.isEmpty()) {
- return GlobalAnalysisListener.noop();
- }
-
- List rendererListeners = new ArrayList<>(renderers.size());
- for (Renderer renderer : renderers) {
- try {
- rendererListeners.add(renderer.newListener());
- } catch (IOException ioe) {
- // close listeners so far, throw their close exception or the ioe
- IOUtil.ensureClosed(rendererListeners, ioe);
- throw AssertionUtil.shouldNotReachHere("ensureClosed should have thrown");
- }
- }
- return GlobalAnalysisListener.tee(rendererListeners);
- }
-
-
- /**
- * Run PMD using the given configuration. This replaces the other overload.
- *
- * @param configuration Configuration for the run. Note that the files,
- * and rulesets, are ignored, as they are supplied
- * as parameters
- * @param ruleSets Parsed rulesets
- * @param files Files to process, will be closed by this method.
- * @param listener Listener to which analysis events are forwarded.
- * The listener is NOT closed by this routine and should
- * be closed by the caller.
- *
- * @throws Exception If there was a problem when opening or closing the renderers
- */
- public static void processFiles(PMDConfiguration configuration,
- List ruleSets,
- List files,
- GlobalAnalysisListener listener) throws Exception {
-
- final RuleSets rs = new RuleSets(ruleSets);
-
- // todo Just like we throw for invalid properties, "broken rules"
- // shouldn't be a "config error". This is the only instance of
- // config errors...
-
- for (final Rule rule : removeBrokenRules(rs)) {
- listener.onConfigError(new Report.ConfigurationError(rule, rule.dysfunctionReason()));
- }
-
- encourageToUseIncrementalAnalysis(configuration);
-
- List sortedFiles = sortFiles(configuration, files);
-
- // Make sure the cache is listening for analysis results
- listener = GlobalAnalysisListener.tee(listOf(listener, configuration.getAnalysisCache()));
-
- configuration.getAnalysisCache().checkValidity(rs, configuration.getClassLoader());
-
- Exception ex = null;
- try (AbstractPMDProcessor processor = AbstractPMDProcessor.newFileProcessor(configuration)) {
- processor.processFiles(rs, sortedFiles, listener);
- } catch (Exception e) {
- ex = e;
- } finally {
- configuration.getAnalysisCache().persist();
- IOUtil.ensureClosed(sortedFiles, ex);
- }
- }
-
-
- /**
- * Remove and return the misconfigured rules from the rulesets and log them
- * for good measure.
- *
- * @param ruleSets RuleSets to prune of broken rules.
- *
- * @return Set
- */
- private static Set removeBrokenRules(final RuleSets ruleSets) {
- final Set brokenRules = new HashSet<>();
- ruleSets.removeDysfunctionalRules(brokenRules);
-
- for (final Rule rule : brokenRules) {
- log.warn("Removed misconfigured rule: {} cause: {}", rule.getName(), rule.dysfunctionReason());
- }
-
- return brokenRules;
- }
-
-
- private static List sortFiles(final PMDConfiguration configuration, Collection extends DataSource> files) {
- // the input collection may be unmodifiable
- List result = new ArrayList<>(files);
-
- if (configuration.isStressTest()) {
- // randomize processing order
- Collections.shuffle(result);
- } else {
- final boolean useShortNames = configuration.isReportShortNames();
- final String inputPaths = configuration.getInputPaths();
- result.sort((left, right) -> {
- String leftString = left.getNiceFileName(useShortNames, inputPaths);
- String rightString = right.getNiceFileName(useShortNames, inputPaths);
- return leftString.compareTo(rightString);
- });
- }
-
- return result;
- }
-
- private static void encourageToUseIncrementalAnalysis(final PMDConfiguration configuration) {
- if (!configuration.isIgnoreIncrementalAnalysis()
- && configuration.getAnalysisCache() instanceof NoopAnalysisCache
- && log.isWarnEnabled()) {
- final String version =
- PMDVersion.isUnknown() || PMDVersion.isSnapshot() ? "latest" : "pmd-" + PMDVersion.VERSION;
- log.warn("This analysis could be faster, please consider using Incremental Analysis: "
- + "https://pmd.github.io/{}/pmd_userdocs_incremental_analysis.html", version);
- }
- }
-
- /**
- * 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 getApplicableFiles(PMDConfiguration configuration, Set languages) throws IOException {
- try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.COLLECT_FILES)) {
- return internalGetApplicableFiles(configuration, languages);
- }
- }
-
- private static List internalGetApplicableFiles(PMDConfiguration configuration,
- Set languages) throws IOException {
- FilenameFilter fileSelector = configuration.isForceLanguageVersion() ? new AcceptAllFilenames() : new LanguageFilenameFilter(languages);
- List files = new ArrayList<>();
-
- if (null != configuration.getInputPaths()) {
- files.addAll(FileUtil.collectFiles(configuration.getInputPaths(), fileSelector));
- }
-
- if (null != configuration.getInputUri()) {
- String uriString = configuration.getInputUri();
- files.addAll(getURIDataSources(uriString));
- }
-
- if (null != configuration.getInputFilePath()) {
- String inputFilePath = configuration.getInputFilePath();
- File file = new File(inputFilePath);
- if (!file.exists()) {
- throw new FileNotFoundException(inputFilePath);
- }
-
- try {
- String filePaths = FileUtil.readFilelist(file);
- files.addAll(FileUtil.collectFiles(filePaths, fileSelector));
- } catch (IOException ex) {
- throw new IOException("Problem with Input File Path: " + inputFilePath, ex);
- }
-
- }
-
- if (null != configuration.getIgnoreFilePath()) {
- String ignoreFilePath = configuration.getIgnoreFilePath();
- File file = new File(ignoreFilePath);
- if (!file.exists()) {
- throw new FileNotFoundException(ignoreFilePath);
- }
-
- try {
- String filePaths = FileUtil.readFilelist(file);
- files.removeAll(FileUtil.collectFiles(filePaths, fileSelector));
- } catch (IOException ex) {
- log.error("Problem with Ignore File", ex);
- throw new RuntimeException("Problem with Ignore File Path: " + ignoreFilePath, ex);
- }
- }
- return files;
- }
-
- private static Set getApplicableLanguages(final PMDConfiguration configuration, final List ruleSets) {
- final Set languages = new HashSet<>();
- final LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer();
-
- for (final RuleSet ruleSet : ruleSets) {
- for (Rule rule : ruleSet.getRules()) {
- final Language ruleLanguage = rule.getLanguage();
- if (!languages.contains(ruleLanguage)) {
- final LanguageVersion version = discoverer.getDefaultLanguageVersion(ruleLanguage);
- if (RuleSet.applies(rule, version)) {
- languages.add(ruleLanguage);
- log.debug("Using {} version: {}", ruleLanguage.getShortName(), version.getShortName());
- }
- }
- }
- }
- return languages;
}
/**
@@ -483,23 +155,6 @@ public final class PMD {
PMDCommandLineInterface.setStatusCodeOrExit(exitCode.toInt());
}
- /**
- * Parses the command line arguments and executes PMD. Returns the
- * exit code without exiting the VM.
- *
- * @param args command line arguments
- *
- * @return the exit code, where 0
means successful execution,
- * 1
means error, 4
means there have been
- * violations found.
- *
- * @deprecated Use {@link #runPmd(String...)}.
- */
- @Deprecated
- public static int run(final String[] args) {
- return runPmd(args).toInt();
- }
-
/**
* Parses the command line arguments and executes PMD. Returns the
* status code without exiting the VM. Note that the arguments parsing
@@ -515,6 +170,7 @@ public final class PMD {
public static StatusCode runPmd(String... args) {
PmdParametersParseResult parseResult = PmdParametersParseResult.extractParameters(args);
+ // todo these warnings/errors should be output on a PmdRenderer
if (!parseResult.getDeprecatedOptionsUsed().isEmpty()) {
Entry first = parseResult.getDeprecatedOptionsUsed().entrySet().iterator().next();
log.warn("Some deprecated options were used on the command-line, including {}", first.getKey());
@@ -560,44 +216,68 @@ public final class PMD {
// only reconfigure logging, if debug flag was used on command line
// otherwise just use whatever is in conf/simplelogger.properties which happens automatically
if (configuration.isDebug()) {
- Slf4jSimpleConfiguration.reconfigureDefaultLogLevel(Level.DEBUG);
+ Slf4jSimpleConfiguration.reconfigureDefaultLogLevel(Level.TRACE);
// need to reload the logger with the new configuration
log = LoggerFactory.getLogger(PMD.class);
}
+ // create a top-level reporter
+ // TODO CLI errors should also be reported through this
+ // TODO this should not use the logger as backend, otherwise without
+ // slf4j implementation binding, errors are entirely ignored.
+ MessageReporter pmdReporter = new SimpleMessageReporter(log);
// always install java.util.logging to slf4j bridge
Slf4jSimpleConfiguration.installJulBridge();
// logging, mostly for testing purposes
Level defaultLogLevel = Slf4jSimpleConfiguration.getDefaultLogLevel();
- log.atLevel(defaultLogLevel).log("Log level is at {}", defaultLogLevel);
+ log.info("Log level is at {}", defaultLogLevel);
- StatusCode status;
try {
- int violations = PMD.doPMD(configuration);
- if (violations > 0 && configuration.isFailOnViolation()) {
- status = StatusCode.VIOLATIONS_FOUND;
- } else {
- status = StatusCode.OK;
+ PmdAnalysis pmd;
+ try {
+ pmd = PmdAnalysis.create(configuration, pmdReporter);
+ } catch (Exception e) {
+ pmdReporter.errorEx("Could not initialize analysis", e);
+ return StatusCode.ERROR;
}
- } catch (Exception e) {
- System.err.println(e.getMessage());
- status = StatusCode.ERROR;
- } finally {
- if (configuration.isBenchmark()) {
- final TimingReport timingReport = TimeTracker.stopGlobalTracking();
-
- // TODO get specified report format from config
- final TimingReportRenderer renderer = new TextTimingReportRenderer();
- try {
- // Don't close this writer, we don't want to close stderr
- @SuppressWarnings("PMD.CloseResource")
- final Writer writer = new OutputStreamWriter(System.err);
- renderer.render(timingReport, writer);
- } catch (final IOException e) {
- System.err.println(e.getMessage());
+ try {
+ ReportStats stats;
+ stats = PMD.runAndReturnStats(pmd);
+ if (pmdReporter.numErrors() > 0) {
+ // processing errors are ignored
+ return StatusCode.ERROR;
+ } else if (stats.getNumViolations() > 0 && configuration.isFailOnViolation()) {
+ return StatusCode.VIOLATIONS_FOUND;
+ } else {
+ return StatusCode.OK;
}
+ } finally {
+ pmd.close();
+ }
+
+ } catch (Exception e) {
+ pmdReporter.errorEx("Exception while running PMD.", e);
+ printErrorDetected(1);
+ return StatusCode.ERROR;
+ } finally {
+ finishBenchmarker(configuration);
+ }
+ }
+
+ private static void finishBenchmarker(PMDConfiguration configuration) {
+ if (configuration.isBenchmark()) {
+ final TimingReport timingReport = TimeTracker.stopGlobalTracking();
+
+ // TODO get specified report format from config
+ final TimingReportRenderer renderer = new TextTimingReportRenderer();
+ try {
+ // Don't close this writer, we don't want to close stderr
+ @SuppressWarnings("PMD.CloseResource")
+ final Writer writer = new OutputStreamWriter(System.err);
+ renderer.render(timingReport, writer);
+ } catch (final IOException e) {
+ System.err.println(e.getMessage());
}
}
- return status;
}
/**
@@ -633,10 +313,4 @@ public final class PMD {
}
- private static final class AcceptAllFilenames implements FilenameFilter {
- @Override
- public boolean accept(File dir, String name) {
- return true;
- }
- }
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java b/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java
index 1b9931f537..bc6e618aa2 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java
@@ -13,12 +13,14 @@ import java.util.Objects;
import java.util.Properties;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import net.sourceforge.pmd.annotation.DeprecatedUntil700;
import net.sourceforge.pmd.cache.AnalysisCache;
import net.sourceforge.pmd.cache.FileAnalysisCache;
import net.sourceforge.pmd.cache.NoopAnalysisCache;
import net.sourceforge.pmd.cli.PmdParametersParseResult;
+import net.sourceforge.pmd.internal.util.AssertionUtil;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
@@ -44,7 +46,7 @@ import net.sourceforge.pmd.util.ClasspathClassLoader;
* {@link #getClassLoader()}
* A means to configure a ClassLoader using a prepended classpath String,
* instead of directly setting it programmatically.
- * {@link #prependClasspath(String)}
+ * {@link #prependAuxClasspath(String)}
* A LanguageVersionDiscoverer instance, which defaults to using the default
* LanguageVersion of each Language. Means are provided to change the
* LanguageVersion for each Language.
@@ -88,15 +90,19 @@ import net.sourceforge.pmd.util.ClasspathClassLoader;
*
*/
public class PMDConfiguration extends AbstractConfiguration {
+
+ /** The default suppress marker string. */
+ public static final String DEFAULT_SUPPRESS_MARKER = "NOPMD";
+
// General behavior options
- private String suppressMarker = PMD.SUPPRESS_MARKER;
+ private String suppressMarker = DEFAULT_SUPPRESS_MARKER;
private int threads = Runtime.getRuntime().availableProcessors();
private ClassLoader classLoader = getClass().getClassLoader();
private LanguageVersionDiscoverer languageVersionDiscoverer = new LanguageVersionDiscoverer();
private LanguageVersion forceLanguageVersion;
// Rule and source file options
- private List ruleSets;
+ private List ruleSets = new ArrayList<>();
private RulePriority minimumPriority = RulePriority.LOW;
private String inputPaths;
private String inputUri;
@@ -197,13 +203,46 @@ public class PMDConfiguration extends AbstractConfiguration {
* if the given classpath is invalid (e.g. does not exist)
* @see PMDConfiguration#setClassLoader(ClassLoader)
* @see ClasspathClassLoader
+ *
+ * @deprecated Use {@link #prependAuxClasspath(String)}, which doesn't
+ * throw a checked {@link IOException}
*/
+ @Deprecated
public void prependClasspath(String classpath) throws IOException {
- if (classLoader == null) {
- classLoader = PMDConfiguration.class.getClassLoader();
+ try {
+ prependAuxClasspath(classpath);
+ } catch (IllegalArgumentException e) {
+ throw new IOException(e);
}
- if (classpath != null) {
- classLoader = new ClasspathClassLoader(classpath, classLoader);
+ }
+
+ /**
+ * Prepend the specified classpath like string to the current ClassLoader of
+ * the configuration. If no ClassLoader is currently configured, the
+ * ClassLoader used to load the {@link PMDConfiguration} class will be used
+ * as the parent ClassLoader of the created ClassLoader.
+ *
+ * If the classpath String looks like a URL to a file (i.e. starts with
+ * file://
) the file will be read with each line representing
+ * an entry on the classpath.
+ *
+ * @param classpath The prepended classpath.
+ *
+ * @throws IllegalArgumentException if the given classpath is invalid (e.g. does not exist)
+ * @see PMDConfiguration#setClassLoader(ClassLoader)
+ */
+ public void prependAuxClasspath(String classpath) {
+ try {
+ if (classLoader == null) {
+ classLoader = PMDConfiguration.class.getClassLoader();
+ }
+ if (classpath != null) {
+ classLoader = new ClasspathClassLoader(classpath, classLoader);
+ }
+ } catch (IOException e) {
+ // Note: IOExceptions shouldn't appear anymore, they should already be converted
+ // to IllegalArgumentException in ClasspathClassLoader.
+ throw new IllegalArgumentException(e);
}
}
@@ -244,6 +283,7 @@ public class PMDConfiguration extends AbstractConfiguration {
*/
public void setForceLanguageVersion(LanguageVersion forceLanguageVersion) {
this.forceLanguageVersion = forceLanguageVersion;
+ languageVersionDiscoverer.setForcedVersion(forceLanguageVersion);
}
/**
@@ -253,6 +293,7 @@ public class PMDConfiguration extends AbstractConfiguration {
* the LanguageVersion
*/
public void setDefaultLanguageVersion(LanguageVersion languageVersion) {
+ Objects.requireNonNull(languageVersion);
setDefaultLanguageVersions(Arrays.asList(languageVersion));
}
@@ -265,6 +306,7 @@ public class PMDConfiguration extends AbstractConfiguration {
*/
public void setDefaultLanguageVersions(List languageVersions) {
for (LanguageVersion languageVersion : languageVersions) {
+ Objects.requireNonNull(languageVersion);
languageVersionDiscoverer.setDefaultLanguageVersion(languageVersion);
}
}
@@ -310,7 +352,10 @@ public class PMDConfiguration extends AbstractConfiguration {
*/
@Deprecated
@DeprecatedUntil700
- public String getRuleSets() {
+ public @Nullable String getRuleSets() {
+ if (ruleSets.isEmpty()) {
+ return null;
+ }
return String.join(",", ruleSets);
}
@@ -319,17 +364,34 @@ public class PMDConfiguration extends AbstractConfiguration {
*
* @see RuleSetLoader#loadFromResource(String)
*/
- public List getRuleSetPaths() {
+ public @NonNull List<@NonNull String> getRuleSetPaths() {
return ruleSets;
}
/**
- * Sets the rulesets.
+ * Sets the list of ruleset paths to load when starting the analysis.
+ *
+ * @param ruleSetPaths A list of ruleset paths, understandable by {@link RuleSetLoader#loadFromResource(String)}.
*
* @throws NullPointerException If the parameter is null
*/
- public void setRuleSets(@NonNull List ruleSets) {
- this.ruleSets = new ArrayList<>(ruleSets);
+ public void setRuleSets(@NonNull List<@NonNull String> ruleSetPaths) {
+ AssertionUtil.requireParamNotNull("ruleSetPaths", ruleSetPaths);
+ AssertionUtil.requireContainsNoNullValue("ruleSetPaths", ruleSetPaths);
+ this.ruleSets = new ArrayList<>(ruleSetPaths);
+ }
+
+ /**
+ * Add a new ruleset paths to load when starting the analysis.
+ * This list is initially empty.
+ *
+ * @param rulesetPath A ruleset path, understandable by {@link RuleSetLoader#loadFromResource(String)}.
+ *
+ * @throws NullPointerException If the parameter is null
+ */
+ public void addRuleSet(@NonNull String rulesetPath) {
+ AssertionUtil.requireParamNotNull("rulesetPath", rulesetPath);
+ this.ruleSets.add(rulesetPath);
}
/**
@@ -337,12 +399,16 @@ public class PMDConfiguration extends AbstractConfiguration {
*
* @param ruleSets the rulesets to set
*
- * @deprecated Use {@link #setRuleSets(List)}
+ * @deprecated Use {@link #setRuleSets(List)} or {@link #addRuleSet(String)}.
*/
@Deprecated
@DeprecatedUntil700
- public void setRuleSets(String ruleSets) {
- this.ruleSets = Arrays.asList(ruleSets.split(","));
+ public void setRuleSets(@Nullable String ruleSets) {
+ if (ruleSets == null) {
+ this.ruleSets = new ArrayList<>();
+ } else {
+ this.ruleSets = new ArrayList<>(Arrays.asList(ruleSets.split(",")));
+ }
}
/**
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PmdAnalysis.java b/pmd-core/src/main/java/net/sourceforge/pmd/PmdAnalysis.java
new file mode 100644
index 0000000000..1b74fa5ab8
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/PmdAnalysis.java
@@ -0,0 +1,403 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd;
+
+import static net.sourceforge.pmd.util.CollectionUtil.listOf;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sourceforge.pmd.Report.GlobalReportBuilderListener;
+import net.sourceforge.pmd.annotation.InternalApi;
+import net.sourceforge.pmd.benchmark.TimeTracker;
+import net.sourceforge.pmd.benchmark.TimedOperation;
+import net.sourceforge.pmd.benchmark.TimedOperationCategory;
+import net.sourceforge.pmd.cache.AnalysisCacheListener;
+import net.sourceforge.pmd.internal.util.AssertionUtil;
+import net.sourceforge.pmd.internal.util.FileCollectionUtil;
+import net.sourceforge.pmd.lang.Language;
+import net.sourceforge.pmd.lang.LanguageVersion;
+import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
+import net.sourceforge.pmd.lang.document.FileCollector;
+import net.sourceforge.pmd.processor.AbstractPMDProcessor;
+import net.sourceforge.pmd.renderers.Renderer;
+import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
+import net.sourceforge.pmd.util.ClasspathClassLoader;
+import net.sourceforge.pmd.util.IOUtil;
+import net.sourceforge.pmd.util.datasource.DataSource;
+import net.sourceforge.pmd.util.log.MessageReporter;
+import net.sourceforge.pmd.util.log.internal.SimpleMessageReporter;
+
+/**
+ * Main programmatic API of PMD. Create and configure a {@link PMDConfiguration},
+ * then use {@link #create(PMDConfiguration)} to obtain an instance.
+ * You can perform additional configuration on the instance, eg adding
+ * files to process, or additional rulesets and renderers. Then, call
+ * {@link #performAnalysis()}. Example:
+ * {@code
+ * PMDConfiguration config = new PMDConfiguration();
+ * config.setDefaultLanguageVersion(LanguageRegistry.findLanguageByTerseName("java").getVersion("11"));
+ * config.setInputPaths("src/main/java");
+ * config.prependClasspath("target/classes");
+ * config.setMinimumPriority(RulePriority.HIGH);
+ * config.addRuleSet("rulesets/java/quickstart.xml");
+ * config.setReportFormat("xml");
+ * config.setReportFile("target/pmd-report.xml");
+ *
+ * try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
+ * // note: don't use `config` once a PmdAnalysis has been created.
+ * // optional: add more rulesets
+ * pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml"));
+ * // optional: add more files
+ * pmd.files().addFile(Paths.get("src", "main", "more-java", "ExtraSource.java"));
+ * // optional: add more renderers
+ * pmd.addRenderer(renderer);
+ *
+ * pmd.performAnalysis();
+ * }
+ * }
+ *
+ */
+public final class PmdAnalysis implements AutoCloseable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PmdAnalysis.class);
+
+ private final FileCollector collector;
+ private final List renderers = new ArrayList<>();
+ private final List listeners = new ArrayList<>();
+ private final List ruleSets = new ArrayList<>();
+ private final PMDConfiguration configuration;
+ private final MessageReporter reporter;
+
+ private boolean closed;
+
+ /**
+ * Constructs a new instance. The files paths (input files, filelist,
+ * exclude list, etc) given in the configuration are collected into
+ * the file collector ({@link #files()}), but more can be added
+ * programmatically using the file collector.
+ */
+ private PmdAnalysis(PMDConfiguration config, MessageReporter reporter) {
+ this.configuration = config;
+ this.reporter = reporter;
+ this.collector = FileCollector.newCollector(
+ config.getLanguageVersionDiscoverer(),
+ reporter
+ );
+ }
+
+ /**
+ * Constructs a new instance from a configuration.
+ *
+ *
+ * - The files paths (input files, filelist,
+ * exclude list, etc) are explored and the files to analyse are
+ * collected into the file collector ({@link #files()}).
+ * More can be added programmatically using the file collector.
+ *
- The rulesets given in the configuration are loaded ({@link PMDConfiguration#getRuleSets()})
+ *
- A renderer corresponding to the parameters of the configuration
+ * is created and added (but not started).
+ *
+ */
+ public static PmdAnalysis create(PMDConfiguration config) {
+ return create(
+ config,
+ new SimpleMessageReporter(LoggerFactory.getLogger(PmdAnalysis.class))
+ );
+ }
+
+ @InternalApi
+ static PmdAnalysis create(PMDConfiguration config, MessageReporter reporter) {
+ PmdAnalysis pmd = new PmdAnalysis(config, reporter);
+
+ // note: do not filter files by language
+ // they could be ignored later. The problem is if you call
+ // addRuleSet later, then you could be enabling new languages
+ // So the files should not be pruned in advance
+ FileCollectionUtil.collectFiles(config, pmd.files());
+
+ if (config.getReportFormat() != null) {
+ Renderer renderer = config.createRenderer(true);
+ pmd.addRenderer(renderer);
+ }
+
+ if (!config.getRuleSetPaths().isEmpty()) {
+ final RuleSetLoader ruleSetLoader = pmd.newRuleSetLoader();
+ final List ruleSets = ruleSetLoader.loadRuleSetsWithoutException(config.getRuleSetPaths());
+ pmd.addRuleSets(ruleSets);
+ }
+ return pmd;
+ }
+
+ // test only
+ List rulesets() {
+ return ruleSets;
+ }
+
+ // test only
+ List renderers() {
+ return renderers;
+ }
+
+
+ /**
+ * Returns the file collector for the analysed sources.
+ */
+ public FileCollector files() {
+ return collector; // todo user can close collector programmatically
+ }
+
+ /**
+ * Returns a new ruleset loader, which can be used to create new
+ * rulesets (add them then with {@link #addRuleSet(RuleSet)}).
+ *
+ * {@code
+ * try (PmdAnalysis pmd = create(config)) {
+ * pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml"));
+ * }
+ * }
+ */
+ public RuleSetLoader newRuleSetLoader() {
+ RuleSetLoader loader = RuleSetLoader.fromPmdConfig(configuration);
+ loader.setReporter(this.reporter);
+ return loader;
+ }
+
+ /**
+ * Add a new renderer. The given renderer must not already be started,
+ * it will be started by {@link #performAnalysis()}.
+ *
+ * @throws NullPointerException If the parameter is null
+ */
+ public void addRenderer(Renderer renderer) {
+ AssertionUtil.requireParamNotNull("renderer", renderer);
+ this.renderers.add(renderer);
+ }
+
+ /**
+ * Add several renderers at once.
+ *
+ * @throws NullPointerException If the parameter is null, or any of its items is null.
+ */
+ public void addRenderers(Collection renderers) {
+ renderers.forEach(this::addRenderer);
+ }
+
+ /**
+ * Add a new listener. As per the contract of {@link GlobalAnalysisListener},
+ * this object must be ready for interaction. However, nothing will
+ * be done with the listener until {@link #performAnalysis()} is called.
+ * The listener will be closed by {@link #performAnalysis()}, or
+ * {@link #close()}, whichever happens first.
+ *
+ * @throws NullPointerException If the parameter is null
+ */
+ public void addListener(GlobalAnalysisListener listener) {
+ AssertionUtil.requireParamNotNull("listener", listener);
+ this.listeners.add(listener);
+ }
+
+ /**
+ * Add several listeners at once.
+ *
+ * @throws NullPointerException If the parameter is null, or any of its items is null.
+ * @see #addListener(GlobalAnalysisListener)
+ */
+ public void addListeners(Collection extends GlobalAnalysisListener> listeners) {
+ listeners.forEach(this::addListener);
+ }
+
+ /**
+ * Add a new ruleset.
+ *
+ * @throws NullPointerException If the parameter is null
+ */
+ public void addRuleSet(RuleSet ruleSet) {
+ AssertionUtil.requireParamNotNull("rule set", ruleSet);
+ this.ruleSets.add(ruleSet);
+ }
+
+ /**
+ * Add several rulesets at once.
+ *
+ * @throws NullPointerException If the parameter is null, or any of its items is null.
+ */
+ public void addRuleSets(Collection ruleSets) {
+ ruleSets.forEach(this::addRuleSet);
+ }
+
+ /**
+ * Returns an unmodifiable view of the ruleset list. That will be
+ * processed.
+ */
+ public List getRulesets() {
+ return Collections.unmodifiableList(ruleSets);
+ }
+
+
+ /**
+ * Run PMD with the current state of this instance. This will start
+ * and finish the registered renderers, and close all
+ * {@linkplain #addListener(GlobalAnalysisListener) registered listeners}.
+ * All files collected in the {@linkplain #files() file collector} are
+ * processed. This does not return a report, as the analysis results
+ * are consumed by {@link GlobalAnalysisListener} instances (of which
+ * Renderers are a special case). Note that this does
+ * not throw, errors are instead accumulated into a {@link MessageReporter}.
+ */
+ public void performAnalysis() {
+ performAnalysisImpl(Collections.emptyList());
+ }
+
+ /**
+ * Run PMD with the current state of this instance. This will start
+ * and finish the registered renderers. All files collected in the
+ * {@linkplain #files() file collector} are processed. Returns the
+ * output report. Note that this does not throw, errors are instead
+ * accumulated into a {@link MessageReporter}.
+ */
+ public Report performAnalysisAndCollectReport() {
+ try (GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener()) {
+ performAnalysisImpl(listOf(reportBuilder)); // closes the report builder
+ return reportBuilder.getResultImpl();
+ }
+ }
+
+ void performAnalysisImpl(List extends GlobalReportBuilderListener> extraListeners) {
+ try (FileCollector files = collector) {
+ files.filterLanguages(getApplicableLanguages());
+ List dataSources = FileCollectionUtil.collectorToDataSource(files);
+ performAnalysisImpl(extraListeners, dataSources);
+ }
+ }
+
+ void performAnalysisImpl(List extends GlobalReportBuilderListener> extraListeners, List dataSources) {
+ RuleSets rulesets = new RuleSets(this.ruleSets);
+
+ GlobalAnalysisListener listener;
+ try {
+ @SuppressWarnings("PMD.CloseResource") AnalysisCacheListener cacheListener = new AnalysisCacheListener(configuration.getAnalysisCache(), rulesets, configuration.getClassLoader());
+ listener = GlobalAnalysisListener.tee(listOf(createComposedRendererListener(renderers), GlobalAnalysisListener.tee(listeners), GlobalAnalysisListener.tee(extraListeners), cacheListener));
+ } catch (Exception e) {
+ reporter.errorEx("Exception while initializing analysis listeners", e);
+ throw new RuntimeException("Exception while initializing analysis listeners", e);
+ }
+
+ try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) {
+
+ for (final Rule rule : removeBrokenRules(rulesets)) {
+ // todo Just like we throw for invalid properties, "broken rules"
+ // shouldn't be a "config error". This is the only instance of
+ // config errors...
+ listener.onConfigError(new Report.ConfigurationError(rule, rule.dysfunctionReason()));
+ }
+
+ PMD.encourageToUseIncrementalAnalysis(configuration);
+ try (AbstractPMDProcessor processor = AbstractPMDProcessor.newFileProcessor(configuration)) {
+ processor.processFiles(rulesets, dataSources, listener);
+ }
+ } finally {
+ try {
+ listener.close();
+ } catch (Exception e) {
+ reporter.errorEx("Exception while initializing analysis listeners", e);
+ // todo better exception
+ throw new RuntimeException("Exception while initializing analysis listeners", e);
+ }
+ }
+ }
+
+
+ private static GlobalAnalysisListener createComposedRendererListener(List renderers) throws Exception {
+ if (renderers.isEmpty()) {
+ return GlobalAnalysisListener.noop();
+ }
+
+ List rendererListeners = new ArrayList<>(renderers.size());
+ for (Renderer renderer : renderers) {
+ try {
+ @SuppressWarnings("PMD.CloseResource")
+ GlobalAnalysisListener listener =
+ Objects.requireNonNull(renderer.newListener(), "Renderer should provide non-null listener");
+ rendererListeners.add(listener);
+ } catch (Exception ioe) {
+ // close listeners so far, throw their close exception or the ioe
+ IOUtil.ensureClosed(rendererListeners, ioe);
+ throw AssertionUtil.shouldNotReachHere("ensureClosed should have thrown");
+ }
+ }
+ return GlobalAnalysisListener.tee(rendererListeners);
+ }
+
+ private Set getApplicableLanguages() {
+ final Set languages = new HashSet<>();
+ final LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer();
+
+ for (RuleSet ruleSet : ruleSets) {
+ for (final Rule rule : ruleSet.getRules()) {
+ final Language ruleLanguage = rule.getLanguage();
+ if (!languages.contains(ruleLanguage)) {
+ final LanguageVersion version = discoverer.getDefaultLanguageVersion(ruleLanguage);
+ if (RuleSet.applies(rule, version)) {
+ languages.add(ruleLanguage);
+ LOG.trace("Using {} version ''{}''", version.getLanguage().getName(), version.getTerseName());
+ }
+ }
+ }
+ }
+ return languages;
+ }
+
+ /**
+ * Remove and return the misconfigured rules from the rulesets and log them
+ * for good measure.
+ */
+ private Set removeBrokenRules(final RuleSets ruleSets) {
+ final Set brokenRules = new HashSet<>();
+ ruleSets.removeDysfunctionalRules(brokenRules);
+
+ for (final Rule rule : brokenRules) {
+ reporter.warn("Removed misconfigured rule: {} cause: {}",
+ rule.getName(), rule.dysfunctionReason());
+ }
+
+ return brokenRules;
+ }
+
+
+ public MessageReporter getReporter() {
+ return reporter;
+ }
+
+ @Override
+ public void close() {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ collector.close();
+
+ // close listeners if analysis is not run.
+ IOUtil.closeAll(listeners);
+
+ /*
+ * 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());
+ }
+ }
+
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/Report.java b/pmd-core/src/main/java/net/sourceforge/pmd/Report.java
index 9513f5901e..78f4861cc4 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/Report.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/Report.java
@@ -28,7 +28,7 @@ import net.sourceforge.pmd.util.datasource.DataSource;
* and configuration errors.
*
* A report may be created by a {@link GlobalReportBuilderListener} that you
- * use as the {@link GlobalAnalysisListener} in {@linkplain PMD#processFiles(PMDConfiguration, List, List, GlobalAnalysisListener) PMD's entry point}.
+ * use as the {@linkplain GlobalAnalysisListener} in {@link PmdAnalysis#performAnalysisAndCollectReport() PMD's entry point}.
* You can also create one manually with {@link #buildReport(Consumer)}.
*/
public final class Report {
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java
index e177ee4eb6..1a75e5d3a5 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java
@@ -16,11 +16,16 @@ import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import net.sourceforge.pmd.annotation.InternalApi;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.util.CollectionUtil;
import net.sourceforge.pmd.util.ResourceLoader;
+import net.sourceforge.pmd.util.log.MessageReporter;
+import net.sourceforge.pmd.util.log.internal.NoopReporter;
/**
* Configurable object to load rulesets from XML resources.
@@ -29,12 +34,26 @@ import net.sourceforge.pmd.util.ResourceLoader;
* or some such overload.
*/
public final class RuleSetLoader {
+ private static final Logger LOG = LoggerFactory.getLogger(RuleSetLoader.class);
private ResourceLoader resourceLoader = new ResourceLoader(RuleSetLoader.class.getClassLoader());
private RulePriority minimumPriority = RulePriority.LOW;
private boolean warnDeprecated = true;
private @NonNull RuleSetFactoryCompatibility compatFilter = RuleSetFactoryCompatibility.DEFAULT;
private boolean includeDeprecatedRuleReferences = false;
+ private MessageReporter reporter = new NoopReporter(); // non-null
+
+ /**
+ * Create a new RuleSetLoader with a default configuration.
+ * The defaults are described on each configuration method of this class.
+ */
+ public RuleSetLoader() { // NOPMD UnnecessaryConstructor
+ // default
+ }
+
+ void setReporter(MessageReporter reporter) {
+ this.reporter = reporter;
+ }
/**
* Specify that the given classloader should be used to resolve
@@ -145,7 +164,7 @@ public final class RuleSetLoader {
*
* @throws RuleSetLoadException If any error occurs (eg, invalid syntax)
*/
- public RuleSet loadFromString(String filename, String rulesetXmlContent) {
+ public RuleSet loadFromString(String filename, final String rulesetXmlContent) {
return loadFromResource(new RuleSetReferenceId(filename) {
@Override
public InputStream getInputStream(ResourceLoader rl) {
@@ -171,6 +190,52 @@ public final class RuleSetLoader {
return ruleSets;
}
+ /**
+ * Loads a list of rulesets, if any has an error, report it on the contextual
+ * error reporter instead of aborting, and continue loading the rest.
+ *
+ *
Internal API: might be published later, or maybe in PMD 7 this
+ * will be the default behaviour of every method of this class.
+ */
+ @InternalApi
+ public List loadRuleSetsWithoutException(List rulesetPaths) {
+ List ruleSets = new ArrayList<>(rulesetPaths.size());
+ boolean anyRules = false;
+ for (String path : rulesetPaths) {
+ try {
+ RuleSet ruleset = this.loadFromResource(path);
+ anyRules |= !ruleset.getRules().isEmpty();
+ printRulesInDebug(path, ruleset);
+ ruleSets.add(ruleset);
+ } catch (RuleSetLoadException e) {
+ if (e.getCause() != null) {
+ // eg RuleSetNotFoundException
+ reporter.errorEx("Cannot load ruleset {0}", new Object[] { path }, e.getCause());
+ } else {
+ reporter.errorEx("Cannot load ruleset {0}", new Object[] { path }, e);
+ }
+ }
+ }
+ if (!anyRules) {
+ reporter.warn("No rules found. Maybe you misspelled a rule name? ({})",
+ StringUtils.join(rulesetPaths, ','));
+ }
+ return ruleSets;
+ }
+
+ void printRulesInDebug(String path, RuleSet ruleset) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Rules loaded from {}:", path);
+ for (Rule rule : ruleset.getRules()) {
+ LOG.debug("- {} ({})", rule.getName(), rule.getLanguage().getName());
+ }
+ }
+ if (ruleset.getRules().isEmpty()) {
+ reporter.warn("No rules found in ruleset {}", path);
+ }
+
+ }
+
/**
* Parses several resources into a list of rulesets.
*
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java b/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java
index 0f3f8f3114..4117010f49 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java
@@ -4,10 +4,9 @@
package net.sourceforge.pmd.ant.internal;
-import java.io.File;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
@@ -22,12 +21,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
-import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.PMDConfiguration;
-import net.sourceforge.pmd.Rule;
+import net.sourceforge.pmd.PmdAnalysis;
import net.sourceforge.pmd.RulePriority;
-import net.sourceforge.pmd.RuleSet;
-import net.sourceforge.pmd.RuleSetLoadException;
import net.sourceforge.pmd.RuleSetLoader;
import net.sourceforge.pmd.ant.Formatter;
import net.sourceforge.pmd.ant.PMDTask;
@@ -38,11 +34,11 @@ import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.reporting.FileAnalysisListener;
import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
-import net.sourceforge.pmd.reporting.GlobalAnalysisListener.ViolationCounterListener;
+import net.sourceforge.pmd.reporting.ReportStats;
+import net.sourceforge.pmd.reporting.ReportStatsListener;
import net.sourceforge.pmd.util.ClasspathClassLoader;
import net.sourceforge.pmd.util.IOUtil;
import net.sourceforge.pmd.util.datasource.DataSource;
-import net.sourceforge.pmd.util.datasource.FileDataSource;
public class PMDTaskImpl {
@@ -51,7 +47,7 @@ public class PMDTaskImpl {
private final List formatters = new ArrayList<>();
private final List filesets = new ArrayList<>();
private final PMDConfiguration configuration = new PMDConfiguration();
- private final String rulesetPaths;
+ private boolean failOnError;
private boolean failOnRuleViolation;
private int maxRuleViolations = 0;
private String failuresPropertyName;
@@ -62,12 +58,16 @@ public class PMDTaskImpl {
if (task.getSuppressMarker() != null) {
configuration.setSuppressMarker(task.getSuppressMarker());
}
+ this.failOnError = task.isFailOnError();
this.failOnRuleViolation = task.isFailOnRuleViolation();
this.maxRuleViolations = task.getMaxRuleViolations();
if (this.maxRuleViolations > 0) {
this.failOnRuleViolation = true;
}
- this.rulesetPaths = task.getRulesetFiles() == null ? "" : task.getRulesetFiles();
+ if (task.getRulesetFiles() != null) {
+ configuration.setRuleSets(Arrays.asList(task.getRulesetFiles().split(",")));
+ }
+
configuration.setRuleSetFactoryCompatibilityEnabled(!task.isNoRuleSetCompatibility());
if (task.getEncoding() != null) {
configuration.setSourceEncoding(task.getEncoding());
@@ -100,46 +100,49 @@ public class PMDTaskImpl {
private void doTask() {
setupClassLoader();
- // Setup RuleSetFactory and validate RuleSets
- RuleSetLoader rulesetLoader = RuleSetLoader.fromPmdConfig(configuration)
- .loadResourcesWith(setupResourceLoader());
-
- List rules = loadRulesets(rulesetLoader);
-
if (configuration.getSuppressMarker() != null) {
project.log("Setting suppress marker to be " + configuration.getSuppressMarker(), Project.MSG_VERBOSE);
}
- @SuppressWarnings("PMD.CloseResource")
- ViolationCounterListener reportSizeListener = new ViolationCounterListener();
-
- final List files = new ArrayList<>();
- final List reportShortNamesPaths = new ArrayList<>();
+ @SuppressWarnings("PMD.CloseResource") final List reportShortNamesPaths = new ArrayList<>();
StringJoiner fullInputPath = new StringJoiner(",");
- for (FileSet fs : filesets) {
- DirectoryScanner ds = fs.getDirectoryScanner(project);
- for (String srcFile : ds.getIncludedFiles()) {
- File file = new File(ds.getBasedir() + File.separator + srcFile);
- files.add(new FileDataSource(file));
+ List ruleSetPaths = expandRuleSetPaths(configuration.getRuleSetPaths());
+ // don't let PmdAnalysis.create create rulesets itself.
+ configuration.setRuleSets(Collections.emptyList());
+
+ ReportStats stats;
+ try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) {
+ RuleSetLoader rulesetLoader =
+ pmd.newRuleSetLoader().loadResourcesWith(setupResourceLoader());
+ pmd.addRuleSets(rulesetLoader.loadRuleSetsWithoutException(ruleSetPaths));
+
+ for (FileSet fileset : filesets) {
+ DirectoryScanner ds = fileset.getDirectoryScanner(project);
+ for (String srcFile : ds.getIncludedFiles()) {
+ pmd.files().addFile(ds.getBasedir().toPath().resolve(srcFile));
+ }
+
+ final String commonInputPath = ds.getBasedir().getPath();
+ fullInputPath.add(commonInputPath);
+ if (configuration.isReportShortNames()) {
+ reportShortNamesPaths.add(commonInputPath);
+ }
}
- final String commonInputPath = ds.getBasedir().getPath();
- fullInputPath.add(commonInputPath);
- if (configuration.isReportShortNames()) {
- reportShortNamesPaths.add(commonInputPath);
+ @SuppressWarnings("PMD.CloseResource")
+ ReportStatsListener reportStatsListener = new ReportStatsListener();
+ pmd.addListener(getListener(reportStatsListener, reportShortNamesPaths, fullInputPath.toString()));
+
+ pmd.performAnalysis();
+ stats = reportStatsListener.getResult();
+ if (failOnError && pmd.getReporter().numErrors() > 0) {
+ throw new BuildException("Some errors occurred while running PMD");
}
}
- configuration.setInputPaths(fullInputPath.toString());
- try (GlobalAnalysisListener listener = getListener(reportSizeListener, reportShortNamesPaths)) {
- PMD.processFiles(configuration, rules, files, listener);
- } catch (Exception e) {
- throw new BuildException("Exception while closing data sources", e);
- }
-
- int problemCount = reportSizeListener.getResult();
+ int problemCount = stats.getNumViolations();
project.log(problemCount + " problems found", Project.MSG_VERBOSE);
if (failuresPropertyName != null && problemCount > 0) {
@@ -152,34 +155,27 @@ public class PMDTaskImpl {
}
}
- private List loadRulesets(RuleSetLoader rulesetLoader) {
- try {
- // This is just used to validate and display rules. Each thread will create its own ruleset
- // Substitute env variables/properties
- String ruleSetString = project.replaceProperties(rulesetPaths);
-
- List rulesets = Arrays.asList(ruleSetString.split(","));
- List rulesetList = rulesetLoader.loadFromResources(rulesets);
- if (rulesetList.isEmpty()) {
- throw new BuildException("No rulesets");
- }
- logRulesUsed(rulesetList);
- return rulesetList;
- } catch (RuleSetLoadException e) {
- throw new BuildException(e.getMessage(), e);
+ private List expandRuleSetPaths(List ruleSetPaths) {
+ List paths = new ArrayList<>(ruleSetPaths);
+ for (int i = 0; i < paths.size(); i++) {
+ paths.set(i, project.replaceProperties(paths.get(i)));
}
+ return paths;
}
- private @NonNull GlobalAnalysisListener getListener(ViolationCounterListener reportSizeListener, List reportShortNamesPaths) {
+ private @NonNull GlobalAnalysisListener getListener(ReportStatsListener reportSizeListener,
+ List reportShortNamesPaths,
+ String inputPaths) {
List renderers = new ArrayList<>(formatters.size() + 1);
try {
- renderers.add(makeLogListener(configuration.getInputPaths()));
+ renderers.add(makeLogListener(inputPaths));
renderers.add(reportSizeListener);
for (Formatter formatter : formatters) {
project.log("Sending a report to " + formatter, Project.MSG_VERBOSE);
renderers.add(formatter.newListener(project, reportShortNamesPaths));
}
- } catch (IOException e) {
+ return GlobalAnalysisListener.tee(renderers);
+ } catch (Exception e) {
// close those opened so far
Exception e2 = IOUtil.closeAll(renderers);
if (e2 != null) {
@@ -187,8 +183,6 @@ public class PMDTaskImpl {
}
throw new BuildException("Exception while initializing renderers", e);
}
-
- return GlobalAnalysisListener.tee(renderers);
}
private GlobalAnalysisListener makeLogListener(String commonInputPath) {
@@ -233,9 +227,9 @@ public class PMDTaskImpl {
try {
if (auxClasspath != null) {
project.log("Using auxclasspath: " + auxClasspath, Project.MSG_VERBOSE);
- configuration.prependClasspath(auxClasspath.toString());
+ configuration.prependAuxClasspath(auxClasspath.toString());
}
- } catch (IOException ioe) {
+ } catch (IllegalArgumentException ioe) {
throw new BuildException(ioe.getMessage(), ioe);
}
}
@@ -257,13 +251,4 @@ public class PMDTaskImpl {
}
}
- private void logRulesUsed(List rulesets) {
- project.log("Using these rulesets: " + rulesetPaths, Project.MSG_VERBOSE);
-
- for (RuleSet ruleSet : rulesets) {
- for (Rule rule : ruleSet.getRules()) {
- project.log("Using rule " + rule.getName(), Project.MSG_VERBOSE);
- }
- }
- }
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AbstractAnalysisCache.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AbstractAnalysisCache.java
index 9089eb05bd..624515579c 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AbstractAnalysisCache.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AbstractAnalysisCache.java
@@ -26,6 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sourceforge.pmd.PMDVersion;
+import net.sourceforge.pmd.Report.ProcessingError;
import net.sourceforge.pmd.RuleSets;
import net.sourceforge.pmd.RuleViolation;
import net.sourceforge.pmd.annotation.InternalApi;
@@ -210,13 +211,26 @@ public abstract class AbstractAnalysisCache implements AnalysisCache {
}
@Override
- public FileAnalysisListener startFileAnalysis(DataSource filename) {
- return violation -> {
- final AnalysisResult analysisResult =
- updatedResultsCache.get(violation.getFilename());
+ public FileAnalysisListener startFileAnalysis(DataSource dataSource) {
+ String fileName = dataSource.getNiceFileName(false, "");
+ File sourceFile = new File(fileName);
+ AnalysisResult analysisResult = updatedResultsCache.get(fileName);
+ if (analysisResult == null) {
+ analysisResult = new AnalysisResult(sourceFile);
+ }
+ final AnalysisResult nonNullAnalysisResult = analysisResult;
- synchronized (analysisResult) {
- analysisResult.addViolation(violation);
+ return new FileAnalysisListener() {
+ @Override
+ public void onRuleViolation(RuleViolation violation) {
+ synchronized (nonNullAnalysisResult) {
+ nonNullAnalysisResult.addViolation(violation);
+ }
+ }
+
+ @Override
+ public void onError(ProcessingError error) {
+ analysisFailed(sourceFile);
}
};
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCache.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCache.java
index ea1f6f5273..f16490263a 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCache.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCache.java
@@ -5,6 +5,7 @@
package net.sourceforge.pmd.cache;
import java.io.File;
+import java.io.IOException;
import java.util.List;
import net.sourceforge.pmd.RuleSets;
@@ -12,6 +13,7 @@ import net.sourceforge.pmd.RuleViolation;
import net.sourceforge.pmd.annotation.InternalApi;
import net.sourceforge.pmd.reporting.FileAnalysisListener;
import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
+import net.sourceforge.pmd.util.datasource.DataSource;
/**
* An analysis cache for incremental analysis.
@@ -22,12 +24,12 @@ import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
*/
@Deprecated
@InternalApi
-public interface AnalysisCache extends GlobalAnalysisListener {
+public interface AnalysisCache {
/**
* Persists the updated analysis results on whatever medium is used by the cache.
*/
- void persist();
+ void persist() throws IOException;
/**
* Checks if a given file is up to date in the cache and can be skipped from analysis.
@@ -59,8 +61,16 @@ public interface AnalysisCache extends GlobalAnalysisListener {
* cache is invalidated. This needs to be called before analysis, as it
* conditions the good behaviour of {@link #isUpToDate(File)}.
*
- * @param ruleSets The rulesets configured for this analysis.
+ * @param ruleSets The rulesets configured for this analysis.
* @param auxclassPathClassLoader The class loader for auxclasspath configured for this analysis.
*/
void checkValidity(RuleSets ruleSets, ClassLoader auxclassPathClassLoader);
+
+ /**
+ * Returns a listener that will be used like in {@link GlobalAnalysisListener#startFileAnalysis(DataSource)}.
+ * This should record violations, and call {@link #analysisFailed(File)}
+ * upon error.
+ */
+ FileAnalysisListener startFileAnalysis(DataSource file);
+
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCacheListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCacheListener.java
new file mode 100644
index 0000000000..f6c4d12558
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/AnalysisCacheListener.java
@@ -0,0 +1,40 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.cache;
+
+import java.io.IOException;
+
+import net.sourceforge.pmd.RuleSets;
+import net.sourceforge.pmd.annotation.InternalApi;
+import net.sourceforge.pmd.reporting.FileAnalysisListener;
+import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
+import net.sourceforge.pmd.util.datasource.DataSource;
+
+/**
+ * Adapter to wrap {@link AnalysisCache} behaviour in a {@link GlobalAnalysisListener}.
+ */
+@Deprecated
+@InternalApi
+public class AnalysisCacheListener implements GlobalAnalysisListener {
+
+ private final AnalysisCache cache;
+
+ public AnalysisCacheListener(AnalysisCache cache, RuleSets ruleSets, ClassLoader classLoader) {
+ this.cache = cache;
+ cache.checkValidity(ruleSets, classLoader);
+ }
+
+
+ @Override
+ public FileAnalysisListener startFileAnalysis(DataSource file) {
+ return cache.startFileAnalysis(file);
+ }
+
+ @Override
+ public void close() throws IOException {
+ cache.persist();
+ }
+
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/FileAnalysisCache.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/FileAnalysisCache.java
index 5c1f29f4db..d2739cfc92 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/cache/FileAnalysisCache.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/FileAnalysisCache.java
@@ -151,11 +151,6 @@ public class FileAnalysisCache extends AbstractAnalysisCache {
}
}
- @Override
- public void close() throws Exception {
- // nothing to do, PMD calls persist explicitly
- }
-
@Override
protected boolean cacheExists() {
return cacheFile.exists() && cacheFile.isFile() && cacheFile.length() > 0;
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cache/NoopAnalysisCache.java b/pmd-core/src/main/java/net/sourceforge/pmd/cache/NoopAnalysisCache.java
index 56127ea4a7..3a3dee4470 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/cache/NoopAnalysisCache.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/cache/NoopAnalysisCache.java
@@ -53,8 +53,4 @@ public class NoopAnalysisCache implements AnalysisCache {
return FileAnalysisListener.noop();
}
- @Override
- public void close() throws Exception {
- // noop
- }
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java
index 7419d11afd..5e90676c06 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java
@@ -8,6 +8,7 @@ import java.util.Properties;
import java.util.stream.Collectors;
import net.sourceforge.pmd.PMD;
+import net.sourceforge.pmd.PMD.StatusCode;
import net.sourceforge.pmd.PMDVersion;
import net.sourceforge.pmd.annotation.InternalApi;
import net.sourceforge.pmd.lang.Language;
@@ -28,13 +29,39 @@ import com.beust.jcommander.ParameterException;
@InternalApi
public final class PMDCommandLineInterface {
+ @Deprecated
public static final String PROG_NAME = "pmd";
+ /**
+ * @deprecated This is used for testing, but support for it will be removed in PMD 7.
+ * Use {@link PMD#runPmd(String...)} or an overload to avoid exiting the VM. In PMD 7,
+ * {@link PMD#main(String[])} will call {@link System#exit(int)} always.
+ */
+ @Deprecated
public static final String NO_EXIT_AFTER_RUN = "net.sourceforge.pmd.cli.noExit";
+
+ /**
+ * @deprecated This is used for testing, but support for it will be removed in PMD 7.
+ * Use {@link PMD#runPmd(String...)} or an overload to avoid exiting the VM. In PMD 7,
+ * {@link PMD#main(String[])} will call {@link System#exit(int)} always.
+ */
+ @Deprecated
public static final String STATUS_CODE_PROPERTY = "net.sourceforge.pmd.cli.status";
+ /**
+ * @deprecated Use {@link StatusCode#OK}
+ */
+ @Deprecated
public static final int NO_ERRORS_STATUS = 0;
+ /**
+ * @deprecated Use {@link StatusCode#ERROR}
+ */
+ @Deprecated
public static final int ERROR_STATUS = 1;
+ /**
+ * @deprecated Use {@link StatusCode#VIOLATIONS_FOUND}
+ */
+ @Deprecated
public static final int VIOLATIONS_FOUND = 4;
private PMDCommandLineInterface() { }
@@ -126,7 +153,10 @@ public final class PMDCommandLineInterface {
* For testing purpose only...
*
* @param args
+ *
+ * @deprecated Use {@link PMD#runPmd(String...)}
*/
+ @Deprecated
public static void main(String[] args) {
System.out.println(PMDCommandLineInterface.buildUsageText());
}
@@ -160,14 +190,6 @@ public final class PMDCommandLineInterface {
return buf.toString();
}
- /**
- * @deprecated Use {@link PMD#main(String[])}
- */
- @Deprecated
- public static void run(String[] args) {
- setStatusCodeOrExit(PMD.run(args));
- }
-
public static void setStatusCodeOrExit(int status) {
if (isExitAfterRunSet()) {
System.exit(status);
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java
index 892093af24..7cea583a26 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java
@@ -4,7 +4,6 @@
package net.sourceforge.pmd.cli;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -235,8 +234,8 @@ public class PMDParameters {
}
try {
- configuration.prependClasspath(this.getAuxclasspath());
- } catch (IOException e) {
+ configuration.prependAuxClasspath(this.getAuxclasspath());
+ } catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid auxiliary classpath: " + e.getMessage(), e);
}
return configuration;
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/AssertionUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/AssertionUtil.java
index b1034a6252..fca762b178 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/AssertionUtil.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/AssertionUtil.java
@@ -25,10 +25,12 @@ public final class AssertionUtil {
/** @throws NullPointerException if $name */
public static void requireContainsNoNullValue(String name, Collection> c) {
+ int i = 0;
for (Object o : c) {
if (o == null) {
- throw new NullPointerException(name + " contains null elements");
+ throw new NullPointerException(name + " contains a null element at index " + i);
}
+ i++;
}
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/FileCollectionUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/FileCollectionUtil.java
new file mode 100644
index 0000000000..697d16f927
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/FileCollectionUtil.java
@@ -0,0 +1,183 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.internal.util;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sourceforge.pmd.PMDConfiguration;
+import net.sourceforge.pmd.lang.Language;
+import net.sourceforge.pmd.lang.document.FileCollector;
+import net.sourceforge.pmd.lang.document.TextFile;
+import net.sourceforge.pmd.util.FileUtil;
+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.log.MessageReporter;
+import net.sourceforge.pmd.util.log.internal.ErrorsAsWarningsReporter;
+
+/**
+ * @author ClΓ©ment Fournier
+ */
+public final class FileCollectionUtil {
+
+ private static final Logger LOG = LoggerFactory.getLogger(FileCollectionUtil.class);
+
+ private FileCollectionUtil() {
+
+ }
+
+ public static List collectorToDataSource(FileCollector collector) {
+ List result = new ArrayList<>();
+ for (TextFile file : collector.getCollectedFiles()) {
+ result.add(file.toDataSourceCompat());
+ }
+ return result;
+ }
+
+ public static FileCollector collectFiles(PMDConfiguration configuration, Set languages, MessageReporter reporter) {
+ FileCollector collector = collectFiles(configuration, reporter);
+ collector.filterLanguages(languages);
+ return collector;
+ }
+
+ private static FileCollector collectFiles(PMDConfiguration configuration, MessageReporter reporter) {
+ FileCollector collector = FileCollector.newCollector(
+ configuration.getLanguageVersionDiscoverer(),
+ reporter
+ );
+ collectFiles(configuration, collector);
+ return collector;
+ }
+
+ public static void collectFiles(PMDConfiguration configuration, FileCollector collector) {
+ if (configuration.getSourceEncoding() != null) {
+ collector.setCharset(configuration.getSourceEncoding());
+ }
+
+ if (configuration.getInputPaths() != null) {
+ collectFiles(collector, configuration.getInputPaths());
+ }
+
+ if (configuration.getInputUri() != null) {
+ collectDB(collector, configuration.getInputUri());
+ }
+
+ if (configuration.getInputFilePath() != null) {
+ collectFileList(collector, configuration.getInputFilePath());
+ }
+
+ if (configuration.getIgnoreFilePath() != null) {
+ // This is to be able to interpret the log (will report 'adding' xxx)
+ LOG.debug("Now collecting files to exclude.");
+ // errors like "excluded file does not exist" are reported as warnings.
+ // todo better reporting of *where* exactly the path is
+ MessageReporter mutedLog = new ErrorsAsWarningsReporter(collector.getReporter());
+ try (FileCollector excludeCollector = collector.newCollector(mutedLog)) {
+ collectFileList(excludeCollector, configuration.getIgnoreFilePath());
+ collector.exclude(excludeCollector);
+ }
+ }
+ }
+
+
+ public static void collectFiles(FileCollector collector, String fileLocations) {
+ for (String rootLocation : fileLocations.split(",")) {
+ try {
+ collector.relativizeWith(rootLocation);
+ addRoot(collector, rootLocation);
+ } catch (IOException e) {
+ collector.getReporter().errorEx("Error collecting " + rootLocation, e);
+ }
+ }
+ }
+
+ public static void collectFileList(FileCollector collector, String fileListLocation) {
+ LOG.debug("Reading file list {}.", fileListLocation);
+ Path path = Paths.get(fileListLocation);
+ if (!Files.exists(path)) {
+ collector.getReporter().error("No such file {}", fileListLocation);
+ return;
+ }
+
+ String filePaths;
+ try {
+ filePaths = FileUtil.readFilelist(path.toFile());
+ } catch (IOException e) {
+ collector.getReporter().errorEx("Error reading {}", new Object[] { fileListLocation }, e);
+ return;
+ }
+ collectFiles(collector, filePaths);
+ }
+
+ private static void addRoot(FileCollector collector, String rootLocation) throws IOException {
+ Path path = Paths.get(rootLocation);
+ if (!Files.exists(path)) {
+ collector.getReporter().error("No such file {}", path);
+ return;
+ }
+
+ if (Files.isDirectory(path)) {
+ LOG.debug("Adding directory {}.", path);
+ collector.addDirectory(path);
+ } else if (rootLocation.endsWith(".zip") || rootLocation.endsWith(".jar")) {
+ LOG.debug("Adding zip file {}.", path);
+ @SuppressWarnings("PMD.CloseResource")
+ FileSystem fs = collector.addZipFile(path);
+ if (fs == null) {
+ return;
+ }
+ for (Path zipRoot : fs.getRootDirectories()) {
+ collector.addFileOrDirectory(zipRoot);
+ }
+ } else if (Files.isRegularFile(path)) {
+ LOG.debug("Adding regular file {}.", path);
+ collector.addFile(path);
+ } else {
+ LOG.debug("Ignoring {}: not a regular file or directory", path);
+ }
+ }
+
+ public static void collectDB(FileCollector collector, String uriString) {
+ try {
+ LOG.debug("Connecting to {}", uriString);
+ DBURI dbUri = new DBURI(uriString);
+ DBMSMetadata dbmsMetadata = new DBMSMetadata(dbUri);
+ LOG.trace("DBMSMetadata retrieved");
+ List sourceObjectList = dbmsMetadata.getSourceObjectList();
+ LOG.trace("Located {} database source objects", sourceObjectList.size());
+ for (SourceObject sourceObject : sourceObjectList) {
+ String falseFilePath = sourceObject.getPseudoFileName();
+ LOG.trace("Adding database source object {}", falseFilePath);
+
+ try (Reader sourceCode = dbmsMetadata.getSourceCode(sourceObject)) {
+ String source = IOUtils.toString(sourceCode);
+ collector.addSourceFile(source, falseFilePath);
+ } catch (SQLException ex) {
+ collector.getReporter().warnEx("Cannot get SourceCode for {} - skipping ...",
+ new Object[] { falseFilePath },
+ ex);
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ collector.getReporter().errorEx("Cannot get files from DB - probably missing database JDBC driver", e);
+ } catch (Exception e) {
+ collector.getReporter().errorEx("Cannot get files from DB - ''{}''", new Object[] { uriString }, e);
+ }
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java
index 4413c302ca..4491ff2a42 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/BaseLanguageModule.java
@@ -154,7 +154,7 @@ public abstract class BaseLanguageModule implements Language {
@Override
public String toString() {
- return "LanguageModule:" + name + '(' + this.getClass().getSimpleName() + ')';
+ return getTerseName();
}
@Override
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersion.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersion.java
index 94bf68af88..b1ac43e348 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersion.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersion.java
@@ -35,7 +35,7 @@ public class LanguageVersion implements Comparable {
private final Language language;
private final String version;
- private final LanguageVersionHandler languageVersionHandler;
+ private final LanguageVersionHandler languageVersionHandler; // note: this is null if this is a cpd-only language...
/**
* @deprecated Use {@link Language#getVersion(String)}. This is only
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersionDiscoverer.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersionDiscoverer.java
index 61e997c29c..4e22b03a4f 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersionDiscoverer.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageVersionDiscoverer.java
@@ -8,6 +8,11 @@ import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+
+import org.apache.commons.lang3.StringUtils;
+
+import net.sourceforge.pmd.internal.util.AssertionUtil;
/**
* This class can discover the LanguageVersion of a source file. Further, every
@@ -17,6 +22,23 @@ import java.util.Map;
public class LanguageVersionDiscoverer {
private Map languageToLanguageVersion = new HashMap<>();
+ private LanguageVersion forcedVersion;
+
+ public LanguageVersionDiscoverer() {
+ this(null);
+ }
+
+ /**
+ * Build a new instance.
+ *
+ * @param forcedVersion If non-null, all files should be assigned this version.
+ * The methods of this class still work as usual and do not
+ * care about the forced language version.
+ */
+ public LanguageVersionDiscoverer(LanguageVersion forcedVersion) {
+ this.forcedVersion = forcedVersion;
+ }
+
/**
* Set the given LanguageVersion as the current default for it's Language.
*
@@ -25,6 +47,7 @@ public class LanguageVersionDiscoverer {
* @return The previous default version for the language.
*/
public LanguageVersion setDefaultLanguageVersion(LanguageVersion languageVersion) {
+ AssertionUtil.requireParamNotNull("languageVersion", languageVersion);
LanguageVersion currentLanguageVersion = languageToLanguageVersion.put(languageVersion.getLanguage(),
languageVersion);
if (currentLanguageVersion == null) {
@@ -41,6 +64,7 @@ public class LanguageVersionDiscoverer {
* @return The current default version for the language.
*/
public LanguageVersion getDefaultLanguageVersion(Language language) {
+ Objects.requireNonNull(language);
LanguageVersion languageVersion = languageToLanguageVersion.get(language);
if (languageVersion == null) {
languageVersion = language.getDefaultVersion();
@@ -81,6 +105,14 @@ public class LanguageVersionDiscoverer {
return languageVersion;
}
+ public LanguageVersion getForcedVersion() {
+ return forcedVersion;
+ }
+
+ public void setForcedVersion(LanguageVersion forceLanguageVersion) {
+ this.forcedVersion = forceLanguageVersion;
+ }
+
/**
* Get the Languages of a given source file.
*
@@ -106,11 +138,8 @@ public class LanguageVersionDiscoverer {
// Get the extensions from a file
private String getExtension(String fileName) {
- String extension = null;
- int extensionIndex = 1 + fileName.lastIndexOf('.');
- if (extensionIndex > 0) {
- extension = fileName.substring(extensionIndex);
- }
- return extension;
+ return StringUtils.substringAfterLast(fileName, ".");
}
+
+
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Parser.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Parser.java
index 3697ff7ddb..348d104a3a 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Parser.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Parser.java
@@ -10,7 +10,7 @@ import java.util.Objects;
import org.checkerframework.checker.nullness.qual.NonNull;
-import net.sourceforge.pmd.PMD;
+import net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.properties.AbstractPropertySource;
import net.sourceforge.pmd.properties.PropertyDescriptor;
@@ -72,7 +72,7 @@ public interface Parser {
public static final PropertyDescriptor COMMENT_MARKER =
PropertyFactory.stringProperty("suppressionCommentMarker")
.desc("deprecated! NOPMD")
- .defaultValue(PMD.SUPPRESS_MARKER)
+ .defaultValue(PMDConfiguration.DEFAULT_SUPPRESS_MARKER)
.build();
@Deprecated // transitional until language properties are implemented
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java
new file mode 100644
index 0000000000..55c7fb5d24
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java
@@ -0,0 +1,400 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.document;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.ProviderNotFoundException;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sourceforge.pmd.PmdAnalysis;
+import net.sourceforge.pmd.annotation.InternalApi;
+import net.sourceforge.pmd.internal.util.AssertionUtil;
+import net.sourceforge.pmd.lang.Language;
+import net.sourceforge.pmd.lang.LanguageVersion;
+import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
+import net.sourceforge.pmd.util.IOUtil;
+import net.sourceforge.pmd.util.log.MessageReporter;
+
+/**
+ * Collects files to analyse before a PMD run. This API allows opening
+ * zip files and makes sure they will be closed at the end of a run.
+ *
+ * @author ClΓ©ment Fournier
+ */
+@SuppressWarnings("PMD.CloseResource")
+public final class FileCollector implements AutoCloseable {
+ private static final Logger LOG = LoggerFactory.getLogger(FileCollector.class);
+
+ private final List allFilesToProcess = new ArrayList<>();
+ private final List resourcesToClose = new ArrayList<>();
+ private Charset charset = StandardCharsets.UTF_8;
+ private final LanguageVersionDiscoverer discoverer;
+ private final MessageReporter reporter;
+ private final List relativizeRoots = new ArrayList<>();
+ private boolean closed;
+
+ // construction
+
+ private FileCollector(LanguageVersionDiscoverer discoverer, MessageReporter reporter) {
+ this.discoverer = discoverer;
+ this.reporter = reporter;
+ }
+
+ /**
+ * Internal API: please use {@link PmdAnalysis#files()} instead of
+ * creating a collector yourself.
+ */
+ @InternalApi
+ public static FileCollector newCollector(LanguageVersionDiscoverer discoverer, MessageReporter reporter) {
+ return new FileCollector(discoverer, reporter);
+ }
+
+ /**
+ * Returns a new collector using the configuration except for the logger.
+ */
+ @InternalApi
+ public FileCollector newCollector(MessageReporter logger) {
+ FileCollector fileCollector = new FileCollector(discoverer, logger);
+ fileCollector.charset = this.charset;
+ fileCollector.relativizeRoots.addAll(this.relativizeRoots);
+ return fileCollector;
+ }
+
+ // public behaviour
+
+ /**
+ * Returns an unmodifiable list of all files that have been collected.
+ *
+ * Internal: This might be unstable until PMD 7, but it's internal.
+ */
+ @InternalApi
+ public List getCollectedFiles() {
+ if (closed) {
+ throw new IllegalStateException("Collector was closed!");
+ }
+ allFilesToProcess.sort(Comparator.comparing(TextFile::getPathId));
+ return Collections.unmodifiableList(allFilesToProcess);
+ }
+
+
+ /**
+ * Returns the reporter for the file collection phase.
+ */
+ @InternalApi
+ public MessageReporter getReporter() {
+ return reporter;
+ }
+
+ /**
+ * Close registered resources like zip files.
+ */
+ @Override
+ public void close() {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ Exception exception = IOUtil.closeAll(resourcesToClose);
+ if (exception != null) {
+ reporter.errorEx("Error while closing resources", exception);
+ }
+ }
+
+ // collection
+
+ /**
+ * Add a file, language is determined automatically from
+ * the extension/file patterns. The encoding is the current
+ * encoding ({@link #setCharset(Charset)}).
+ *
+ * @param file File to add
+ *
+ * @return True if the file has been added
+ */
+ public boolean addFile(Path file) {
+ if (!Files.isRegularFile(file)) {
+ reporter.error("Not a regular file {}", file);
+ return false;
+ }
+ LanguageVersion languageVersion = discoverLanguage(file.toString());
+ if (languageVersion != null) {
+ addFileImpl(new NioTextFile(file, charset, languageVersion, getDisplayName(file)));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Add a file with the given language (which overrides the file patterns).
+ * The encoding is the current encoding ({@link #setCharset(Charset)}).
+ *
+ * @param file Path to a file
+ * @param language A language. The language version will be taken to be the
+ * contextual default version.
+ *
+ * @return True if the file has been added
+ */
+ public boolean addFile(Path file, Language language) {
+ AssertionUtil.requireParamNotNull("language", language);
+ if (!Files.isRegularFile(file)) {
+ reporter.error("Not a regular file {}", file);
+ return false;
+ }
+ NioTextFile nioTextFile = new NioTextFile(file, charset, discoverer.getDefaultLanguageVersion(language), getDisplayName(file));
+ addFileImpl(nioTextFile);
+ return true;
+ }
+
+ /**
+ * Add a pre-configured text file. The language version will be checked
+ * to match the contextual default for the language (the file cannot be added
+ * if it has a different version).
+ *
+ * @return True if the file has been added
+ */
+ public boolean addFile(TextFile textFile) {
+ AssertionUtil.requireParamNotNull("textFile", textFile);
+ if (checkContextualVersion(textFile)) {
+ addFileImpl(textFile);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Add a text file given its contents and a name. The language version
+ * will be determined from the name as usual.
+ *
+ * @return True if the file has been added
+ */
+ public boolean addSourceFile(String pathId, String sourceContents) {
+ AssertionUtil.requireParamNotNull("sourceContents", sourceContents);
+ AssertionUtil.requireParamNotNull("pathId", pathId);
+
+ LanguageVersion version = discoverLanguage(pathId);
+ if (version != null) {
+ addFileImpl(new StringTextFile(sourceContents, pathId, pathId, version));
+ return true;
+ }
+
+ return false;
+ }
+
+ private void addFileImpl(TextFile textFile) {
+ LOG.trace("Adding file {} (lang: {}) ", textFile.getPathId(), textFile.getLanguageVersion().getTerseName());
+ allFilesToProcess.add(textFile);
+ }
+
+ private LanguageVersion discoverLanguage(String file) {
+ if (discoverer.getForcedVersion() != null) {
+ return discoverer.getForcedVersion();
+ }
+ List languages = discoverer.getLanguagesForFile(file);
+
+ if (languages.isEmpty()) {
+ LOG.trace("File {} matches no known language, ignoring", file);
+ return null;
+ }
+ Language lang = languages.get(0);
+ if (languages.size() > 1) {
+ LOG.trace("File {} matches multiple languages ({}), selecting {}", file, languages, lang);
+ }
+ return discoverer.getDefaultLanguageVersion(lang);
+ }
+
+ /**
+ * Whether the LanguageVersion of the file matches the one set in
+ * the {@link LanguageVersionDiscoverer}. This is required to ensure
+ * that all files for a given language have the same language version.
+ */
+ private boolean checkContextualVersion(TextFile textFile) {
+ LanguageVersion fileVersion = textFile.getLanguageVersion();
+ Language language = fileVersion.getLanguage();
+ LanguageVersion contextVersion = discoverer.getDefaultLanguageVersion(language);
+ if (!fileVersion.equals(contextVersion)) {
+ reporter.error(
+ "Cannot add file {}: version ''{}'' does not match ''{}''",
+ textFile.getPathId(),
+ fileVersion,
+ contextVersion
+ );
+ return false;
+ }
+ return true;
+ }
+
+ private String getDisplayName(Path file) {
+ return getDisplayName(file, relativizeRoots);
+ }
+
+ /**
+ * Return the textfile's display name.
+ * test only
+ */
+ static String getDisplayName(Path file, List relativizeRoots) {
+ String fileName = file.toString();
+ for (String root : relativizeRoots) {
+ if (file.startsWith(root)) {
+ if (fileName.startsWith(File.separator, root.length())) {
+ // remove following '/'
+ return fileName.substring(root.length() + 1);
+ }
+ return fileName.substring(root.length());
+ }
+ }
+ return fileName;
+ }
+
+
+ /**
+ * Add a directory recursively using {@link #addFile(Path)} on
+ * all regular files.
+ *
+ * @param dir Directory path
+ *
+ * @return True if the directory has been added
+ */
+ public boolean addDirectory(Path dir) throws IOException {
+ if (!Files.isDirectory(dir)) {
+ reporter.error("Not a directory {}", dir);
+ return false;
+ }
+ Files.walkFileTree(dir, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ if (attrs.isRegularFile()) {
+ FileCollector.this.addFile(file);
+ }
+ return super.visitFile(file, attrs);
+ }
+ });
+ return true;
+ }
+
+
+ /**
+ * Add a file or directory recursively. Language is determined automatically
+ * from the extension/file patterns.
+ *
+ * @return True if the file or directory has been added
+ */
+ public boolean addFileOrDirectory(Path file) throws IOException {
+ if (Files.isDirectory(file)) {
+ return addDirectory(file);
+ } else if (Files.isRegularFile(file)) {
+ return addFile(file);
+ } else {
+ reporter.error("Not a file or directory {}", file);
+ return false;
+ }
+ }
+
+ /**
+ * Opens a zip file and returns a FileSystem for its contents, so
+ * it can be explored with the {@link Path} API. You can then call
+ * {@link #addFile(Path)} and such. The zip file is registered as
+ * a resource to close at the end of analysis.
+ */
+ public FileSystem addZipFile(Path zipFile) {
+ if (!Files.isRegularFile(zipFile)) {
+ throw new IllegalArgumentException("Not a regular file: " + zipFile);
+ }
+ URI zipUri = URI.create("zip:" + zipFile.toUri());
+ try {
+ FileSystem fs = FileSystems.getFileSystem(zipUri);
+ resourcesToClose.add(fs);
+ return fs;
+ } catch (FileSystemNotFoundException | ProviderNotFoundException e) {
+ reporter.errorEx("Cannot open zip file " + zipFile, e);
+ return null;
+ }
+ }
+
+ // configuration
+
+ /**
+ * Sets the charset to use for subsequent calls to {@link #addFile(Path)}
+ * and other overloads using a {@link Path}.
+ *
+ * @param charset A charset
+ */
+ public void setCharset(Charset charset) {
+ this.charset = Objects.requireNonNull(charset);
+ }
+
+ /**
+ * Add a prefix that is used to relativize file paths as their display name.
+ * For instance, when adding a file {@code /tmp/src/main/java/org/foo.java},
+ * and relativizing with {@code /tmp/src/}, the registered {@link TextFile}
+ * will have a path id of {@code /tmp/src/main/java/org/foo.java}, and a
+ * display name of {@code main/java/org/foo.java}.
+ *
+ * This only matters for files added from a {@link Path} object.
+ *
+ * @param prefix Prefix to relativize (if a directory, include a trailing slash)
+ */
+ public void relativizeWith(String prefix) {
+ this.relativizeRoots.add(Objects.requireNonNull(prefix));
+ }
+
+ // filtering
+
+ /**
+ * Remove all files collected by the given collector from this one.
+ */
+ public void exclude(FileCollector excludeCollector) {
+ Set toExclude = new HashSet<>(excludeCollector.allFilesToProcess);
+ for (Iterator iterator = allFilesToProcess.iterator(); iterator.hasNext();) {
+ TextFile file = iterator.next();
+ if (toExclude.contains(file)) {
+ LOG.trace("Excluding file {}", file.getPathId());
+ iterator.remove();
+ }
+ }
+ }
+
+ /**
+ * Exclude all collected files whose language is not part of the given
+ * collection.
+ */
+ public void filterLanguages(Set languages) {
+ for (Iterator iterator = allFilesToProcess.iterator(); iterator.hasNext();) {
+ TextFile file = iterator.next();
+ Language lang = file.getLanguageVersion().getLanguage();
+ if (!languages.contains(lang)) {
+ LOG.trace("Filtering out {}, no rules for language {}", file.getPathId(), lang);
+ iterator.remove();
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "FileCollector{filesToProcess=" + allFilesToProcess + '}';
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java
new file mode 100644
index 0000000000..705ef530a7
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java
@@ -0,0 +1,101 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.document;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+
+import org.apache.commons.io.IOUtils;
+
+import net.sourceforge.pmd.annotation.Experimental;
+import net.sourceforge.pmd.internal.util.AssertionUtil;
+import net.sourceforge.pmd.lang.LanguageVersion;
+import net.sourceforge.pmd.util.datasource.DataSource;
+import net.sourceforge.pmd.util.datasource.FileDataSource;
+
+/**
+ * A {@link TextFile} backed by a file in some {@link FileSystem}.
+ */
+@Experimental
+class NioTextFile implements TextFile {
+
+ private final Path path;
+ private final Charset charset;
+ private final LanguageVersion languageVersion;
+ private final String displayName;
+ private final String pathId;
+
+ NioTextFile(Path path, Charset charset, LanguageVersion languageVersion, String displayName) {
+ AssertionUtil.requireParamNotNull("path", path);
+ AssertionUtil.requireParamNotNull("charset", charset);
+ AssertionUtil.requireParamNotNull("language version", languageVersion);
+
+ this.displayName = displayName;
+ this.path = path;
+ this.charset = charset;
+ this.languageVersion = languageVersion;
+ this.pathId = path.toAbsolutePath().toString();
+ }
+
+ @Override
+ public LanguageVersion getLanguageVersion() {
+ return languageVersion;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ public String getPathId() {
+ return pathId;
+ }
+
+
+ @Override
+ public String readContents() throws IOException {
+
+ if (!Files.isRegularFile(path)) {
+ throw new IOException("Not a regular file: " + path);
+ }
+
+ try (BufferedReader br = Files.newBufferedReader(path, charset)) {
+ return IOUtils.toString(br);
+ }
+ }
+
+ @Override
+ public DataSource toDataSourceCompat() {
+ return new FileDataSource(path.toFile());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ NioTextFile that = (NioTextFile) o;
+ return Objects.equals(path, that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pathId);
+ }
+
+ @Override
+ public String toString() {
+ return getPathId();
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/StringTextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/StringTextFile.java
new file mode 100644
index 0000000000..598823e555
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/StringTextFile.java
@@ -0,0 +1,94 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.document;
+
+import java.io.StringReader;
+import java.util.Objects;
+
+import net.sourceforge.pmd.annotation.Experimental;
+import net.sourceforge.pmd.internal.util.AssertionUtil;
+import net.sourceforge.pmd.lang.LanguageVersion;
+import net.sourceforge.pmd.util.datasource.DataSource;
+import net.sourceforge.pmd.util.datasource.ReaderDataSource;
+
+/**
+ * Read-only view on a string.
+ *
+ * @author ClΓ©ment Fournier
+ */
+@Experimental
+class StringTextFile implements TextFile {
+
+ private final String content;
+ private final String pathId;
+ private final String displayName;
+ private final LanguageVersion languageVersion;
+
+ StringTextFile(String content,
+ String pathId,
+ String displayName,
+ LanguageVersion languageVersion) {
+ AssertionUtil.requireParamNotNull("source text", content);
+ AssertionUtil.requireParamNotNull("file name", displayName);
+ AssertionUtil.requireParamNotNull("file ID", pathId);
+ AssertionUtil.requireParamNotNull("language version", languageVersion);
+
+ this.languageVersion = languageVersion;
+ this.content = content;
+ this.pathId = pathId;
+ this.displayName = displayName;
+ }
+
+
+ @Override
+ public LanguageVersion getLanguageVersion() {
+ return languageVersion;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ public String getPathId() {
+ return pathId;
+ }
+
+ @Override
+ public String readContents() {
+ return content;
+ }
+
+ @Override
+ public DataSource toDataSourceCompat() {
+ return new ReaderDataSource(
+ new StringReader(content),
+ pathId
+ );
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ StringTextFile that = (StringTextFile) o;
+ return Objects.equals(pathId, that.pathId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pathId);
+ }
+
+ @Override
+ public String toString() {
+ return getPathId();
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java
new file mode 100644
index 0000000000..34d232d38a
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java
@@ -0,0 +1,98 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.document;
+
+import java.io.File;
+import java.io.IOException;
+
+import net.sourceforge.pmd.PmdAnalysis;
+import net.sourceforge.pmd.annotation.Experimental;
+import net.sourceforge.pmd.cpd.SourceCode;
+import net.sourceforge.pmd.lang.LanguageVersion;
+import net.sourceforge.pmd.util.datasource.DataSource;
+
+/**
+ * Represents some location containing character data. Despite the name,
+ * it's not necessarily backed by a file in the file-system: it may be
+ * eg an in-memory buffer, or a zip entry, ie it's an abstraction. Text
+ * files are the input which PMD and CPD process.
+ *
+ * Text files must provide read access, and may provide write access.
+ * This interface only provides block IO operations, while {@link TextDocument} adds logic
+ * about incremental edition (eg replacing a single region of text).
+ *
+ *
This interface is meant to replace {@link DataSource} and {@link SourceCode.CodeLoader}.
+ * "DataSource" is not an appropriate name for a file which can be written
+ * to, also, the "data" it provides is text, not bytes.
+ *
+ *
Experimental
+ * This interface will change in PMD 7 to support read/write operations
+ * and other things. You don't need to use it in PMD 6, as {@link FileCollector}
+ * decouples you from this. A file collector is available through {@link PmdAnalysis#files()}.
+ */
+@Experimental
+public interface TextFile {
+
+ /**
+ * The name used for a file that has no name. This is mostly only
+ * relevant for unit tests.
+ */
+ String UNKNOWN_FILENAME = "(unknown file)";
+
+
+ /**
+ * Returns the language version which should be used to process this
+ * file. This is a property of the file, which allows sources for
+ * several different language versions to be processed in the same
+ * PMD run. It also makes it so, that the file extension is not interpreted
+ * to find out the language version after the initial file collection
+ * phase.
+ *
+ * @return A language version
+ */
+ LanguageVersion getLanguageVersion();
+
+
+ /**
+ * Returns an identifier for the path of this file. This should not
+ * be interpreted as a {@link File}, it may not be a file on this
+ * filesystem. The only requirement for this method, is that two
+ * distinct text files should have distinct path IDs, and that from
+ * one analysis to the next, the path ID of logically identical files
+ * be the same.
+ *
+ * Basically this may be implemented as a URL, or a file path. It
+ * is used to index violation caches.
+ */
+ String getPathId();
+
+
+ /**
+ * Returns a display name for the file. This name is used for
+ * reporting and should not be interpreted. It may be relative
+ * to a directory, may use platform-specific path separators,
+ * may not be normalized. Use {@link #getPathId()} when you
+ * want an identifier.
+ */
+ String getDisplayName();
+
+
+ /**
+ * Reads the contents of the underlying character source.
+ *
+ * @return The most up-to-date content
+ *
+ * @throws IOException If this instance is closed
+ * @throws IOException If reading causes an IOException
+ */
+ String readContents() throws IOException;
+
+ /**
+ * Compatibility with {@link DataSource} (pmd internals still use DataSource in PMD 6).
+ */
+ @Deprecated
+ DataSource toDataSourceCompat();
+
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/internal/LanguageDiscoverer.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/internal/LanguageDiscoverer.java
new file mode 100644
index 0000000000..0544cc175c
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/internal/LanguageDiscoverer.java
@@ -0,0 +1,63 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.document.internal;
+
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+import net.sourceforge.pmd.lang.Language;
+import net.sourceforge.pmd.lang.LanguageRegistry;
+
+/**
+ * Discovers the languages applicable to a file.
+ */
+public class LanguageDiscoverer {
+
+
+ private final Language forcedLanguage;
+
+ /**
+ * Build a new instance.
+ *
+ * @param forcedLanguage If non-null, all files will be assigned this language.
+ */
+ public LanguageDiscoverer(Language forcedLanguage) {
+ this.forcedLanguage = forcedLanguage;
+ }
+
+ /**
+ * Get the Languages of a given source file.
+ *
+ * @param sourceFile The file.
+ *
+ * @return The Languages for the source file, may be empty.
+ */
+ public List getLanguagesForFile(Path sourceFile) {
+ return getLanguagesForFile(sourceFile.getFileName().toString());
+ }
+
+ /**
+ * Get the Languages of a given source file.
+ *
+ * @param fileName The file name.
+ *
+ * @return The Languages for the source file, may be empty.
+ */
+ public List getLanguagesForFile(String fileName) {
+ if (forcedLanguage != null) {
+ return Collections.singletonList(forcedLanguage);
+ }
+ String extension = getExtension(fileName);
+ return LanguageRegistry.findByExtension(extension);
+ }
+
+ // Get the extensions from a file
+ private String getExtension(String fileName) {
+ return StringUtils.substringAfterLast(fileName, ".");
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java
index 29382142a4..87df7b0eb6 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java
@@ -82,7 +82,6 @@ abstract class PmdRunnable implements Runnable {
if (e instanceof Error && !SystemProps.isErrorRecoveryMode()) { // NOPMD:
throw e;
}
- configuration.getAnalysisCache().analysisFailed(file);
// The listener handles logging if needed,
// it may also rethrow the error, as a FileAnalysisException (which we let through below)
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/RendererFactory.java b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/RendererFactory.java
index 05385eff45..226f2737e7 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/RendererFactory.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/RendererFactory.java
@@ -15,6 +15,7 @@ import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import net.sourceforge.pmd.internal.util.AssertionUtil;
import net.sourceforge.pmd.properties.PropertyDescriptor;
/**
@@ -61,6 +62,7 @@ public final class RendererFactory {
* @return A Renderer instance.
*/
public static Renderer createRenderer(String reportFormat, Properties properties) {
+ AssertionUtil.requireParamNotNull("reportFormat", reportFormat);
Class extends Renderer> rendererClass = getRendererClass(reportFormat);
Constructor extends Renderer> constructor = getRendererConstructor(rendererClass);
@@ -101,6 +103,7 @@ public final class RendererFactory {
@SuppressWarnings("unchecked")
private static Class extends Renderer> getRendererClass(String reportFormat) {
+ AssertionUtil.requireParamNotNull("reportFormat", reportFormat);
Class extends Renderer> rendererClass = REPORT_FORMAT_TO_RENDERER.get(reportFormat);
// Look up a custom renderer class
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/GlobalAnalysisListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/GlobalAnalysisListener.java
index ca71b7a608..c2a8f7d586 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/GlobalAnalysisListener.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/GlobalAnalysisListener.java
@@ -85,17 +85,7 @@ public interface GlobalAnalysisListener extends AutoCloseable {
* A listener that does nothing.
*/
static GlobalAnalysisListener noop() {
- return new GlobalAnalysisListener() {
- @Override
- public FileAnalysisListener startFileAnalysis(DataSource file) {
- return FileAnalysisListener.noop();
- }
-
- @Override
- public void close() {
- // do nothing
- }
- };
+ return NoopAnalysisListener.INSTANCE;
}
/**
@@ -111,10 +101,11 @@ public interface GlobalAnalysisListener extends AutoCloseable {
*/
static GlobalAnalysisListener tee(Collection extends GlobalAnalysisListener> listeners) {
AssertionUtil.requireParamNotNull("Listeners", listeners);
- AssertionUtil.requireNotEmpty("Listeners", listeners);
AssertionUtil.requireContainsNoNullValue("Listeners", listeners);
- if (listeners.size() == 1) {
+ if (listeners.isEmpty()) {
+ return noop();
+ } else if (listeners.size() == 1) {
return listeners.iterator().next();
}
@@ -150,6 +141,7 @@ public interface GlobalAnalysisListener extends AutoCloseable {
List myList =
listeners.stream()
.flatMap(l -> l instanceof TeeListener ? ((TeeListener) l).myList.stream() : Stream.of(l))
+ .filter(l -> !(l instanceof NoopAnalysisListener))
.collect(CollectionUtil.toUnmodifiableList());
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/NoopAnalysisListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/NoopAnalysisListener.java
new file mode 100644
index 0000000000..fcd5da121c
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/NoopAnalysisListener.java
@@ -0,0 +1,29 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.reporting;
+
+import net.sourceforge.pmd.util.datasource.DataSource;
+
+/**
+ * @author ClΓ©ment Fournier
+ */
+final class NoopAnalysisListener implements GlobalAnalysisListener {
+
+ static final NoopAnalysisListener INSTANCE = new NoopAnalysisListener();
+
+ private NoopAnalysisListener() {
+
+ }
+
+ @Override
+ public FileAnalysisListener startFileAnalysis(DataSource file) {
+ return FileAnalysisListener.noop();
+ }
+
+ @Override
+ public void close() {
+ // do nothing
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStats.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStats.java
new file mode 100644
index 0000000000..eef7cd7dc5
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStats.java
@@ -0,0 +1,38 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.reporting;
+
+/**
+ * Summarized info about a report.
+ *
+ * @author ClΓ©ment Fournier
+ */
+public final class ReportStats {
+
+ private final int numErrors;
+ private final int numViolations;
+
+ ReportStats(int numErrors, int numViolations) {
+ this.numErrors = numErrors;
+ this.numViolations = numViolations;
+ }
+
+ public static ReportStats empty() {
+ return new ReportStats(0, 0);
+ }
+
+ public int getNumErrors() {
+ return numErrors;
+ }
+
+ public int getNumViolations() {
+ return numViolations;
+ }
+
+ @Override
+ public String toString() {
+ return "ReportStats{numErrors=" + numErrors + ", numViolations=" + numViolations + '}';
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStatsListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStatsListener.java
new file mode 100644
index 0000000000..baabb2d12b
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ReportStatsListener.java
@@ -0,0 +1,63 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.reporting;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import net.sourceforge.pmd.Report.ProcessingError;
+import net.sourceforge.pmd.RuleViolation;
+import net.sourceforge.pmd.util.BaseResultProducingCloseable;
+import net.sourceforge.pmd.util.datasource.DataSource;
+
+/**
+ * Collects summarized info about a PMD run.
+ *
+ * @author ClΓ©ment Fournier
+ */
+public final class ReportStatsListener extends BaseResultProducingCloseable implements GlobalAnalysisListener {
+
+ private final AtomicInteger numErrors = new AtomicInteger(0);
+ private final AtomicInteger numViolations = new AtomicInteger(0);
+
+ @Override
+ public FileAnalysisListener startFileAnalysis(DataSource file) {
+ return new FileAnalysisListener() {
+ // this object does not need thread-safety so we avoid using atomics,
+ // except during the merge.
+ private int numErrors = 0;
+ private int numViolations = 0;
+
+ @Override
+ public void onRuleViolation(RuleViolation violation) {
+ numViolations++;
+ }
+
+ @Override
+ public void onError(ProcessingError error) {
+ numErrors++;
+ }
+
+ @Override
+ public void close() {
+ if (numErrors > 0) {
+ ReportStatsListener.this.numErrors.addAndGet(this.numErrors);
+ }
+ if (numViolations > 0) {
+ ReportStatsListener.this.numViolations.addAndGet(this.numViolations);
+ }
+ }
+ };
+ }
+
+ @Override
+ protected ReportStats getResultImpl() {
+ return new ReportStats(
+ numErrors.get(),
+ numViolations.get()
+ );
+ }
+
+
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java
index 47313a12f7..beabbc6080 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java
@@ -75,7 +75,7 @@ public class RuleBuilder {
Language lang = LanguageRegistry.findLanguageByTerseName(languageName);
if (lang == null) {
throw new IllegalArgumentException(
- "Unknown Language '" + languageName + "' for rule" + name + ", supported Languages are "
+ "Unknown Language '" + languageName + "' for rule " + name + ", supported Languages are "
+ LanguageRegistry.getLanguages().stream().map(Language::getTerseName).collect(Collectors.joining(", "))
);
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/ClasspathClassLoader.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/ClasspathClassLoader.java
index 6e31ee3986..0266574901 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/util/ClasspathClassLoader.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/ClasspathClassLoader.java
@@ -20,6 +20,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sourceforge.pmd.annotation.InternalApi;
+import net.sourceforge.pmd.internal.util.AssertionUtil;
/**
* Create a ClassLoader which loads classes using a CLASSPATH like String. If
@@ -57,17 +58,19 @@ public class ClasspathClassLoader extends URLClassLoader {
return urlList.toArray(new URL[0]);
}
- private static URL[] initURLs(String classpath) throws IOException {
- if (classpath == null) {
- throw new IllegalArgumentException("classpath argument cannot be null");
- }
+ private static URL[] initURLs(String classpath) {
+ AssertionUtil.requireParamNotNull("classpath", classpath);
final List urls = new ArrayList<>();
- if (classpath.startsWith("file:")) {
- // Treat as file URL
- addFileURLs(urls, new URL(classpath));
- } else {
- // Treat as classpath
- addClasspathURLs(urls, classpath);
+ try {
+ if (classpath.startsWith("file:")) {
+ // Treat as file URL
+ addFileURLs(urls, new URL(classpath));
+ } else {
+ // Treat as classpath
+ addClasspathURLs(urls, classpath);
+ }
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Cannot prepend classpath " + classpath + "\n" + e.getMessage(), e);
}
return urls.toArray(new URL[0]);
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java
index 28f1aa886c..38484692ed 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/FileUtil.java
@@ -99,7 +99,7 @@ public final class FileUtil {
}
private static List collect(List dataSources, String fileLocation,
- FilenameFilter filenameFilter) {
+ FilenameFilter filenameFilter) {
File file = new File(fileLocation);
if (!file.exists()) {
throw new RuntimeException("File " + file.getName() + " doesn't exist");
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/StringUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/StringUtil.java
index d25bf1027a..cde70a9b78 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/util/StringUtil.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/StringUtil.java
@@ -4,6 +4,7 @@
package net.sourceforge.pmd.util;
+import java.text.MessageFormat;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
@@ -513,6 +514,13 @@ public final class StringUtil {
return retval.toString();
}
+ /**
+ * Escape the string so that it appears literally when interpreted
+ * by a {@link MessageFormat}.
+ */
+ public static String quoteMessageFormat(String str) {
+ return str.replaceAll("'", "''");
+ }
public enum CaseConvention {
/** SCREAMING_SNAKE_CASE. */
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/MessageReporter.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/MessageReporter.java
new file mode 100644
index 0000000000..689e4f41c4
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/MessageReporter.java
@@ -0,0 +1,69 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.util.log;
+
+import java.text.MessageFormat;
+
+import org.slf4j.event.Level;
+
+import net.sourceforge.pmd.annotation.InternalApi;
+
+/**
+ * Façade to report user-facing messages (info, warning and error).
+ * Note: messages are formatted using {@link MessageFormat}.
+ *
+ * Internal API: this is a transitional API that will be significantly
+ * changed in PMD 7, with the transition to SLF4J. See https://github.com/pmd/pmd/issues/3816
+ *
+ * TODO rename to PmdReporter
+ *
+ * @author ClΓ©ment Fournier
+ */
+@InternalApi
+public interface MessageReporter {
+
+ boolean isLoggable(Level level);
+
+ void log(Level level, String message, Object... formatArgs);
+
+ void logEx(Level level, String message, Object[] formatArgs, Throwable error);
+
+ default void info(String message, Object... formatArgs) {
+ log(Level.INFO, message, formatArgs);
+ }
+
+ default void warn(String message, Object... formatArgs) {
+ log(Level.WARN, message, formatArgs);
+ }
+
+ default void warnEx(String message, Throwable error) {
+ logEx(Level.WARN, message, new Object[0], error);
+ }
+
+ default void warnEx(String message, Object[] formatArgs, Throwable error) {
+ logEx(Level.WARN, message, formatArgs, error);
+ }
+
+ default void error(String message, Object... formatArgs) {
+ log(Level.ERROR, message, formatArgs);
+ }
+
+ default void errorEx(String message, Throwable error) {
+ logEx(Level.ERROR, message, new Object[0], error);
+ }
+
+ default void errorEx(String message, Object[] formatArgs, Throwable error) {
+ logEx(Level.ERROR, message, formatArgs, error);
+ }
+
+ /**
+ * Returns the number of errors reported on this instance.
+ * Any call to {@link #log(Level, String, Object...)} or
+ * {@link #logEx(Level, String, Object[], Throwable)} with a level
+ * of {@link Level#ERROR} should increment this number.
+ */
+ int numErrors();
+
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/ErrorsAsWarningsReporter.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/ErrorsAsWarningsReporter.java
new file mode 100644
index 0000000000..80270d9163
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/ErrorsAsWarningsReporter.java
@@ -0,0 +1,41 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.util.log.internal;
+
+import org.slf4j.event.Level;
+
+import net.sourceforge.pmd.annotation.InternalApi;
+import net.sourceforge.pmd.util.log.MessageReporter;
+
+/**
+ * Turns errors into warnings reported on another logger.
+ *
+ * @author ClΓ©ment Fournier
+ */
+@InternalApi
+public final class ErrorsAsWarningsReporter extends MessageReporterBase {
+
+ private final MessageReporter backend;
+
+ public ErrorsAsWarningsReporter(MessageReporter backend) {
+ this.backend = backend;
+ }
+
+ @Override
+ protected boolean isLoggableImpl(Level level) {
+ if (level == Level.ERROR) {
+ level = Level.WARN;
+ }
+ return super.isLoggableImpl(level);
+ }
+
+ @Override
+ protected void logImpl(Level level, String message, Object[] formatArgs) {
+ if (level == Level.ERROR) {
+ level = Level.WARN;
+ }
+ backend.log(level, message, formatArgs);
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/MessageReporterBase.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/MessageReporterBase.java
new file mode 100644
index 0000000000..64f045174d
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/MessageReporterBase.java
@@ -0,0 +1,80 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.util.log.internal;
+
+import java.text.MessageFormat;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.slf4j.event.Level;
+
+import net.sourceforge.pmd.util.StringUtil;
+import net.sourceforge.pmd.util.log.MessageReporter;
+
+/**
+ * Base implementation.
+ *
+ * @author ClΓ©ment Fournier
+ */
+abstract class MessageReporterBase implements MessageReporter {
+
+ private int numErrors;
+ private Level minLevel = Level.TRACE;
+
+ /**
+ * null level means off.
+ */
+ public final void setLevel(Level minLevel) {
+ this.minLevel = minLevel;
+ }
+
+ @Override
+ public final boolean isLoggable(Level level) {
+ return minLevel != null
+ && minLevel.compareTo(level) >= 0
+ && isLoggableImpl(level);
+ }
+
+ protected boolean isLoggableImpl(Level level) {
+ return true;
+ }
+
+ @Override
+ public void logEx(Level level, String message, Object[] formatArgs, Throwable error) {
+ if (isLoggable(level)) {
+ message = MessageFormat.format(message, formatArgs);
+ String errorMessage = error.getMessage();
+ if (errorMessage == null) {
+ errorMessage = error.getClass().getSimpleName();
+ }
+ errorMessage = StringUtil.quoteMessageFormat(errorMessage);
+ log(level, message + ": " + errorMessage);
+ if (isLoggable(Level.DEBUG)) {
+ String stackTrace = StringUtil.quoteMessageFormat(ExceptionUtils.getStackTrace(error));
+ log(Level.DEBUG, stackTrace);
+ }
+ }
+ }
+
+ @Override
+ public final void log(Level level, String message, Object... formatArgs) {
+ if (level == Level.ERROR) {
+ this.numErrors++;
+ }
+ if (isLoggable(level)) {
+ logImpl(level, message, formatArgs);
+ }
+ }
+
+ /**
+ * Perform logging assuming {@link #isLoggable(Level)} is true.
+ */
+ protected abstract void logImpl(Level level, String message, Object[] formatArgs);
+
+
+ @Override
+ public int numErrors() {
+ return numErrors;
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/NoopReporter.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/NoopReporter.java
new file mode 100644
index 0000000000..98eb4a72a3
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/NoopReporter.java
@@ -0,0 +1,31 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.util.log.internal;
+
+import org.slf4j.event.Level;
+
+import net.sourceforge.pmd.annotation.InternalApi;
+import net.sourceforge.pmd.util.log.MessageReporter;
+
+/**
+ * A logger that ignores all messages.
+ *
+ * @author ClΓ©ment Fournier
+ */
+@InternalApi
+public final class NoopReporter extends MessageReporterBase implements MessageReporter {
+
+ // note: not singleton because PmdLogger accumulates error count.
+
+ @Override
+ protected boolean isLoggableImpl(Level level) {
+ return false;
+ }
+
+ @Override
+ protected void logImpl(Level level, String message, Object[] formatArgs) {
+ // noop
+ }
+}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/SimpleMessageReporter.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/SimpleMessageReporter.java
new file mode 100644
index 0000000000..672e40df98
--- /dev/null
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/log/internal/SimpleMessageReporter.java
@@ -0,0 +1,36 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.util.log.internal;
+
+import org.slf4j.Logger;
+import org.slf4j.event.Level;
+
+import net.sourceforge.pmd.annotation.InternalApi;
+import net.sourceforge.pmd.util.log.MessageReporter;
+
+/**
+ * A {@link Logger} (java.util) based logger impl.
+ *
+ * @author ClΓ©ment Fournier
+ */
+@InternalApi
+public class SimpleMessageReporter extends MessageReporterBase implements MessageReporter {
+
+ private final Logger backend;
+
+ public SimpleMessageReporter(Logger backend) {
+ this.backend = backend;
+ }
+
+ @Override
+ protected boolean isLoggableImpl(Level level) {
+ return backend.isEnabledForLevel(level);
+ }
+
+ @Override
+ protected void logImpl(Level level, String message, Object[] formatArgs) {
+ backend.atLevel(level).log(message, formatArgs);
+ }
+}
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/PmdAnalysisTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/PmdAnalysisTest.java
new file mode 100644
index 0000000000..5c437fc1a9
--- /dev/null
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/PmdAnalysisTest.java
@@ -0,0 +1,75 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+
+import org.junit.Test;
+import org.mockito.ArgumentMatchers;
+
+import net.sourceforge.pmd.renderers.Renderer;
+
+/**
+ * @author ClΓ©ment Fournier
+ */
+public class PmdAnalysisTest {
+
+ @Test
+ public void testPmdAnalysisWithEmptyConfig() {
+ PMDConfiguration config = new PMDConfiguration();
+ try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
+ assertThat(pmd.files().getCollectedFiles(), empty());
+ assertThat(pmd.rulesets(), empty());
+ assertThat(pmd.renderers(), empty());
+ }
+ }
+
+ @Test
+ public void testRendererInteractions() throws IOException {
+ PMDConfiguration config = new PMDConfiguration();
+ config.setInputPaths("sample-source/dummy");
+ Renderer renderer = spy(Renderer.class);
+ try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
+ pmd.addRenderer(renderer);
+ verify(renderer, never()).start();
+ pmd.performAnalysis();
+ }
+
+ verify(renderer, times(1)).renderFileReport(ArgumentMatchers.any());
+ verify(renderer, times(1)).start();
+ verify(renderer, times(1)).end();
+ verify(renderer, times(1)).flush();
+ }
+
+ @Test
+ public void testRulesetLoading() {
+ PMDConfiguration config = new PMDConfiguration();
+ config.addRuleSet("rulesets/dummy/basic.xml");
+ try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
+ assertThat(pmd.rulesets(), hasSize(1));
+ }
+ }
+
+ @Test
+ public void testRulesetWhenSomeoneHasAnError() {
+ PMDConfiguration config = new PMDConfiguration();
+ config.addRuleSet("rulesets/dummy/basic.xml");
+ config.addRuleSet("rulesets/xxxe/notaruleset.xml");
+ try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
+ assertThat(pmd.rulesets(), hasSize(1)); // no failure
+ assertThat(pmd.getReporter().numErrors(), equalTo(1));
+ }
+ }
+
+}
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/PmdConfigurationTest.java
similarity index 84%
rename from pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java
rename to pmd-core/src/test/java/net/sourceforge/pmd/PmdConfigurationTest.java
index d765920004..99c321530f 100644
--- a/pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/PmdConfigurationTest.java
@@ -4,9 +4,13 @@
package net.sourceforge.pmd;
+import static net.sourceforge.pmd.util.CollectionUtil.listOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -16,6 +20,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.util.Collections;
import java.util.Properties;
import org.junit.Assert;
@@ -29,7 +34,7 @@ import net.sourceforge.pmd.renderers.CSVRenderer;
import net.sourceforge.pmd.renderers.Renderer;
import net.sourceforge.pmd.util.ClasspathClassLoader;
-public class ConfigurationTest {
+public class PmdConfigurationTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@@ -37,7 +42,7 @@ public class ConfigurationTest {
@Test
public void testSuppressMarker() {
PMDConfiguration configuration = new PMDConfiguration();
- assertEquals("Default suppress marker", PMD.SUPPRESS_MARKER, configuration.getSuppressMarker());
+ assertEquals("Default suppress marker", PMDConfiguration.DEFAULT_SUPPRESS_MARKER, configuration.getSuppressMarker());
configuration.setSuppressMarker("CUSTOM_MARKER");
assertEquals("Changed suppress marker", "CUSTOM_MARKER", configuration.getSuppressMarker());
}
@@ -51,10 +56,10 @@ public class ConfigurationTest {
}
@Test
- public void testClassLoader() throws IOException {
+ public void testClassLoader() {
PMDConfiguration configuration = new PMDConfiguration();
assertEquals("Default ClassLoader", PMDConfiguration.class.getClassLoader(), configuration.getClassLoader());
- configuration.prependClasspath("some.jar");
+ configuration.prependAuxClasspath("some.jar");
assertEquals("Prepended ClassLoader class", ClasspathClassLoader.class,
configuration.getClassLoader().getClass());
URL[] urls = ((ClasspathClassLoader) configuration.getClassLoader()).getURLs();
@@ -68,31 +73,31 @@ public class ConfigurationTest {
}
@Test
- public void auxClasspathWithRelativeFileEmpty() throws IOException {
+ public void auxClasspathWithRelativeFileEmpty() {
String relativeFilePath = "src/test/resources/net/sourceforge/pmd/cli/auxclasspath-empty.cp";
PMDConfiguration configuration = new PMDConfiguration();
- configuration.prependClasspath("file:" + relativeFilePath);
+ configuration.prependAuxClasspath("file:" + relativeFilePath);
URL[] urls = ((ClasspathClassLoader) configuration.getClassLoader()).getURLs();
Assert.assertEquals(0, urls.length);
}
@Test
- public void auxClasspathWithRelativeFileEmpty2() throws IOException {
+ public void auxClasspathWithRelativeFileEmpty2() {
String relativeFilePath = "./src/test/resources/net/sourceforge/pmd/cli/auxclasspath-empty.cp";
PMDConfiguration configuration = new PMDConfiguration();
- configuration.prependClasspath("file:" + relativeFilePath);
+ configuration.prependAuxClasspath("file:" + relativeFilePath);
URL[] urls = ((ClasspathClassLoader) configuration.getClassLoader()).getURLs();
Assert.assertEquals(0, urls.length);
}
@Test
- public void auxClasspathWithRelativeFile() throws IOException, URISyntaxException {
+ public void auxClasspathWithRelativeFile() throws URISyntaxException {
final String FILE_SCHEME = "file";
String currentWorkingDirectory = new File("").getAbsoluteFile().toURI().getPath();
String relativeFilePath = "src/test/resources/net/sourceforge/pmd/cli/auxclasspath.cp";
PMDConfiguration configuration = new PMDConfiguration();
- configuration.prependClasspath("file:" + relativeFilePath);
+ configuration.prependAuxClasspath("file:" + relativeFilePath);
URL[] urls = ((ClasspathClassLoader) configuration.getClassLoader()).getURLs();
URI[] uris = new URI[urls.length];
for (int i = 0; i < urls.length; i++) {
@@ -111,6 +116,30 @@ public class ConfigurationTest {
Assert.assertArrayEquals(expectedUris, uris);
}
+ @Test
+ public void testRuleSetsLegacy() {
+ PMDConfiguration configuration = new PMDConfiguration();
+ assertNull("Default RuleSets", configuration.getRuleSets());
+ configuration.setRuleSets("/rulesets/basic.xml");
+ assertEquals("Changed RuleSets", "/rulesets/basic.xml", configuration.getRuleSets());
+ configuration.setRuleSets((String) null);
+ assertNull(configuration.getRuleSets());
+ }
+
+ @Test
+ public void testRuleSets() {
+ PMDConfiguration configuration = new PMDConfiguration();
+ assertThat(configuration.getRuleSetPaths(), empty());
+ configuration.setRuleSets(listOf("/rulesets/basic.xml"));
+ assertEquals(listOf("/rulesets/basic.xml"), configuration.getRuleSetPaths());
+ configuration.addRuleSet("foo.xml");
+ assertEquals(listOf("/rulesets/basic.xml", "foo.xml"), configuration.getRuleSetPaths());
+ configuration.setRuleSets(Collections.emptyList());
+ assertThat(configuration.getRuleSetPaths(), empty());
+ // should be addable even though we set it to an unmodifiable empty list
+ configuration.addRuleSet("foo.xml");
+ assertEquals(listOf("foo.xml"), configuration.getRuleSetPaths());
+ }
@Test
public void testMinimumPriority() {
@@ -232,7 +261,7 @@ public class ConfigurationTest {
}
@Test
- public void testAnalysisCacheLocation() throws IOException {
+ public void testAnalysisCacheLocation() {
final PMDConfiguration configuration = new PMDConfiguration();
configuration.setAnalysisCacheLocation(null);
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java
index c7bfe16b0c..81acee181d 100644
--- a/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java
@@ -91,7 +91,7 @@ public class PMDTaskTest {
try (InputStream in = new FileInputStream("target/pmd-ant-test.txt")) {
String actual = IOUtils.toString(in, StandardCharsets.UTF_8);
// remove any trailing newline
- actual = actual.replaceAll("\n|\r", "");
+ actual = actual.trim();
Assert.assertEquals("sample.dummy:1:\tSampleXPathRule:\tTest Rule 2", actual);
}
}
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cache/FileAnalysisCacheTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cache/FileAnalysisCacheTest.java
index 78bbb13f4f..a81cbb55b5 100644
--- a/pmd-core/src/test/java/net/sourceforge/pmd/cache/FileAnalysisCacheTest.java
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/cache/FileAnalysisCacheTest.java
@@ -82,21 +82,21 @@ public class FileAnalysisCacheTest {
}
@Test
- public void testStoreCreatesFile() {
+ public void testStoreCreatesFile() throws Exception {
final FileAnalysisCache cache = new FileAnalysisCache(unexistingCacheFile);
cache.persist();
assertTrue("Cache file doesn't exist after store", unexistingCacheFile.exists());
}
@Test
- public void testStoreOnUnwritableFileShouldntThrow() {
+ public void testStoreOnUnwritableFileShouldntThrow() throws IOException {
emptyCacheFile.setWritable(false);
final FileAnalysisCache cache = new FileAnalysisCache(emptyCacheFile);
cache.persist();
}
@Test
- public void testStorePersistsFilesWithViolations() {
+ public void testStorePersistsFilesWithViolations() throws IOException {
final FileAnalysisCache cache = new FileAnalysisCache(newCacheFile);
cache.checkValidity(mock(RuleSets.class), mock(ClassLoader.class));
cache.isUpToDate(sourceFile);
@@ -107,7 +107,9 @@ public class FileAnalysisCacheTest {
when(rule.getLanguage()).thenReturn(mock(Language.class));
when(rv.getRule()).thenReturn(rule);
- cache.startFileAnalysis(mock(DataSource.class)).onRuleViolation(rv);
+ DataSource ds = mock(DataSource.class);
+ when(ds.getNiceFileName(false, "")).thenReturn(sourceFile.getPath());
+ cache.startFileAnalysis(ds).onRuleViolation(rv);
cache.persist();
final FileAnalysisCache reloadedCache = new FileAnalysisCache(newCacheFile);
@@ -120,7 +122,7 @@ public class FileAnalysisCacheTest {
}
@Test
- public void testCacheValidityWithNoChanges() {
+ public void testCacheValidityWithNoChanges() throws IOException {
final RuleSets rs = mock(RuleSets.class);
final ClassLoader cl = mock(ClassLoader.class);
@@ -150,7 +152,7 @@ public class FileAnalysisCacheTest {
}
@Test
- public void testRulesetChangeInvalidatesCache() {
+ public void testRulesetChangeInvalidatesCache() throws IOException {
final RuleSets rs = mock(RuleSets.class);
final ClassLoader cl = mock(ClassLoader.class);
@@ -367,7 +369,7 @@ public class FileAnalysisCacheTest {
}
private void setupCacheWithFiles(final File cacheFile, final RuleSets ruleSets,
- final ClassLoader classLoader, final File... files) {
+ final ClassLoader classLoader, final File... files) throws IOException {
// Setup a cache file with an entry for an empty Source.java with no violations
final FileAnalysisCache cache = new FileAnalysisCache(cacheFile);
cache.checkValidity(ruleSets, classLoader);
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cli/CoreCliTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cli/CoreCliTest.java
index ed7078f2af..47106eb626 100644
--- a/pmd-core/src/test/java/net/sourceforge/pmd/cli/CoreCliTest.java
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/cli/CoreCliTest.java
@@ -5,6 +5,7 @@
package net.sourceforge.pmd.cli;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.containsStringIgnoringCase;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
@@ -19,6 +20,7 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
+import org.apache.commons.io.IOUtils;
import org.hamcrest.Matcher;
import org.junit.AfterClass;
import org.junit.Before;
@@ -124,6 +126,19 @@ public class CoreCliTest {
assertTrue("Report file should have been created", Files.exists(reportFile));
}
+ @Test
+ public void testFileCollectionWithUnknownFiles() throws IOException {
+ Path reportFile = tempRoot().resolve("out/reportFile.txt");
+ Files.createFile(srcDir.resolve("foo.not_analysable"));
+ assertFalse("Report file should not exist", Files.exists(reportFile));
+
+ runPmdSuccessfully("--no-cache", "--dir", srcDir, "--rulesets", DUMMY_RULESET, "--report-file", reportFile, "--debug");
+
+ assertTrue("Report file should have been created", Files.exists(reportFile));
+ String reportText = IOUtils.toString(Files.newBufferedReader(reportFile, StandardCharsets.UTF_8));
+ assertThat(reportText, not(containsStringIgnoringCase("error")));
+ }
+
@Test
public void testNonExistentReportFileDeprecatedOptions() {
Path reportFile = tempRoot().resolve("out/reportFile.txt");
@@ -180,13 +195,13 @@ public class CoreCliTest {
@Test
public void debugLogging() {
runPmdSuccessfully("--debug", "--no-cache", "--dir", srcDir, "--rulesets", DUMMY_RULESET);
- errStreamCaptor.getLog().contains("[main] DEBUG net.sourceforge.pmd.PMD - Log level is at DEBUG");
+ assertThat(errStreamCaptor.getLog(), containsString("[main] INFO net.sourceforge.pmd.PMD - Log level is at TRACE"));
}
@Test
public void defaultLogging() {
runPmdSuccessfully("--no-cache", "--dir", srcDir, "--rulesets", DUMMY_RULESET);
- errStreamCaptor.getLog().contains("[main] INFO net.sourceforge.pmd.PMD - Log level is at INFO");
+ assertThat(errStreamCaptor.getLog(), containsString("[main] INFO net.sourceforge.pmd.PMD - Log level is at INFO"));
}
@@ -240,8 +255,8 @@ public class CoreCliTest {
}
private static void runPmd(int expectedExitCode, Object[] args) {
- int actualExitCode = PMD.run(argsToString(args));
- assertEquals("Exit code", expectedExitCode, actualExitCode);
+ StatusCode actualExitCode = PMD.runPmd(argsToString(args));
+ assertEquals("Exit code", expectedExitCode, actualExitCode.toInt());
}
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/cli/PMDFilelistTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/cli/PMDFilelistTest.java
index d93dbd738b..e0e5d2d8e6 100644
--- a/pmd-core/src/test/java/net/sourceforge/pmd/cli/PMDFilelistTest.java
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/cli/PMDFilelistTest.java
@@ -4,93 +4,85 @@
package net.sourceforge.pmd.cli;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.hasSize;
-import org.junit.Assert;
+import java.io.IOException;
+import java.util.List;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.junit.Test;
-import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.PMDConfiguration;
-import net.sourceforge.pmd.lang.DummyLanguageModule;
-import net.sourceforge.pmd.lang.Language;
-import net.sourceforge.pmd.util.datasource.DataSource;
+import net.sourceforge.pmd.internal.util.FileCollectionUtil;
+import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
+import net.sourceforge.pmd.lang.document.FileCollector;
+import net.sourceforge.pmd.lang.document.TextFile;
+import net.sourceforge.pmd.util.log.internal.NoopReporter;
public class PMDFilelistTest {
+
+ private static @NonNull FileCollector newCollector() {
+ return FileCollector.newCollector(new LanguageVersionDiscoverer(), new NoopReporter());
+ }
+
@Test
public void testGetApplicableFiles() throws IOException {
- Set languages = new HashSet<>();
- languages.add(new DummyLanguageModule());
+ FileCollector collector = newCollector();
- PMDConfiguration configuration = new PMDConfiguration();
- configuration.setInputFilePath("src/test/resources/net/sourceforge/pmd/cli/filelist.txt");
+ FileCollectionUtil.collectFileList(collector, "src/test/resources/net/sourceforge/pmd/cli/filelist.txt");
- List applicableFiles = PMD.getApplicableFiles(configuration, languages);
- Assert.assertEquals(2, applicableFiles.size());
- Assert.assertTrue(applicableFiles.get(0).getNiceFileName(false, "").endsWith("somefile.dummy"));
- Assert.assertTrue(applicableFiles.get(1).getNiceFileName(false, "").endsWith("anotherfile.dummy"));
+ List applicableFiles = collector.getCollectedFiles();
+ assertThat(applicableFiles, hasSize(2));
+ assertThat(applicableFiles.get(0).getPathId(), endsWith("anotherfile.dummy"));
+ assertThat(applicableFiles.get(1).getPathId(), endsWith("somefile.dummy"));
}
@Test
public void testGetApplicableFilesMultipleLines() throws IOException {
- Set languages = new HashSet<>();
- languages.add(new DummyLanguageModule());
+ FileCollector collector = newCollector();
- PMDConfiguration configuration = new PMDConfiguration();
- configuration.setInputFilePath("src/test/resources/net/sourceforge/pmd/cli/filelist2.txt");
+ FileCollectionUtil.collectFileList(collector, "src/test/resources/net/sourceforge/pmd/cli/filelist2.txt");
- List applicableFiles = PMD.getApplicableFiles(configuration, languages);
- Assert.assertEquals(3, applicableFiles.size());
- Assert.assertTrue(applicableFiles.get(0).getNiceFileName(false, "").endsWith("somefile.dummy"));
- Assert.assertTrue(applicableFiles.get(1).getNiceFileName(false, "").endsWith("anotherfile.dummy"));
- Assert.assertTrue(applicableFiles.get(2).getNiceFileName(false, "").endsWith("somefile.dummy"));
+ List applicableFiles = collector.getCollectedFiles();
+ assertThat(applicableFiles, hasSize(3));
+ assertThat(applicableFiles.get(0).getPathId(), endsWith("anotherfile.dummy"));
+ assertThat(applicableFiles.get(1).getPathId(), endsWith("somefile.dummy"));
+ assertThat(applicableFiles.get(2).getPathId(), endsWith("somefile.dummy"));
}
@Test
- public void testGetApplicatbleFilesWithIgnores() throws IOException {
- Set languages = new HashSet<>();
- languages.add(new DummyLanguageModule());
+ public void testGetApplicableFilesWithIgnores() throws IOException {
+ FileCollector collector = newCollector();
PMDConfiguration configuration = new PMDConfiguration();
configuration.setInputFilePath("src/test/resources/net/sourceforge/pmd/cli/filelist3.txt");
configuration.setIgnoreFilePath("src/test/resources/net/sourceforge/pmd/cli/ignorelist.txt");
+ FileCollectionUtil.collectFiles(configuration, collector);
- List applicableFiles = PMD.getApplicableFiles(configuration, languages);
- Assert.assertEquals(2, applicableFiles.size());
- Assert.assertTrue(applicableFiles.get(0).getNiceFileName(false, "").endsWith("somefile2.dummy"));
- Assert.assertTrue(applicableFiles.get(1).getNiceFileName(false, "").endsWith("somefile4.dummy"));
+ List applicableFiles = collector.getCollectedFiles();
+ assertThat(applicableFiles, hasSize(2));
+ assertThat(applicableFiles.get(0).getPathId(), endsWith("somefile2.dummy"));
+ assertThat(applicableFiles.get(1).getPathId(), endsWith("somefile4.dummy"));
}
@Test
- public void testGetApplicatbleFilesWithDirAndIgnores() throws IOException {
- Set languages = new HashSet<>();
- languages.add(new DummyLanguageModule());
+ public void testGetApplicableFilesWithDirAndIgnores() throws IOException {
PMDConfiguration configuration = new PMDConfiguration();
configuration.setInputPaths("src/test/resources/net/sourceforge/pmd/cli/src");
configuration.setIgnoreFilePath("src/test/resources/net/sourceforge/pmd/cli/ignorelist.txt");
- List applicableFiles = PMD.getApplicableFiles(configuration, languages);
- Assert.assertEquals(4, applicableFiles.size());
- Collections.sort(applicableFiles, new Comparator() {
- @Override
- public int compare(DataSource o1, DataSource o2) {
- if (o1 == null && o2 != null) {
- return -1;
- } else if (o1 != null && o2 == null) {
- return 1;
- } else {
- return o1.getNiceFileName(false, "").compareTo(o2.getNiceFileName(false, ""));
- }
- }
- });
- Assert.assertTrue(applicableFiles.get(0).getNiceFileName(false, "").endsWith("anotherfile.dummy"));
- Assert.assertTrue(applicableFiles.get(1).getNiceFileName(false, "").endsWith("somefile.dummy"));
- Assert.assertTrue(applicableFiles.get(2).getNiceFileName(false, "").endsWith("somefile2.dummy"));
- Assert.assertTrue(applicableFiles.get(3).getNiceFileName(false, "").endsWith("somefile4.dummy"));
+ FileCollector collector = newCollector();
+ FileCollectionUtil.collectFiles(configuration, collector);
+
+ List applicableFiles = collector.getCollectedFiles();
+ assertThat(applicableFiles, hasSize(4));
+ assertThat(applicableFiles.get(0).getPathId(), endsWith("anotherfile.dummy"));
+ assertThat(applicableFiles.get(1).getPathId(), endsWith("somefile.dummy"));
+ assertThat(applicableFiles.get(2).getPathId(), endsWith("somefile2.dummy"));
+ assertThat(applicableFiles.get(3).getPathId(), endsWith("somefile4.dummy"));
}
+
}
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/FileCollectorTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/FileCollectorTest.java
new file mode 100644
index 0000000000..191c004db0
--- /dev/null
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/FileCollectorTest.java
@@ -0,0 +1,144 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.document;
+
+import static net.sourceforge.pmd.util.CollectionUtil.listOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import net.sourceforge.pmd.lang.Language;
+import net.sourceforge.pmd.lang.LanguageRegistry;
+import net.sourceforge.pmd.lang.LanguageVersion;
+import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
+
+/**
+ * @author ClΓ©ment Fournier
+ */
+public class FileCollectorTest {
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Test
+ public void testAddFile() throws IOException {
+ Path root = tempFolder.getRoot().toPath();
+ Path foo = newFile(root, "foo.dummy");
+ Path bar = newFile(root, "bar.unknown");
+
+ FileCollector collector = newCollector();
+
+ assertTrue("should be dummy language", collector.addFile(foo));
+ assertFalse("should be unknown language", collector.addFile(bar));
+
+ assertCollected(collector, listOf("foo.dummy"));
+ }
+
+ @Test
+ public void testAddFileForceLanguage() throws IOException {
+ Path root = tempFolder.getRoot().toPath();
+ Path bar = newFile(root, "bar.unknown");
+
+ Language dummy = LanguageRegistry.findLanguageByTerseName("dummy");
+
+ FileCollector collector = newCollector(dummy.getDefaultVersion());
+
+ assertTrue("should be unknown language", collector.addFile(bar, dummy));
+ assertCollected(collector, listOf("bar.unknown"));
+ assertNoErrors(collector);
+ }
+
+ @Test
+ public void testAddFileNotExists() {
+ Path root = tempFolder.getRoot().toPath();
+
+ FileCollector collector = newCollector();
+
+ assertFalse(collector.addFile(root.resolve("does_not_exist.dummy")));
+ assertEquals(1, collector.getReporter().numErrors());
+ }
+
+ @Test
+ public void testAddFileNotAFile() throws IOException {
+ Path root = tempFolder.getRoot().toPath();
+ Path dir = root.resolve("src");
+ Files.createDirectories(dir);
+
+ FileCollector collector = newCollector();
+ assertFalse(collector.addFile(dir));
+ assertEquals(1, collector.getReporter().numErrors());
+ }
+
+ @Test
+ public void testAddDirectory() throws IOException {
+ Path root = tempFolder.getRoot().toPath();
+ newFile(root, "src/foo.dummy");
+ newFile(root, "src/bar.unknown");
+ newFile(root, "src/x/bar.dummy");
+
+ FileCollector collector = newCollector();
+
+ collector.addDirectory(root.resolve("src"));
+
+ assertCollected(collector, listOf("src/foo.dummy", "src/x/bar.dummy"));
+ }
+
+ @Test
+ public void testRelativize() throws IOException {
+ String displayName = FileCollector.getDisplayName(Paths.get("a", "b", "c"), listOf(Paths.get("a").toString()));
+ assertEquals(displayName, Paths.get("b", "c").toString());
+ }
+
+ private Path newFile(Path root, String path) throws IOException {
+ Path resolved = root.resolve(path);
+ Files.createDirectories(resolved.getParent());
+ Files.createFile(resolved);
+ return resolved;
+ }
+
+ private void assertCollected(FileCollector collector, List relPaths) {
+ Map actual = new LinkedHashMap<>();
+ for (TextFile file : collector.getCollectedFiles()) {
+ actual.put(file.getDisplayName(), file.getLanguageVersion().getTerseName());
+ }
+
+ relPaths = new ArrayList<>(relPaths);
+ for (int i = 0; i < relPaths.size(); i++) {
+ // normalize, we want display names to be platform-specific
+ relPaths.set(i, relPaths.get(i).replace('/', File.separatorChar));
+ }
+
+ assertEquals(relPaths, new ArrayList<>(actual.keySet()));
+ }
+
+ private void assertNoErrors(FileCollector collector) {
+ assertEquals("No errors expected", 0, collector.getReporter().numErrors());
+ }
+
+ private FileCollector newCollector() {
+ return newCollector(null);
+ }
+
+ private FileCollector newCollector(LanguageVersion forcedVersion) {
+ LanguageVersionDiscoverer discoverer = new LanguageVersionDiscoverer(forcedVersion);
+ FileCollector collector = FileCollector.newCollector(discoverer, new TestMessageReporter());
+ collector.relativizeWith(tempFolder.getRoot().getAbsolutePath());
+ return collector;
+ }
+}
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TestMessageReporter.java b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TestMessageReporter.java
new file mode 100644
index 0000000000..8689530e30
--- /dev/null
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TestMessageReporter.java
@@ -0,0 +1,23 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.lang.document;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sourceforge.pmd.util.log.internal.SimpleMessageReporter;
+
+/**
+ * @author ClΓ©ment Fournier
+ */
+public class TestMessageReporter extends SimpleMessageReporter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TestMessageReporter.class.getName());
+
+ public TestMessageReporter() {
+ super(LOG);
+ setLevel(null);
+ }
+}
diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/processor/GlobalListenerTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/processor/GlobalListenerTest.java
index 6d749e5775..d10f676ca5 100644
--- a/pmd-core/src/test/java/net/sourceforge/pmd/processor/GlobalListenerTest.java
+++ b/pmd-core/src/test/java/net/sourceforge/pmd/processor/GlobalListenerTest.java
@@ -4,7 +4,6 @@
package net.sourceforge.pmd.processor;
-import static net.sourceforge.pmd.util.CollectionUtil.listOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
@@ -13,15 +12,13 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import java.util.List;
-
import org.checkerframework.checker.nullness.qual.NonNull;
import org.junit.Test;
import org.mockito.Mockito;
import net.sourceforge.pmd.FooRule;
-import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.PMDConfiguration;
+import net.sourceforge.pmd.PmdAnalysis;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.RuleSet;
@@ -31,20 +28,11 @@ import net.sourceforge.pmd.lang.ast.FileAnalysisException;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
import net.sourceforge.pmd.reporting.GlobalAnalysisListener.ViolationCounterListener;
-import net.sourceforge.pmd.util.datasource.DataSource;
public class GlobalListenerTest {
static final int NUM_DATA_SOURCES = 3;
- static List mockDataSources() {
- return listOf(
- DataSource.forString("abc", "fname1.dummy"),
- DataSource.forString("abcd", "fname2.dummy"),
- DataSource.forString("abcd", "fname21.dummy")
- );
- }
-
@Test
public void testViolationCounter() throws Exception {
@@ -89,7 +77,7 @@ public class GlobalListenerTest {
runPmd(config, GlobalAnalysisListener.noop(), rule);
verify(mockCache).checkValidity(any(), any());
- verify(mockCache).persist();
+ verify(mockCache, times(1)).persist();
verify(mockCache, times(NUM_DATA_SOURCES)).isUpToDate(any());
}
@@ -105,7 +93,7 @@ public class GlobalListenerTest {
// cache methods are called regardless
verify(mockCache).checkValidity(any(), any());
- verify(mockCache).persist();
+ verify(mockCache, times(1)).persist();
verify(mockCache, times(NUM_DATA_SOURCES)).isUpToDate(any());
}
@@ -127,7 +115,7 @@ public class GlobalListenerTest {
// cache methods are called regardless
verify(mockCache).checkValidity(any(), any());
- verify(mockCache).persist();
+ verify(mockCache, times(1)).persist();
verify(mockCache, times(1)).isUpToDate(any());
}
@@ -140,16 +128,14 @@ public class GlobalListenerTest {
return config;
}
- private void runPmd(PMDConfiguration config, GlobalAnalysisListener listener, Rule rule) throws Exception {
- try {
- PMD.processFiles(
- config,
- listOf(RuleSet.forSingleRule(rule)),
- mockDataSources(),
- listener
- );
- } finally {
- listener.close();
+ private void runPmd(PMDConfiguration config, GlobalAnalysisListener listener, Rule rule) {
+ try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
+ pmd.addRuleSet(RuleSet.forSingleRule(rule));
+ pmd.files().addSourceFile("fname1.dummy", "abc");
+ pmd.files().addSourceFile("fname2.dummy", "abcd");
+ pmd.files().addSourceFile("fname21.dummy", "abcd");
+ pmd.addListener(listener);
+ pmd.performAnalysis();
}
}
@@ -159,7 +145,7 @@ public class GlobalListenerTest {
@Override
public void apply(Node node, RuleContext ctx) {
if (node.getAstInfo().getFileName().contains("1")) {
- addViolation(ctx, node);
+ ctx.addViolation(node);
}
}
}
diff --git a/pmd-core/src/test/resources/sample-source/dummy/foo.dummy b/pmd-core/src/test/resources/sample-source/dummy/foo.dummy
new file mode 100644
index 0000000000..4b1d189a0f
--- /dev/null
+++ b/pmd-core/src/test/resources/sample-source/dummy/foo.dummy
@@ -0,0 +1 @@
+A dummy file.
diff --git a/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java b/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java
index 51b6b34992..5def16ed3b 100644
--- a/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java
+++ b/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java
@@ -108,7 +108,7 @@ public class BinaryDistributionIT extends AbstractBinaryDistributionTest {
result = PMDExecutor.runPMD(tempDir, "-d", srcDir, "-R", "src/test/resources/rulesets/sample-ruleset.xml",
"-r", folder.newFile().toString(), "--debug");
result.assertExecutionResult(4);
- result.assertErrorOutputContains("[main] DEBUG net.sourceforge.pmd.PMD - Log level is at DEBUG");
+ result.assertErrorOutputContains("[main] INFO net.sourceforge.pmd.PMD - Log level is at TRACE");
}
@Test
diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java b/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java
index 892f1b82ae..016f208fd6 100644
--- a/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java
+++ b/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java
@@ -4,20 +4,18 @@
package net.sourceforge.pmd.cli;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.util.regex.Pattern;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
import org.junit.AfterClass;
-import org.junit.Assert;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.rules.TestRule;
+import net.sourceforge.pmd.PMD.StatusCode;
import net.sourceforge.pmd.internal.Slf4jSimpleConfiguration;
-import net.sourceforge.pmd.util.FileUtil;
/**
* @author Romain Pelisse <belaran@gmail.com>
@@ -35,64 +33,65 @@ public class CLITest extends BaseCLITest {
Slf4jSimpleConfiguration.reconfigureDefaultLogLevel(null);
}
+ @Before
+ public void setupLogging() {
+ Slf4jSimpleConfiguration.reconfigureDefaultLogLevel(null);
+ }
+
+
@Test
public void minimalArgs() {
- String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/bestpractices.xml,category/java/design.xml", };
- runTest(args, "minimalArgs");
+ runTest("-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/bestpractices.xml,category/java/design.xml");
}
@Test
public void minimumPriority() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-min", "1", };
- runTest(args, "minimumPriority");
+ runTest(args);
}
@Test
public void usingDebug() {
- String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-debug", };
- runTest(args, "minimalArgsWithDebug");
+ runTest("-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-debug");
}
@Test
public void usingDebugLongOption() {
- String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "--debug", };
- runTest(args, "minimalArgsWithDebug");
+ runTest("-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "--debug");
}
@Test
public void changeJavaVersion() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-version", "1.5", "-language",
- "java", "-debug", };
- String resultFilename = runTest(args, "chgJavaVersion");
- assertTrue("Invalid Java version",
- FileUtil.findPatternInFile(new File(resultFilename), "Using Java version: Java 1.5"));
+ "java", "--debug", };
+ String log = runTest(args);
+ assertThat(log, containsPattern("Adding file .*\\.java \\(lang: java 1\\.5\\)"));
}
@Test
public void exitStatusNoViolations() {
- String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", };
- runTest(args, "exitStatusNoViolations");
+ runTest("-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml");
}
@Test
public void exitStatusWithViolations() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/errorprone.xml", };
- String resultFilename = runTest(args, "exitStatusWithViolations", 4);
- assertTrue(FileUtil.findPatternInFile(new File(resultFilename), "Avoid empty if"));
+ String log = runTest(StatusCode.VIOLATIONS_FOUND, args);
+ assertThat(log, containsString("Avoid empty if"));
}
@Test
public void exitStatusWithViolationsAndWithoutFailOnViolations() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/errorprone.xml", "-failOnViolation", "false", };
- String resultFilename = runTest(args, "exitStatusWithViolationsAndWithoutFailOnViolations", 0);
- assertTrue(FileUtil.findPatternInFile(new File(resultFilename), "Avoid empty if"));
+ String log = runTest(StatusCode.OK, args);
+ assertThat(log, containsString("Avoid empty if"));
}
@Test
public void exitStatusWithViolationsAndWithoutFailOnViolationsLongOption() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/errorprone.xml", "--fail-on-violation", "false", };
- String resultFilename = runTest(args, "exitStatusWithViolationsAndWithoutFailOnViolations", 0);
- assertTrue(FileUtil.findPatternInFile(new File(resultFilename), "Avoid empty if"));
+ String log = runTest(StatusCode.OK, args);
+ assertThat(log, containsString("Avoid empty if"));
}
/**
@@ -101,12 +100,9 @@ public class CLITest extends BaseCLITest {
@Test
public void testWrongRuleset() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/designn.xml", };
- String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt";
- createTestOutputFile(filename);
- runPMDWith(args);
- Assert.assertEquals(1, getStatusCode());
- assertTrue(FileUtil.findPatternInFile(new File(filename),
- "Can't find resource 'category/java/designn.xml' for rule 'null'." + " Make sure the resource is a valid file"));
+ String log = runTest(StatusCode.ERROR, args);
+ assertThat(log, containsString("Can't find resource 'category/java/designn.xml' for rule 'null'."
+ + " Make sure the resource is a valid file"));
}
/**
@@ -115,12 +111,9 @@ public class CLITest extends BaseCLITest {
@Test
public void testWrongRulesetWithRulename() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/designn.xml/UseCollectionIsEmpty", };
- String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt";
- createTestOutputFile(filename);
- runPMDWith(args);
- Assert.assertEquals(1, getStatusCode());
- assertTrue(FileUtil.findPatternInFile(new File(filename),
- "Can't find resource 'category/java/designn.xml' for rule " + "'UseCollectionIsEmpty'."));
+ String log = runTest(StatusCode.ERROR, args);
+ assertThat(log, containsString("Can't find resource 'category/java/designn.xml' for rule "
+ + "'UseCollectionIsEmpty'."));
}
/**
@@ -129,11 +122,8 @@ public class CLITest extends BaseCLITest {
@Test
public void testWrongRulename() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml/ThisRuleDoesNotExist", };
- String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt";
- createTestOutputFile(filename);
- runPMDWith(args);
- Assert.assertEquals(1, getStatusCode());
- assertTrue(FileUtil.findPatternInFile(new File(filename), Pattern
- .quote("No rules found. Maybe you misspelled a rule name?" + " (category/java/design.xml/ThisRuleDoesNotExist)")));
+ String log = runTest(StatusCode.OK, args);
+ assertThat(log, containsString("No rules found. Maybe you misspelled a rule name?"
+ + " (category/java/design.xml/ThisRuleDoesNotExist)"));
}
}
diff --git a/pmd-javascript/src/test/java/net/sourceforge/pmd/cli/CLITest.java b/pmd-javascript/src/test/java/net/sourceforge/pmd/cli/CLITest.java
index a051f56a6f..718995de62 100644
--- a/pmd-javascript/src/test/java/net/sourceforge/pmd/cli/CLITest.java
+++ b/pmd-javascript/src/test/java/net/sourceforge/pmd/cli/CLITest.java
@@ -4,14 +4,10 @@
package net.sourceforge.pmd.cli;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
+import static org.hamcrest.MatcherAssert.assertThat;
import org.junit.Test;
-import net.sourceforge.pmd.util.FileUtil;
-
/**
* @author Romain Pelisse <belaran@gmail.com>
*
@@ -20,9 +16,8 @@ public class CLITest extends BaseCLITest {
@Test
public void useEcmaScript() {
String[] args = { "-d", SOURCE_FOLDER, "-f", "xml", "-R", "ecmascript-basic", "-l",
- "ecmascript", "-debug", };
- String resultFilename = runTest(args, "useEcmaScript");
- assertTrue("Invalid JavaScript version",
- FileUtil.findPatternInFile(new File(resultFilename), "Using Ecmascript version: Ecmascript ES6"));
+ "ecmascript", "--debug", };
+ String log = runTest(args);
+ assertThat(log, containsPattern("Adding file .*\\.js \\(lang: ecmascript ES6\\)"));
}
}
diff --git a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt
index bedcc9adcf..268bcd9745 100644
--- a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt
+++ b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt
@@ -225,5 +225,12 @@ abstract class BaseParsingHelper, T : RootNode
}
fun executeRuleOnResource(rule: Rule, resourcePath: String): Report =
- executeRule(rule, readResource(resourcePath))
+ executeRule(rule, code = readResource(resourcePath))
+
+ fun executeRuleOnFile(rule: Rule, path: Path): Report =
+ executeRule(
+ rule,
+ code = Files.newBufferedReader(path).readText(),
+ fileName = path.toString()
+ )
}
diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/cli/BaseCLITest.java b/pmd-test/src/main/java/net/sourceforge/pmd/cli/BaseCLITest.java
index 1c8645fc78..54e9739159 100644
--- a/pmd-test/src/main/java/net/sourceforge/pmd/cli/BaseCLITest.java
+++ b/pmd-test/src/main/java/net/sourceforge/pmd/cli/BaseCLITest.java
@@ -4,20 +4,28 @@
package net.sourceforge.pmd.cli;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
+import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import net.sourceforge.pmd.PMD;
+import net.sourceforge.pmd.PMD.StatusCode;
+import net.sourceforge.pmd.internal.util.AssertionUtil;
/**
* @author Romain Pelisse <belaran@gmail.com>
@@ -72,25 +80,79 @@ public abstract class BaseCLITest {
}
}
+ /**
+ * @deprecated Use {@link #runTest(String...)}, note that
+ * it returns the log while this returns the name of a file containing the log.
+ */
+ @Deprecated
protected String runTest(String[] args, String testname) {
return runTest(args, testname, 0);
}
+ /**
+ * @deprecated Use {@link #runTest(StatusCode, String...)}, note that
+ * it returns the log while this returns the name of a file containing the log.
+ */
+ @Deprecated
protected String runTest(String[] args, String testname, int expectedExitCode) {
String filename = TEST_OUPUT_DIRECTORY + testname + ".txt";
long start = System.currentTimeMillis();
createTestOutputFile(filename);
System.out.println("Start running test " + testname);
- runPMDWith(args);
- checkStatusCode(expectedExitCode);
+ StatusCode statusCode = PMD.runPmd(args);
+ assertEquals(expectedExitCode, statusCode.toInt());
System.out.println("Test finished successfully after " + (System.currentTimeMillis() - start) + "ms.");
return filename;
}
+ /**
+ * Returns the log output.
+ */
+ protected String runTest(String... args) {
+ return runTest(StatusCode.OK, args);
+ }
+
+ /**
+ * Returns the log output.
+ *
+ * @deprecated Use {@link #runTest(StatusCode, String...)}
+ */
+ @Deprecated
+ protected String runTest(int expectedExitCode, String... args) {
+ switch (expectedExitCode) {
+ case 0:
+ return runTest(StatusCode.OK, args);
+ case 1:
+ return runTest(StatusCode.ERROR, args);
+ case 4:
+ return runTest(StatusCode.VIOLATIONS_FOUND, args);
+ default:
+ throw AssertionUtil.shouldNotReachHere("unknown status code " + expectedExitCode);
+ }
+ }
+
+ protected String runTest(StatusCode expectedExitCode, String... args) {
+ ByteArrayOutputStream console = new ByteArrayOutputStream();
+ PrintStream out = new PrintStream(console);
+ System.setOut(out);
+ System.setErr(out);
+ StatusCode statusCode = PMD.runPmd(args);
+ assertEquals(expectedExitCode, statusCode);
+ return console.toString();
+ }
+
+ /**
+ * @deprecated Use {@link #runTest(StatusCode, String...)}
+ */
+ @Deprecated
protected void runPMDWith(String[] args) {
PMD.main(args);
}
+ /**
+ * @deprecated Use {@link #runTest(StatusCode, String...)} instead of checking the return code manually
+ */
+ @Deprecated
protected void checkStatusCode(int expectedExitCode) {
int statusCode = getStatusCode();
if (statusCode != expectedExitCode) {
@@ -98,7 +160,28 @@ public abstract class BaseCLITest {
}
}
+ /**
+ * @deprecated Use {@link #runTest(StatusCode, String...)} instead
+ * of checking the return code manually
+ */
+ @Deprecated
protected int getStatusCode() {
return Integer.parseInt(System.getProperty(PMDCommandLineInterface.STATUS_CODE_PROPERTY));
}
+
+ public static Matcher containsPattern(final String regex) {
+ return new BaseMatcher() {
+ final Pattern pattern = Pattern.compile(regex);
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("a string containing the pattern '" + this.pattern + "'");
+ }
+
+ @Override
+ public boolean matches(Object o) {
+ return o instanceof String && pattern.matcher((String) o).find();
+ }
+ };
+ }
}
diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java b/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java
index d9ceed4b85..51d4de0543 100644
--- a/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java
+++ b/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java
@@ -95,11 +95,7 @@ public abstract class RuleTst {
private ClassLoader makeClassPathClassLoader() {
final ClassLoader classpathClassLoader;
PMDConfiguration config = new PMDConfiguration();
- try {
- config.prependClasspath(".");
- } catch (IOException ignored) {
-
- }
+ config.prependAuxClasspath(".");
classpathClassLoader = config.getClassLoader();
return classpathClassLoader;
}
diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java
index 3cd0019f9b..1f76408722 100644
--- a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java
+++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java
@@ -4,28 +4,20 @@
package net.sourceforge.pmd.lang.vf.rule.security;
-import static net.sourceforge.pmd.util.CollectionUtil.listOf;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.Collections;
import java.util.List;
import org.junit.Test;
-import net.sourceforge.pmd.PMD;
-import net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.Report;
import net.sourceforge.pmd.Rule;
-import net.sourceforge.pmd.RuleSet;
import net.sourceforge.pmd.RuleViolation;
-import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.vf.VFTestUtils;
import net.sourceforge.pmd.lang.vf.ast.VfParsingHelper;
import net.sourceforge.pmd.testframework.PmdRuleTst;
-import net.sourceforge.pmd.util.datasource.FileDataSource;
public class VfUnescapeElTest extends PmdRuleTst {
public static final String EXPECTED_RULE_MESSAGE = "Avoid unescaped user controlled content in EL";
@@ -34,7 +26,7 @@ public class VfUnescapeElTest extends PmdRuleTst {
* Verify that CustomFields stored in sfdx project format are correctly parsed
*/
@Test
- public void testSfdxCustomFields() throws Exception {
+ public void testSfdxCustomFields() {
Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf)
.resolve("StandardAccount.page");
@@ -47,7 +39,7 @@ public class VfUnescapeElTest extends PmdRuleTst {
RuleViolation ruleViolation = ruleViolations.get(i);
assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription());
int expectedLineNumber = firstLineWithErrors + i;
- if ((ruleViolations.size() + firstLineWithErrors - 1) == expectedLineNumber) {
+ if (ruleViolations.size() + firstLineWithErrors - 1 == expectedLineNumber) {
// The last line has two errors on the same page
expectedLineNumber = expectedLineNumber - 1;
}
@@ -59,7 +51,7 @@ public class VfUnescapeElTest extends PmdRuleTst {
* Verify that CustomFields stored in mdapi format are correctly parsed
*/
@Test
- public void testMdapiCustomFields() throws Exception {
+ public void testMdapiCustomFields() {
Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.MDAPI, VFTestUtils.MetadataType.Vf).resolve("StandardAccount.page");
Report report = runRule(vfPagePath);
@@ -77,7 +69,7 @@ public class VfUnescapeElTest extends PmdRuleTst {
* Tests a page with a single Apex controller
*/
@Test
- public void testApexController() throws Exception {
+ public void testApexController() {
Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf).resolve("ApexController.page");
Report report = runRule(vfPagePath);
@@ -96,7 +88,7 @@ public class VfUnescapeElTest extends PmdRuleTst {
* Tests a page with a standard controller and two Apex extensions
*/
@Test
- public void testExtensions() throws Exception {
+ public void testExtensions() {
Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf)
.resolve(Paths.get("StandardAccountWithExtensions.page"));
@@ -114,32 +106,8 @@ public class VfUnescapeElTest extends PmdRuleTst {
/**
* Runs a rule against a Visualforce page on the file system.
*/
- private Report runRule(Path vfPagePath) throws Exception {
- Node node = VfParsingHelper.DEFAULT.parseFile(vfPagePath);
- assertNotNull(node);
-
- PMDConfiguration config = new PMDConfiguration();
- config.setIgnoreIncrementalAnalysis(true);
- // simple class loader, that doesn't delegate to parent.
- // this allows us in the tests to simulate PMD run without
- // auxclasspath, not even the classes from the test dependencies
- // will be found.
- config.setClassLoader(new ClassLoader() {
- @Override
- protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- if (name.startsWith("java.") || name.startsWith("javax.")) {
- return super.loadClass(name, resolve);
- }
- throw new ClassNotFoundException(name);
- }
- });
+ private Report runRule(Path vfPagePath) {
Rule rule = findRule("category/vf/security.xml", "VfUnescapeEl");
-
- return PMD.processFiles(
- config,
- listOf(RuleSet.forSingleRule(rule)),
- listOf(new FileDataSource(vfPagePath.toAbsolutePath().toFile())),
- Collections.emptyList()
- );
+ return VfParsingHelper.DEFAULT.executeRuleOnFile(rule, vfPagePath);
}
}
diff --git a/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlCliTest.java b/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlCliTest.java
index ee5722798b..14e717b237 100644
--- a/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlCliTest.java
+++ b/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlCliTest.java
@@ -4,18 +4,17 @@
package net.sourceforge.pmd.lang.xml;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
+import static net.sourceforge.pmd.util.CollectionUtil.listOf;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.junit.Test;
+import net.sourceforge.pmd.PMD.StatusCode;
import net.sourceforge.pmd.cli.BaseCLITest;
public class XmlCliTest extends BaseCLITest {
@@ -23,44 +22,38 @@ public class XmlCliTest extends BaseCLITest {
private static final String RULE_MESSAGE = "A tags are not allowed";
private String[] createArgs(String directory, String... args) {
- List arguments = new ArrayList<>();
- arguments.add("-f");
- arguments.add("text");
- arguments.add("-no-cache");
- arguments.add("-R");
- arguments.add(BASE_DIR + "/ruleset.xml");
- arguments.add("-d");
- arguments.add(BASE_DIR + directory);
+ List arguments = new ArrayList<>(listOf(
+ "-f",
+ "text",
+ "-no-cache",
+ "-R",
+ BASE_DIR + "/ruleset.xml",
+ "-d",
+ BASE_DIR + directory
+ ));
arguments.addAll(Arrays.asList(args));
return arguments.toArray(new String[0]);
}
@Test
public void analyzeSingleXmlWithoutForceLanguage() {
- String resultFilename = runTest(createArgs("/src/file1.ext"), "analyzeSingleXmlWithoutForceLanguage", 0);
- assertRuleMessage(0, resultFilename);
+ String log = runTest(StatusCode.OK, createArgs("/src/file1.ext"));
+ assertRuleMessage(0, log);
}
@Test
public void analyzeSingleXmlWithForceLanguage() {
- String resultFilename = runTest(createArgs("/src/file1.ext", "-force-language", "xml"),
- "analyzeSingleXmlWithForceLanguage", 4);
- assertRuleMessage(1, resultFilename);
+ String log = runTest(StatusCode.VIOLATIONS_FOUND, createArgs("/src/file1.ext", "-force-language", "xml"));
+ assertRuleMessage(1, log);
}
@Test
public void analyzeDirectoryWithForceLanguage() {
- String resultFilename = runTest(createArgs("/src/", "-force-language", "xml"),
- "analyzeDirectoryWithForceLanguage", 4);
- assertRuleMessage(3, resultFilename);
+ String log = runTest(StatusCode.VIOLATIONS_FOUND, createArgs("/src/", "-force-language", "xml"));
+ assertRuleMessage(3, log);
}
- private void assertRuleMessage(int expectedCount, String resultFilename) {
- try {
- String result = FileUtils.readFileToString(new File(resultFilename), StandardCharsets.UTF_8);
- Assert.assertEquals(expectedCount, StringUtils.countMatches(result, RULE_MESSAGE));
- } catch (IOException e) {
- throw new AssertionError(e);
- }
+ private void assertRuleMessage(int expectedCount, String log) {
+ Assert.assertEquals(expectedCount, StringUtils.countMatches(log, RULE_MESSAGE));
}
}