@ -128,6 +128,20 @@ This for loop can be replaced by a foreach loop
|
||||
* sourcePath:
|
||||
* fileName:
|
||||
|
||||
## json
|
||||
|
||||
JSON format.
|
||||
|
||||
This prints a single JSON object containing some header information,
|
||||
and then the violations grouped by file. The root object fields are
|
||||
* `formatVersion`: an integer which will be incremented if we change the serialization format
|
||||
* `pmdVersion`: the version of PMD that produced the report
|
||||
* `timestamp`: explicit
|
||||
* `files`: an array of objects (see the example)
|
||||
|
||||
[Example](report-examples/pmd-report-json.json)
|
||||
|
||||
|
||||
## summaryhtml
|
||||
|
||||
Summary HTML format.
|
||||
|
@ -49,6 +49,11 @@ The default version has been set to `ES6`, so that some ECMAScript 2015 features
|
||||
supported. E.g. `let` statements and `for-of` loops are now parsed. However Rhino does
|
||||
not support all features.
|
||||
|
||||
#### New JSON renderer
|
||||
|
||||
PMD now supports a JSON renderer (use it with `-f json` on the CLI).
|
||||
See [the documentation and example](https://pmd.github.io/latest/pmd_userdocs_report_formats.hmtl#json)
|
||||
|
||||
#### New Rules
|
||||
|
||||
* The new Apex rule {% rule "apex/codestyle/FieldDeclarationsShouldBeAtStart" %} (`apex-codestyle`)
|
||||
@ -65,6 +70,7 @@ not support all features.
|
||||
* [#2210](https://github.com/pmd/pmd/issues/2210): \[apex] ApexCRUDViolation: Support WITH SECURITY_ENFORCED
|
||||
* [#2399](https://github.com/pmd/pmd/issues/2399): \[apex] ApexCRUDViolation: false positive with security enforced with line break
|
||||
* core
|
||||
* [#1286](https://github.com/pmd/pmd/issues/1286): \[core] Export Supporting JSON Format
|
||||
* [#2019](https://github.com/pmd/pmd/issues/2019): \[core] Insufficient deprecation warnings for XPath attributes
|
||||
* [#2357](https://github.com/pmd/pmd/issues/2357): Add code of conduct: Contributor Covenant
|
||||
* [#2426](https://github.com/pmd/pmd/issues/2426): \[core] CodeClimate renderer links are dead
|
||||
|
39
docs/report-examples/pmd-report-json.json
Normal file
39
docs/report-examples/pmd-report-json.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"formatVersion": 0,
|
||||
"pmdVersion": "6.23.0-SNAPSHOT",
|
||||
"timestamp": "2020-04-05T20:13:49.698+02:00",
|
||||
"files": [
|
||||
{
|
||||
"filename": "/home/me/pmd/src/main/java/net/sourceforge/pmd/PMDVersion.java",
|
||||
"violations": [
|
||||
{
|
||||
"beginline": 16,
|
||||
"begincolumn": 14,
|
||||
"endline": 79,
|
||||
"endcolumn": 1,
|
||||
"description": "The utility class name \u0027PMDVersion\u0027 doesn\u0027t match \u0027[A-Z][a-zA-Z0-9]+(Utils?|Helper)\u0027",
|
||||
"rule": "ClassNamingConventions",
|
||||
"ruleset": "Code Style",
|
||||
"priority": 1,
|
||||
"externalInfoUrl": "https://pmd.github.io/pmd/pmd_rules_java_codestyle.html#classnamingconventions"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"filename": "/home/me/pmd/src/main/java/net/sourceforge/pmd/RuleContext.java",
|
||||
"violations": [
|
||||
{
|
||||
"beginline": 124,
|
||||
"begincolumn": 9,
|
||||
"endline": 125,
|
||||
"endcolumn": 111,
|
||||
"description": "Logger calls should be surrounded by log level guards.",
|
||||
"rule": "GuardLogStatement",
|
||||
"ruleset": "Best Practices",
|
||||
"priority": 2,
|
||||
"externalInfoUrl": "https://pmd.github.io/pmd/pmd_rules_java_bestpractices.html#guardlogstatement"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
|
||||
package net.sourceforge.pmd.renderers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import net.sourceforge.pmd.PMDVersion;
|
||||
import net.sourceforge.pmd.Report;
|
||||
import net.sourceforge.pmd.RuleViolation;
|
||||
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
public class JsonRenderer extends AbstractIncrementingRenderer {
|
||||
public static final String NAME = "json";
|
||||
|
||||
// TODO do we make this public? It would make it possible to write eg
|
||||
// if (jsonObject.getInt("formatVersion") > JsonRenderer.FORMAT_VERSION)
|
||||
// /* handle unsupported version */
|
||||
// because the JsonRenderer.FORMAT_VERSION would be hardcoded by the compiler
|
||||
private static final int FORMAT_VERSION = 0;
|
||||
|
||||
private JsonWriter jsonWriter;
|
||||
|
||||
public JsonRenderer() {
|
||||
super(NAME, "JSON format.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String defaultFileExtension() {
|
||||
return "json";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws IOException {
|
||||
jsonWriter = new JsonWriter(writer);
|
||||
jsonWriter.setHtmlSafe(true);
|
||||
jsonWriter.setIndent(" ");
|
||||
|
||||
jsonWriter.beginObject();
|
||||
jsonWriter.name("formatVersion").value(FORMAT_VERSION);
|
||||
jsonWriter.name("pmdVersion").value(PMDVersion.VERSION);
|
||||
jsonWriter.name("timestamp").value(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").format(new Date()));
|
||||
jsonWriter.name("files").beginArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderFileViolations(Iterator<RuleViolation> violations) throws IOException {
|
||||
String filename = null;
|
||||
|
||||
while (violations.hasNext()) {
|
||||
RuleViolation rv = violations.next();
|
||||
String nextFilename = determineFileName(rv.getFilename());
|
||||
if (!nextFilename.equals(filename)) {
|
||||
// New File
|
||||
if (filename != null) {
|
||||
// Not first file ?
|
||||
jsonWriter.endArray(); // violations
|
||||
jsonWriter.endObject(); // file object
|
||||
}
|
||||
filename = nextFilename;
|
||||
jsonWriter.beginObject();
|
||||
jsonWriter.name("filename").value(filename);
|
||||
jsonWriter.name("violations").beginArray();
|
||||
}
|
||||
renderSingleViolation(rv);
|
||||
}
|
||||
|
||||
jsonWriter.endArray(); // violations
|
||||
jsonWriter.endObject(); // file object
|
||||
}
|
||||
|
||||
private void renderSingleViolation(RuleViolation rv) throws IOException {
|
||||
renderSingleViolation(rv, null, null);
|
||||
}
|
||||
|
||||
private void renderSingleViolation(RuleViolation rv, String suppressionType, String userMsg) throws IOException {
|
||||
jsonWriter.beginObject();
|
||||
jsonWriter.name("beginline").value(rv.getBeginLine());
|
||||
jsonWriter.name("begincolumn").value(rv.getBeginColumn());
|
||||
jsonWriter.name("endline").value(rv.getEndLine());
|
||||
jsonWriter.name("endcolumn").value(rv.getEndColumn());
|
||||
jsonWriter.name("description").value(rv.getDescription());
|
||||
jsonWriter.name("rule").value(rv.getRule().getName());
|
||||
jsonWriter.name("ruleset").value(rv.getRule().getRuleSetName());
|
||||
jsonWriter.name("priority").value(rv.getRule().getPriority().getPriority());
|
||||
if (StringUtils.isNotBlank(rv.getRule().getExternalInfoUrl())) {
|
||||
jsonWriter.name("externalInfoUrl").value(rv.getRule().getExternalInfoUrl());
|
||||
}
|
||||
if (StringUtils.isNotBlank(suppressionType)) {
|
||||
jsonWriter.name("suppressiontype").value(suppressionType);
|
||||
}
|
||||
if (StringUtils.isNotBlank(userMsg)) {
|
||||
jsonWriter.name("usermsg").value(userMsg);
|
||||
}
|
||||
jsonWriter.endObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() throws IOException {
|
||||
jsonWriter.endArray(); // files
|
||||
|
||||
jsonWriter.name("suppressedViolations").beginArray();
|
||||
String filename = null;
|
||||
if (!this.suppressed.isEmpty()) {
|
||||
for (Report.SuppressedViolation s : this.suppressed) {
|
||||
RuleViolation rv = s.getRuleViolation();
|
||||
String nextFilename = determineFileName(rv.getFilename());
|
||||
if (!nextFilename.equals(filename)) {
|
||||
// New File
|
||||
if (filename != null) {
|
||||
// Not first file ?
|
||||
jsonWriter.endArray(); // violations
|
||||
jsonWriter.endObject(); // file object
|
||||
}
|
||||
filename = nextFilename;
|
||||
jsonWriter.beginObject();
|
||||
jsonWriter.name("filename").value(filename);
|
||||
jsonWriter.name("violations").beginArray();
|
||||
}
|
||||
renderSingleViolation(rv, s.suppressedByNOPMD() ? "nopmd" : "annotation", s.getUserMessage());
|
||||
}
|
||||
jsonWriter.endArray(); // violations
|
||||
jsonWriter.endObject(); // file object
|
||||
}
|
||||
jsonWriter.endArray();
|
||||
|
||||
jsonWriter.name("processingErrors").beginArray();
|
||||
for (Report.ProcessingError error : this.errors) {
|
||||
jsonWriter.beginObject();
|
||||
jsonWriter.name("filename").value(error.getFile());
|
||||
jsonWriter.name("message").value(error.getMsg());
|
||||
jsonWriter.name("detail").value(error.getDetail());
|
||||
jsonWriter.endObject();
|
||||
}
|
||||
jsonWriter.endArray();
|
||||
|
||||
jsonWriter.name("configurationErrors").beginArray();
|
||||
for (Report.ConfigurationError error : this.configErrors) {
|
||||
jsonWriter.beginObject();
|
||||
jsonWriter.name("rule").value(error.rule().getName());
|
||||
jsonWriter.name("ruleset").value(error.rule().getRuleSetName());
|
||||
jsonWriter.name("message").value(error.issue());
|
||||
jsonWriter.endObject();
|
||||
}
|
||||
jsonWriter.endArray();
|
||||
|
||||
jsonWriter.endObject();
|
||||
jsonWriter.flush();
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@ public final class RendererFactory {
|
||||
map.put(SummaryHTMLRenderer.NAME, SummaryHTMLRenderer.class);
|
||||
map.put(VBHTMLRenderer.NAME, VBHTMLRenderer.class);
|
||||
map.put(EmptyRenderer.NAME, EmptyRenderer.class);
|
||||
map.put(JsonRenderer.NAME, JsonRenderer.class);
|
||||
REPORT_FORMAT_TO_RENDERER = Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
|
||||
package net.sourceforge.pmd.renderers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import net.sourceforge.pmd.Report;
|
||||
import net.sourceforge.pmd.Report.ConfigurationError;
|
||||
import net.sourceforge.pmd.Report.ProcessingError;
|
||||
import net.sourceforge.pmd.ReportTest;
|
||||
|
||||
public class JsonRendererTest extends AbstractRendererTest {
|
||||
|
||||
@Override
|
||||
public Renderer getRenderer() {
|
||||
return new JsonRenderer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpected() {
|
||||
return readFile("expected.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedEmpty() {
|
||||
return readFile("empty.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedMultiple() {
|
||||
return readFile("expected-multiple.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedError(ProcessingError error) {
|
||||
String expected = readFile("expected-processingerror.json");
|
||||
expected = expected.replace("###REPLACE_ME###", error.getDetail()
|
||||
.replaceAll("\r", "\\\\r")
|
||||
.replaceAll("\n", "\\\\n")
|
||||
.replaceAll("\t", "\\\\t"));
|
||||
return expected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedError(ConfigurationError error) {
|
||||
return readFile("expected-configurationerror.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedErrorWithoutMessage(ProcessingError error) {
|
||||
String expected = readFile("expected-processingerror-no-message.json");
|
||||
expected = expected.replace("###REPLACE_ME###", error.getDetail()
|
||||
.replaceAll("\r", "\\\\r")
|
||||
.replaceAll("\n", "\\\\n")
|
||||
.replaceAll("\t", "\\\\t"));
|
||||
return expected;
|
||||
}
|
||||
|
||||
private String readFile(String name) {
|
||||
try (InputStream in = JsonRendererTest.class.getResourceAsStream("json/" + name)) {
|
||||
return IOUtils.toString(in, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filter(String expected) {
|
||||
String result = expected
|
||||
.replaceAll("\"timestamp\":\\s*\"[^\"]+\"", "\"timestamp\": \"--replaced--\"")
|
||||
.replaceAll("\r\n", "\n"); // make the test run on Windows, too
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void suppressedViolations() throws IOException {
|
||||
Report rep = new Report();
|
||||
Map<Integer, String> suppressedLines = new HashMap<>();
|
||||
suppressedLines.put(1, "test");
|
||||
rep.suppress(suppressedLines);
|
||||
rep.addRuleViolation(newRuleViolation(1));
|
||||
String actual = ReportTest.render(getRenderer(), rep);
|
||||
String expected = readFile("expected-suppressed.json");
|
||||
Assert.assertEquals(filter(expected), filter(actual));
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"formatVersion": 0,
|
||||
"pmdVersion": "unknown",
|
||||
"timestamp": "--replaced--",
|
||||
"files": [],
|
||||
"suppressedViolations": [],
|
||||
"processingErrors": [],
|
||||
"configurationErrors": []
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"formatVersion": 0,
|
||||
"pmdVersion": "unknown",
|
||||
"timestamp": "--replaced--",
|
||||
"files": [],
|
||||
"suppressedViolations": [],
|
||||
"processingErrors": [],
|
||||
"configurationErrors": [
|
||||
{
|
||||
"rule": "Foo",
|
||||
"ruleset": "RuleSet",
|
||||
"message": "a configuration error"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"formatVersion": 0,
|
||||
"pmdVersion": "unknown",
|
||||
"timestamp": "--replaced--",
|
||||
"files": [
|
||||
{
|
||||
"filename": "notAvailable.ext",
|
||||
"violations": [
|
||||
{
|
||||
"beginline": 1,
|
||||
"begincolumn": 1,
|
||||
"endline": 1,
|
||||
"endcolumn": 1,
|
||||
"description": "blah",
|
||||
"rule": "Foo",
|
||||
"ruleset": "RuleSet",
|
||||
"priority": 5
|
||||
},
|
||||
{
|
||||
"beginline": 1,
|
||||
"begincolumn": 1,
|
||||
"endline": 1,
|
||||
"endcolumn": 2,
|
||||
"description": "blah",
|
||||
"rule": "Foo",
|
||||
"ruleset": "RuleSet",
|
||||
"priority": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"suppressedViolations": [],
|
||||
"processingErrors": [],
|
||||
"configurationErrors": []
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"formatVersion": 0,
|
||||
"pmdVersion": "unknown",
|
||||
"timestamp": "--replaced--",
|
||||
"files": [],
|
||||
"suppressedViolations": [],
|
||||
"processingErrors": [
|
||||
{
|
||||
"filename": "file",
|
||||
"message": "NullPointerException: null",
|
||||
"detail": "###REPLACE_ME###"
|
||||
}
|
||||
],
|
||||
"configurationErrors": []
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"formatVersion": 0,
|
||||
"pmdVersion": "unknown",
|
||||
"timestamp": "--replaced--",
|
||||
"files": [],
|
||||
"suppressedViolations": [],
|
||||
"processingErrors": [
|
||||
{
|
||||
"filename": "file",
|
||||
"message": "RuntimeException: Error",
|
||||
"detail": "###REPLACE_ME###"
|
||||
}
|
||||
],
|
||||
"configurationErrors": []
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
{
|
||||
"formatVersion": 0,
|
||||
"pmdVersion": "unknown",
|
||||
"timestamp": "2020-04-05T19:42:21.800+02:00",
|
||||
"files": [],
|
||||
"suppressedViolations": [
|
||||
{
|
||||
"filename": "notAvailable.ext",
|
||||
"violations": [
|
||||
{
|
||||
"beginline": 1,
|
||||
"begincolumn": 1,
|
||||
"endline": 1,
|
||||
"endcolumn": 1,
|
||||
"description": "blah",
|
||||
"rule": "Foo",
|
||||
"ruleset": "RuleSet",
|
||||
"priority": 5,
|
||||
"suppressiontype": "nopmd",
|
||||
"usermsg": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"processingErrors": [],
|
||||
"configurationErrors": []
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"formatVersion": 0,
|
||||
"pmdVersion": "unknown",
|
||||
"timestamp": "--replaced--",
|
||||
"files": [
|
||||
{
|
||||
"filename": "notAvailable.ext",
|
||||
"violations": [
|
||||
{
|
||||
"beginline": 1,
|
||||
"begincolumn": 1,
|
||||
"endline": 1,
|
||||
"endcolumn": 1,
|
||||
"description": "blah",
|
||||
"rule": "Foo",
|
||||
"ruleset": "RuleSet",
|
||||
"priority": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"suppressedViolations": [],
|
||||
"processingErrors": [],
|
||||
"configurationErrors": []
|
||||
}
|
Reference in New Issue
Block a user