Merge branch 'pr-2404'

[core] Add JsonRenderer #2404
This commit is contained in:
Andreas Dangel
2020-04-23 11:10:15 +02:00
13 changed files with 456 additions and 0 deletions

View File

@ -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.

View File

@ -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

View 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"
}
]
}
]
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -0,0 +1,9 @@
{
"formatVersion": 0,
"pmdVersion": "unknown",
"timestamp": "--replaced--",
"files": [],
"suppressedViolations": [],
"processingErrors": [],
"configurationErrors": []
}

View File

@ -0,0 +1,15 @@
{
"formatVersion": 0,
"pmdVersion": "unknown",
"timestamp": "--replaced--",
"files": [],
"suppressedViolations": [],
"processingErrors": [],
"configurationErrors": [
{
"rule": "Foo",
"ruleset": "RuleSet",
"message": "a configuration error"
}
]
}

View File

@ -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": []
}

View File

@ -0,0 +1,15 @@
{
"formatVersion": 0,
"pmdVersion": "unknown",
"timestamp": "--replaced--",
"files": [],
"suppressedViolations": [],
"processingErrors": [
{
"filename": "file",
"message": "NullPointerException: null",
"detail": "###REPLACE_ME###"
}
],
"configurationErrors": []
}

View File

@ -0,0 +1,15 @@
{
"formatVersion": 0,
"pmdVersion": "unknown",
"timestamp": "--replaced--",
"files": [],
"suppressedViolations": [],
"processingErrors": [
{
"filename": "file",
"message": "RuntimeException: Error",
"detail": "###REPLACE_ME###"
}
],
"configurationErrors": []
}

View File

@ -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": []
}

View File

@ -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": []
}