diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-10-29 00:18:13 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-11-25 16:42:46 +0100 |
commit | 40b5c0d064fb0d553c880c03976df39902ed52bc (patch) | |
tree | db490fb8c268953f061d0129985e2d223bd0a9ec /sonar-plugin-api/src | |
parent | 93c6a9457b2bde6ac21699a860224887dce76080 (diff) | |
download | sonarqube-40b5c0d064fb0d553c880c03976df39902ed52bc.tar.gz sonarqube-40b5c0d064fb0d553c880c03976df39902ed52bc.zip |
SONAR-6591 Add SQALE metadata to XML rule format
Diffstat (limited to 'sonar-plugin-api/src')
5 files changed, 387 insertions, 110 deletions
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/debt/DebtRemediationFunction.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/debt/DebtRemediationFunction.java index 73160afcab2..ac2c722221d 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/debt/DebtRemediationFunction.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/debt/DebtRemediationFunction.java @@ -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(); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/DefaultDebtRemediationFunctions.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/DefaultDebtRemediationFunctions.java index 797833fad8b..b30568ae6c8 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/DefaultDebtRemediationFunctions.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/DefaultDebtRemediationFunctions.java @@ -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) { diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java index 751c4263dd5..e9c878c0218 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java @@ -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". */ diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinitionXmlLoader.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinitionXmlLoader.java index 6e0bfeda433..8a8096e4ebf 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinitionXmlLoader.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinitionXmlLoader.java @@ -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; } } diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest.java index a7c96cd4206..c0c2a765405 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest.java @@ -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]"); + } + } } |