Merge branch 'master' into pmd/7.0.x

This commit is contained in:
Andreas Dangel committed 2022-07-21 17:48:54 +02:00
commit 006d2a9e0e
17 files changed
+382 -111

No files matched your search

@@ -101,6 +101,8 @@ Example:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<pmd-cpd>
<file path="/home/pmd/source/pmd-core/src/test/java/net/sourceforge/pmd/RuleReferenceTest.java" totalNumberOfTokens="523"/>
<file path="/home/pmd/source/pmd-core/src/test/java/net/sourceforge/pmd/lang/rule/xpath/JaxenXPathRuleQueryTest.java" totalNumberOfTokens="120"/>
<duplication lines="33" tokens="239">
<file column="29" endcolumn="75" endline="64" line="32"
path="/home/pmd/source/pmd-core/src/test/java/net/sourceforge/pmd/RuleReferenceTest.java"/>
+19
View File
@@ -46,6 +46,9 @@ Being based on a proper Antlr grammar, CPD can:
* honor [comment-based suppressions](pmd_userdocs_cpd.html#suppression)
### Fixed Issues
* core
* [#4021](https://github.com/pmd/pmd/pull/4021): \[core] CPD: Add total number of tokens to XML reports
* [#4031](https://github.com/pmd/pmd/issues/4031): \[core] If report is written to stdout, stdout should not be closed
* java
* [#4015](https://github.com/pmd/pmd/issues/4015): \[java] Support JDK 19
* java-bestpractices
@@ -63,6 +66,8 @@ Being based on a proper Antlr grammar, CPD can:
* The experimental Java AST class {% jdoc java::lang.java.ast.ASTGuardedPattern %} has been deprecated and
will be removed. It was introduced for Java 17 and Java 18 Preview as part of pattern matching for switch,
but it is no longer supported with Java 19 Preview.
* The interface {% jdoc core::cpd.renderer.CPDRenderer %} is deprecated. For custom CPD renderers
the new interface {% jdoc core::cpd.renderer.CPDReportRenderer %} should be used.
#### Experimental APIs
@@ -72,12 +77,26 @@ Being based on a proper Antlr grammar, CPD can:
* {% jdoc java::lang.java.ast.ASTRecordPattern %}
* {% jdoc java::lang.java.ast.ASTComponentPatternList %}
#### Internal API
Those APIs are not intended to be used by clients, and will be hidden or removed with PMD 7.0.0.
You can identify them with the `@InternalApi` annotation. You'll also get a deprecation warning.
* {%jdoc !!core::cpd.CPDConfiguration#setRenderer(net.sourceforge.pmd.cpd.Renderer) %}
* {%jdoc !!core::cpd.CPDConfiguration#setCPDRenderer(net.sourceforge.pmd.cpd.renderer.CPDRenderer) %}
* {%jdoc !!core::cpd.CPDConfiguration#getRenderer() %}
* {%jdoc !!core::cpd.CPDConfiguration#getCPDRenderer() %}
* {%jdoc !!core::cpd.CPDConfiguration#getRendererFromString(java.lang.String,java.lang.String) %}
* {%jdoc !!core::cpd.CPDConfiguration#getCPDRendererFromString(java.lang.String,java.lang.String) %}
* {%jdoc core::cpd.renderer.CPDRendererAdapter %}
### External Contributions
* [#3984](https://github.com/pmd/pmd/pull/3984): \[java] Fix AddEmptyString false-negative issue - [@LiGaOg](https://github.com/LiGaOg)
* [#3988](https://github.com/pmd/pmd/pull/3988): \[java] Modify WhileLoopWithLiteralBoolean to meet the missing case #3455 - [@VoidxHoshi](https://github.com/VoidxHoshi)
* [#3992](https://github.com/pmd/pmd/pull/3992): \[java] FinalFieldCouldBeStatic - fix false negative with unnecessary parenthesis - [@dalizi007](https://github.com/dalizi007)
* [#3994](https://github.com/pmd/pmd/pull/3994): \[java] TooManyMethods - improve getter/setter detection (#3729) - [@341816041](https://github.com/341816041)
* [#4017](https://github.com/pmd/pmd/pull/4017): Add Gherkin support to CPD - [@ASBrouwers](https://github.com/ASBrouwers)
* [#4021](https://github.com/pmd/pmd/pull/4021): \[core] CPD: Add total number of tokens to XML reports - [@maikelsteneker](https://github.com/maikelsteneker)
{% endtocmaker %}
@@ -4,12 +4,12 @@
package net.sourceforge.pmd.cpd;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory;
import net.sourceforge.pmd.annotation.Experimental;
import net.sourceforge.pmd.cli.internal.CliMessages;
import net.sourceforge.pmd.cpd.renderer.CPDReportRenderer;
import net.sourceforge.pmd.lang.ast.TokenMgrError;
import net.sourceforge.pmd.util.FileFinder;
import net.sourceforge.pmd.util.IOUtil;
@@ -39,6 +40,8 @@ public class CPD {
private Tokens tokens = new Tokens();
private MatchAlgorithm matchAlgorithm;
private Set<String> current = new HashSet<>();
private final Map<String, Integer> numberOfTokensPerFile = new HashMap<>();
private int lastTokenSize = 0;
public CPD(CPDConfiguration theConfiguration) {
configuration = theConfiguration;
@@ -146,6 +149,8 @@ public class CPD {
configuration.tokenizer().tokenize(sourceCode, tokens);
listener.addedFile(1, new File(sourceCode.getFileName()));
source.put(sourceCode.getFileName(), sourceCode);
numberOfTokensPerFile.put(sourceCode.getFileName(), tokens.size() - lastTokenSize - 1 /*EOF*/);
lastTokenSize = tokens.size();
}
private void addAndSkipLexicalErrors(SourceCode sourceCode) throws IOException {
@@ -208,11 +213,13 @@ public class CPD {
CPDCommandLineInterface.addSourceFilesToCPD(cpd, arguments);
cpd.go();
if (arguments.getCPDRenderer() == null) {
final CPDReportRenderer renderer = arguments.getCPDReportRenderer();
if (renderer == null) {
// legacy writer
System.out.println(arguments.getRenderer().render(cpd.getMatches()));
} else {
arguments.getCPDRenderer().render(cpd.getMatches(), new BufferedWriter(new OutputStreamWriter(System.out)));
final CPDReport report = cpd.toReport();
renderer.render(report, IOUtil.createWriter(Charset.defaultCharset(), null));
}
if (cpd.getMatches().hasNext()) {
if (arguments.isFailOnViolation()) {
@@ -231,6 +238,10 @@ public class CPD {
return statusCode;
}
public CPDReport toReport() {
return new CPDReport(matchAlgorithm.getMatches(), numberOfTokensPerFile);
}
public enum StatusCode {
OK(0),
ERROR(1),
@@ -21,7 +21,10 @@ import java.util.Properties;
import java.util.Set;
import net.sourceforge.pmd.AbstractConfiguration;
import net.sourceforge.pmd.annotation.InternalApi;
import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
import net.sourceforge.pmd.cpd.renderer.CPDRendererAdapter;
import net.sourceforge.pmd.cpd.renderer.CPDReportRenderer;
import net.sourceforge.pmd.util.FileFinder;
import net.sourceforge.pmd.util.FileUtil;
@@ -39,7 +42,7 @@ public class CPDConfiguration extends AbstractConfiguration {
public static final String DEFAULT_LANGUAGE = "java";
public static final String DEFAULT_RENDERER = "text";
private static final Map<String, Class<? extends CPDRenderer>> RENDERERS = new HashMap<>();
private static final Map<String, Class<?>> RENDERERS = new HashMap<>();
static {
RENDERERS.put(DEFAULT_RENDERER, SimpleRenderer.class);
@@ -72,8 +75,11 @@ public class CPDConfiguration extends AbstractConfiguration {
@Deprecated
private Renderer renderer;
@Deprecated
private CPDRenderer cpdRenderer;
private CPDReportRenderer cpdReportRenderer;
private String encoding;
@Parameter(names = "--ignore-literals",
@@ -169,66 +175,65 @@ public class CPDConfiguration extends AbstractConfiguration {
setRendererName(DEFAULT_RENDERER);
}
if (getRenderer() == null && getCPDRenderer() == null) {
try {
setCPDRenderer(getCPDRendererFromString(getRendererName(), getEncoding()));
} catch (ClassCastException e) {
// The renderer class configured is not using the new CPDRenderer interface...
setRenderer(getRendererFromString(getRendererName(), getEncoding()));
Object renderer = createRendererByName(getRendererName(), getEncoding());
String className = getRendererName();
if (renderer instanceof CPDReportRenderer) {
setRenderer((CPDReportRenderer) renderer);
} else if (renderer instanceof CPDRenderer) {
setCPDRenderer((CPDRenderer) renderer);
} else if (renderer instanceof Renderer) {
setRenderer((Renderer) renderer);
} else {
System.err.println("Class '" + className + "' is not a supported renderer, defaulting to SimpleRenderer.");
setRenderer(new SimpleRenderer());
}
}
}
private static Object createRendererByName(String name, String encoding) {
if (name == null || "".equals(name)) {
name = DEFAULT_RENDERER;
}
Class<?> rendererClass = RENDERERS.get(name.toLowerCase(Locale.ROOT));
if (rendererClass == null) {
try {
rendererClass = Class.forName(name);
} catch (ClassNotFoundException e) {
System.err.println("Can't find class '" + name + "', defaulting to SimpleRenderer.");
rendererClass = SimpleRenderer.class;
}
}
Object renderer = null;
try {
renderer = rendererClass.getDeclaredConstructor().newInstance();
setRendererEncoding(renderer, encoding);
} catch (Exception e) {
System.err.println("Couldn't instantiate renderer, defaulting to SimpleRenderer: " + e);
renderer = new SimpleRenderer();
}
return renderer;
}
/**
* @deprecated Use {@link #getCPDRendererFromString(String, String)} instead
* @deprecated Internal API
*/
@Deprecated
@InternalApi
public static Renderer getRendererFromString(String name, String encoding) {
String clazzname = name;
if (clazzname == null || "".equals(clazzname)) {
clazzname = DEFAULT_RENDERER;
}
@SuppressWarnings("unchecked") // Safe, all standard implementations implement both interfaces
Class<? extends Renderer> clazz = (Class<? extends Renderer>) RENDERERS.get(clazzname.toLowerCase(Locale.ROOT));
if (clazz == null) {
try {
clazz = Class.forName(clazzname).asSubclass(Renderer.class);
} catch (ClassNotFoundException e) {
System.err.println("Can't find class '" + name + "', defaulting to SimpleRenderer.");
clazz = SimpleRenderer.class;
}
}
try {
Renderer renderer = clazz.getDeclaredConstructor().newInstance();
setRendererEncoding(renderer, encoding);
return renderer;
} catch (Exception e) {
System.err.println("Couldn't instantiate renderer, defaulting to SimpleRenderer: " + e);
return new SimpleRenderer();
}
// will throw a ClassCastException if the renderer is of wrong type
return (Renderer) createRendererByName(name, encoding);
}
/**
* @deprecated Internal API
*/
@Deprecated
@InternalApi
public static CPDRenderer getCPDRendererFromString(String name, String encoding) {
String clazzname = name;
if (clazzname == null || "".equals(clazzname)) {
clazzname = DEFAULT_RENDERER;
}
Class<? extends CPDRenderer> clazz = RENDERERS.get(clazzname.toLowerCase(Locale.ROOT));
if (clazz == null) {
try {
clazz = Class.forName(clazzname).asSubclass(CPDRenderer.class);
} catch (ClassNotFoundException e) {
System.err.println("Can't find class '" + name + "', defaulting to SimpleRenderer.");
clazz = SimpleRenderer.class;
}
}
try {
CPDRenderer renderer = clazz.getDeclaredConstructor().newInstance();
setRendererEncoding(renderer, encoding);
return renderer;
} catch (Exception e) {
System.err.println("Couldn't instantiate renderer, defaulting to SimpleRenderer: " + e);
return new SimpleRenderer();
}
// will throw a ClassCastException if the renderer is of wrong type
return (CPDRenderer) createRendererByName(name, encoding);
}
private static void setRendererEncoding(Object renderer, String encoding)
@@ -319,17 +324,27 @@ public class CPDConfiguration extends AbstractConfiguration {
}
/**
* @deprecated Use {@link #getCPDRenderer()} instead
* @deprecated Internal API.
*/
@Deprecated
@InternalApi
public Renderer getRenderer() {
return renderer;
}
/**
* @deprecated Internal API.
*/
@Deprecated
@InternalApi
public CPDRenderer getCPDRenderer() {
return cpdRenderer;
}
CPDReportRenderer getCPDReportRenderer() {
return cpdReportRenderer;
}
public Tokenizer tokenizer() {
if (language == null) {
throw new IllegalStateException("Language is null.");
@@ -373,18 +388,33 @@ public class CPDConfiguration extends AbstractConfiguration {
}
/**
* @deprecated Use {@link #setCPDRenderer(CPDRenderer)} instead
* @deprecated Internal API. Use {@link #setRendererName(String)} instead.
* @param renderer
*/
@Deprecated
@InternalApi
public void setRenderer(Renderer renderer) {
this.renderer = renderer;
this.cpdRenderer = null;
this.cpdReportRenderer = null;
}
/**
* @deprecated Internal API. Use {@link #setRendererName(String)} instead.
* @param renderer
*/
@Deprecated
@InternalApi
public void setCPDRenderer(CPDRenderer renderer) {
this.cpdRenderer = renderer;
this.renderer = null;
this.cpdRenderer = renderer;
this.cpdReportRenderer = new CPDRendererAdapter(renderer);
}
void setRenderer(CPDReportRenderer renderer) {
this.renderer = null;
this.cpdRenderer = null;
this.cpdReportRenderer = renderer;
}
public boolean isIgnoreLiterals() {
@@ -0,0 +1,31 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.cpd;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* @since 6.48.0
*/
public class CPDReport {
private final List<Match> matches;
private final Map<String, Integer> numberOfTokensPerFile;
CPDReport(final List<Match> matches, final Map<String, Integer> numberOfTokensPerFile) {
this.matches = Collections.unmodifiableList(matches);
this.numberOfTokensPerFile = Collections.unmodifiableMap(new TreeMap<>(numberOfTokensPerFile));
}
public List<Match> getMatches() {
return matches;
}
public Map<String, Integer> getNumberOfTokensPerFile() {
return numberOfTokensPerFile;
}
}
@@ -23,7 +23,8 @@ import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
import net.sourceforge.pmd.cpd.renderer.CPDRendererAdapter;
import net.sourceforge.pmd.cpd.renderer.CPDReportRenderer;
/**
* CPDTask
@@ -130,7 +131,8 @@ public class CPDTask extends Task {
if (!cpd.getMatches().hasNext()) {
log("No duplicates over " + minimumTokenCount + " tokens found", Project.MSG_INFO);
}
CPDRenderer renderer = createRenderer();
CPDReportRenderer renderer = createRenderer();
CPDReport report = cpd.toReport();
try {
// will be closed via BufferedWriter/OutputStreamWriter chain down below
@@ -148,7 +150,7 @@ public class CPDTask extends Task {
}
try (Writer writer = new BufferedWriter(new OutputStreamWriter(os, encoding))) {
renderer.render(cpd.getMatches(), writer);
renderer.render(report, writer);
}
} catch (IOException ioe) {
throw new ReportException(ioe);
@@ -175,11 +177,11 @@ public class CPDTask extends Task {
return stop - start;
}
private CPDRenderer createRenderer() {
private CPDReportRenderer createRenderer() {
if (TEXT_FORMAT.equals(format)) {
return new SimpleRenderer();
return new CPDRendererAdapter(new SimpleRenderer());
} else if (CSV_FORMAT.equals(format)) {
return new CSVRenderer();
return new CPDRendererAdapter(new CSVRenderer());
}
return new XMLRenderer();
}
@@ -20,7 +20,6 @@ import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
@@ -66,35 +65,16 @@ import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import net.sourceforge.pmd.PMDVersion;
import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
import net.sourceforge.pmd.cpd.renderer.CPDRendererAdapter;
import net.sourceforge.pmd.cpd.renderer.CPDReportRenderer;
public class GUI implements CPDListener {
// private interface Renderer {
// String render(Iterator<Match> items);
// }
private static final Object[][] RENDERER_SETS = new Object[][] { { "Text", new CPDRenderer() {
@Override
public void render(Iterator<Match> items, Writer writer) throws IOException {
new SimpleRenderer().render(items, writer);
}
}, }, { "XML", new CPDRenderer() {
@Override
public void render(Iterator<Match> items, Writer writer) throws IOException {
new XMLRenderer().render(items, writer);
}
}, }, { "CSV (comma)", new CPDRenderer() {
@Override
public void render(Iterator<Match> items, Writer writer) throws IOException {
new CSVRenderer(',').render(items, writer);
}
}, }, { "CSV (tab)", new CPDRenderer() {
@Override
public void render(Iterator<Match> items, Writer writer) throws IOException {
new CSVRenderer('\t').render(items, writer);
}
}, }, };
private static final Object[][] RENDERER_SETS = new Object[][] {
{ "Text", new CPDRendererAdapter(new SimpleRenderer()), },
{ "XML", new XMLRenderer(), },
{ "CSV (comma)", new CPDRendererAdapter(new CSVRenderer(',')), },
{ "CSV (tab)", new CPDRendererAdapter(new CSVRenderer('\t')), }, };
private abstract static class LanguageConfig {
public abstract Language languageFor(Properties p);
@@ -280,9 +260,9 @@ public class GUI implements CPDListener {
private class SaveListener implements ActionListener {
final CPDRenderer renderer;
final CPDReportRenderer renderer;
SaveListener(CPDRenderer theRenderer) {
SaveListener(CPDReportRenderer theRenderer) {
renderer = theRenderer;
}
@@ -296,8 +276,9 @@ public class GUI implements CPDListener {
}
if (!f.canWrite()) {
final CPDReport report = new CPDReport(matches, numberOfTokensPerFile);
try (PrintWriter pw = new PrintWriter(Files.newOutputStream(f.toPath()))) {
renderer.render(matches.iterator(), pw);
renderer.render(report, pw);
pw.flush();
JOptionPane.showMessageDialog(frame, "Saved " + matches.size() + " matches");
} catch (IOException e) {
@@ -372,14 +353,15 @@ public class GUI implements CPDListener {
private boolean trimLeadingWhitespace;
private List<Match> matches = new ArrayList<>();
private Map<String, Integer> numberOfTokensPerFile;
private void addSaveOptionsTo(JMenu menu) {
JMenuItem saveItem;
for (int i = 0; i < RENDERER_SETS.length; i++) {
saveItem = new JMenuItem("Save as " + RENDERER_SETS[i][0]);
saveItem.addActionListener(new SaveListener((CPDRenderer) RENDERER_SETS[i][1]));
for (final Object[] rendererSet : RENDERER_SETS) {
saveItem = new JMenuItem("Save as " + rendererSet[0]);
saveItem.addActionListener(new SaveListener((CPDReportRenderer) rendererSet[1]));
menu.add(saveItem);
}
}
@@ -728,6 +710,7 @@ public class GUI implements CPDListener {
cpd.go();
t.stop();
numberOfTokensPerFile = cpd.toReport().getNumberOfTokensPerFile();
matches = new ArrayList<>();
for (Iterator<Match> i = cpd.getMatches(); i.hasNext();) {
Match match = i.next();
@@ -46,6 +46,10 @@ public class MatchAlgorithm {
return matches.iterator();
}
List<Match> getMatches() {
return matches;
}
public TokenEntry tokenAt(int offset, TokenEntry m) {
return code.get(offset + m.getIndex());
}
@@ -7,7 +7,11 @@ package net.sourceforge.pmd.cpd;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@@ -22,6 +26,8 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
import net.sourceforge.pmd.cpd.renderer.CPDReportRenderer;
import net.sourceforge.pmd.util.CollectionUtil;
import net.sourceforge.pmd.util.StringUtil;
/**
@@ -29,7 +35,7 @@ import net.sourceforge.pmd.util.StringUtil;
* @author Romain Pelisse - javax.xml implementation
*
*/
public final class XMLRenderer implements Renderer, CPDRenderer {
public final class XMLRenderer implements Renderer, CPDRenderer, CPDReportRenderer {
private String encoding;
@@ -101,13 +107,26 @@ public final class XMLRenderer implements Renderer, CPDRenderer {
@Override
public void render(Iterator<Match> matches, Writer writer) throws IOException {
Document doc = createDocument();
Element root = doc.createElement("pmd-cpd");
render(new CPDReport(CollectionUtil.toList(matches), Collections.<String, Integer>emptyMap()), writer);
}
@Override
public void render(final CPDReport report, final Writer writer) throws IOException {
final Document doc = createDocument();
final Element root = doc.createElement("pmd-cpd");
final Map<String, Integer> numberOfTokensPerFile = report.getNumberOfTokensPerFile();
final List<Match> matches = report.getMatches();
doc.appendChild(root);
Match match;
while (matches.hasNext()) {
match = matches.next();
final List<Map.Entry<String, Integer>> entries = new ArrayList<>(numberOfTokensPerFile.entrySet());
for (final Map.Entry<String, Integer> pair : entries) {
final Element fileElement = doc.createElement("file");
fileElement.setAttribute("path", pair.getKey());
fileElement.setAttribute("totalNumberOfTokens", String.valueOf(pair.getValue()));
root.appendChild(fileElement);
}
for (Match match : matches) {
root.appendChild(addCodeSnippet(doc,
addFilesToDuplicationElement(doc, createDuplicationElement(doc, match), match), match));
}
@@ -10,6 +10,10 @@ import java.util.Iterator;
import net.sourceforge.pmd.cpd.Match;
/**
* @deprecated Use {@link CPDReportRenderer}
*/
@Deprecated
public interface CPDRenderer {
void render(Iterator<Match> matches, Writer writer) throws IOException;
}
@@ -0,0 +1,32 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.cpd.renderer;
import java.io.IOException;
import java.io.Writer;
import net.sourceforge.pmd.annotation.InternalApi;
import net.sourceforge.pmd.cpd.CPDReport;
/**
* Adapter to convert an old {@link CPDRenderer} into a {@link CPDReportRenderer}.
*
* @deprecated This is internal API. If you want to write your own renderer, directly implement
* {@link CPDReportRenderer}.
*/
@Deprecated
@InternalApi
public class CPDRendererAdapter implements CPDReportRenderer {
private final CPDRenderer renderer;
public CPDRendererAdapter(final CPDRenderer renderer) {
this.renderer = renderer;
}
@Override
public void render(final CPDReport report, final Writer writer) throws IOException {
renderer.render(report.getMatches().iterator(), writer);
}
}
@@ -0,0 +1,14 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.cpd.renderer;
import java.io.IOException;
import java.io.Writer;
import net.sourceforge.pmd.cpd.CPDReport;
public interface CPDReportRenderer {
void render(CPDReport report, Writer writer) throws IOException;
}
@@ -7,6 +7,7 @@ package net.sourceforge.pmd.util;
import java.io.Closeable;
import java.io.File;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -54,8 +55,16 @@ public final class IOUtil {
private IOUtil() {
}
/**
* Creates a writer that writes to stdout using the system default charset.
*
* @return a writer, never null
*
* @see #createWriter(String)
* @see #createWriter(Charset, String)
*/
public static Writer createWriter() {
return new OutputStreamWriter(System.out);
return createWriter(null);
}
/**
@@ -108,7 +117,12 @@ public final class IOUtil {
public static Writer createWriter(Charset charset, String reportFile) {
try {
if (StringUtils.isBlank(reportFile)) {
return new OutputStreamWriter(System.out, charset);
return new OutputStreamWriter(new FilterOutputStream(System.out) {
@Override
public void close() {
// do nothing, avoid closing stdout
}
}, charset);
}
Path path = new File(reportFile).toPath().toAbsolutePath();
Files.createDirectories(path.getParent()); // ensure parent dir exists
@@ -12,8 +12,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
@@ -209,6 +212,24 @@ class CoreCliTest {
assertThat(log, containsString("Ruleset reference 'dummy-basic' uses a deprecated form, use 'rulesets/dummy/basic.xml' instead"));
}
@Test
void testReportToStdoutNotClosing() throws Exception {
PrintStream originalOut = System.out;
PrintStream out = new PrintStream(new FilterOutputStream(originalOut) {
@Override
public void close() {
fail("Stream must not be closed");
}
});
try {
System.setOut(out);
SystemLambda.tapSystemErrAndOut(() -> {
runPmd(StatusCode.VIOLATIONS_FOUND, "--no-cache", "--dir", srcDir, "--rulesets", "dummy-basic");
});
} finally {
System.setOut(originalOut);
}
}
@Test
void testWrongCliOptionsDoNotPrintUsage() throws Exception {
@@ -262,5 +283,4 @@ class CoreCliTest {
assertEquals(expectedExitCode, actualExitCode, "Exit code");
}
}
@@ -13,15 +13,25 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import com.github.stefanbirkner.systemlambda.SystemLambda;
import com.google.common.collect.ImmutableMap;
class CPDCommandLineInterfaceTest {
private static final String SRC_DIR = "src/test/resources/net/sourceforge/pmd/cpd/files/";
private static final Map<String, Integer> NUMBER_OF_TOKENS = ImmutableMap.of(
new File(SRC_DIR, "dup1.java").getAbsolutePath(), 126,
new File(SRC_DIR, "dup2.java").getAbsolutePath(), 126,
new File(SRC_DIR, "file_with_ISO-8859-1_encoding.java").getAbsolutePath(), 32,
new File(SRC_DIR, "file_with_utf8_bom.java").getAbsolutePath(), 29
);
@TempDir
private Path tempDir;
@@ -34,6 +44,7 @@ class CPDCommandLineInterfaceTest {
@Test
void testEmptyResultRendering() throws Exception {
final String expectedFilesXml = getExpectedFileEntriesXml(NUMBER_OF_TOKENS.keySet());
String stdout = SystemLambda.tapSystemOut(() -> {
SystemLambda.tapSystemErr(() -> {
CPD.StatusCode statusCode = CPD.runCpd("--minimum-tokens", "340", "--language", "java", "--files",
@@ -41,23 +52,41 @@ class CPDCommandLineInterfaceTest {
assertEquals(CPD.StatusCode.OK, statusCode);
});
});
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "\n" + "<pmd-cpd/>", stdout.trim());
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "\n" + "<pmd-cpd>\n" + expectedFilesXml + "</pmd-cpd>", stdout.trim());
}
private String getExpectedFileEntryXml(final String filename) {
final int numberOfTokens = NUMBER_OF_TOKENS.get(filename);
return String.format(" <file path=\"%s\"\n totalNumberOfTokens=\"%d\"/>\n",
new File(filename).getAbsolutePath(),
numberOfTokens);
}
private String getExpectedFileEntriesXml(final Collection<String> filenames) {
final StringBuilder expectedFilesXmlBuilder = new StringBuilder();
for (final String filename : filenames) {
expectedFilesXmlBuilder.append(getExpectedFileEntryXml(filename));
}
return expectedFilesXmlBuilder.toString();
}
@Test
void testDeprecatedOptionsWarning() throws Exception {
File filelist = new File(tempDir.toFile(), "cpd-test-file-list.txt");
Files.write(filelist.toPath(), Arrays.asList(
final List<String> filepaths = Arrays.asList(
new File(SRC_DIR, "dup1.java").getAbsolutePath(),
new File(SRC_DIR, "dup2.java").getAbsolutePath()), StandardCharsets.UTF_8);
new File(SRC_DIR, "dup2.java").getAbsolutePath());
Path filelist = tempDir.resolve("cpd-test-file-list.txt");
Files.write(filelist, filepaths, StandardCharsets.UTF_8);
final String expectedFilesXml = getExpectedFileEntriesXml(filepaths);
String stderr = SystemLambda.tapSystemErr(() -> {
String stdout = SystemLambda.tapSystemOut(() -> {
CPD.StatusCode statusCode = CPD.runCpd("--minimum-tokens", "340", "--language", "java", "--filelist",
filelist.getAbsolutePath(), "--format", "xml", "-failOnViolation", "true");
filelist.toAbsolutePath().toString(), "--format", "xml", "-failOnViolation", "true");
assertEquals(CPD.StatusCode.OK, statusCode);
});
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "\n" + "<pmd-cpd/>", stdout.trim());
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "\n" + "<pmd-cpd>\n" + expectedFilesXml + "</pmd-cpd>", stdout.trim());
});
assertTrue(stderr.contains("Some deprecated options were used on the command-line, including -failOnViolation"));
assertTrue(stderr.contains("Consider replacing it with --fail-on-violation"));
@@ -12,17 +12,21 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
import net.sourceforge.pmd.cpd.renderer.CPDReportRenderer;
/**
* @author Philippe T'Seyen
@@ -186,6 +190,33 @@ class XMLRendererTest {
assertFalse(report.contains(FORM_FEED_ENTITY));
}
@Test
void testFilesWithNumberOfTokens() throws IOException, ParserConfigurationException, SAXException {
final CPDReportRenderer renderer = new XMLRenderer();
final List<Match> matches = new ArrayList<>();
final String filename = "/var/Foo.java";
final int lineCount = 6;
final String codeFragment = "code\nfragment";
final Mark mark1 = createMark("public", filename, 1, lineCount, codeFragment, 2, 3);
final Mark mark2 = createMark("stuff", filename, 73, lineCount, codeFragment, 4, 5);
final Match match = new Match(75, mark1, mark2);
matches.add(match);
final Map<String, Integer> numberOfTokensPerFile = new HashMap<>();
numberOfTokensPerFile.put(filename, 888);
final CPDReport report = new CPDReport(matches, numberOfTokensPerFile);
final StringWriter writer = new StringWriter();
renderer.render(report, writer);
final String xmlOutput = writer.toString();
final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(xmlOutput.getBytes(ENCODING)));
final NodeList files = doc.getElementsByTagName("file");
final Node file = files.item(0);
final NamedNodeMap attributes = file.getAttributes();
assertEquals("/var/Foo.java", attributes.getNamedItem("path").getNodeValue());
assertEquals("888", attributes.getNamedItem("totalNumberOfTokens").getNodeValue());
}
@Test
void testRendererXMLEscaping() throws IOException {
String codefragment = "code fragment" + FORM_FEED + "\nline2\nline3\nno & escaping necessary in CDATA\nx=\"]]>\";";
@@ -11,16 +11,20 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharArrayReader;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -297,4 +301,26 @@ class IOUtilTest {
InputStream stream = new ByteArrayInputStream(testString.getBytes(StandardCharsets.UTF_8));
assertEquals(testString, IOUtil.readToString(stream, StandardCharsets.UTF_8));
}
@Test
void testCreateWriterStdout() throws IOException {
PrintStream originalOut = System.out;
ByteArrayOutputStream data = new ByteArrayOutputStream();
PrintStream out = new PrintStream(new FilterOutputStream(data) {
@Override
public void close() {
fail("Stream must not be closed");
}
});
try {
System.setOut(out);
Writer writer = IOUtil.createWriter();
writer.write("Test");
writer.close();
assertEquals("Test", data.toString());
} finally {
System.setOut(originalOut);
}
}
}