From 5917f884d758cd4947a6e793ac3310372b4fcae7 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Thu, 9 Jan 2014 00:25:50 +0100 Subject: [PATCH] SONAR-4908 New API to declare coding rules The extension point RuleDefinitions is not used yet. --- .../org/sonar/api/rule/RuleDefinitions.java | 391 ++++++++++++++++++ .../org/sonar/api/rule/RuleTagFormat.java | 42 ++ .../sonar/api/rule/RuleDefinitionsTest.java | 220 ++++++++++ .../org/sonar/api/rule/RuleTagFormatTest.java | 53 +++ 4 files changed, 706 insertions(+) create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleDefinitions.java create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleTagFormat.java create mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleDefinitionsTest.java create mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleTagFormatTest.java 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 new file mode 100644 index 00000000000..ff570e72f61 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleDefinitions.java @@ -0,0 +1,391 @@ +/* + * 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.collect.*; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.ServerExtension; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Defines the coding rules. For example the Java Findbugs plugin provides an implementation of + * this extension point in order to define the rules that it supports. + *

+ * This interface replaces the deprecated class {@link org.sonar.api.rules.RuleRepository}. + * + * @since 4.2 + */ +public interface RuleDefinitions extends ServerExtension { + + /** + * Instantiated by core but not by plugins. + */ + public static class Context { + private final Map newRepositories = Maps.newHashMap(); + private final ListMultimap extendedRepositories = ArrayListMultimap.create(); + + public NewRepository newRepository(String key, String language) { + if (newRepositories.containsKey(key)) { + throw new IllegalArgumentException("The rule repository '" + key + "' is defined several times"); + } + NewRepository repo = new NewRepository(key, language); + newRepositories.put(key, repo); + return repo; + } + + /** + * Add rules to a repository defined by another plugin. For example the Java FB-Contrib plugin + * provides new rules for the Findbugs engine. + * + * @param key the key of the repository to extend, "findbugs" in the example. + */ + public ExtendedRepository extendRepository(String key) { + ExtendedRepository repo = new NewRepository(key); + extendedRepositories.put(key, repo); + return repo; + } + + @CheckForNull + public NewRepository getRepository(String key) { + return newRepositories.get(key); + } + + public List getRepositories() { + return ImmutableList.copyOf(newRepositories.values()); + } + + @CheckForNull + public List getExtendedRepositories(String key) { + return extendedRepositories.get(key); + } + + public List getExtendedRepositories() { + return ImmutableList.copyOf(extendedRepositories.values()); + } + } + + public static interface ExtendedRepository { + String key(); + + NewRule newRule(String ruleKey); + + @CheckForNull + NewRule getRule(String ruleKey); + + List getRules(); + } + + public static class NewRepository implements ExtendedRepository { + private final String key; + private String language; + private String name; + private final Map newRules = Maps.newHashMap(); + + private NewRepository(String key, String language) { + this.key = this.name = key; + this.language = language; + } + + // Used to expose ExtendedRepository + private NewRepository(String key) { + this.key = key; + } + + public NewRepository setName(String s) { + this.name = s; + return this; + } + + @Override + public String key() { + return key; + } + + public String language() { + return language; + } + + public String name() { + return name; + } + + @Override + public NewRule newRule(String ruleKey) { + if (newRules.containsKey(ruleKey)) { + throw new IllegalArgumentException("The rule '" + ruleKey + "' of repository '" + key + "' is declared several times"); + } + NewRule newRule = new NewRule(key, ruleKey); + newRules.put(ruleKey, newRule); + return newRule; + } + + @Override + @CheckForNull + public NewRule getRule(String ruleKey) { + return newRules.get(ruleKey); + } + + @Override + public List getRules() { + return ImmutableList.copyOf(newRules.values()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NewRepository that = (NewRepository) o; + return key.equals(that.key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + } + + public static class NewRule { + private final String repoKey, key; + private String name, htmlDescription, metadata, severity = Severity.MAJOR; + private final Set tags = Sets.newHashSet(); + private final Map params = Maps.newHashMap(); + // TODO cardinality ? or template boolean ? + + public NewRule(String repoKey, String key) { + this.repoKey = repoKey; + this.key = this.name = key; + } + + public String key() { + return key; + } + + public String name() { + return name; + } + + public NewRule setName(String s) { + if (StringUtils.isBlank(s)) { + throw new IllegalArgumentException("Name of rule " + this + " is blank"); + } + this.name = s; + return this; + } + + public String severity() { + return severity; + } + + public NewRule setSeverity(String s) { + if (!Severity.ALL.contains(s)) { + throw new IllegalArgumentException("Severity of rule " + this + " is not correct: " + s); + } + this.severity = s; + return this; + } + + @CheckForNull + public String htmlDescription() { + return htmlDescription; + } + + public NewRule setHtmlDescription(String s) { + if (StringUtils.isBlank(s)) { + throw new IllegalArgumentException("HTML description of rule " + this + " is blank"); + } + this.htmlDescription = s; + return this; + } + + public NewParam newParam(String paramKey) { + if (params.containsKey(paramKey)) { + throw new IllegalArgumentException("The parameter '" + key + "' is declared several times on the rule " + this); + } + NewParam param = new NewParam(this, paramKey); + params.put(paramKey, param); + return param; + } + + @CheckForNull + public NewParam getParam(String key) { + return params.get(key); + } + + public List getParams() { + return ImmutableList.copyOf(params.values()); + } + + public Set tags() { + return ImmutableSet.copyOf(tags); + } + + /** + * @see org.sonar.api.rule.RuleTagFormat + */ + public NewRule addTag(String s) { + RuleTagFormat.validate(s); + tags.add(s); + return this; + } + + /** + * @see org.sonar.api.rule.RuleTagFormat + */ + public NewRule setTags(String... list) { + tags.clear(); + for (String tag : list) { + addTag(tag); + } + return this; + } + + /** + * @see org.sonar.api.rule.RuleDefinitions.NewRule#setMetadata(String) + */ + @CheckForNull + public String metadata() { + return metadata; + } + + /** + * Optional metadata that can be used by the rule engine. Not displayed + * in webapp. For example the Java Checkstyle plugin feeds this field + * with the internal path ("Checker/TreeWalker/AnnotationUseStyle"). + */ + public NewRule setMetadata(@Nullable String s) { + this.metadata = s; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NewRule newRule = (NewRule) o; + return key.equals(newRule.key) && repoKey.equals(newRule.repoKey); + } + + @Override + public int hashCode() { + int result = repoKey.hashCode(); + result = 31 * result + key.hashCode(); + return result; + } + + @Override + public String toString() { + return String.format("[repository=%s, key=%s]", repoKey, key); + } + } + + public static class NewParam { + private final NewRule rule; + private final String key; + private String name, description, defaultValue; + // TODO type + + private NewParam(NewRule rule, String key) { + this.rule = rule; + this.key = this.name = key; + } + + public String key() { + return key; + } + + public String name() { + return name; + } + + public NewParam setName(String s) { + this.name = s; + return this; + } + + /** + * @see org.sonar.api.rule.RuleDefinitions.NewParam#setDescription(String) + */ + @Nullable + public String description() { + return description; + } + + /** + * Plain-text description. Can be null. + */ + public NewParam setDescription(@Nullable String s) { + this.description = StringUtils.defaultIfBlank(s, null); + return this; + } + + @Nullable + public String defaultValue() { + return defaultValue; + } + + public NewParam setDefaultValue(@Nullable String s) { + this.defaultValue = s; + return this; + } + + /** + * Helpful for method chaining. + */ + public NewRule rule() { + return rule; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NewParam that = (NewParam) o; + return key.equals(that.key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + } + + /** + * This method is executed when server is started. + */ + void define(Context context); + +} \ No newline at end of file diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleTagFormat.java b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleTagFormat.java new file mode 100644 index 00000000000..68bdf27c0b4 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleTagFormat.java @@ -0,0 +1,42 @@ +/* + * 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.apache.commons.lang.StringUtils; + +/** + * @since 4.2 + */ +public class RuleTagFormat { + + private RuleTagFormat() { + // only static methods + } + + public static boolean isValid(String tag) { + return StringUtils.isNotBlank(tag) && StringUtils.indexOf(tag, " ") < 0; + } + + public static void validate(String tag) { + if (!isValid(tag)) { + throw new IllegalArgumentException(String.format("Whitespaces are not allowed in rule tags: '%s'", tag)); + } + } +} 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 new file mode 100644 index 00000000000..fc04abd7616 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleDefinitionsTest.java @@ -0,0 +1,220 @@ +/* + * 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.Test; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class RuleDefinitionsTest { + + RuleDefinitions.Context context = new RuleDefinitions.Context(); + + @Test + public void define_repositories() throws Exception { + assertThat(context.getRepositories()).isEmpty(); + + RuleDefinitions.NewRepository findbugs = context.newRepository("findbugs", "java") + .setName("Findbugs"); + RuleDefinitions.NewRepository checkstyle = context.newRepository("checkstyle", "java"); + + assertThat(findbugs).isNotNull(); + assertThat(findbugs.key()).isEqualTo("findbugs"); + assertThat(findbugs.language()).isEqualTo("java"); + assertThat(findbugs.name()).isEqualTo("Findbugs"); + assertThat(findbugs.getRules()).isEmpty(); + + assertThat(context.getRepositories()).hasSize(2); + assertThat(context.getRepository("findbugs")).isSameAs(findbugs); + assertThat(context.getRepository("unknown")).isNull(); + + // test equals() and hashCode() + assertThat(findbugs).isEqualTo(findbugs).isNotEqualTo(checkstyle).isNotEqualTo("findbugs"); + assertThat(findbugs.hashCode()).isEqualTo(findbugs.hashCode()); + } + + @Test + public void default_repository_name_is_key() { + RuleDefinitions.NewRepository findbugs = context.newRepository("findbugs", "java"); + assertThat(findbugs.name()).isEqualTo(findbugs.key()).isEqualTo("findbugs"); + } + + @Test + public void define_rules() { + RuleDefinitions.NewRepository findbugs = context.newRepository("findbugs", "java"); + findbugs.newRule("NPE") + .setName("Detect NPE") + .setHtmlDescription("Detect java.lang.NullPointerException") + .setSeverity(Severity.BLOCKER) + .setMetadata("/something") + .setTags("valuable", "bug"); + findbugs.newRule("ABC"); + + assertThat(findbugs.getRules()).hasSize(2); + + RuleDefinitions.NewRule npeRule = findbugs.getRule("NPE"); + assertThat(npeRule.key()).isEqualTo("NPE"); + assertThat(npeRule.name()).isEqualTo("Detect NPE"); + assertThat(npeRule.severity()).isEqualTo(Severity.BLOCKER); + assertThat(npeRule.htmlDescription()).isEqualTo("Detect java.lang.NullPointerException"); + assertThat(npeRule.tags()).containsOnly("valuable", "bug"); + assertThat(npeRule.getParams()).isEmpty(); + assertThat(npeRule.metadata()).isEqualTo("/something"); + + // test equals() and hashCode() + RuleDefinitions.NewRule otherRule = findbugs.getRule("ABC"); + assertThat(npeRule).isEqualTo(npeRule).isNotEqualTo(otherRule).isNotEqualTo("NPE"); + assertThat(npeRule.hashCode()).isEqualTo(npeRule.hashCode()); + } + + @Test + public void define_rule_with_default_fields() { + context.newRepository("findbugs", "java").newRule("NPE"); + + RuleDefinitions.NewRule rule = context.getRepository("findbugs").getRule("NPE"); + assertThat(rule.key()).isEqualTo("NPE"); + assertThat(rule.name()).isEqualTo("NPE"); + assertThat(rule.severity()).isEqualTo(Severity.MAJOR); + assertThat(rule.htmlDescription()).isNull(); + assertThat(rule.getParams()).isEmpty(); + assertThat(rule.metadata()).isNull(); + assertThat(rule.tags()).isEmpty(); + } + + @Test + public void define_rule_parameters() { + context.newRepository("findbugs", "java") + .newRule("NPE") + .newParam("level").setDefaultValue("LOW").setName("Level").setDescription("The level") + .rule() + .newParam("effort"); + + RuleDefinitions.NewRule rule = context.getRepository("findbugs").getRule("NPE"); + assertThat(rule.getParams()).hasSize(2); + + RuleDefinitions.NewParam level = rule.getParam("level"); + assertThat(level.key()).isEqualTo("level"); + assertThat(level.name()).isEqualTo("Level"); + assertThat(level.description()).isEqualTo("The level"); + assertThat(level.defaultValue()).isEqualTo("LOW"); + + RuleDefinitions.NewParam effort = rule.getParam("effort"); + assertThat(effort.key()).isEqualTo("effort").isEqualTo(effort.name()); + assertThat(effort.description()).isNull(); + assertThat(effort.defaultValue()).isNull(); + + // test equals() and hashCode() + assertThat(level).isEqualTo(level).isNotEqualTo(effort).isNotEqualTo("level"); + assertThat(level.hashCode()).isEqualTo(level.hashCode()); + } + + @Test + public void extend_repository() { + assertThat(context.getExtendedRepositories()).isEmpty(); + + // for example fb-contrib + context.extendRepository("findbugs").newRule("NPE"); + + assertThat(context.getRepositories()).isEmpty(); + assertThat(context.getExtendedRepositories()).hasSize(1); + assertThat(context.getExtendedRepositories("other")).isEmpty(); + assertThat(context.getExtendedRepositories("findbugs")).hasSize(1); + + RuleDefinitions.ExtendedRepository findbugs = context.getExtendedRepositories("findbugs").get(0); + assertThat(findbugs.getRule("NPE")).isNotNull(); + } + + @Test + public void fail_if_duplicated_repo_keys() { + context.newRepository("findbugs", "java"); + try { + context.newRepository("findbugs", "whatever_the_language"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("The rule repository 'findbugs' is defined several times"); + } + } + + @Test + public void fail_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"); + } + } + + @Test + public void fail_if_duplicated_rule_param_keys() { + RuleDefinitions.NewRule rule = context.newRepository("findbugs", "java").newRule("NPE"); + rule.newParam("level"); + try { + rule.newParam("level"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("The parameter 'NPE' is declared several times on the rule [repository=findbugs, key=NPE]"); + } + } + + @Test + public void fail_if_blank_rule_name() { + try { + context.newRepository("findbugs", "java").newRule("NPE").setName(null); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("Name of rule [repository=findbugs, key=NPE] is blank"); + } + } + + @Test + public void fail_if_bad_rule_tag() { + try { + // whitespaces are not allowed in tags + context.newRepository("findbugs", "java").newRule("NPE").setTags("coding style"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("Whitespaces are not allowed in rule tags: 'coding style'"); + } + } + + @Test + public void fail_if_blank_rule_html_description() { + try { + context.newRepository("findbugs", "java").newRule("NPE").setHtmlDescription(null); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("HTML description of rule [repository=findbugs, key=NPE] is blank"); + } + } + + @Test + public void fail_if_bad_rule_severity() { + try { + context.newRepository("findbugs", "java").newRule("NPE").setSeverity("VERY HIGH"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("Severity of rule [repository=findbugs, key=NPE] is not correct: VERY HIGH"); + } + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleTagFormatTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleTagFormatTest.java new file mode 100644 index 00000000000..c4b9263588c --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/rule/RuleTagFormatTest.java @@ -0,0 +1,53 @@ +/* + * 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.Test; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class RuleTagFormatTest { + @Test + public void isValid() { + assertThat(RuleTagFormat.isValid(null)).isFalse(); + assertThat(RuleTagFormat.isValid("")).isFalse(); + assertThat(RuleTagFormat.isValid(" ")).isFalse(); + assertThat(RuleTagFormat.isValid("coding style")).isFalse(); + + assertThat(RuleTagFormat.isValid("style")).isTrue(); + } + + @Test + public void validate() { + RuleTagFormat.validate("style"); + // no error + } + + @Test + public void validate_and_fail() { + try { + RuleTagFormat.validate(" "); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("Whitespaces are not allowed in rule tags: ' '"); + } + } +} -- 2.39.5