/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.renderers;
import static net.sourceforge.pmd.renderers.CodeClimateRule.CODECLIMATE_BLOCK_HIGHLIGHTING;
import static net.sourceforge.pmd.renderers.CodeClimateRule.CODECLIMATE_CATEGORIES;
import static net.sourceforge.pmd.renderers.CodeClimateRule.CODECLIMATE_REMEDIATION_MULTIPLIER;
import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.Iterator;
import org.apache.commons.lang3.StringUtils;
import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.PropertyDescriptor;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.RuleViolation;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Renderer for Code Climate JSON format
*/
public class CodeClimateRenderer extends AbstractIncrementingRenderer {
public static final String NAME = "codeclimate";
public static final String BODY_PLACEHOLDER = "REPLACE_THIS_WITH_MARKDOWN";
public static final int REMEDIATION_POINTS_DEFAULT = 50000;
public static final String[] CODECLIMATE_DEFAULT_CATEGORIES = new String[] { "Style" };
// Note: required by https://github.com/codeclimate/spec/blob/master/SPEC.md
protected static final String NULL_CHARACTER = "\u0000";
private Rule rule;
private final String pmdDeveloperUrl;
public CodeClimateRenderer() {
super(NAME, "Code Climate integration.");
pmdDeveloperUrl = getPmdDeveloperURL();
}
/**
* {@inheritDoc}
*/
@Override
public void renderFileViolations(Iterator<RuleViolation> violations) throws IOException {
Writer writer = getWriter();
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
while (violations.hasNext()) {
RuleViolation rv = violations.next();
rule = rv.getRule();
String json = gson.toJson(asIssue(rv));
json = json.replace(BODY_PLACEHOLDER, getBody());
writer.write(json + NULL_CHARACTER + PMD.EOL);
}
}
/**
* Generate a CodeClimateIssue suitable for processing into JSON from the
* given RuleViolation.
*
* @param rv
* RuleViolation to convert.
* @return The generated issue.
*/
private CodeClimateIssue asIssue(RuleViolation rv) {
CodeClimateIssue issue = new CodeClimateIssue();
issue.check_name = rule.getName();
issue.description = cleaned(rv.getDescription());
issue.content = new CodeClimateIssue.Content(BODY_PLACEHOLDER);
issue.location = getLocation(rv);
issue.remediation_points = getRemediationPoints();
issue.categories = getCategories();
switch (rule.getPriority()) {
case HIGH:
issue.severity = "critical";
break;
case MEDIUM_HIGH:
case MEDIUM:
case MEDIUM_LOW:
issue.severity = "normal";
break;
case LOW:
default:
issue.severity = "info";
break;
}
return issue;
}
@Override
public String defaultFileExtension() {
return "json";
}
private CodeClimateIssue.Location getLocation(RuleViolation rv) {
CodeClimateIssue.Location result;
String pathWithoutCcRoot = StringUtils.removeStartIgnoreCase(rv.getFilename(), "/code/");
if (rule.hasDescriptor(CODECLIMATE_REMEDIATION_MULTIPLIER)
&& !rule.getProperty(CODECLIMATE_BLOCK_HIGHLIGHTING)) {
result = new CodeClimateIssue.Location(pathWithoutCcRoot, rv.getBeginLine(), rv.getBeginLine());
} else {
result = new CodeClimateIssue.Location(pathWithoutCcRoot, rv.getBeginLine(), rv.getEndLine());
}
return result;
}
private int getRemediationPoints() {
int remediationPoints = REMEDIATION_POINTS_DEFAULT;
if (rule.hasDescriptor(CODECLIMATE_REMEDIATION_MULTIPLIER)) {
remediationPoints *= rule.getProperty(CODECLIMATE_REMEDIATION_MULTIPLIER);
}
return remediationPoints;
}
private String[] getCategories() {
String[] result;
if (rule.hasDescriptor(CODECLIMATE_CATEGORIES)) {
Object[] categories = rule.getProperty(CODECLIMATE_CATEGORIES);
result = new String[categories.length];
for (int i = 0; i < categories.length; i++) {
result[i] = String.valueOf(categories[i]);
}
} else {
result = CODECLIMATE_DEFAULT_CATEGORIES;
}
return result;
}
private static String getPmdDeveloperURL() {
String url = "http://pmd.github.io/pmd-" + PMD.VERSION + "/customizing/pmd-developer.html";
if (PMD.VERSION.contains("SNAPSHOT") || "unknown".equals(PMD.VERSION)) {
url = "http://pmd.sourceforge.net/snapshot/customizing/pmd-developer.html";
}
return url;
}
private <T> String getBody() {
String result = "## " + rule.getName() + "\\n\\n" + "Since: PMD " + rule.getSince() + "\\n\\n" + "Priority: "
+ rule.getPriority() + "\\n\\n"
+ "[Categories](https://github.com/codeclimate/spec/blob/master/SPEC.md#categories): "
+ Arrays.toString(getCategories()).replaceAll("[\\[\\]]", "") + "\\n\\n"
+ "[Remediation Points](https://github.com/codeclimate/spec/blob/master/SPEC.md#remediation-points): "
+ getRemediationPoints() + "\\n\\n" + cleaned(rule.getDescription());
if (!rule.getExamples().isEmpty()) {
result += "\\n\\n### Example:\\n\\n";
for (String snippet : rule.getExamples()) {
String exampleSnippet = snippet.replaceAll("\\n", "\\\\n");
exampleSnippet = exampleSnippet.replaceAll("\\t", "\\\\t");
result += "```java\\n" + exampleSnippet + "\\n``` ";
}
}
if (!rule.getPropertyDescriptors().isEmpty()) {
result += "\\n\\n### [PMD properties](" + pmdDeveloperUrl + ")\\n\\n";
result += "Name | Value | Description\\n";
result += "--- | --- | ---\\n";
for (PropertyDescriptor<?> property : rule.getPropertyDescriptors()) {
@SuppressWarnings("unchecked")
PropertyDescriptor<T> typed = (PropertyDescriptor<T>) property;
T value = rule.getProperty(typed);
String propertyValue = typed.asDelimitedString(value);
if (propertyValue == null) {
propertyValue = "";
}
propertyValue = propertyValue.replaceAll("(\n|\r\n|\r)", "\\\\n");
String porpertyName = property.name();
porpertyName = porpertyName.replaceAll("\\_", "\\\\_");
result += porpertyName + " | " + propertyValue + " | " + property.description() + "\\n";
}
}
return cleaned(result);
}
private String cleaned(String original) {
String result = original.trim();
result = result.replaceAll("\\s+", " ");
result = result.replaceAll("\\s*[\\r\\n]+\\s*", "");
result = result.replaceAll("\"", "'");
return result;
}
}