]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10304 add deprecatedRuleKeys to RuleDefinition#Rule
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 24 Jan 2018 14:45:12 +0000 (15:45 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 8 Feb 2018 12:41:00 +0000 (13:41 +0100)
sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java
sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionTest.java

index c7633fc3c545e5c0d82b41ea80cee63db01d2c37..cb1ce8ceac372aaa05ecf8e9a11e259abac3617b 100644 (file)
@@ -38,6 +38,7 @@ import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.api.ExtensionPoint;
 import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleScope;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.rule.Severity;
@@ -695,6 +696,7 @@ public interface RulesDefinition {
     private final DebtRemediationFunctions functions;
     private boolean activatedByDefault;
     private RuleScope scope;
+    private final Set<RuleKey> deprecatedRuleKeys = new TreeSet<>();
 
     private NewRule(@Nullable String pluginKey, String repoKey, String key) {
       this.pluginKey = pluginKey;
@@ -943,6 +945,21 @@ public interface RulesDefinition {
       }
     }
 
+    /**
+     * Register a repository and key under which this rule used to be known
+     * (see {@link Rule#deprecatedRuleKeys} for details).
+     * <p>
+     * Deprecated keys should be added with this method in order, oldest first, for documentation purpose.
+     *
+     * @since 7.1
+     * @throws IllegalArgumentException if {@code repository} or {@code key} is {@code null} or empty.
+     * @see Rule#deprecatedRuleKeys
+     */
+    public NewRule addDeprecatedRuleKey(String repository, String key) {
+      deprecatedRuleKeys.add(RuleKey.of(repository, key));
+      return this;
+    }
+
     @Override
     public String toString() {
       return format("[repository=%s, key=%s]", repoKey, key);
@@ -969,6 +986,7 @@ public interface RulesDefinition {
     private final RuleStatus status;
     private final boolean activatedByDefault;
     private final RuleScope scope;
+    private final Set<RuleKey> deprecatedRuleKeys;
 
     private Rule(Repository repository, NewRule newRule) {
       this.pluginKey = newRule.pluginKey;
@@ -993,6 +1011,7 @@ public interface RulesDefinition {
       }
       this.params = Collections.unmodifiableMap(paramsBuilder);
       this.activatedByDefault = newRule.activatedByDefault;
+      this.deprecatedRuleKeys = ImmutableSortedSet.copyOf(newRule.deprecatedRuleKeys);
     }
 
     public Repository repository() {
@@ -1105,6 +1124,69 @@ public interface RulesDefinition {
       return tags;
     }
 
+    /**
+     * Deprecated rules keys for this rule.
+     * <p>
+     * If you want to rename the key of a rule or change its repository or both, register the rule's previous repository
+     * and key (see {@link NewRule#addDeprecatedRuleKey(String, String) addDeprecatedRuleKey}). This will allow
+     * SonarQube to support "issue re-keying" for this rule.
+     * <p>
+     * If the repository and/or key of an existing rule is changed without declaring deprecated keys, existing issues
+     * for this rule, created under the rule's previous repository and/or key, will be closed and new ones will be
+     * created under the issue's new repository and/or key.
+     * <p>
+     * Several deprecated keys can be provided to allow SonarQube to support several key (and/or repository) changes
+     * across multiple versions of a plugin.
+     * <br>
+     * Consider the following use case scenario:
+     * <ul>
+     *   <li>Rule {@code Foo:A} is defined in version 1 of the plugin
+     * <pre>
+     * NewRepository newRepository = context.createRepository("Foo", "my_language");
+     * NewRule r = newRepository.createRule("A");
+     * </pre>
+     *   </li>
+     *   <li>Rule's key is renamed to B in version 2 of the plugin
+     * <pre>
+     * NewRepository newRepository = context.createRepository("Foo", "my_language");
+     * NewRule r = newRepository.createRule("B")
+     *   .addDeprecatedRuleKey("Foo", "A");
+     * </pre>
+     *   </li>
+     *   <li>All rules, including {@code Foo:B}, are moved to a new repository Bar in version 3 of the plugin
+     * <pre>
+     * NewRepository newRepository = context.createRepository("Bar", "my_language");
+     * NewRule r = newRepository.createRule("B")
+     *   .addDeprecatedRuleKey("Foo", "A")
+     *   .addDeprecatedRuleKey("Bar", "B");
+     * </pre>
+     *   </li>
+     * </ul>
+     *
+     * With all deprecated keys defined in version 3 of the plugin, SonarQube will be able to support "issue re-keying"
+     * for this rule in all cases:
+     * <ul>
+     *   <li>plugin upgrade from v1 to v2,</li>
+     *   <li>plugin upgrade from v2 to v3</li>
+     *   <li>AND plugin upgrade from v1 to v3</li>
+     * </ul>
+     * <p>
+     * Finally, repository/key pairs must be unique across all rules and their deprecated keys.
+     * <br>
+     * This implies that no rule can use the same repository and key as the deprecated key of another rule. This
+     * uniqueness applies across plugins.
+     * <p>
+     * Note that, even though this method returns a {@code Set}, its elements are ordered according to calls to
+     * {@link NewRule#addDeprecatedRuleKey(String, String) addDeprecatedRuleKey}. This allows to describe the history
+     * of a rule's repositories and keys over time. Oldest repository and key must be specified first.
+     *
+     * @since 7.1
+     * @see NewRule#addDeprecatedRuleKey(String, String)
+     */
+    public Set<RuleKey> deprecatedRuleKeys() {
+      return deprecatedRuleKeys;
+    }
+
     /**
      * @see RulesDefinition.NewRule#setInternalKey(String)
      */
index 64f3b0a96e3c9bdac63581757935a08b2645f5eb..8398f50a2aed9a0800eda05e05b271378281ea5f 100644 (file)
  */
 package org.sonar.api.server.rule;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
 import java.net.URL;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Assume;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleScope;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.rule.Severity;
@@ -33,6 +45,7 @@ import org.sonar.api.utils.log.LogTester;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.fail;
 
+@RunWith(DataProviderRunner.class)
 public class RulesDefinitionTest {
 
   RulesDefinition.Context context = new RulesDefinition.Context();
@@ -200,6 +213,99 @@ public class RulesDefinitionTest {
     assertThat(level.type()).isEqualTo(RuleParamType.INTEGER);
   }
 
+  @Test
+  @UseDataProvider("nullOrEmpty")
+  public void addDeprecatedRuleKey_fails_with_IAE_if_repository_is_null_or_empty(String nullOrEmpty) {
+    RulesDefinition.NewRepository newRepository = context.createRepository("foo", "bar");
+    RulesDefinition.NewRule newRule = newRepository.createRule("doh");
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Repository must be set");
+
+    newRule.addDeprecatedRuleKey(nullOrEmpty, "oldKey");
+  }
+
+  @Test
+  @UseDataProvider("nullOrEmpty")
+  public void addDeprecatedRuleKey_fails_with_IAE_if_key_is_null_or_empty(String nullOrEmpty) {
+    RulesDefinition.NewRepository newRepository = context.createRepository("foo", "bar");
+    RulesDefinition.NewRule newRule = newRepository.createRule("doh");
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Rule must be set");
+
+    newRule.addDeprecatedRuleKey("oldRepo", nullOrEmpty);
+  }
+
+  @DataProvider
+  public static Object[][] nullOrEmpty() {
+    return new Object[][] {
+      {null},
+      {""}
+    };
+  }
+
+  @Test
+  public void getDeprecatedKeys_returns_empty_if_addDeprecatedKeys_never_called() {
+    String repositoryKey = "foo";
+    String ruleKey = "doh";
+    RulesDefinition.NewRepository newRepository = context.createRepository(repositoryKey, "bar");
+    newRepository.createRule(ruleKey)
+      .setName("doh rule")
+      .setHtmlDescription("doh description");
+    newRepository.done();
+    RulesDefinition.Repository repository = context.repository(repositoryKey);
+    RulesDefinition.Rule rule = repository.rule(ruleKey);
+
+    assertThat(rule.deprecatedRuleKeys()).isEmpty();
+  }
+
+  @Test
+  public void getDeprecatedKeys_returns_keys_in_order_of_addDeprecatedKeys_calls() {
+    Set<RuleKey> ruleKeys = ImmutableSet.of(RuleKey.of("foo", "AAA"),
+      RuleKey.of("bar", "CCCC"), RuleKey.of("doh", "CCCC"), RuleKey.of("foo", "BBBBBBBBBB"));
+    List<RuleKey> sortedRuleKeys = ruleKeys.stream().sorted(Ordering.natural().onResultOf(RuleKey::toString)).collect(Collectors.toList());
+
+    // ensure we don't have the same order
+    Assume.assumeTrue(!ImmutableList.copyOf(ruleKeys).equals(sortedRuleKeys));
+
+    String repositoryKey = "foo";
+    String ruleKey = "doh";
+    RulesDefinition.NewRepository newRepository = context.createRepository(repositoryKey, "bar");
+    RulesDefinition.NewRule newRule = newRepository.createRule(ruleKey)
+      .setName("doh rule")
+      .setHtmlDescription("doh description");
+    sortedRuleKeys.forEach(r -> newRule.addDeprecatedRuleKey(r.repository(), r.rule()));
+    newRepository.done();
+    RulesDefinition.Repository repository = context.repository(repositoryKey);
+    RulesDefinition.Rule rule = repository.rule(ruleKey);
+
+    assertThat(ImmutableList.copyOf(rule.deprecatedRuleKeys()))
+      .isEqualTo(sortedRuleKeys);
+  }
+
+  @Test
+  public void getDeprecatedKeys_does_not_return_the_same_key_more_than_once() {
+    RuleKey duplicatedRuleKey = RuleKey.of("foo", "AAA");
+    RuleKey ruleKey2 = RuleKey.of("bar", "CCCC");
+    RuleKey ruleKey3 = RuleKey.of("foo", "BBBBBBBBBB");
+    List<RuleKey> ruleKeys = ImmutableList.of(duplicatedRuleKey, ruleKey2, duplicatedRuleKey, duplicatedRuleKey, ruleKey3);
+
+    String repositoryKey = "foo";
+    String ruleKey = "doh";
+    RulesDefinition.NewRepository newRepository = context.createRepository(repositoryKey, "bar");
+    RulesDefinition.NewRule newRule = newRepository.createRule(ruleKey)
+      .setName("doh rule")
+      .setHtmlDescription("doh description");
+    ruleKeys.forEach(r -> newRule.addDeprecatedRuleKey(r.repository(), r.rule()));
+    newRepository.done();
+    RulesDefinition.Repository repository = context.repository(repositoryKey);
+    RulesDefinition.Rule rule = repository.rule(ruleKey);
+
+    assertThat(rule.deprecatedRuleKeys())
+      .containsExactly(ruleKey2, duplicatedRuleKey, ruleKey3);
+  }
+
   @Test
   public void sanitize_rule_name() {
     RulesDefinition.NewRepository newFindbugs = context.createRepository("findbugs", "java");