]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6591 Add SQALE metadata to XML rule format
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 28 Oct 2015 23:18:13 +0000 (00:18 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 25 Nov 2015 15:42:46 +0000 (16:42 +0100)
sonar-plugin-api/src/main/java/org/sonar/api/server/debt/DebtRemediationFunction.java
sonar-plugin-api/src/main/java/org/sonar/api/server/rule/DefaultDebtRemediationFunctions.java
sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java
sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinitionXmlLoader.java
sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionXmlLoaderTest.java

index 73160afcab2abc383908c28706c3270aa6fb2a6b..ac2c722221d6046c6a5edb4aaef730ee8bf919ac 100644 (file)
@@ -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();
index 797833fad8b8d2a61b80f8355add31c45572c33d..b30568ae6c8b1ab2b4df23df9c69539227288bca 100644 (file)
@@ -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) {
index 751c4263dd5b010429a1ce56181c56ef1b52ca47..e9c878c0218517503e696380489886e0b062d8cf 100644 (file)
@@ -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".
      */
index 6e0bfeda4339532690d401ad0deb10c36ba18c90..8a8096e4ebf13e6531341c95748d8388d0a22e81 100644 (file)
  */
 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>
  * &lt;rules&gt;
  *   &lt;rule&gt;
- *     &lt;key&gt;the-required-rule-key&lt;/key&gt;*
- *     &lt;name&gt;The required purpose of the rule&lt;/name&gt;
- **     &lt;description&gt;
- *       &lt;![CDATA[Required HTML description]]&gt;
+ *     &lt;!-- required key --&gt;
+ *     &lt;key&gt;the-rule-key&lt;/key&gt;
+ *
+ *     &lt;!-- required name --&gt;
+ *     &lt;name&gt;The purpose of the rule&lt;/name&gt;
+ *
+ *     &lt;!-- required description, in HTML format --&gt;
+ *     &lt;description&gt;
+ *       &lt;![CDATA[The HTML description]]&gt;
  *     &lt;/description&gt;
  *
  *     &lt;!-- Optional key for configuration of some rule engines --&gt;
@@ -91,30 +105,81 @@ import java.util.List;
  *     &lt;param&gt;
  *       &lt;key&gt;the-param-key&lt;/key&gt;
  *       &lt;description&gt;
- *         &lt;![CDATA[the optional param description]]&gt;
+ *         &lt;![CDATA[the optional description, in HTML format]]&gt;
  *       &lt;/description&gt;
- *       &lt;!-- Optional field to define the default value used when enabling the rule in a Quality profile --&gt;
+ *       &lt;!-- Optional default value, used when enabling the rule in a Quality profile --&gt;
  *       &lt;defaultValue&gt;42&lt;/defaultValue&gt;
  *     &lt;/param&gt;
  *     &lt;param&gt;
  *       &lt;key&gt;another-param&lt;/key&gt;
  *     &lt;/param&gt;
  *
- *     &lt;!-- Deprecated field, replaced by "internalKey" --&gt;
+ *     &lt;!-- SQALE debt - key of sub-characteristic --&gt;
+ *     &lt;!-- 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. --&gt;
+ *     &lt;!-- Since 5.3 --&gt;
+ *     &lt;debtSubCharacteristic&gt;MODULARITY&lt;/debtSubCharacteristic&gt;
+ *
+ *     &lt;!-- SQALE debt - type of debt remediation function --&gt;
+ *     &lt;!-- See enum {@link org.sonar.api.server.debt.DebtRemediationFunction.Type} for supported values --&gt;
+ *     &lt;!-- Since 5.3 --&gt;
+ *     &lt;debtRemediationFunction&gt;LINEAR_OFFSET&lt;/debtRemediationFunction&gt;
+ *
+ *     &lt;!-- SQALE debt - raw description of the "effort to fix", used for some types of remediation functions. --&gt;
+ *     &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.NewRule#setEffortToFixDescription(String)} --&gt;
+ *     &lt;!-- Since 5.3 --&gt;
+ *     &lt;effortToFixDescription&gt;Effort to test one uncovered condition&lt;/effortToFixDescription&gt;
+ *
+ *     &lt;!-- SQALE debt - coefficient of debt remediation function. Must be defined only for some function types. --&gt;
+ *     &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --&gt;
+ *     &lt;!-- Since 5.3 --&gt;
+ *     &lt;debtRemediationFunctionCoefficient&gt;10min&lt;/debtRemediationFunctionCoefficient&gt;
+ *
+ *     &lt;!-- SQALE debt - offset of debt remediation function. Must be defined only for some function types. --&gt;
+ *     &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --&gt;
+ *     &lt;!-- Since 5.3 --&gt;
+ *     &lt;debtRemediationFunctionOffset&gt;2min&lt;/debtRemediationFunctionOffset&gt;
+ *
+ *     &lt;!-- deprecated field, replaced by "internalKey" --&gt;
  *     &lt;configKey&gt;Checker/TreeWalker/LocalVariableName&lt;/configKey&gt;
  *
- *     &lt;!-- Deprecated field, replaced by "severity" --&gt;
+ *     &lt;!-- deprecated field, replaced by "severity" --&gt;
  *     &lt;priority&gt;BLOCKER&lt;/priority&gt;
  *   &lt;/rule&gt;
  * &lt;/rules&gt;
  * </pre>
  *
+ * <h3>XML Example</h3>
+ * <pre>
+ * &lt;rules&gt;
+ *   &lt;rule&gt;
+ *     &lt;key&gt;S1442&lt;/key&gt;
+ *     &lt;name&gt;"alert(...)" should not be used&lt;/name&gt;
+ *     &lt;description&gt;alert(...) can be useful for debugging during development, but ...&lt;/description&gt;
+ *     &lt;tag&gt;cwe&lt;/tag&gt;
+ *     &lt;tag&gt;security&lt;/tag&gt;
+ *     &lt;tag&gt;user-experience&lt;/tag&gt;
+ *     &lt;debtSubCharacteristic&gt;SECURITY_FEATURES&lt;/debtSubCharacteristic&gt;
+ *     &lt;debtRemediationFunction&gt;CONSTANT_ISSUE&lt;/debtRemediationFunction&gt;
+ *     &lt;debtRemediationFunctionOffset&gt;10min&lt;/debtRemediationFunctionOffset&gt;
+ *   &lt;/rule&gt;
+ *
+ *   &lt;!-- another rules... --&gt;
+ * &lt;/rules&gt;
+ * </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;
       }
     }
index a7c96cd4206df3e26d0c5d1fe5919d74aaf1b083..c0c2a765405a41dce5ab812ad16589c2c10d0c85 100644 (file)
  */
 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]");
+    }
+  }
 }