From 40b5c0d064fb0d553c880c03976df39902ed52bc Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Thu, 29 Oct 2015 00:18:13 +0100 Subject: SONAR-6591 Add SQALE metadata to XML rule format --- .../api/server/debt/DebtRemediationFunction.java | 53 +++-- .../rule/DefaultDebtRemediationFunctions.java | 3 +- .../org/sonar/api/server/rule/RulesDefinition.java | 30 ++- .../api/server/rule/RulesDefinitionXmlLoader.java | 259 +++++++++++++++------ .../server/rule/RulesDefinitionXmlLoaderTest.java | 152 ++++++++++-- 5 files changed, 387 insertions(+), 110 deletions(-) (limited to 'sonar-plugin-api/src') 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 : - * + * Function used to calculate the remediation cost of an issue. See {@link Type} for details. + *

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

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

+ * Total remediation cost = offset + (number of noncompliance x coefficient) + *

+ *

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 + *

+ */ + 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. *

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

Example

+ *

Usage

*
- * 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);
+ *     }
  *   }
  * }
  * 
@@ -65,10 +74,15 @@ import java.util.List; *
  * <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>
  * 
* + *

XML Example

+ *
+ * <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>
+ * 
+ * * @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 params = new ArrayList<>(); List 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 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("Foo"), StandardCharsets.UTF_8.name()); } @Test public void fail_if_missing_property_key() { - thrown.expect(IllegalStateException.class); + expectedException.expect(IllegalStateException.class); load(IOUtils.toInputStream("fooFoo"), StandardCharsets.UTF_8.name()); } @Test public void fail_on_invalid_rule_parameter_type() { - thrown.expect(IllegalStateException.class); + expectedException.expect(IllegalStateException.class); load(IOUtils.toInputStream("fooFookeyINVALID"), 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 = "" + + "" + + " " + + " 1" + + " One" + + " Desc" + + + " lines" + + " BUG" + + " LINEAR" + + " 2d 3h" + + " " + + ""; + 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 = "" + + "" + + " " + + " 1" + + " One" + + " Desc" + + + " lines" + + " BUG" + + " LINEAR_OFFSET" + + " 2d 3h" + + " 5min" + + " " + + ""; + 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 = "" + + "" + + " " + + " 1" + + " One" + + " Desc" + + " BUG" + + " CONSTANT_ISSUE" + + " 5min" + + " " + + ""; + 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("" + + "" + + " " + + " 1" + + " One" + + " Desc" + + " BUG" + + " UNKNOWN" + + " " + + ""); + 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("" + + " " + + " 1" + + " One" + + " Desc" + + " LINEAR" + + " 1min" + + " " + + ""); + } catch (IllegalStateException e) { + assertThat(e).hasMessageContaining("Both debt sub-characteristic and debt remediation function should be defined on rule '[repository=squid, key=1]"); + } + } } -- cgit v1.2.3