Merge pull request #4992 from adangel/cpd-report-processing-errors

[core] CPD: Include processing errors in XML report
This commit is contained in:
Juan Martín Sotuyo Dodero
2024-06-24 10:47:30 -03:00
committed by GitHub
19 changed files with 522 additions and 67 deletions

View File

@ -5,6 +5,7 @@ keywords: [formats, renderers]
summary: "Overview of the built-in report formats for CPD"
permalink: pmd_userdocs_cpd_report_formats.html
author: Andreas Dangel <andreas.dangel@pmd-code.org>
last_updated: June 2024 (7.3.0)
---
## Overview
@ -97,11 +98,19 @@ Starting at line 110 of /home/pmd/source/pmd-core/src/test/java/net/sourceforge/
This format uses XML to output the duplications in a more structured format.
The XML format can then further be processed using XSLT transformations. See [section xslt](#xslt) for examples.
Since PMD 7.3.0 any recoverable errors are also reported as additional elements `error` to help investigate
any errors occurred during analysis.
Example:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<pmd-cpd>
<pmd-cpd xmlns="https://pmd-code.org/schema/cpd-report"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
pmdVersion="7.3.0"
timestamp="2024-06-23T09:00:00+02:00"
version="1.0.0"
xsi:schemaLocation="https://pmd-code.org/schema/cpd-report https://pmd.github.io/schema/cpd-report_1_0_0.xsd">
<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">
@ -167,6 +176,32 @@ Example:
Assert.assertEquals(2, query.nodeNameToXPaths.size());
Assert.assertEquals("self::node()[(attribute::Test1 = \"false\")][(attribute::Test2 = \"true\")]", query.nodeNameToXPaths.get("dummyNode").get(0).toString());]]></codefragment>
</duplication>
<error filename="/home/pmd/source/pmd-cli/src/test/resources/net/sourceforge/pmd/cli/cpd/badandgood/BadFile.java"
msg="LexException: Lexical error in file '/home/pmd/source/pmd-cli/src/test/resources/net/sourceforge/pmd/cli/cpd/badandgood/BadFile.java' at line 4, column 14: &#34;\ufffd&#34; (65533), after : &#34;&#34; (in lexical state DEFAULT)">net.sourceforge.pmd.lang.ast.LexException: Lexical error in file '/home/pmd/source/pmd-cli/src/test/resources/net/sourceforge/pmd/cli/cpd/badandgood/BadFile.java' at line 4, column 14: "\ufffd" (65533), after : "" (in lexical state DEFAULT)
at net.sourceforge.pmd.lang.ast.InternalApiBridge.newLexException(InternalApiBridge.java:25)
at net.sourceforge.pmd.lang.java.ast.JavaParserImplTokenManager.getNextToken(JavaParserImplTokenManager.java:2698)
at net.sourceforge.pmd.lang.java.ast.JavaParserImplTokenManager.getNextToken(JavaParserImplTokenManager.java:18)
at net.sourceforge.pmd.cpd.impl.BaseTokenFilter.getNextToken(BaseTokenFilter.java:44)
at net.sourceforge.pmd.cpd.impl.CpdLexerBase.tokenize(CpdLexerBase.java:40)
at net.sourceforge.pmd.cpd.CpdLexer.tokenize(CpdLexer.java:29)
at net.sourceforge.pmd.cpd.CpdAnalysis.doTokenize(CpdAnalysis.java:146)
at net.sourceforge.pmd.cpd.CpdAnalysis.performAnalysis(CpdAnalysis.java:173)
at net.sourceforge.pmd.cli.commands.internal.CpdCommand.doExecute(CpdCommand.java:134)
at net.sourceforge.pmd.cli.commands.internal.CpdCommand.doExecute(CpdCommand.java:29)
at net.sourceforge.pmd.cli.internal.PmdRootLogger.executeInLoggingContext(PmdRootLogger.java:55)
at net.sourceforge.pmd.cli.commands.internal.AbstractAnalysisPmdSubcommand.execute(AbstractAnalysisPmdSubcommand.java:111)
at net.sourceforge.pmd.cli.commands.internal.AbstractPmdSubcommand.call(AbstractPmdSubcommand.java:30)
at net.sourceforge.pmd.cli.commands.internal.AbstractPmdSubcommand.call(AbstractPmdSubcommand.java:16)
at picocli.CommandLine.executeUserObject(CommandLine.java:2041)
at picocli.CommandLine.access$1500(CommandLine.java:148)
at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2453)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2415)
at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273)
at picocli.CommandLine$RunLast.execute(CommandLine.java:2417)
at picocli.CommandLine.execute(CommandLine.java:2170)
at net.sourceforge.pmd.cli.PmdCli.main(PmdCli.java:24)
</error>
</pmd-cpd>
```

View File

@ -15,6 +15,8 @@ This is a {{ site.pmd.release_type }} release.
### 🚀 New and noteworthy
### 🐛 Fixed Issues
* core
* [#4992](https://github.com/pmd/pmd/pull/4992): \[core] CPD: Include processing errors in XML report
* apex
* [#5053](https://github.com/pmd/pmd/issues/5053): \[apex] CPD fails to parse string literals with escaped characters
* java-bestpractices
@ -25,6 +27,29 @@ This is a {{ site.pmd.release_type }} release.
### 🚨 API Changes
#### CPD Report Format XML
There are some important changes:
1. The XML format will now use an XSD schema, that is available at <https://pmd.github.io/schema/cpd-report_1_0_0.xsd>.
This schema defines the valid elements and attributes that one can expect from a CPD report.
2. The root element `pmd-cpd` contains the new attributes `pmdVersion`, `timestamp` and `version`. The latter is
the schema version and is currently "1.0.0".
3. The CPD XML report will now also contain recoverable errors as additional `<error>` elements.
See [Report formats for CPD](pmd_userdocs_cpd_report_formats.html#xml) for an example.
The XML format should be compatible as only attributes and elements have been added. However, if you parse
the document with a namespace aware parser, you might encounter some issues like no elements being found.
In case the new format doesn't work for you (e.g. namespaces, unexpected error elements), you can
go back using the old format with the renderer "xmlold" ({%jdoc core::cpd.XMLOldRenderer %}). Note, that
this old renderer is deprecated and only there for compatibility reasons. Whatever tooling is used to
read the XML format should be updated.
#### Deprecated for removal
* {%jdoc !!core::cpd.XMLOldRenderer %} (the CPD format "xmlold").
### ✨ External Contributions
{% endtocmaker %}

View File

@ -29,6 +29,7 @@ import net.sourceforge.pmd.cpd.CPDReportRenderer;
import net.sourceforge.pmd.cpd.CSVRenderer;
import net.sourceforge.pmd.cpd.CpdAnalysis;
import net.sourceforge.pmd.cpd.SimpleRenderer;
import net.sourceforge.pmd.cpd.XMLOldRenderer;
import net.sourceforge.pmd.cpd.XMLRenderer;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.LanguageRegistry;
@ -66,6 +67,8 @@ public class CPDTask extends Task {
private static final String TEXT_FORMAT = "text";
private static final String XML_FORMAT = "xml";
@Deprecated
private static final String XMLOLD_FORMAT = "xmlold";
private static final String CSV_FORMAT = "csv";
private String format = TEXT_FORMAT;
@ -177,6 +180,8 @@ public class CPDTask extends Task {
return new SimpleRenderer();
} else if (CSV_FORMAT.equals(format)) {
return new CSVRenderer();
} else if (XMLOLD_FORMAT.equals(format)) {
return new XMLOldRenderer();
}
return new XMLRenderer();
}
@ -253,7 +258,7 @@ public class CPDTask extends Task {
}
public static class FormatAttribute extends EnumeratedAttribute {
private static final String[] FORMATS = new String[] { XML_FORMAT, TEXT_FORMAT, CSV_FORMAT };
private static final String[] FORMATS = new String[] { XML_FORMAT, TEXT_FORMAT, CSV_FORMAT, XMLOLD_FORMAT };
@Override
public String[] getValues() {

View File

@ -10,7 +10,6 @@ import static net.sourceforge.pmd.util.CollectionUtil.listOf;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import java.nio.charset.StandardCharsets;
@ -38,6 +37,14 @@ class CpdCliTest extends BaseCliTest {
private static final String SRC_DIR = BASE_RES_PATH + "files/";
private static final Path SRC_PATH = Paths.get(SRC_DIR).toAbsolutePath();
private static final String CPD_REPORT_HEADER_PATTERN = "<\\?xml version=\"1.0\" encoding=\"UTF-8\"\\?>\n"
+ "<pmd-cpd xmlns=\"https://pmd-code.org/schema/cpd-report\"\n"
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ " pmdVersion=\".+?\"\n"
+ " timestamp=\".+?\"\n"
+ " version=\"1.0.0\"\n"
+ " xsi:schemaLocation=\"https://pmd-code.org/schema/cpd-report https://pmd.github.io/schema/cpd-report_1_0_0.xsd\">\n";
private static final Map<String, Integer> NUMBER_OF_TOKENS;
static {
@ -68,8 +75,11 @@ class CpdCliTest extends BaseCliTest {
void testEmptyResultRendering() throws Exception {
final String expectedFilesXml = getExpectedFileEntriesXml(NUMBER_OF_TOKENS.keySet());
runCliSuccessfully("--minimum-tokens", "340", "--language", "java", "--dir", SRC_DIR, "--format", "xml")
.verify(result -> result.checkStdOut(equalTo(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "\n" + "<pmd-cpd>\n" + expectedFilesXml + "</pmd-cpd>\n"
.verify(result -> result.checkStdOut(containsPattern(CPD_REPORT_HEADER_PATTERN
+ "\\Q" // quote start
+ expectedFilesXml
+ "</pmd-cpd>\n"
+ "\\E" // quote end
)));
}
@ -176,8 +186,8 @@ class CpdCliTest extends BaseCliTest {
@Test
void testNoDuplicatesResultRendering() throws Exception {
String expectedReport = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<pmd-cpd>\n"
String expectedReportPattern = CPD_REPORT_HEADER_PATTERN
+ "\\Q" // quote start
+ " <file path=\"" + SRC_PATH.resolve("dup1.java") + "\"\n"
+ " totalNumberOfTokens=\"89\"/>\n"
+ " <file path=\"" + SRC_PATH.resolve("dup2.java") + "\"\n"
@ -186,10 +196,11 @@ class CpdCliTest extends BaseCliTest {
+ " totalNumberOfTokens=\"5\"/>\n"
+ " <file path=\"" + SRC_PATH.resolve("fileWith_UTF_8_BOM_Encoding.java") + "\"\n"
+ " totalNumberOfTokens=\"5\"/>\n"
+ "</pmd-cpd>\n";
+ "</pmd-cpd>\n"
+ "\\E"; // quote end
runCliSuccessfully("--minimum-tokens", "340", "--language", "java", "--dir", SRC_DIR, "--format", "xml")
.verify(result -> result.checkStdOut(equalTo(expectedReport)));
.verify(result -> result.checkStdOut(containsPattern(expectedReportPattern)));
}
/**
@ -251,9 +262,7 @@ class CpdCliTest extends BaseCliTest {
runCli(OK, "--minimum-tokens", "5", "--language", "ecmascript",
"-f", "xml",
"-d", BASE_RES_PATH + "tsFiles/")
.checkStdOut(equalTo(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<pmd-cpd/>\n"
));
.checkStdOut(containsPattern(CPD_REPORT_HEADER_PATTERN.substring(0, CPD_REPORT_HEADER_PATTERN.length() - 2) + "/>"));
}
@Test

View File

@ -39,7 +39,7 @@ final class RendererHelper {
}
try (SourceManager sourceManager = new SourceManager(textFiles)) {
CPDReport report = new CPDReport(sourceManager, matchesList, Collections.emptyMap());
CPDReport report = new CPDReport(sourceManager, matchesList, Collections.emptyMap(), Collections.emptyList());
renderer.render(report, writer);
} catch (Exception e) {
throw new RuntimeException(e);

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:cpd="https://pmd-code.org/schema/cpd-report"
exclude-result-prefixes="cpd">
<!--
PMD CPD (Copy and Paste Detector) XML to HTML transformer
-->
@ -68,10 +70,10 @@
<th>Approximate number of bytes</th>
</tr>
<tr>
<td class="SummaryNumber"><xsl:value-of select="count(//duplication)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//duplication/@lines)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//duplication/@tokens)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//duplication/@tokens) * 4"/></td>
<td class="SummaryNumber"><xsl:value-of select="count(//cpd:duplication)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//cpd:duplication/@lines)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//cpd:duplication/@tokens)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//cpd:duplication/@tokens) * 4"/></td>
</tr>
</table>
</div>
@ -91,7 +93,7 @@
</tr>
</thead>
<tbody>
<xsl:apply-templates select="pmd-cpd/duplication" />
<xsl:apply-templates select="cpd:pmd-cpd/cpd:duplication" />
</tbody>
</table>
</div>
@ -133,7 +135,7 @@
</xsl:template>
<!-- templates -->
<xsl:template match="pmd-cpd/duplication">
<xsl:template match="cpd:pmd-cpd/cpd:duplication">
<xsl:for-each select=".">
<tr>
<td><xsl:value-of select="@lines"/></td>
@ -141,7 +143,7 @@
<td>
<table class="table table-light table-bordered table-striped table-hover">
<tr><th>column</th><th>endcolumn</th><th>line</th><th>endline</th><th>path</th></tr>
<xsl:for-each select="file">
<xsl:for-each select="cpd:file">
<tr>
<td><xsl:value-of select="@column"/></td>
<td><xsl:value-of select="@endcolumn"/></td>
@ -152,7 +154,7 @@
</xsl:for-each>
</table>
</td>
<td><pre><xsl:value-of select="codefragment"/></pre></td>
<td><pre><xsl:value-of select="cpd:codefragment"/></pre></td>
</tr>
</xsl:for-each>
</xsl:template>

View File

@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Stylesheet to turn the XML output of CPD into a nice-looking HTML page -->
<!-- $Id$ -->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:cpd="https://pmd-code.org/schema/cpd-report"
exclude-result-prefixes="cpd" version="2.0">
<xsl:output method="html" encoding="utf-8" doctype-system="about:legacy-compat"/>
<xsl:param name="lines" required="yes">30</xsl:param>
<xsl:template match="pmd-cpd">
<xsl:template match="cpd:pmd-cpd">
<html>
<head>
<meta charset="utf-8"/>
@ -46,10 +48,10 @@
<th>Approx # bytes</th>
</tr>
<tr>
<td class="SummaryNumber"><xsl:value-of select="count(//duplication[@lines>$lines])"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//duplication[@lines>$lines]/@lines)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//duplication[@lines>$lines]/@tokens)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//duplication[@lines>$lines]/@tokens) * 4"/></td>
<td class="SummaryNumber"><xsl:value-of select="count(//cpd:duplication[@lines>$lines])"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//cpd:duplication[@lines>$lines]/@lines)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//cpd:duplication[@lines>$lines]/@tokens)"/></td>
<td class="SummaryNumber"><xsl:value-of select="sum(//cpd:duplication[@lines>$lines]/@tokens) * 4"/></td>
</tr>
</table>
<p/>
@ -58,13 +60,13 @@
<p/>
<table>
<tr style="background-color: #444444; color: #DDDDDD;"><td>ID</td><td>Files</td><td>Lines</td></tr>
<xsl:for-each select="//duplication[@lines>$lines]">
<xsl:for-each select="//cpd:duplication[@lines>$lines]">
<xsl:sort data-type="number" order="descending" select="@lines"/>
<tr>
<td class="ItemNumber"><xsl:value-of select="position()"/></td>
<td>
<table>
<xsl:for-each select="file">
<xsl:for-each select="cpd:file">
<tr><td><a><xsl:attribute name="href">../src/<xsl:value-of select="@path"/>.html#<xsl:value-of select="@line"/></xsl:attribute><xsl:value-of select="@path"/></a></td><td> line <xsl:value-of select="@line"/></td></tr>
</xsl:for-each>
</table>
@ -82,7 +84,7 @@
<textarea cols="100" wrap="off" class='CodeFragment' style='display:none;'>
<xsl:attribute name="rows"><xsl:value-of select="$lines"/></xsl:attribute>
<xsl:attribute name="id">frag_<xsl:value-of select="position()"/></xsl:attribute>
<xsl:value-of select="codefragment"/>
<xsl:value-of select="cpd:codefragment"/>
</textarea>
</td>
</tr></table>

View File

@ -39,6 +39,7 @@ public class CPDConfiguration extends AbstractConfiguration {
static {
RENDERERS.put(DEFAULT_RENDERER, SimpleRenderer.class);
RENDERERS.put("xml", XMLRenderer.class);
RENDERERS.put("xmlold", XMLOldRenderer.class);
RENDERERS.put("csv", CSVRenderer.class);
RENDERERS.put("csv_with_linecount_per_file", CSVWithLinecountPerFileRenderer.class);
RENDERERS.put("vs", VSRenderer.class);

View File

@ -13,6 +13,7 @@ import java.util.stream.Collectors;
import net.sourceforge.pmd.lang.document.Chars;
import net.sourceforge.pmd.lang.document.FileId;
import net.sourceforge.pmd.reporting.Report;
/**
* The result of a CPD analysis. This is rendered by a {@link CPDReportRenderer}.
@ -24,13 +25,16 @@ public class CPDReport {
private final SourceManager sourceManager;
private final List<Match> matches;
private final Map<FileId, Integer> numberOfTokensPerFile;
private final List<Report.ProcessingError> processingErrors;
CPDReport(SourceManager sourceManager,
List<Match> matches,
Map<FileId, Integer> numberOfTokensPerFile) {
Map<FileId, Integer> numberOfTokensPerFile,
List<Report.ProcessingError> processingErrors) {
this.sourceManager = sourceManager;
this.matches = Collections.unmodifiableList(matches);
this.numberOfTokensPerFile = Collections.unmodifiableMap(new TreeMap<>(numberOfTokensPerFile));
this.processingErrors = Collections.unmodifiableList(processingErrors);
}
/** Return the list of duplication matches found by the CPD analysis. */
@ -39,11 +43,15 @@ public class CPDReport {
}
/** Return a map containing the number of tokens by processed file. */
public Map<FileId, Integer> getNumberOfTokensPerFile() {
return numberOfTokensPerFile;
}
/** Returns the list of occurred processing errors. */
public List<Report.ProcessingError> getProcessingErrors() {
return processingErrors;
}
/**
* Return the slice of source code where the mark was found. This
* returns the entire lines from the start to the end line of the
@ -66,7 +74,7 @@ public class CPDReport {
public CPDReport filterMatches(Predicate<Match> filter) {
List<Match> filtered = this.matches.stream().filter(filter).collect(Collectors.toList());
return new CPDReport(sourceManager, filtered, this.getNumberOfTokensPerFile());
return new CPDReport(sourceManager, filtered, this.getNumberOfTokensPerFile(), this.getProcessingErrors());
}
/**

View File

@ -7,6 +7,7 @@ package net.sourceforge.pmd.cpd;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -30,6 +31,7 @@ import net.sourceforge.pmd.lang.document.InternalApiBridge;
import net.sourceforge.pmd.lang.document.TextDocument;
import net.sourceforge.pmd.lang.document.TextFile;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.reporting.Report;
import net.sourceforge.pmd.util.log.PmdReporter;
/**
@ -162,7 +164,7 @@ public final class CpdAnalysis implements AutoCloseable {
Map<FileId, Integer> numberOfTokensPerFile = new HashMap<>();
boolean hasErrors = false;
List<Report.ProcessingError> processingErrors = new ArrayList<>();
Tokens tokens = new Tokens();
for (TextFile textFile : sourceManager.getTextFiles()) {
TextDocument textDocument = sourceManager.get(textFile);
@ -177,11 +179,11 @@ public final class CpdAnalysis implements AutoCloseable {
}
String message = configuration.isSkipLexicalErrors() ? "Skipping file" : "Error while tokenizing";
reporter.errorEx(message, e);
hasErrors = true;
processingErrors.add(new Report.ProcessingError(e, textFile.getFileId()));
savedState.restore(tokens);
}
}
if (hasErrors && !configuration.isSkipLexicalErrors()) {
if (!processingErrors.isEmpty() && !configuration.isSkipLexicalErrors()) {
// will be caught by CPD command
throw new IllegalStateException("Errors were detected while lexing source, exiting because --skip-lexical-errors is unset.");
}
@ -192,7 +194,7 @@ public final class CpdAnalysis implements AutoCloseable {
tokens = null; // NOPMD null it out before rendering
LOGGER.debug("Finished: {} duplicates found", matches.size());
CPDReport cpdReport = new CPDReport(sourceManager, matches, numberOfTokensPerFile);
CPDReport cpdReport = new CPDReport(sourceManager, matches, numberOfTokensPerFile, processingErrors);
if (renderer != null) {
try (Writer writer = IOUtil.createWriter(Charset.defaultCharset(), null)) {

View File

@ -267,7 +267,7 @@ public class GUI implements CPDListener {
}
if (!f.canWrite()) {
final CPDReport report = new CPDReport(sourceManager, matches, numberOfTokensPerFile);
final CPDReport report = new CPDReport(sourceManager, matches, numberOfTokensPerFile, Collections.emptyList());
try (PrintWriter pw = new PrintWriter(Files.newOutputStream(f.toPath()))) {
renderer.render(report, pw);
pw.flush();
@ -549,7 +549,7 @@ public class GUI implements CPDListener {
for (int selectionIndex : selectionIndices) {
selections.add((Match) model.getValueAt(selectionIndex, 99));
}
CPDReport toRender = new CPDReport(sourceManager, selections, Collections.emptyMap());
CPDReport toRender = new CPDReport(sourceManager, selections, Collections.emptyMap(), Collections.emptyList());
String report = new SimpleRenderer(trimLeadingWhitespace).renderToString(toRender);
resultsTextArea.setText(report);
resultsTextArea.setCaretPosition(0); // move to the top

View File

@ -0,0 +1,34 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.cpd;
import java.io.IOException;
import java.io.Writer;
/**
* Provides backwards compatible XML renderer, which doesn't use namespaces, schema and
* doesn't output error information.
*
* <p>This renderer is available as "xmlold".
*
* @deprecated Update your tools to use the standard XML renderer "xml" again.
*/
@Deprecated
public class XMLOldRenderer implements CPDReportRenderer {
private final XMLRenderer xmlRenderer;
public XMLOldRenderer() {
this(null);
}
public XMLOldRenderer(String encoding) {
this.xmlRenderer = new XMLRenderer(encoding, false);
}
@Override
public void render(CPDReport report, Writer writer) throws IOException {
xmlRenderer.render(report, writer);
}
}

View File

@ -6,7 +6,10 @@ package net.sourceforge.pmd.cpd;
import java.io.IOException;
import java.io.Writer;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@ -20,9 +23,11 @@ import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import net.sourceforge.pmd.PMDVersion;
import net.sourceforge.pmd.lang.document.Chars;
import net.sourceforge.pmd.lang.document.FileId;
import net.sourceforge.pmd.lang.document.FileLocation;
import net.sourceforge.pmd.reporting.Report;
import net.sourceforge.pmd.util.StringUtil;
/**
@ -31,14 +36,19 @@ import net.sourceforge.pmd.util.StringUtil;
*
*/
public final class XMLRenderer implements CPDReportRenderer {
private static final String NAMESPACE_URI = "https://pmd-code.org/schema/cpd-report";
private static final String NAMESPACE_LOCATION = "https://pmd.github.io/schema/cpd-report_1_0_0.xsd";
private static final String SCHEMA_VERSION = "1.0.0";
private String encoding;
private final boolean newFormat;
/**
* Creates a XML Renderer with the default (platform dependent) encoding.
*/
public XMLRenderer() {
this(null);
this(null, true);
}
/**
@ -49,7 +59,12 @@ public final class XMLRenderer implements CPDReportRenderer {
* dependent) encoding is used.
*/
public XMLRenderer(String encoding) {
this(encoding, true);
}
XMLRenderer(String encoding, boolean newFormat) {
setEncoding(encoding);
this.newFormat = newFormat;
}
public void setEncoding(String encoding) {
@ -82,7 +97,11 @@ public final class XMLRenderer implements CPDReportRenderer {
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "codefragment");
if (newFormat) {
transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "{" + NAMESPACE_URI + "}codefragment");
} else {
transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "codefragment");
}
transformer.transform(new DOMSource(doc), new StreamResult(writer));
} catch (TransformerException e) {
throw new IllegalStateException(e);
@ -93,14 +112,22 @@ public final class XMLRenderer implements CPDReportRenderer {
@Override
public void render(final CPDReport report, final Writer writer) throws IOException {
final Document doc = createDocument();
final Element root = doc.createElement("pmd-cpd");
final Element root = createElement(doc, "pmd-cpd");
if (newFormat) {
root.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", NAMESPACE_URI + " " + NAMESPACE_LOCATION);
root.setAttributeNS(NAMESPACE_URI, "version", SCHEMA_VERSION);
root.setAttributeNS(NAMESPACE_URI, "pmdVersion", PMDVersion.VERSION);
root.setAttributeNS(NAMESPACE_URI, "timestamp", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
}
final Map<FileId, Integer> numberOfTokensPerFile = report.getNumberOfTokensPerFile();
doc.appendChild(root);
for (final Map.Entry<FileId, Integer> pair : numberOfTokensPerFile.entrySet()) {
final Element fileElement = doc.createElement("file");
fileElement.setAttribute("path", report.getDisplayName(pair.getKey()));
fileElement.setAttribute("totalNumberOfTokens", String.valueOf(pair.getValue()));
final Element fileElement = createElement(doc, "file");
setAttribute(fileElement, "path", report.getDisplayName(pair.getKey()));
setAttribute(fileElement, "totalNumberOfTokens", String.valueOf(pair.getValue()));
root.appendChild(fileElement);
}
@ -110,23 +137,34 @@ public final class XMLRenderer implements CPDReportRenderer {
addCodeSnippet(doc, dupElt, match, report);
root.appendChild(dupElt);
}
if (newFormat) {
for (Report.ProcessingError error : report.getProcessingErrors()) {
Element errorElt = doc.createElementNS(NAMESPACE_URI, "error");
errorElt.setAttributeNS(NAMESPACE_URI, "filename", report.getDisplayName(error.getFileId()));
errorElt.setAttributeNS(NAMESPACE_URI, "msg", error.getMsg());
errorElt.setTextContent(error.getDetail());
root.appendChild(errorElt);
}
}
dumpDocToWriter(doc, writer);
writer.flush();
}
private void addFilesToDuplicationElement(Document doc, Element duplication, Match match, CPDReport report) {
for (Mark mark : match) {
final Element file = doc.createElement("file");
final Element file = createElement(doc, "file");
FileLocation loc = mark.getLocation();
file.setAttribute("line", String.valueOf(loc.getStartLine()));
setAttribute(file, "line", String.valueOf(loc.getStartLine()));
// only remove invalid characters, escaping is done by the DOM impl.
String filenameXml10 = StringUtil.removedInvalidXml10Characters(report.getDisplayName(loc.getFileId()));
file.setAttribute("path", filenameXml10);
file.setAttribute("endline", String.valueOf(loc.getEndLine()));
file.setAttribute("column", String.valueOf(loc.getStartColumn()));
file.setAttribute("endcolumn", String.valueOf(loc.getEndColumn()));
file.setAttribute("begintoken", String.valueOf(mark.getBeginTokenIndex()));
file.setAttribute("endtoken", String.valueOf(mark.getEndTokenIndex()));
setAttribute(file, "path", filenameXml10);
setAttribute(file, "endline", String.valueOf(loc.getEndLine()));
setAttribute(file, "column", String.valueOf(loc.getStartColumn()));
setAttribute(file, "endcolumn", String.valueOf(loc.getEndColumn()));
setAttribute(file, "begintoken", String.valueOf(mark.getBeginTokenIndex()));
setAttribute(file, "endtoken", String.valueOf(mark.getEndTokenIndex()));
duplication.appendChild(file);
}
}
@ -136,7 +174,7 @@ public final class XMLRenderer implements CPDReportRenderer {
if (codeSnippet != null) {
// the code snippet has normalized line endings
String platformSpecific = codeSnippet.toString().replace("\n", System.lineSeparator());
Element codefragment = doc.createElement("codefragment");
Element codefragment = createElement(doc, "codefragment");
// only remove invalid characters, escaping is not necessary in CDATA.
// if the string contains the end marker of a CDATA section, then the DOM impl will
// create two cdata sections automatically.
@ -146,9 +184,24 @@ public final class XMLRenderer implements CPDReportRenderer {
}
private Element createDuplicationElement(Document doc, Match match) {
Element duplication = doc.createElement("duplication");
duplication.setAttribute("lines", String.valueOf(match.getLineCount()));
duplication.setAttribute("tokens", String.valueOf(match.getTokenCount()));
Element duplication = createElement(doc, "duplication");
setAttribute(duplication, "lines", String.valueOf(match.getLineCount()));
setAttribute(duplication, "tokens", String.valueOf(match.getTokenCount()));
return duplication;
}
private Element createElement(Document doc, String name) {
if (newFormat) {
return doc.createElementNS(NAMESPACE_URI, name);
}
return doc.createElement(name);
}
private void setAttribute(Element element, String name, String value) {
if (newFormat) {
element.setAttributeNS(NAMESPACE_URI, name, value);
} else {
element.setAttribute(name, value);
}
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="https://pmd-code.org/schema/cpd-report" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="https://pmd-code.org/schema/cpd-report"
elementFormDefault="qualified">
<xsd:annotation>
<xs:documentation><![CDATA[
PMD CPD Report Schema, version 1.0.0
This XML format is produced by CPD's XMLRenderer "xml".
]]></xs:documentation>
</xsd:annotation>
<xs:element name="pmd-cpd">
<xs:complexType>
<xs:sequence>
<xs:element name="file" type="file" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="duplication" type="duplication" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="error" type="error" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="version" type="xs:string" use="required"/>
<xs:attribute name="pmdVersion" type="xs:string" use="required"/>
<xs:attribute name="timestamp" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
<xs:complexType name="file">
<xs:attribute name="path" type="xs:string" use="required"/>
<xs:attribute name="totalNumberOfTokens" type="xs:nonNegativeInteger" use="required"/>
</xs:complexType>
<xs:complexType name="duplication">
<xs:sequence>
<xs:element name="file" type="fileLocation" minOccurs="2" maxOccurs="unbounded"/>
<xs:element name="codefragment" type="codefragment" minOccurs="1" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="lines" type="xs:positiveInteger" use="required"/>
<xs:attribute name="tokens" type="xs:positiveInteger" use="required"/>
</xs:complexType>
<xs:complexType name="error">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="filename" type="xs:string" use="required"/>
<xs:attribute name="msg" type="xs:string" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="fileLocation">
<xs:attribute name="column" type="xs:positiveInteger" use="required"/>
<xs:attribute name="endcolumn" type="xs:positiveInteger" use="required"/>
<xs:attribute name="endline" type="xs:positiveInteger" use="required"/>
<xs:attribute name="line" type="xs:positiveInteger" use="required"/>
<xs:attribute name="begintoken" type="xs:nonNegativeInteger" use="required"/>
<xs:attribute name="endtoken" type="xs:nonNegativeInteger" use="required"/>
<xs:attribute name="path" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="codefragment">
<xs:simpleContent>
<xs:extension base="xs:string"/>
</xs:simpleContent>
</xs:complexType>
</xs:schema>

View File

@ -4,8 +4,11 @@
package net.sourceforge.pmd.cpd;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.any;
@ -22,6 +25,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -34,6 +38,7 @@ import net.sourceforge.pmd.lang.ast.LexException;
import net.sourceforge.pmd.lang.ast.impl.javacc.MalformedSourceException;
import net.sourceforge.pmd.lang.document.FileId;
import net.sourceforge.pmd.lang.document.TextFile;
import net.sourceforge.pmd.reporting.Report;
import net.sourceforge.pmd.util.log.PmdReporter;
/**
@ -212,6 +217,36 @@ class CpdAnalysisTest {
verifyNoMoreInteractions(reporter);
}
@Test
void reportShouldContainProcessingErrors() throws IOException {
AtomicReference<CPDReport> report = new AtomicReference<>();
PmdReporter reporter = mock(PmdReporter.class);
config.setReporter(reporter);
config.setSkipLexicalErrors(true); // must be true, otherwise CPD is aborted with first processing error
try (CpdAnalysis cpd = CpdAnalysis.create(config)) {
assertTrue(cpd.files().addSourceFile(FileId.fromPathLikeString("foo.dummy"), DummyLanguageModule.CPD_THROW_LEX_EXCEPTION));
assertTrue(cpd.files().addSourceFile(FileId.fromPathLikeString("foo2.dummy"), DummyLanguageModule.CPD_THROW_MALFORMED_SOURCE_EXCEPTION));
cpd.performAnalysis(report::set);
}
assertNotNull(report.get(), "CPD aborted early without producing a report");
List<Report.ProcessingError> processingErrors = report.get().getProcessingErrors();
assertEquals(2, processingErrors.size());
Report.ProcessingError error1 = processingErrors.get(0);
assertEquals("foo.dummy", error1.getFileId().getFileName());
assertThat(error1.getDetail(), containsString(LexException.class.getSimpleName()));
Report.ProcessingError error2 = processingErrors.get(1);
assertEquals("foo2.dummy", error2.getFileId().getFileName());
assertThat(error2.getDetail(), containsString(MalformedSourceException.class.getSimpleName()));
verify(reporter).errorEx(eq("Skipping file"), any(LexException.class));
verify(reporter).errorEx(eq("Skipping file"), any(MalformedSourceException.class));
verifyNoMoreInteractions(reporter);
}
@Test
void testSkipLexicalErrors() throws IOException {
PmdReporter reporter = mock(PmdReporter.class);

View File

@ -15,6 +15,7 @@ import java.util.Set;
import net.sourceforge.pmd.lang.DummyLanguageModule;
import net.sourceforge.pmd.lang.document.FileId;
import net.sourceforge.pmd.lang.document.TextFile;
import net.sourceforge.pmd.reporting.Report;
final class CpdTestUtils {
@ -26,10 +27,10 @@ final class CpdTestUtils {
}
static CPDReport makeReport(List<Match> matches) {
return makeReport(matches, Collections.emptyMap());
return makeReport(matches, Collections.emptyMap(), Collections.emptyList());
}
static CPDReport makeReport(List<Match> matches, Map<FileId, Integer> numTokensPerFile) {
static CPDReport makeReport(List<Match> matches, Map<FileId, Integer> numTokensPerFile, List<Report.ProcessingError> processingErrors) {
Set<TextFile> textFiles = new HashSet<>();
for (Match match : matches) {
match.iterator().forEachRemaining(
@ -41,7 +42,8 @@ final class CpdTestUtils {
return new CPDReport(
new SourceManager(new ArrayList<>(textFiles)),
matches,
numTokensPerFile
numTokensPerFile,
processingErrors
);
}
@ -73,7 +75,8 @@ final class CpdTestUtils {
return new CPDReport(
new SourceManager(new ArrayList<>(textFiles)),
matches,
numTokensPerFile
numTokensPerFile,
Collections.emptyList()
);
}

View File

@ -0,0 +1,88 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.cpd;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections;
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.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import net.sourceforge.pmd.lang.document.FileId;
class XMLOldRendererTest {
private static final String ENCODING = (String) System.getProperties().get("file.encoding");
@Test
void testWithNoDuplication() throws IOException, ParserConfigurationException, SAXException {
CPDReportRenderer renderer = new XMLOldRenderer();
StringWriter sw = new StringWriter();
renderer.render(CpdTestUtils.makeReport(Collections.emptyList()), sw);
String report = sw.toString();
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<pmd-cpd/>\n",
report,
"no namespace expected");
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
NodeList nodes = doc.getChildNodes();
Node n = nodes.item(0);
assertEquals("pmd-cpd", n.getNodeName());
assertEquals(0, doc.getElementsByTagName("duplication").getLength());
}
@Test
void testWithOneDuplication() throws Exception {
CPDReportRenderer renderer = new XMLOldRenderer();
CpdTestUtils.CpdReportBuilder builder = new CpdTestUtils.CpdReportBuilder();
int lineCount = 6;
FileId foo1 = CpdTestUtils.FOO_FILE_ID;
Mark mark1 = builder.createMark("public", foo1, 1, lineCount);
Mark mark2 = builder.createMark("stuff", foo1, 73, lineCount);
builder.addMatch(new Match(75, mark1, mark2));
StringWriter sw = new StringWriter();
renderer.render(builder.build(), sw);
String report = sw.toString();
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
NodeList dupes = doc.getElementsByTagName("duplication");
assertEquals(1, dupes.getLength());
Node file = dupes.item(0).getFirstChild();
while (file != null && file.getNodeType() != Node.ELEMENT_NODE) {
file = file.getNextSibling();
}
if (file != null) {
assertEquals("1", file.getAttributes().getNamedItem("line").getNodeValue());
assertEquals(foo1.getAbsolutePath(), file.getAttributes().getNamedItem("path").getNodeValue());
assertEquals("6", file.getAttributes().getNamedItem("endline").getNodeValue());
assertEquals("1", file.getAttributes().getNamedItem("column").getNodeValue());
assertEquals("1", file.getAttributes().getNamedItem("endcolumn").getNodeValue());
file = file.getNextSibling();
while (file != null && file.getNodeType() != Node.ELEMENT_NODE) {
file = file.getNextSibling();
}
}
if (file != null) {
assertEquals("73", file.getAttributes().getNamedItem("line").getNodeValue());
assertEquals("78", file.getAttributes().getNamedItem("endline").getNodeValue());
assertEquals("1", file.getAttributes().getNamedItem("column").getNodeValue());
assertEquals("1", file.getAttributes().getNamedItem("endcolumn").getNodeValue());
}
assertEquals(1, doc.getElementsByTagName("codefragment").getLength());
assertEquals(CpdTestUtils.generateDummyContent(lineCount), doc.getElementsByTagName("codefragment").item(0).getTextContent());
}
}

View File

@ -11,20 +11,32 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Collections;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
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.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import net.sourceforge.pmd.cpd.CpdTestUtils.CpdReportBuilder;
import net.sourceforge.pmd.lang.ast.LexException;
import net.sourceforge.pmd.lang.document.FileId;
import net.sourceforge.pmd.reporting.Report;
/**
* @author Philippe T'Seyen
@ -42,6 +54,18 @@ class XMLRendererTest {
StringWriter sw = new StringWriter();
renderer.render(CpdTestUtils.makeReport(Collections.emptyList()), sw);
String report = sw.toString();
assertReportIsValidSchema(report);
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<pmd-cpd xmlns=\"https://pmd-code.org/schema/cpd-report\"\n"
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ " pmdVersion=\"XXX\"\n"
+ " timestamp=\"XXX\"\n"
+ " version=\"1.0.0\"\n"
+ " xsi:schemaLocation=\"https://pmd-code.org/schema/cpd-report https://pmd.github.io/schema/cpd-report_1_0_0.xsd\"/>\n",
report.replaceAll("timestamp=\".+?\"", "timestamp=\"XXX\"")
.replaceAll("pmdVersion=\".+?\"", "pmdVersion=\"XXX\""),
"namespace is missing or wrong");
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
@ -64,6 +88,7 @@ class XMLRendererTest {
StringWriter sw = new StringWriter();
renderer.render(builder.build(), sw);
String report = sw.toString();
assertReportIsValidSchema(report);
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
@ -113,6 +138,7 @@ class XMLRendererTest {
StringWriter sw = new StringWriter();
renderer.render(builder.build(), sw);
String report = sw.toString();
assertReportIsValidSchema(report);
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
@ -133,6 +159,7 @@ class XMLRendererTest {
StringWriter sw = new StringWriter();
renderer.render(builder.build(), sw);
String report = sw.toString();
assertReportIsValidSchema(report);
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
@ -164,7 +191,7 @@ class XMLRendererTest {
}
@Test
void testRendererEncodedPath() throws IOException {
void testRendererEncodedPath() throws Exception {
CPDReportRenderer renderer = new XMLRenderer();
CpdReportBuilder builder = new CpdReportBuilder();
final String escapeChar = "&amp;";
@ -175,6 +202,7 @@ class XMLRendererTest {
StringWriter sw = new StringWriter();
renderer.render(builder.build(), sw);
String report = sw.toString();
assertReportIsValidSchema(report);
assertThat(report, containsString(escapeChar));
}
@ -194,6 +222,7 @@ class XMLRendererTest {
final StringWriter writer = new StringWriter();
renderer.render(report, writer);
final String xmlOutput = writer.toString();
assertReportIsValidSchema(xmlOutput);
final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(xmlOutput.getBytes(ENCODING)));
final NodeList files = doc.getElementsByTagName("file");
@ -219,6 +248,7 @@ class XMLRendererTest {
final StringWriter writer = new StringWriter();
renderer.render(report, writer);
final String xmlOutput = writer.toString();
assertReportIsValidSchema(xmlOutput);
final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(xmlOutput.getBytes(ENCODING)));
final NodeList files = doc.getElementsByTagName("file");
@ -234,7 +264,7 @@ class XMLRendererTest {
}
@Test
void testRendererXMLEscaping() throws IOException {
void testRendererXMLEscaping() throws Exception {
String codefragment = "code fragment" + FORM_FEED
+ "\nline2\nline3\nno & escaping necessary in CDATA\nx=\"]]>\";";
CPDReportRenderer renderer = new XMLRenderer();
@ -251,10 +281,61 @@ class XMLRendererTest {
StringWriter sw = new StringWriter();
renderer.render(builder.build(), sw);
String report = sw.toString();
assertReportIsValidSchema(report);
assertThat(report, not(containsString(FORM_FEED)));
assertThat(report, not(containsString(FORM_FEED_ENTITY)));
assertThat(report, containsString("no & escaping necessary in CDATA"));
assertThat(report, containsString("x=\"]]]]><![CDATA[>\";"));
assertThat(report, not(containsString("x=\"]]>\";"))); // must be escaped
}
@Test
void reportContainsProcessingError() throws Exception {
FileId fileId = FileId.fromPathLikeString("file1.txt");
Report.ProcessingError processingError = new Report.ProcessingError(
new LexException(2, 1, fileId, "test exception", new RuntimeException("cause exception")),
fileId);
CPDReportRenderer renderer = new XMLRenderer();
StringWriter sw = new StringWriter();
renderer.render(CpdTestUtils.makeReport(Collections.emptyList(), Collections.emptyMap(), Collections.singletonList(processingError)), sw);
String report = sw.toString();
assertReportIsValidSchema(report);
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
NodeList nodes = doc.getChildNodes();
Node n = nodes.item(0);
assertEquals("pmd-cpd", n.getNodeName());
assertEquals(1, doc.getElementsByTagName("error").getLength());
Node error = doc.getElementsByTagName("error").item(0);
String filename = error.getAttributes().getNamedItem("filename").getNodeValue();
assertEquals(processingError.getFileId().getAbsolutePath(), filename);
String msg = error.getAttributes().getNamedItem("msg").getNodeValue();
assertEquals(processingError.getMsg(), msg);
String textContent = error.getTextContent();
assertEquals(processingError.getDetail(), textContent);
}
private static void assertReportIsValidSchema(String report) throws SAXException, ParserConfigurationException, IOException {
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = schemaFactory.newSchema(new StreamSource(XMLRenderer.class.getResourceAsStream("/cpd-report_1_0_0.xsd")));
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setValidating(false);
saxParserFactory.setSchema(schema);
SAXParser saxParser = saxParserFactory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(report)), new DefaultHandler() {
@Override
public void error(SAXParseException e) throws SAXException {
throw e;
}
@Override
public void warning(SAXParseException e) throws SAXException {
throw e;
}
});
}
}

View File

@ -1,8 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<pmd-cpd>
<pmd-cpd xmlns="https://pmd-code.org/schema/cpd-report"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
pmdVersion="7.3.0"
timestamp="2024-06-23T09:00:00+02:00"
version="1.0.0"
xsi:schemaLocation="https://pmd-code.org/schema/cpd-report https://pmd.github.io/schema/cpd-report_1_0_0.xsd">
<duplication lines="60" tokens="75">
<file column="2" endcolumn="3" endline="6" line="1" path="/var/Foo.java"/>
<file column="4" endcolumn="5" endline="78" line="73" path="/var/Foo.java"/>
<file column="2" endcolumn="3" endline="6" line="1" begintoken="0" endtoken="1" path="/var/Foo.java"/>
<file column="4" endcolumn="5" endline="78" line="73" begintoken="0" endtoken="1" path="/var/Foo.java"/>
<codefragment><![CDATA[code
fragment]]></codefragment>
</duplication>