From eb4a773da108157e48998c84cb0477434fd4add5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Wed, 24 Jan 2018 15:45:12 +0100 Subject: [PATCH] SONAR-10304 add deprecatedRuleKeys to RuleDefinition#Rule --- .../api/server/rule/RulesDefinition.java | 82 ++++++++++++++ .../api/server/rule/RulesDefinitionTest.java | 106 ++++++++++++++++++ 2 files changed, 188 insertions(+) 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 c7633fc3c54..cb1ce8ceac3 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 @@ -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 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). + *

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

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

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

+ * Several deprecated keys can be provided to allow SonarQube to support several key (and/or repository) changes + * across multiple versions of a plugin. + *
+ * Consider the following use case scenario: + *

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

+ * Finally, repository/key pairs must be unique across all rules and their deprecated keys. + *
+ * This implies that no rule can use the same repository and key as the deprecated key of another rule. This + * uniqueness applies across plugins. + *

+ * 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 deprecatedRuleKeys() { + return deprecatedRuleKeys; + } + /** * @see RulesDefinition.NewRule#setInternalKey(String) */ diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionTest.java index 64f3b0a96e3..8398f50a2ae 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/server/rule/RulesDefinitionTest.java @@ -19,10 +19,22 @@ */ 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 ruleKeys = ImmutableSet.of(RuleKey.of("foo", "AAA"), + RuleKey.of("bar", "CCCC"), RuleKey.of("doh", "CCCC"), RuleKey.of("foo", "BBBBBBBBBB")); + List 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 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"); -- 2.39.5