diff --git a/docs/pages/pmd/userdocs/tools/java-api.md b/docs/pages/pmd/userdocs/tools/java-api.md index 3cc500f45e..f313ba54dc 100644 --- a/docs/pages/pmd/userdocs/tools/java-api.md +++ b/docs/pages/pmd/userdocs/tools/java-api.md @@ -110,18 +110,17 @@ You can also provide your own custom renderers. List files = Arrays.asList(new FileDataSource(new File("/path/to/src/MyClass.java"))); ``` -5. For reporting, you can use a built-in renderer, e.g. `XMLRenderer` or a custom renderer implementing - `Renderer`. Note, that you must manually initialize - the renderer by setting a suitable `Writer` and calling `start()`. After the PMD run, you need to call - `end()` and `flush()`. Then your writer should have received all output. - +5. For reporting, you can use `GlobalAnalysisListener`, which receives events like violations and errors. + Useful implementations are provided by `Renderer` instances. To use a renderer, eg the built-in `XMLRenderer`, + create it and configure it with a suitable `Writer`. + ```java StringWriter rendererOutput = new StringWriter(); Renderer xmlRenderer = new XMLRenderer("UTF-8"); xmlRenderer.setWriter(rendererOutput); - xmlRenderer.start(); + // The listener is created from the renderer in the next listing ``` - + 6. Now, all the preparations are done, and PMD can be executed. This is done by calling `PMD.processFiles(...)`. This method call takes the configuration, the rulesets, the files to process, and the list of renderers. Provide an empty list, if you don't want to use @@ -129,8 +128,8 @@ You can also provide your own custom renderers. remain open and file resources are leaked. ```java - try { - PMD.processFiles(configuration, ruleSets, files, Collections.singletonList(renderer)); + try (GlobalAnalysisListener listener = xmlRenderer.newListener()) { + PMD.processFiles(configuration, ruleSets, files, listener); } finally { ClassLoader auxiliaryClassLoader = configuration.getClassLoader(); if (auxiliaryClassLoader instanceof ClasspathClassLoader) { @@ -139,12 +138,10 @@ You can also provide your own custom renderers. } ``` -7. After the call, you need to finish the renderer via `end()` and `flush()`. +7. After the call, the renderer will have been flushed by PMD (through its `GlobalAnalysisListener`). Then you can check the rendered output. ``` java - renderer.end(); - renderer.flush(); System.out.println("Rendered Report:"); System.out.println(rendererOutput.toString()); ``` @@ -192,10 +189,9 @@ public class PmdExample2 { Writer rendererOutput = new StringWriter(); Renderer renderer = createRenderer(rendererOutput); - renderer.start(); - try { - PMD.processFiles(configuration, ruleSets, files, Collections.singletonList(renderer)); + try (GlobalAnalysisListener listener = renderer.newListener()) { + PMD.processFiles(configuration, ruleSets, files, listener); } finally { ClassLoader auxiliaryClassLoader = configuration.getClassLoader(); if (auxiliaryClassLoader instanceof ClasspathClassLoader) { @@ -203,8 +199,6 @@ public class PmdExample2 { } } - renderer.end(); - renderer.flush(); System.out.println("Rendered Report:"); System.out.println(rendererOutput.toString()); } 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 099975ec95..fab62dd6eb 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java @@ -102,7 +102,7 @@ public final class PMD { violationCounter))) { - try (TimedOperation to = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) { + try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) { processTextFiles(configuration, ruleSets, files, listener); } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleContext.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleContext.java index 9a67dda2ea..ec0d5ef152 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleContext.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleContext.java @@ -141,6 +141,7 @@ public final class RuleContext { * * @param rv A violation */ + @InternalApi public void addViolationNoSuppress(RuleViolation rv) { listener.onRuleViolation(rv); } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/ant/Formatter.java b/pmd-core/src/main/java/net/sourceforge/pmd/ant/Formatter.java index cae151247c..af9370b00c 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/ant/Formatter.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/ant/Formatter.java @@ -27,12 +27,14 @@ import org.apache.tools.ant.Project; import org.apache.tools.ant.types.Parameter; import net.sourceforge.pmd.Report; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.renderers.Renderer; import net.sourceforge.pmd.renderers.RendererFactory; import net.sourceforge.pmd.reporting.FileAnalysisListener; import net.sourceforge.pmd.reporting.GlobalAnalysisListener; import net.sourceforge.pmd.util.document.TextFile; +@InternalApi public class Formatter { private File toFile; @@ -236,6 +238,7 @@ public class Formatter { return null; } + @InternalApi public GlobalAnalysisListener newListener(Project project) throws IOException { start(project.getBaseDir().toString()); Renderer renderer = getRenderer(); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/AbstractPMDProcessor.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/AbstractPMDProcessor.java index 840bace5fa..571ddfa236 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/AbstractPMDProcessor.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/AbstractPMDProcessor.java @@ -20,6 +20,7 @@ import net.sourceforge.pmd.util.document.TextFile; * * @author Romain Pelisse <belaran@gmail.com> */ +@InternalApi public abstract class AbstractPMDProcessor implements AutoCloseable { protected final PMDConfiguration configuration; 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 4f2ee96385..780ff89709 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 @@ -31,7 +31,7 @@ import net.sourceforge.pmd.util.document.TextFile; abstract class PmdRunnable implements Runnable { private final TextFile textFile; - private final GlobalAnalysisListener ruleContext; + private final GlobalAnalysisListener globalListener; private final AnalysisCache analysisCache; /** @deprecated Get rid of this */ @@ -41,10 +41,10 @@ abstract class PmdRunnable implements Runnable { private final RulesetStageDependencyHelper dependencyHelper; PmdRunnable(TextFile textFile, - GlobalAnalysisListener ruleContext, + GlobalAnalysisListener globalListener, PMDConfiguration configuration) { this.textFile = textFile; - this.ruleContext = ruleContext; + this.globalListener = globalListener; this.analysisCache = configuration.getAnalysisCache(); this.configuration = configuration; this.dependencyHelper = new RulesetStageDependencyHelper(configuration); @@ -63,7 +63,7 @@ abstract class PmdRunnable implements Runnable { RuleSets ruleSets = getRulesets(); - try (FileAnalysisListener listener = ruleContext.startFileAnalysis(textFile)) { + try (FileAnalysisListener listener = globalListener.startFileAnalysis(textFile)) { // Coarse check to see if any RuleSet applies to file, will need to do a finer RuleSet specific check later if (ruleSets.applies(textFile)) { diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/AbstractAccumulatingRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/AbstractAccumulatingRenderer.java index 1064be79cf..857230da96 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/AbstractAccumulatingRenderer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/AbstractAccumulatingRenderer.java @@ -44,8 +44,7 @@ public abstract class AbstractAccumulatingRenderer extends AbstractRenderer { } @Override - public final void startFileAnalysis(TextFile dataSource) { - // do nothing, final because it will never be called by the listener + public void startFileAnalysis(TextFile dataSource) { Objects.requireNonNull(dataSource); } @@ -65,7 +64,7 @@ public abstract class AbstractAccumulatingRenderer extends AbstractRenderer { @Override public GlobalAnalysisListener newListener() throws IOException { - try (TimedOperation to = TimeTracker.startOperation(TimedOperationCategory.REPORTING)) { + try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.REPORTING)) { this.start(); } @@ -74,6 +73,7 @@ public abstract class AbstractAccumulatingRenderer extends AbstractRenderer { @Override public FileAnalysisListener startFileAnalysis(TextFile file) { + AbstractAccumulatingRenderer.this.startFileAnalysis(file); return reportBuilder.startFileAnalysis(file); } @@ -85,7 +85,7 @@ public abstract class AbstractAccumulatingRenderer extends AbstractRenderer { @Override public void close() throws Exception { reportBuilder.close(); - try (TimedOperation to = TimeTracker.startOperation(TimedOperationCategory.REPORTING)) { + try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.REPORTING)) { outputReport(reportBuilder.getResult()); end(); flush(); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/FileAnalysisListener.java b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/FileAnalysisListener.java index 22b49efa22..5241797100 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/reporting/FileAnalysisListener.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/reporting/FileAnalysisListener.java @@ -87,13 +87,15 @@ public interface FileAnalysisListener extends AutoCloseable { AssertionUtil.requireNotEmpty("Listeners", listeners); AssertionUtil.requireContainsNoNullValue("Listeners", listeners); - if (listeners.size() == 1) { - return listeners.iterator().next(); - } - List list = new ArrayList<>(listeners); list.removeIf(it -> it == NoopFileListener.INSTANCE); + if (listeners.isEmpty()) { + return noop(); + } else if (listeners.size() == 1) { + return listeners.iterator().next(); + } + class TeeListener implements FileAnalysisListener { @Override 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 24373ef6b0..38220d64da 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 @@ -81,6 +81,9 @@ public interface GlobalAnalysisListener extends AutoCloseable { // do nothing } + /** + * A listener that does nothing. + */ static GlobalAnalysisListener noop() { return new GlobalAnalysisListener() { @Override diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/BaseResultProducingCloseable.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/BaseResultProducingCloseable.java index 5a02bc6a61..ea978cc9bd 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/util/BaseResultProducingCloseable.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/BaseResultProducingCloseable.java @@ -4,11 +4,12 @@ package net.sourceforge.pmd.util; +import java.io.Closeable; import java.util.function.Consumer; /** * Base class for an autocloseable that produce a result once it has - * been closed. + * been closed. None of the methods of this class are synchronized. * * @param Type of the result */ @@ -39,13 +40,25 @@ public abstract class BaseResultProducingCloseable implements AutoCloseable { protected abstract T getResultImpl(); /** - * @implNote Call super + * Close this object. Idempotent. + * + * @implNote Override {@link #closeImpl()} instead. */ @Override - public void close() { - closed = true; + public final void close() { + if (!closed) { + closed = true; + closeImpl(); + } } + /** + * Close this closeable as per the contract of {@link Closeable#close()}. + * Called exactly once. By default does nothing. + */ + protected void closeImpl() { + // override + } public static > U using(C closeable, Consumer it) { try { diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/test/lang/DummyLanguageModule.java b/pmd-test/src/main/java/net/sourceforge/pmd/test/lang/DummyLanguageModule.java index 599ff818aa..ad015a4597 100644 --- a/pmd-test/src/main/java/net/sourceforge/pmd/test/lang/DummyLanguageModule.java +++ b/pmd-test/src/main/java/net/sourceforge/pmd/test/lang/DummyLanguageModule.java @@ -65,6 +65,12 @@ public class DummyLanguageModule extends BaseLanguageModule { this.languageVersion = languageVersion; } + public DummyRootNode withCoords(int bline, int bcol, int eline, int ecol) { + super.setCoords(bline, bcol, eline, ecol); + return this; + } + + @Override public AstInfo getAstInfo() { return new AstInfo<>( diff --git a/pmd-test/src/test/java/net/sourceforge/pmd/testframework/RuleTstTest.java b/pmd-test/src/test/java/net/sourceforge/pmd/testframework/RuleTstTest.java index 04d0a794a1..9f54633ced 100644 --- a/pmd-test/src/test/java/net/sourceforge/pmd/testframework/RuleTstTest.java +++ b/pmd-test/src/test/java/net/sourceforge/pmd/testframework/RuleTstTest.java @@ -14,19 +14,14 @@ import java.util.Arrays; import org.junit.Test; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RuleContext; -import net.sourceforge.pmd.RuleViolation; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.ast.Node; -import net.sourceforge.pmd.lang.rule.ParametricRuleViolation; import net.sourceforge.pmd.lang.rule.RuleTargetSelector; import net.sourceforge.pmd.test.lang.DummyLanguageModule.DummyRootNode; -import net.sourceforge.pmd.test.lang.ast.DummyNode; public class RuleTstTest { private LanguageVersion dummyLanguage = LanguageRegistry.findLanguageByTerseName("dummy").getDefaultVersion(); @@ -60,29 +55,21 @@ public class RuleTstTest { public void shouldAssertLinenumbersSorted() { when(rule.getLanguage()).thenReturn(dummyLanguage.getLanguage()); when(rule.getName()).thenReturn("test rule"); + when(rule.getMessage()).thenReturn("test rule"); when(rule.getTargetSelector()).thenReturn(RuleTargetSelector.forRootOnly()); when(rule.deepCopy()).thenReturn(rule); - Mockito.doAnswer(new Answer() { - private RuleViolation createViolation(int beginLine, String message) { - DummyNode node = new DummyRootNode(); - node.setCoords(beginLine, 1, beginLine + 1, 2); - return new ParametricRuleViolation(rule, node, message); - } - - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - RuleContext context = invocation.getArgument(1, RuleContext.class); - // the violations are reported out of order - context.addViolationNoSuppress(createViolation(15, "first reported violation")); - context.addViolationNoSuppress(createViolation(5, "second reported violation")); - return null; - } + Mockito.doAnswer(invocation -> { + RuleContext context = invocation.getArgument(1, RuleContext.class); + // the violations are reported out of order + context.addViolation(new DummyRootNode().withCoords(15, 1, 15, 5)); + context.addViolation(new DummyRootNode().withCoords(1, 1, 2, 5)); + return null; }).when(rule).apply(any(Node.class), Mockito.any(RuleContext.class)); TestDescriptor testDescriptor = new TestDescriptor("the code", "sample test", 2, rule, dummyLanguage); testDescriptor.setReinitializeRule(false); - testDescriptor.setExpectedLineNumbers(Arrays.asList(5, 15)); + testDescriptor.setExpectedLineNumbers(Arrays.asList(1, 15)); ruleTester.runTest(testDescriptor); }