@@ -23,27 +23,46 @@ package org.sonar.api.server.debt; | |||
import javax.annotation.CheckForNull; | |||
/** | |||
* Function used to calculate the remediation cost of an issue. There are three types : | |||
* <ul> | |||
* <li> | |||
* <b>Linear</b> - Each issue of the rule costs the same amount of time (coefficient) to fix. | |||
* </li> | |||
* <li> | |||
* <b>Linear with offset</b> - It takes a certain amount of time to analyze the issues of such kind on the file (offset). | |||
* Then, each issue of the rule costs the same amount of time (coefficient) to fix. Total remediation cost | |||
* by file = offset + (number of issues x coefficient) | |||
* </li> | |||
* <li><b>Constant/issue</b> - The cost to fix all the issues of the rule is the same whatever the number of issues | |||
* of this rule in the file. Total remediation cost by file = constant | |||
* </li> | |||
* </ul> | |||
* Function used to calculate the remediation cost of an issue. See {@link Type} for details. | |||
* <p>The coefficient and offset involved in the functions are durations. They are defined in hours, minutes and/or | |||
* seconds. Examples: "5min", "1h 10min". Supported units are "d" (days), "h" (hour), and "min" (minutes).</p> | |||
* | |||
* @since 4.3 | |||
*/ | |||
public interface DebtRemediationFunction { | |||
enum Type { | |||
LINEAR(true, false), LINEAR_OFFSET(true, true), CONSTANT_ISSUE(false, true); | |||
/** | |||
* The cost to fix an issue of this type depends on the magnitude of the issue. | |||
* For instance, an issue related to file size might be linear, with the total cost-to-fix incrementing | |||
* (by the coefficient amount) for each line of code above the allowed threshold. | |||
* The rule must provide the "effort to fix" value when raising an issue. | |||
*/ | |||
LINEAR(true, false), | |||
/** | |||
* It takes a certain amount of time to deal with an issue of this type (this is the offset). | |||
* Then, the magnitude of the issue comes in to play. For instance, an issue related to complexity might be linear with offset. | |||
* So the total cost to fix is the time to make the basic analysis (the offset) plus the time required to deal | |||
* with each complexity point above the allowed value. | |||
* <p> | |||
* <code>Total remediation cost = offset + (number of noncompliance x coefficient)</code> | |||
* </p> | |||
* <p>The rule must provide the "effort to fix" value when raising an issue. Let’s take as a example the “Paragraphs should not be too complex” rule. | |||
* If you set the rule threshold to 20, and you have a paragraph with a complexity of 27, you have 7 points of complexity | |||
* to remove. Internally, this is called the Effort to Fix. In that case, if you use the LINEAR_OFFSET configuration | |||
* with an offset of 4h and a remediation cost of 1mn, the technical debt for this issue related to a | |||
* too-complex block of code will be: (7 complexity points x 1min) + 4h = 4h and 7mn | |||
* </p> | |||
*/ | |||
LINEAR_OFFSET(true, true), | |||
/** | |||
* The cost to fix all the issues of the rule is the same whatever the number of issues | |||
* of this rule in the file. Total remediation cost by file = constant | |||
*/ | |||
CONSTANT_ISSUE(false, true); | |||
private final boolean usesCoefficient; | |||
private final boolean usesOffset; | |||
@@ -65,13 +84,13 @@ public interface DebtRemediationFunction { | |||
Type type(); | |||
/** | |||
* Factor is set on types {@link Type#LINEAR} and {@link Type#LINEAR_OFFSET}, else it's null. | |||
* Non-null value on {@link Type#LINEAR} and {@link Type#LINEAR_OFFSET} functions, else {@code null}. | |||
*/ | |||
@CheckForNull | |||
String coefficient(); | |||
/** | |||
* Offset is set on types {@link Type#LINEAR_OFFSET} and {@link Type#CONSTANT_ISSUE}, else it's null. | |||
* Non-null value on {@link Type#LINEAR_OFFSET} and {@link Type#CONSTANT_ISSUE} functions, else {@code null}. | |||
*/ | |||
@CheckForNull | |||
String offset(); |
@@ -56,7 +56,8 @@ class DefaultDebtRemediationFunctions implements RulesDefinition.DebtRemediation | |||
return create(DefaultDebtRemediationFunction.Type.CONSTANT_ISSUE, null, offset); | |||
} | |||
private DebtRemediationFunction create(DefaultDebtRemediationFunction.Type type, @Nullable String coefficient, @Nullable String offset) { | |||
@Override | |||
public DebtRemediationFunction create(DebtRemediationFunction.Type type, @Nullable String coefficient, @Nullable String offset) { | |||
try { | |||
return new DefaultDebtRemediationFunction(type, coefficient, offset); | |||
} catch (Exception e) { |
@@ -613,11 +613,35 @@ public interface RulesDefinition { | |||
* Factory of {@link org.sonar.api.server.debt.DebtRemediationFunction}. | |||
*/ | |||
interface DebtRemediationFunctions { | |||
/** | |||
* Shortcut for {@code create(Type.LINEAR, coefficient, null)}. | |||
* @param coefficient the duration to fix one issue. See {@link DebtRemediationFunction} for details about format. | |||
* @see org.sonar.api.server.debt.DebtRemediationFunction.Type#LINEAR | |||
*/ | |||
DebtRemediationFunction linear(String coefficient); | |||
/** | |||
* Shortcut for {@code create(Type.LINEAR_OFFSET, coefficient, offset)}. | |||
* @param coefficient duration to fix one point of complexity. See {@link DebtRemediationFunction} for details and format. | |||
* @param offset duration to make basic analysis. See {@link DebtRemediationFunction} for details and format. | |||
* @see org.sonar.api.server.debt.DebtRemediationFunction.Type#LINEAR_OFFSET | |||
*/ | |||
DebtRemediationFunction linearWithOffset(String coefficient, String offset); | |||
DebtRemediationFunction constantPerIssue(String offset); | |||
/** | |||
* Shortcut for {@code create(Type.CONSTANT_ISSUE, null, constant)}. | |||
* @param constant cost per issue. See {@link DebtRemediationFunction} for details and format. | |||
* @see org.sonar.api.server.debt.DebtRemediationFunction.Type#CONSTANT_ISSUE | |||
*/ | |||
DebtRemediationFunction constantPerIssue(String constant); | |||
/** | |||
* Flexible way to create a {@link DebtRemediationFunction}. An unchecked exception is thrown if | |||
* coefficient and/or offset are not valid according to the given @{code type}. | |||
* @since 5.3 | |||
*/ | |||
DebtRemediationFunction create(DebtRemediationFunction.Type type, @Nullable String coefficient, @Nullable String offset); | |||
} | |||
class NewRule { | |||
@@ -755,11 +779,11 @@ public interface RulesDefinition { | |||
} | |||
/** | |||
* For rules that use "Linear"/"Linear with offset" remediation functions, the meaning | |||
* For rules that use LINEAR or LINEAR_OFFSET remediation functions, the meaning | |||
* of the function parameter (= "effort to fix") must be set. This description | |||
* explains what 1 point of "effort to fix" represents for the rule. | |||
* <p/> | |||
* Example : : for the "Insufficient condition coverage", this description for the | |||
* Example: for the "Insufficient condition coverage", this description for the | |||
* remediation function coefficient/offset would be something like | |||
* "Effort to test one uncovered condition". | |||
*/ |
@@ -19,18 +19,6 @@ | |||
*/ | |||
package org.sonar.api.server.rule; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.codehaus.staxmate.SMInputFactory; | |||
import org.codehaus.staxmate.in.SMHierarchicCursor; | |||
import org.codehaus.staxmate.in.SMInputCursor; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.rule.RuleStatus; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.check.Cardinality; | |||
import javax.xml.stream.XMLInputFactory; | |||
import javax.xml.stream.XMLStreamException; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.InputStreamReader; | |||
@@ -38,25 +26,46 @@ import java.io.Reader; | |||
import java.nio.charset.Charset; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import javax.annotation.Nullable; | |||
import javax.xml.stream.XMLInputFactory; | |||
import javax.xml.stream.XMLStreamException; | |||
import org.codehaus.staxmate.SMInputFactory; | |||
import org.codehaus.staxmate.in.SMHierarchicCursor; | |||
import org.codehaus.staxmate.in.SMInputCursor; | |||
import org.sonar.api.rule.RuleStatus; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.server.debt.DebtRemediationFunction; | |||
import org.sonar.check.Cardinality; | |||
import static java.lang.String.format; | |||
import static org.apache.commons.lang.StringUtils.equalsIgnoreCase; | |||
import static org.apache.commons.lang.StringUtils.isNotBlank; | |||
import static org.apache.commons.lang.StringUtils.trim; | |||
/** | |||
* Helper class to load {@link RulesDefinition} extension point from a XML file. | |||
* Loads definitions of rules from a XML file. | |||
* | |||
* <h3>Example</h3> | |||
* <h3>Usage</h3> | |||
* <pre> | |||
* public class MyRules implements RulesDefinition { | |||
* public class MyJsRulesDefinition implements RulesDefinition { | |||
* | |||
* private static final String PATH = "my-js-rules.xml"; | |||
* private final RulesDefinitionXmlLoader xmlLoader; | |||
* | |||
* public MyRules(RulesDefinitionXmlLoader xmlLoader) { | |||
* public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader) { | |||
* this.xmlLoader = xmlLoader; | |||
* } | |||
* | |||
* {@literal @}Override | |||
* public void define(Context context) { | |||
* NewRepository repository = context.createRepository("my-repo", "my-lang"); | |||
* xmlLoader.load(repository, getClass().getResourceAsStream("/my-rules.xml"), StandardCharsets.UTF_8.name()); | |||
* repository.done(); | |||
* try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(PATH), StandardCharsets.UTF_8)) { | |||
* NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer"); | |||
* xmlLoader.load(repository, reader); | |||
* repository.done(); | |||
* } catch (IOException e) { | |||
* throw new IllegalStateException(String.format("Fail to read file %s", PATH), e); | |||
* } | |||
* } | |||
* } | |||
* </pre> | |||
@@ -65,10 +74,15 @@ import java.util.List; | |||
* <pre> | |||
* <rules> | |||
* <rule> | |||
* <key>the-required-rule-key</key>* | |||
* <name>The required purpose of the rule</name> | |||
** <description> | |||
* <![CDATA[Required HTML description]]> | |||
* <!-- required key --> | |||
* <key>the-rule-key</key> | |||
* | |||
* <!-- required name --> | |||
* <name>The purpose of the rule</name> | |||
* | |||
* <!-- required description, in HTML format --> | |||
* <description> | |||
* <![CDATA[The HTML description]]> | |||
* </description> | |||
* | |||
* <!-- Optional key for configuration of some rule engines --> | |||
@@ -91,30 +105,81 @@ import java.util.List; | |||
* <param> | |||
* <key>the-param-key</key> | |||
* <description> | |||
* <![CDATA[the optional param description]]> | |||
* <![CDATA[the optional description, in HTML format]]> | |||
* </description> | |||
* <!-- Optional field to define the default value used when enabling the rule in a Quality profile --> | |||
* <!-- Optional default value, used when enabling the rule in a Quality profile --> | |||
* <defaultValue>42</defaultValue> | |||
* </param> | |||
* <param> | |||
* <key>another-param</key> | |||
* </param> | |||
* | |||
* <!-- Deprecated field, replaced by "internalKey" --> | |||
* <!-- SQALE debt - key of sub-characteristic --> | |||
* <!-- See {@link org.sonar.api.server.rule.RulesDefinition.SubCharacteristics} for core supported values. | |||
* Any other values can be used. If sub-characteristic does not exist at runtime in the SQALE model, | |||
* then the rule is created without any sub-characteristic. --> | |||
* <!-- Since 5.3 --> | |||
* <debtSubCharacteristic>MODULARITY</debtSubCharacteristic> | |||
* | |||
* <!-- SQALE debt - type of debt remediation function --> | |||
* <!-- See enum {@link org.sonar.api.server.debt.DebtRemediationFunction.Type} for supported values --> | |||
* <!-- Since 5.3 --> | |||
* <debtRemediationFunction>LINEAR_OFFSET</debtRemediationFunction> | |||
* | |||
* <!-- SQALE debt - raw description of the "effort to fix", used for some types of remediation functions. --> | |||
* <!-- See {@link org.sonar.api.server.rule.RulesDefinition.NewRule#setEffortToFixDescription(String)} --> | |||
* <!-- Since 5.3 --> | |||
* <effortToFixDescription>Effort to test one uncovered condition</effortToFixDescription> | |||
* | |||
* <!-- SQALE debt - coefficient of debt remediation function. Must be defined only for some function types. --> | |||
* <!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --> | |||
* <!-- Since 5.3 --> | |||
* <debtRemediationFunctionCoefficient>10min</debtRemediationFunctionCoefficient> | |||
* | |||
* <!-- SQALE debt - offset of debt remediation function. Must be defined only for some function types. --> | |||
* <!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --> | |||
* <!-- Since 5.3 --> | |||
* <debtRemediationFunctionOffset>2min</debtRemediationFunctionOffset> | |||
* | |||
* <!-- deprecated field, replaced by "internalKey" --> | |||
* <configKey>Checker/TreeWalker/LocalVariableName</configKey> | |||
* | |||
* <!-- Deprecated field, replaced by "severity" --> | |||
* <!-- deprecated field, replaced by "severity" --> | |||
* <priority>BLOCKER</priority> | |||
* </rule> | |||
* </rules> | |||
* </pre> | |||
* | |||
* <h3>XML Example</h3> | |||
* <pre> | |||
* <rules> | |||
* <rule> | |||
* <key>S1442</key> | |||
* <name>"alert(...)" should not be used</name> | |||
* <description>alert(...) can be useful for debugging during development, but ...</description> | |||
* <tag>cwe</tag> | |||
* <tag>security</tag> | |||
* <tag>user-experience</tag> | |||
* <debtSubCharacteristic>SECURITY_FEATURES</debtSubCharacteristic> | |||
* <debtRemediationFunction>CONSTANT_ISSUE</debtRemediationFunction> | |||
* <debtRemediationFunctionOffset>10min</debtRemediationFunctionOffset> | |||
* </rule> | |||
* | |||
* <!-- another rules... --> | |||
* </rules> | |||
* </pre> | |||
* | |||
* @see org.sonar.api.server.rule.RulesDefinition | |||
* @since 4.3 | |||
*/ | |||
@ServerSide | |||
public class RulesDefinitionXmlLoader { | |||
/** | |||
* Loads rules by reading the XML input stream. The input stream is not always closed by the method, so it | |||
* should be handled by the caller. | |||
* @since 4.3 | |||
*/ | |||
public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) { | |||
load(repo, input, Charset.forName(encoding)); | |||
} | |||
@@ -130,6 +195,11 @@ public class RulesDefinitionXmlLoader { | |||
} | |||
} | |||
/** | |||
* Loads rules by reading the XML input stream. The reader is not closed by the method, so it | |||
* should be handled by the caller. | |||
* @since 4.3 | |||
*/ | |||
public void load(RulesDefinition.NewRepository repo, Reader reader) { | |||
XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); | |||
xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); | |||
@@ -159,76 +229,117 @@ public class RulesDefinitionXmlLoader { | |||
String description = null; | |||
String internalKey = null; | |||
String severity = Severity.defaultSeverity(); | |||
String status = null; | |||
Cardinality cardinality = Cardinality.SINGLE; | |||
RuleStatus status = RuleStatus.defaultStatus(); | |||
boolean template = false; | |||
String effortToFixDescription = null; | |||
String debtSubCharacteristic = null; | |||
String debtRemediationFunction = null; | |||
String debtRemediationFunctionOffset = null; | |||
String debtRemediationFunctionCoeff = null; | |||
List<ParamStruct> params = new ArrayList<>(); | |||
List<String> tags = new ArrayList<>(); | |||
/* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */ | |||
String keyAttribute = ruleC.getAttrValue("key"); | |||
if (StringUtils.isNotBlank(keyAttribute)) { | |||
key = StringUtils.trim(keyAttribute); | |||
if (isNotBlank(keyAttribute)) { | |||
key = trim(keyAttribute); | |||
} | |||
String priorityAttribute = ruleC.getAttrValue("priority"); | |||
if (StringUtils.isNotBlank(priorityAttribute)) { | |||
severity = StringUtils.trim(priorityAttribute); | |||
if (isNotBlank(priorityAttribute)) { | |||
severity = trim(priorityAttribute); | |||
} | |||
SMInputCursor cursor = ruleC.childElementCursor(); | |||
while (cursor.getNext() != null) { | |||
String nodeName = cursor.getLocalName(); | |||
if (StringUtils.equalsIgnoreCase("name", nodeName)) { | |||
name = StringUtils.trim(cursor.collectDescendantText(false)); | |||
if (equalsIgnoreCase("name", nodeName)) { | |||
name = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("description", nodeName)) { | |||
description = StringUtils.trim(cursor.collectDescendantText(false)); | |||
} else if (equalsIgnoreCase("description", nodeName)) { | |||
description = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("key", nodeName)) { | |||
key = StringUtils.trim(cursor.collectDescendantText(false)); | |||
} else if (equalsIgnoreCase("key", nodeName)) { | |||
key = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) { | |||
} else if (equalsIgnoreCase("configKey", nodeName)) { | |||
// deprecated field, replaced by internalKey | |||
internalKey = StringUtils.trim(cursor.collectDescendantText(false)); | |||
internalKey = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("internalKey", nodeName)) { | |||
internalKey = StringUtils.trim(cursor.collectDescendantText(false)); | |||
} else if (equalsIgnoreCase("internalKey", nodeName)) { | |||
internalKey = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("priority", nodeName)) { | |||
} else if (equalsIgnoreCase("priority", nodeName)) { | |||
// deprecated field, replaced by severity | |||
severity = StringUtils.trim(cursor.collectDescendantText(false)); | |||
severity = trim(cursor.collectDescendantText(false)); | |||
} else if (equalsIgnoreCase("severity", nodeName)) { | |||
severity = trim(cursor.collectDescendantText(false)); | |||
} else if (equalsIgnoreCase("cardinality", nodeName)) { | |||
template = Cardinality.MULTIPLE == Cardinality.valueOf(trim(cursor.collectDescendantText(false))); | |||
} else if (equalsIgnoreCase("effortToFixDescription", nodeName)) { | |||
effortToFixDescription = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("severity", nodeName)) { | |||
severity = StringUtils.trim(cursor.collectDescendantText(false)); | |||
} else if (equalsIgnoreCase("debtRemediationFunction", nodeName)) { | |||
debtRemediationFunction = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("cardinality", nodeName)) { | |||
cardinality = Cardinality.valueOf(StringUtils.trim(cursor.collectDescendantText(false))); | |||
} else if (equalsIgnoreCase("debtRemediationFunctionOffset", nodeName)) { | |||
debtRemediationFunctionOffset = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("status", nodeName)) { | |||
status = StringUtils.trim(cursor.collectDescendantText(false)); | |||
} else if (equalsIgnoreCase("debtRemediationFunctionCoefficient", nodeName)) { | |||
debtRemediationFunctionCoeff = trim(cursor.collectDescendantText(false)); | |||
} else if (StringUtils.equalsIgnoreCase("param", nodeName)) { | |||
} else if (equalsIgnoreCase("debtSubCharacteristic", nodeName)) { | |||
debtSubCharacteristic = trim(cursor.collectDescendantText(false)); | |||
} else if (equalsIgnoreCase("status", nodeName)) { | |||
String s = trim(cursor.collectDescendantText(false)); | |||
if (s != null) { | |||
status = RuleStatus.valueOf(s); | |||
} | |||
} else if (equalsIgnoreCase("param", nodeName)) { | |||
params.add(processParameter(cursor)); | |||
} else if (StringUtils.equalsIgnoreCase("tag", nodeName)) { | |||
tags.add(StringUtils.trim(cursor.collectDescendantText(false))); | |||
} else if (equalsIgnoreCase("tag", nodeName)) { | |||
tags.add(trim(cursor.collectDescendantText(false))); | |||
} | |||
} | |||
RulesDefinition.NewRule rule = repo.createRule(key) | |||
.setHtmlDescription(description) | |||
.setSeverity(severity) | |||
.setName(name) | |||
.setInternalKey(internalKey) | |||
.setTags(tags.toArray(new String[tags.size()])) | |||
.setTemplate(cardinality == Cardinality.MULTIPLE); | |||
if (status != null) { | |||
rule.setStatus(RuleStatus.valueOf(status)); | |||
try { | |||
RulesDefinition.NewRule rule = repo.createRule(key) | |||
.setHtmlDescription(description) | |||
.setSeverity(severity) | |||
.setName(name) | |||
.setInternalKey(internalKey) | |||
.setTags(tags.toArray(new String[tags.size()])) | |||
.setTemplate(template) | |||
.setStatus(status) | |||
.setEffortToFixDescription(effortToFixDescription) | |||
.setDebtSubCharacteristic(debtSubCharacteristic); | |||
fillRemediationFunction(rule, debtRemediationFunction, debtRemediationFunctionOffset, debtRemediationFunctionCoeff); | |||
fillParams(rule, params); | |||
} catch (Exception e) { | |||
throw new IllegalArgumentException(format("Fail to load the rule with key [%s:%s]", repo.key(), key), e); | |||
} | |||
} | |||
private void fillRemediationFunction(RulesDefinition.NewRule rule, @Nullable String debtRemediationFunction, | |||
@Nullable String functionOffset, @Nullable String functionCoeff) { | |||
if (isNotBlank(debtRemediationFunction)) { | |||
DebtRemediationFunction.Type functionType = DebtRemediationFunction.Type.valueOf(debtRemediationFunction); | |||
rule.setDebtRemediationFunction(rule.debtRemediationFunctions().create(functionType, functionCoeff, functionOffset)); | |||
} | |||
} | |||
private void fillParams(RulesDefinition.NewRule rule, List<ParamStruct> params) { | |||
for (ParamStruct param : params) { | |||
rule.createParam(param.key) | |||
.setDefaultValue(param.defaultValue) | |||
.setType(param.type) | |||
.setDescription(param.description); | |||
.setDefaultValue(param.defaultValue) | |||
.setType(param.type) | |||
.setDescription(param.description); | |||
} | |||
} | |||
@@ -244,30 +355,30 @@ public class RulesDefinitionXmlLoader { | |||
// BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT | |||
String keyAttribute = ruleC.getAttrValue("key"); | |||
if (StringUtils.isNotBlank(keyAttribute)) { | |||
param.key = StringUtils.trim(keyAttribute); | |||
if (isNotBlank(keyAttribute)) { | |||
param.key = trim(keyAttribute); | |||
} | |||
// BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT | |||
String typeAttribute = ruleC.getAttrValue("type"); | |||
if (StringUtils.isNotBlank(typeAttribute)) { | |||
if (isNotBlank(typeAttribute)) { | |||
param.type = RuleParamType.parse(typeAttribute); | |||
} | |||
SMInputCursor paramC = ruleC.childElementCursor(); | |||
while (paramC.getNext() != null) { | |||
String propNodeName = paramC.getLocalName(); | |||
String propText = StringUtils.trim(paramC.collectDescendantText(false)); | |||
if (StringUtils.equalsIgnoreCase("key", propNodeName)) { | |||
String propText = trim(paramC.collectDescendantText(false)); | |||
if (equalsIgnoreCase("key", propNodeName)) { | |||
param.key = propText; | |||
} else if (StringUtils.equalsIgnoreCase("description", propNodeName)) { | |||
} else if (equalsIgnoreCase("description", propNodeName)) { | |||
param.description = propText; | |||
} else if (StringUtils.equalsIgnoreCase("type", propNodeName)) { | |||
} else if (equalsIgnoreCase("type", propNodeName)) { | |||
param.type = RuleParamType.parse(propText); | |||
} else if (StringUtils.equalsIgnoreCase("defaultValue", propNodeName)) { | |||
} else if (equalsIgnoreCase("defaultValue", propNodeName)) { | |||
param.defaultValue = propText; | |||
} | |||
} |
@@ -19,34 +19,46 @@ | |||
*/ | |||
package org.sonar.api.server.rule; | |||
import java.io.InputStream; | |||
import java.io.StringReader; | |||
import java.io.UnsupportedEncodingException; | |||
import java.nio.charset.StandardCharsets; | |||
import org.apache.commons.io.IOUtils; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.api.rule.RuleStatus; | |||
import org.sonar.api.rule.Severity; | |||
import java.io.InputStream; | |||
import java.io.UnsupportedEncodingException; | |||
import java.nio.charset.StandardCharsets; | |||
import org.sonar.api.server.debt.DebtRemediationFunction; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.junit.Assert.fail; | |||
public class RulesDefinitionXmlLoaderTest { | |||
@org.junit.Rule | |||
public final ExpectedException thrown = ExpectedException.none(); | |||
public final ExpectedException expectedException = ExpectedException.none(); | |||
RulesDefinitionXmlLoader underTest = new RulesDefinitionXmlLoader(); | |||
private RulesDefinition.Repository load(InputStream input, String encoding) { | |||
RulesDefinition.Context context = new RulesDefinition.Context(); | |||
RulesDefinition.NewRepository newRepository = context.createRepository("squid", "java"); | |||
new RulesDefinitionXmlLoader().load(newRepository, input, encoding); | |||
underTest.load(newRepository, input, encoding); | |||
newRepository.done(); | |||
return context.repository("squid"); | |||
} | |||
private RulesDefinition.Repository load(String xml) { | |||
RulesDefinition.Context context = new RulesDefinition.Context(); | |||
RulesDefinition.NewRepository newRepository = context.createRepository("squid", "java"); | |||
underTest.load(newRepository, new StringReader(xml)); | |||
newRepository.done(); | |||
return context.repository("squid"); | |||
} | |||
@Test | |||
public void parse_xml() { | |||
InputStream input = getClass().getResourceAsStream("/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest/rules.xml"); | |||
InputStream input = getClass().getResourceAsStream("RulesDefinitionXmlLoaderTest/rules.xml"); | |||
RulesDefinition.Repository repository = load(input, StandardCharsets.UTF_8.name()); | |||
assertThat(repository.rules()).hasSize(2); | |||
@@ -77,34 +89,34 @@ public class RulesDefinitionXmlLoaderTest { | |||
@Test | |||
public void fail_if_missing_rule_key() { | |||
thrown.expect(IllegalStateException.class); | |||
expectedException.expect(IllegalStateException.class); | |||
load(IOUtils.toInputStream("<rules><rule><name>Foo</name></rule></rules>"), StandardCharsets.UTF_8.name()); | |||
} | |||
@Test | |||
public void fail_if_missing_property_key() { | |||
thrown.expect(IllegalStateException.class); | |||
expectedException.expect(IllegalStateException.class); | |||
load(IOUtils.toInputStream("<rules><rule><key>foo</key><name>Foo</name><param></param></rule></rules>"), StandardCharsets.UTF_8.name()); | |||
} | |||
@Test | |||
public void fail_on_invalid_rule_parameter_type() { | |||
thrown.expect(IllegalStateException.class); | |||
expectedException.expect(IllegalStateException.class); | |||
load(IOUtils.toInputStream("<rules><rule><key>foo</key><name>Foo</name><param><key>key</key><type>INVALID</type></param></rule></rules>"), StandardCharsets.UTF_8.name()); | |||
} | |||
@Test | |||
public void fail_if_invalid_xml() { | |||
thrown.expect(IllegalStateException.class); | |||
thrown.expectMessage("XML is not valid"); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("XML is not valid"); | |||
InputStream input = getClass().getResourceAsStream("/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest/invalid.xml"); | |||
InputStream input = getClass().getResourceAsStream("RulesDefinitionXmlLoaderTest/invalid.xml"); | |||
load(input, StandardCharsets.UTF_8.name()); | |||
} | |||
@Test | |||
public void test_utf8_encoding() throws UnsupportedEncodingException { | |||
InputStream input = getClass().getResourceAsStream("/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest/utf8.xml"); | |||
InputStream input = getClass().getResourceAsStream("RulesDefinitionXmlLoaderTest/utf8.xml"); | |||
RulesDefinition.Repository repository = load(input, StandardCharsets.UTF_8.name()); | |||
assertThat(repository.rules()).hasSize(1); | |||
@@ -119,7 +131,7 @@ public class RulesDefinitionXmlLoaderTest { | |||
@Test | |||
public void support_deprecated_format() { | |||
// the deprecated format uses some attributes instead of nodes | |||
InputStream input = getClass().getResourceAsStream("/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest/deprecated.xml"); | |||
InputStream input = getClass().getResourceAsStream("RulesDefinitionXmlLoaderTest/deprecated.xml"); | |||
RulesDefinition.Repository repository = load(input, StandardCharsets.UTF_8.name()); | |||
assertThat(repository.rules()).hasSize(1); | |||
@@ -130,4 +142,114 @@ public class RulesDefinitionXmlLoaderTest { | |||
assertThat(rule.htmlDescription()).isEqualTo("Count methods"); | |||
assertThat(rule.param("minMethodsCount")).isNotNull(); | |||
} | |||
@Test | |||
public void test_linear_remediation_function() throws Exception { | |||
String xml = "" + | |||
"<rules>" + | |||
" <rule>" + | |||
" <key>1</key>" + | |||
" <name>One</name>" + | |||
" <description>Desc</description>" + | |||
" <effortToFixDescription>lines</effortToFixDescription>" + | |||
" <debtSubCharacteristic>BUG</debtSubCharacteristic>" + | |||
" <debtRemediationFunction>LINEAR</debtRemediationFunction>" + | |||
" <debtRemediationFunctionCoefficient>2d 3h</debtRemediationFunctionCoefficient>" + | |||
" </rule>" + | |||
"</rules>"; | |||
RulesDefinition.Rule rule = load(xml).rule("1"); | |||
assertThat(rule.debtSubCharacteristic()).isEqualTo("BUG"); | |||
assertThat(rule.effortToFixDescription()).isEqualTo("lines"); | |||
DebtRemediationFunction function = rule.debtRemediationFunction(); | |||
assertThat(function).isNotNull(); | |||
assertThat(function.type()).isEqualTo(DebtRemediationFunction.Type.LINEAR); | |||
assertThat(function.coefficient()).isEqualTo("2d3h"); | |||
assertThat(function.offset()).isNull(); | |||
} | |||
@Test | |||
public void test_linear_with_offset_remediation_function() { | |||
String xml = "" + | |||
"<rules>" + | |||
" <rule>" + | |||
" <key>1</key>" + | |||
" <name>One</name>" + | |||
" <description>Desc</description>" + | |||
" <effortToFixDescription>lines</effortToFixDescription>" + | |||
" <debtSubCharacteristic>BUG</debtSubCharacteristic>" + | |||
" <debtRemediationFunction>LINEAR_OFFSET</debtRemediationFunction>" + | |||
" <debtRemediationFunctionCoefficient>2d 3h</debtRemediationFunctionCoefficient>" + | |||
" <debtRemediationFunctionOffset>5min</debtRemediationFunctionOffset>" + | |||
" </rule>" + | |||
"</rules>"; | |||
RulesDefinition.Rule rule = load(xml).rule("1"); | |||
assertThat(rule.effortToFixDescription()).isEqualTo("lines"); | |||
assertThat(rule.debtSubCharacteristic()).isEqualTo("BUG"); | |||
DebtRemediationFunction function = rule.debtRemediationFunction(); | |||
assertThat(function).isNotNull(); | |||
assertThat(function.type()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET); | |||
assertThat(function.coefficient()).isEqualTo("2d3h"); | |||
assertThat(function.offset()).isEqualTo("5min"); | |||
} | |||
@Test | |||
public void test_constant_remediation_function() { | |||
String xml = "" + | |||
"<rules>" + | |||
" <rule>" + | |||
" <key>1</key>" + | |||
" <name>One</name>" + | |||
" <description>Desc</description>" + | |||
" <debtSubCharacteristic>BUG</debtSubCharacteristic>" + | |||
" <debtRemediationFunction>CONSTANT_ISSUE</debtRemediationFunction>" + | |||
" <debtRemediationFunctionOffset>5min</debtRemediationFunctionOffset>" + | |||
" </rule>" + | |||
"</rules>"; | |||
RulesDefinition.Rule rule = load(xml).rule("1"); | |||
assertThat(rule.debtSubCharacteristic()).isEqualTo("BUG"); | |||
DebtRemediationFunction function = rule.debtRemediationFunction(); | |||
assertThat(function).isNotNull(); | |||
assertThat(function.type()).isEqualTo(DebtRemediationFunction.Type.CONSTANT_ISSUE); | |||
assertThat(function.coefficient()).isNull(); | |||
assertThat(function.offset()).isEqualTo("5min"); | |||
} | |||
@Test | |||
public void fail_if_invalid_remediation_function() { | |||
try { | |||
load("" + | |||
"<rules>" + | |||
" <rule>" + | |||
" <key>1</key>" + | |||
" <name>One</name>" + | |||
" <description>Desc</description>" + | |||
" <debtSubCharacteristic>BUG</debtSubCharacteristic>" + | |||
" <debtRemediationFunction>UNKNOWN</debtRemediationFunction>" + | |||
" </rule>" + | |||
"</rules>"); | |||
fail(); | |||
} catch (IllegalArgumentException e) { | |||
assertThat(e).hasMessageContaining("Fail to load the rule with key [squid:1]"); | |||
assertThat(e.getCause()).hasMessageContaining("No enum constant org.sonar.api.server.debt.DebtRemediationFunction.Type.UNKNOWN"); | |||
} | |||
} | |||
@Test | |||
public void fail_if_sub_characteristic_is_missing() { | |||
try { | |||
load("<rules>" + | |||
" <rule>" + | |||
" <key>1</key>" + | |||
" <name>One</name>" + | |||
" <description>Desc</description>" + | |||
" <debtRemediationFunction>LINEAR</debtRemediationFunction>" + | |||
" <debtRemediationFunctionCoefficient>1min</debtRemediationFunctionCoefficient>" + | |||
" </rule>" + | |||
"</rules>"); | |||
} catch (IllegalStateException e) { | |||
assertThat(e).hasMessageContaining("Both debt sub-characteristic and debt remediation function should be defined on rule '[repository=squid, key=1]"); | |||
} | |||
} | |||
} |