diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2014-01-15 16:30:02 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2014-01-15 18:25:32 +0100 |
commit | 138e1ff721bddb519362a1f80b5a84950a8bd4d3 (patch) | |
tree | f1f1386fd5ed50fbeab36fe6df3a6aef004b645b /sonar-plugin-api | |
parent | 1cde18a7cee3b5d26bbcfd859d342ca3b3a1c50a (diff) | |
download | sonarqube-138e1ff721bddb519362a1f80b5a84950a8bd4d3.tar.gz sonarqube-138e1ff721bddb519362a1f80b5a84950a8bd4d3.zip |
SONAR-4908 support deprecated types of rule parameters and add RuleDefinitions#loadAnnotatedClasses()
Diffstat (limited to 'sonar-plugin-api')
5 files changed, 295 insertions, 8 deletions
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/rule/AnnotationRuleDefinitions.java b/sonar-plugin-api/src/main/java/org/sonar/api/rule/AnnotationRuleDefinitions.java new file mode 100644 index 00000000000..4fda6549ae2 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/rule/AnnotationRuleDefinitions.java @@ -0,0 +1,115 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.api.rule; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.utils.AnnotationUtils; +import org.sonar.api.utils.FieldUtils2; +import org.sonar.api.utils.SonarException; +import org.sonar.check.Cardinality; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * Read definitions of rules based on the annotations provided by sonar-check-api. + * </p> + * It is internally used by {@link org.sonar.api.rule.RuleDefinitions} and can't be directly + * used by plugins. + * @since 4.2 + */ +class AnnotationRuleDefinitions { + + private static final Logger LOG = LoggerFactory.getLogger(AnnotationRuleDefinitions.class); + + void loadRules(RuleDefinitions.NewRepository repo, Class... annotatedClasses) { + for (Class annotatedClass : annotatedClasses) { + loadRule(repo, annotatedClass); + } + } + + private void loadRule(RuleDefinitions.NewRepository repo, Class clazz) { + org.sonar.check.Rule ruleAnnotation = AnnotationUtils.getAnnotation(clazz, org.sonar.check.Rule.class); + if (ruleAnnotation != null) { + loadRule(repo, clazz, ruleAnnotation); + } else { + LOG.warn("The class " + clazz.getCanonicalName() + " should be annotated with " + org.sonar.check.Rule.class); + } + } + + private void loadRule(RuleDefinitions.NewRepository repo, Class clazz, org.sonar.check.Rule ruleAnnotation) { + String ruleKey = StringUtils.defaultIfEmpty(ruleAnnotation.key(), clazz.getCanonicalName()); + String ruleName = StringUtils.defaultIfEmpty(ruleAnnotation.name(), null); + String description = StringUtils.defaultIfEmpty(ruleAnnotation.description(), null); + + RuleDefinitions.NewRule rule = repo.newRule(ruleKey); + rule.setName(ruleName).setHtmlDescription(description); + rule.setDefaultSeverity(ruleAnnotation.priority().name()); + rule.setTemplate(ruleAnnotation.cardinality() == Cardinality.MULTIPLE); + rule.setStatus(RuleDefinitions.Status.valueOf(ruleAnnotation.status())); + + List<Field> fields = FieldUtils2.getFields(clazz, true); + for (Field field : fields) { + loadParameters(rule, field); + } + } + + private void loadParameters(RuleDefinitions.NewRule rule, Field field) { + org.sonar.check.RuleProperty propertyAnnotation = field.getAnnotation(org.sonar.check.RuleProperty.class); + if (propertyAnnotation != null) { + String fieldKey = StringUtils.defaultIfEmpty(propertyAnnotation.key(), field.getName()); + RuleDefinitions.NewParam param = rule.newParam(fieldKey) + .setDescription(propertyAnnotation.description()) + .setDefaultValue(propertyAnnotation.defaultValue()); + + if (!StringUtils.isBlank(propertyAnnotation.type())) { + try { + param.setType(RuleParamType.parse(propertyAnnotation.type().trim())); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid property type [" + propertyAnnotation.type() + "]", e); + } + } else { + param.setType(guessType(field.getType())); + } + } + } + + private static final Function<Class<?>, RuleParamType> TYPE_FOR_CLASS = Functions.forMap( + ImmutableMap.<Class<?>, RuleParamType>builder() + .put(Integer.class, RuleParamType.INTEGER) + .put(int.class, RuleParamType.INTEGER) + .put(Float.class, RuleParamType.FLOAT) + .put(float.class, RuleParamType.FLOAT) + .put(Boolean.class, RuleParamType.BOOLEAN) + .put(boolean.class, RuleParamType.BOOLEAN) + .build(), + RuleParamType.STRING); + + @VisibleForTesting + static RuleParamType guessType(Class<?> type) { + return TYPE_FOR_CLASS.apply(type); + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleDefinitions.java b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleDefinitions.java index 47ee055782d..6ad3136afb3 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleDefinitions.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleDefinitions.java @@ -21,6 +21,7 @@ package org.sonar.api.rule; import com.google.common.collect.*; import org.apache.commons.lang.StringUtils; +import org.slf4j.LoggerFactory; import org.sonar.api.ServerExtension; import javax.annotation.CheckForNull; @@ -86,6 +87,8 @@ public interface RuleDefinitions extends ServerExtension { static interface NewExtendedRepository { NewRule newRule(String ruleKey); + void loadAnnotatedClasses(Class... classes); + void done(); } @@ -117,13 +120,24 @@ public interface RuleDefinitions extends ServerExtension { @Override public NewRule newRule(String ruleKey) { if (newRules.containsKey(ruleKey)) { - throw new IllegalArgumentException("The rule '" + ruleKey + "' of repository '" + key + "' is declared several times"); + // Should fail in a perfect world, but at the time being the Findbugs plugin + // defines several times the rule EC_INCOMPATIBLE_ARRAY_COMPARE + // See http://jira.codehaus.org/browse/SONARJAVA-428 + LoggerFactory.getLogger(getClass()).warn(String.format("The rule '%s' of repository '%s' is declared several times", ruleKey, key)); } NewRule newRule = new NewRule(key, ruleKey); newRules.put(ruleKey, newRule); return newRule; } + /** + * Load definitions from classes annotated with #{@link org.sonar.check.Rule} of library sonar-check-api + */ + @Override + public void loadAnnotatedClasses(Class... classes) { + new AnnotationRuleDefinitions().loadRules(this, classes); + } + @Override public void done() { // note that some validations can be done here, for example for diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleParamType.java b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleParamType.java index 7b7d5285f3f..0330cab1aa5 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleParamType.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleParamType.java @@ -35,6 +35,7 @@ public final class RuleParamType { public static final RuleParamType TEXT = new RuleParamType("TEXT"); public static final RuleParamType BOOLEAN = new RuleParamType("BOOLEAN"); public static final RuleParamType INTEGER = new RuleParamType("INTEGER"); + public static final RuleParamType FLOAT = new RuleParamType("FLOAT"); private final String type; private final String[] options; diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/rule/AnnotationRuleDefinitionsTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/rule/AnnotationRuleDefinitionsTest.java new file mode 100644 index 00000000000..754878c9b25 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/rule/AnnotationRuleDefinitionsTest.java @@ -0,0 +1,161 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.api.rule; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.check.Priority; + +import static org.fest.assertions.Assertions.assertThat; + +public class AnnotationRuleDefinitionsTest { + + @org.junit.Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void rule_with_property() { + RuleDefinitions.Repository repository = load(RuleWithProperty.class); + assertThat(repository.rules()).hasSize(1); + RuleDefinitions.Rule rule = repository.rules().get(0); + assertThat(rule.key()).isEqualTo("foo"); + assertThat(rule.status()).isEqualTo(RuleDefinitions.Status.BETA); + assertThat(rule.name()).isEqualTo("bar"); + assertThat(rule.htmlDescription()).isEqualTo("Foo Bar"); + assertThat(rule.defaultSeverity()).isEqualTo(Severity.BLOCKER); + assertThat(rule.status()).isEqualTo(RuleDefinitions.Status.BETA); + assertThat(rule.params()).hasSize(1); + + RuleDefinitions.Param prop = rule.param("property"); + assertThat(prop.key()).isEqualTo("property"); + assertThat(prop.description()).isEqualTo("Ignore ?"); + assertThat(prop.defaultValue()).isEqualTo("false"); + assertThat(prop.type()).isEqualTo(RuleParamType.STRING); + } + + @Test + public void rule_with_integer_property() { + RuleDefinitions.Repository repository = load(RuleWithIntegerProperty.class); + + RuleDefinitions.Param prop = repository.rules().get(0).param("property"); + assertThat(prop.description()).isEqualTo("Max"); + assertThat(prop.defaultValue()).isEqualTo("12"); + assertThat(prop.type()).isEqualTo(RuleParamType.INTEGER); + } + + @Test + public void rule_with_text_property() { + RuleDefinitions.Repository repository = load(RuleWithTextProperty.class); + + RuleDefinitions.Param prop = repository.rules().get(0).param("property"); + assertThat(prop.description()).isEqualTo("text"); + assertThat(prop.defaultValue()).isEqualTo("Long text"); + assertThat(prop.type()).isEqualTo(RuleParamType.TEXT); + } + + @Test + @Ignore("TODO list supported types in RuleParamType") + public void should_reject_invalid_property_types() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Invalid property type [INVALID]"); + + load(RuleWithInvalidPropertyType.class); + } + + @Test + public void should_recognize_type() { + assertThat(AnnotationRuleDefinitions.guessType(Integer.class)).isEqualTo(RuleParamType.INTEGER); + assertThat(AnnotationRuleDefinitions.guessType(int.class)).isEqualTo(RuleParamType.INTEGER); + assertThat(AnnotationRuleDefinitions.guessType(Float.class)).isEqualTo(RuleParamType.FLOAT); + assertThat(AnnotationRuleDefinitions.guessType(float.class)).isEqualTo(RuleParamType.FLOAT); + assertThat(AnnotationRuleDefinitions.guessType(Boolean.class)).isEqualTo(RuleParamType.BOOLEAN); + assertThat(AnnotationRuleDefinitions.guessType(boolean.class)).isEqualTo(RuleParamType.BOOLEAN); + assertThat(AnnotationRuleDefinitions.guessType(String.class)).isEqualTo(RuleParamType.STRING); + assertThat(AnnotationRuleDefinitions.guessType(Object.class)).isEqualTo(RuleParamType.STRING); + } + + @Test + public void use_classname_when_missing_key() { + RuleDefinitions.Repository repository = load(RuleWithoutKey.class); + assertThat(repository.rules()).hasSize(1); + RuleDefinitions.Rule rule = repository.rules().get(0); + assertThat(rule.key()).isEqualTo(RuleWithoutKey.class.getCanonicalName()); + assertThat(rule.name()).isEqualTo("foo"); + } + + @Test + public void overridden_class() { + RuleDefinitions.Repository repository = load(OverridingRule.class); + assertThat(repository.rules()).hasSize(1); + RuleDefinitions.Rule rule = repository.rules().get(0); + assertThat(rule.key()).isEqualTo("overriding_foo"); + assertThat(rule.name()).isEqualTo("Overriding Foo"); + assertThat(rule.defaultSeverity()).isEqualTo(Severity.MAJOR); + assertThat(rule.htmlDescription()).isEqualTo("Desc of Overriding Foo"); + assertThat(rule.params()).hasSize(2); + } + + private RuleDefinitions.Repository load(Class annotatedClass) { + RuleDefinitions.Context context = new RuleDefinitions.Context(); + RuleDefinitions.NewRepository newRepository = context.newRepository("squid", "java"); + new AnnotationRuleDefinitions().loadRules(newRepository, annotatedClass); + newRepository.done(); + return context.repository("squid"); + } + + @org.sonar.check.Rule(name = "foo", description = "Foo") + static class RuleWithoutKey { + } + + @org.sonar.check.Rule(key = "foo") + static class RuleWithoutNameNorDescription { + } + + @org.sonar.check.Rule(key = "foo", name = "bar", description = "Foo Bar", priority = Priority.BLOCKER, status="BETA") + static class RuleWithProperty { + @org.sonar.check.RuleProperty(description = "Ignore ?", defaultValue = "false") + private String property; + } + + @org.sonar.check.Rule(key = "overriding_foo", name = "Overriding Foo", description = "Desc of Overriding Foo") + static class OverridingRule extends RuleWithProperty { + @org.sonar.check.RuleProperty + private String additionalProperty; + } + + @org.sonar.check.Rule(key = "foo", name = "bar", description = "Foo Bar", priority = Priority.BLOCKER) + static class RuleWithIntegerProperty { + @org.sonar.check.RuleProperty(description = "Max", defaultValue = "12") + private Integer property; + } + + @org.sonar.check.Rule(key = "foo", name = "bar", description = "Foo Bar", priority = Priority.BLOCKER) + static class RuleWithTextProperty { + @org.sonar.check.RuleProperty(description = "text", defaultValue = "Long text", type = "TEXT") + protected String property; + } + + @org.sonar.check.Rule(key = "foo", name = "bar", description = "Foo Bar", priority = Priority.BLOCKER) + static class RuleWithInvalidPropertyType { + @org.sonar.check.RuleProperty(description = "text", defaultValue = "Long text", type = "INVALID") + public String property; + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleDefinitionsTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleDefinitionsTest.java index e04e1c112c5..33b06647c07 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleDefinitionsTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleDefinitionsTest.java @@ -169,15 +169,11 @@ public class RuleDefinitionsTest { } @Test - public void fail_if_duplicated_rule_keys() { + public void warning_if_duplicated_rule_keys() { RuleDefinitions.NewRepository findbugs = context.newRepository("findbugs", "java"); findbugs.newRule("NPE"); - try { - findbugs.newRule("NPE"); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessage("The rule 'NPE' of repository 'findbugs' is declared several times"); - } + findbugs.newRule("NPE"); + // do not fail as long as http://jira.codehaus.org/browse/SONARJAVA-428 is not fixed } @Test |