String name() default "";
/**
- * The description, optional.
+ * HTML description
*/
String description() default "";
/**
- * Default priority.
+ * Default severity used when activating the rule in a Quality profile.
*/
Priority priority() default Priority.MAJOR;
* @since 3.6
*/
String status() default "READY";
+
+ /**
+ * Rule tags
+ * @since 4.2
+ */
+ String[] tags() default {};
}
*/
package org.sonar.api.server.rule;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSortedSet;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
+import com.google.common.collect.*;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.LoggerFactory;
import org.sonar.api.ServerExtension;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
-
+import java.io.IOException;
import java.io.InputStream;
+import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Set;
private final Map<String, Repository> repositoriesByKey = Maps.newHashMap();
private final ListMultimap<String, ExtendedRepository> extendedRepositoriesByKey = ArrayListMultimap.create();
+
public NewRepository newRepository(String key, String language) {
return new NewRepositoryImpl(this, key, language, false);
}
* </description>
*
* <!-- optional fields -->
- * <configKey>Checker/TreeWalker/LocalVariableName</configKey>
+ * <internalKey>Checker/TreeWalker/LocalVariableName</internalKey>
* <severity>BLOCKER</severity>
* <cardinality>MULTIPLE</cardinality>
* <status>BETA</status>
* <param>
* <key>the-param-key</key>
+ * <tag>style</tag>
+ * <tag>security</tag>
* <description>
* <![CDATA[
* the param-description
* </param>
*
* <!-- deprecated fields -->
+ * <configKey>Checker/TreeWalker/LocalVariableName</configKey>
* <priority>BLOCKER</priority>
* </rule>
* </rules>
class NewRule {
private final String repoKey, key;
- private String name, htmlDescription, engineKey, severity = Severity.MAJOR;
+ private String name, htmlDescription, internalKey, severity = Severity.MAJOR;
private boolean template;
private RuleStatus status = RuleStatus.defaultStatus();
private final Set<String> tags = Sets.newTreeSet();
}
public NewRule setName(String s) {
- // TODO remove newlines
- this.name = s;
+ this.name = StringUtils.trim(s);
return this;
}
}
public NewRule setHtmlDescription(String s) {
- this.htmlDescription = s;
+ this.htmlDescription = StringUtils.trim(s);
+ return this;
+ }
+
+ /**
+ * Load description from a file available in classpath. Example : <code>setHtmlDescription(getClass().getResource("/myrepo/Rule1234.html")</code>
+ */
+ public NewRule setHtmlDescription(@Nullable URL classpathUrl) {
+ if (classpathUrl != null) {
+ try {
+ setHtmlDescription(IOUtils.toString(classpathUrl));
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to read: " + classpathUrl);
+ }
+ } else {
+ this.htmlDescription = null;
+ }
return this;
}
* in webapp. For example the Java Checkstyle plugin feeds this field
* with the internal path ("Checker/TreeWalker/AnnotationUseStyle").
*/
- public NewRule setEngineKey(@Nullable String s) {
- this.engineKey = s;
+ public NewRule setInternalKey(@Nullable String s) {
+ this.internalKey = s;
return this;
}
@Immutable
class Rule {
private final Repository repository;
- private final String repoKey, key, name, htmlDescription, engineKey, severity;
+ private final String repoKey, key, name, htmlDescription, internalKey, severity;
private final boolean template;
private final Set<String> tags;
private final Map<String, Param> params;
this.key = newRule.key;
this.name = newRule.name;
this.htmlDescription = newRule.htmlDescription;
- this.engineKey = newRule.engineKey;
+ this.internalKey = newRule.internalKey;
this.severity = newRule.severity;
this.template = newRule.template;
this.status = newRule.status;
}
/**
- * @see RuleDefinitions.NewRule#setEngineKey(String)
+ * @see RuleDefinitions.NewRule#setInternalKey(String)
*/
@CheckForNull
- public String engineKey() {
- return engineKey;
+ public String internalKey() {
+ return internalKey;
}
@Override
rule.setSeverity(ruleAnnotation.priority().name());
rule.setTemplate(ruleAnnotation.cardinality() == Cardinality.MULTIPLE);
rule.setStatus(RuleStatus.valueOf(ruleAnnotation.status()));
+ rule.setTags(ruleAnnotation.tags());
List<Field> fields = FieldUtils2.getFields(clazz, true);
for (Field field : fields) {
}
private void processRule(RuleDefinitions.NewRepository repo, SMInputCursor ruleC) throws XMLStreamException {
- String key = null, name = null, description = null, engineKey = null, severity = Severity.defaultSeverity(), status = null;
+ String key = null, name = null, description = null, internalKey = null, severity = Severity.defaultSeverity(), status = null;
Cardinality cardinality = Cardinality.SINGLE;
List<ParamStruct> params = new ArrayList<ParamStruct>();
+ List<String> tags = new ArrayList<String>();
/* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */
String keyAttribute = ruleC.getAttrValue("key");
key = StringUtils.trim(cursor.collectDescendantText(false));
} else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) {
- engineKey = StringUtils.trim(cursor.collectDescendantText(false));
+ // deprecated field, replaced by internalKey
+ internalKey = StringUtils.trim(cursor.collectDescendantText(false));
+
+ } else if (StringUtils.equalsIgnoreCase("internalKey", nodeName)) {
+ internalKey = StringUtils.trim(cursor.collectDescendantText(false));
} else if (StringUtils.equalsIgnoreCase("priority", nodeName)) {
// deprecated field, replaced by severity
} else if (StringUtils.equalsIgnoreCase("param", nodeName)) {
params.add(processParameter(cursor));
+
+ } else if (StringUtils.equalsIgnoreCase("tag", nodeName)) {
+ tags.add(StringUtils.trim(cursor.collectDescendantText(false)));
}
}
RuleDefinitions.NewRule rule = repo.newRule(key)
.setHtmlDescription(description)
.setSeverity(severity)
.setName(name)
- .setEngineKey(engineKey)
+ .setInternalKey(internalKey)
+ .setTags(tags.toArray(new String[tags.size()]))
.setTemplate(cardinality == Cardinality.MULTIPLE);
if (status != null) {
rule.setStatus(RuleStatus.valueOf(status));
assertThat(rule.htmlDescription()).isEqualTo("Foo Bar");
assertThat(rule.severity()).isEqualTo(Severity.BLOCKER);
assertThat(rule.params()).hasSize(1);
+ assertThat(rule.tags()).isEmpty();
RuleDefinitions.Param prop = rule.param("property");
assertThat(prop.key()).isEqualTo("property");
assertThat(prop.type()).isEqualTo(RuleParamType.TEXT);
}
- @Test
- @Ignore("TODO list supported types in RuleParamType")
- public void should_reject_invalid_property_types() {
- thrown.expect(IllegalArgumentException.class);
- thrown.expectMessage("Invalid property type [INVALID]");
-
- load(RuleWithInvalidPropertyType.class);
- }
-
@Test
public void should_recognize_type() {
assertThat(RuleDefinitionsFromAnnotations.guessType(Integer.class)).isEqualTo(RuleParamType.INTEGER);
assertThat(rule.name()).isEqualTo("foo");
}
+ @Test
+ public void rule_with_tags() {
+ RuleDefinitions.Repository repository = load(RuleWithTags.class);
+ assertThat(repository.rules()).hasSize(1);
+ RuleDefinitions.Rule rule = repository.rules().get(0);
+ assertThat(rule.tags()).containsOnly("style", "security");
+ }
+
@Test
public void overridden_class() {
RuleDefinitions.Repository repository = load(OverridingRule.class);
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")
@org.sonar.check.RuleProperty(description = "text", defaultValue = "Long text", type = "INVALID")
public String property;
}
+
+ @org.sonar.check.Rule(key = "foo", name = "bar", description = "Bar", tags = {"style", "security"})
+ static class RuleWithTags {
+ }
}
assertThat(rule.severity()).isEqualTo(Severity.BLOCKER);
assertThat(rule.template()).isTrue();
assertThat(rule.status()).isEqualTo(RuleStatus.BETA);
- assertThat(rule.engineKey()).isEqualTo("Checker/TreeWalker/LocalVariableName");
+ assertThat(rule.internalKey()).isEqualTo("Checker/TreeWalker/LocalVariableName");
+ assertThat(rule.tags()).containsOnly("style", "security");
assertThat(rule.params()).hasSize(2);
RuleDefinitions.Param ignore = rule.param("ignore");
assertThat(repository.rules()).hasSize(1);
RuleDefinitions.Rule rule = repository.rules().get(0);
assertThat(rule.key()).isEqualTo("org.sonar.it.checkstyle.MethodsCountCheck");
+ assertThat(rule.internalKey()).isEqualTo("Checker/TreeWalker/org.sonar.it.checkstyle.MethodsCountCheck");
assertThat(rule.severity()).isEqualTo(Severity.CRITICAL);
assertThat(rule.htmlDescription()).isEqualTo("Count methods");
assertThat(rule.param("minMethodsCount")).isNotNull();
import org.sonar.api.rule.Severity;
import org.sonar.api.rule.RuleStatus;
+import java.net.URL;
+
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
.setHtmlDescription("Detect <code>NPE</code>")
.setHtmlDescription("Detect <code>java.lang.NullPointerException</code>")
.setSeverity(Severity.BLOCKER)
- .setEngineKey("/something")
+ .setInternalKey("/something")
.setStatus(RuleStatus.BETA)
.setTags("one", "two")
.addTags("two", "three", "four");
assertThat(npeRule.htmlDescription()).isEqualTo("Detect <code>java.lang.NullPointerException</code>");
assertThat(npeRule.tags()).containsOnly("one", "two", "three", "four");
assertThat(npeRule.params()).isEmpty();
- assertThat(npeRule.engineKey()).isEqualTo("/something");
+ assertThat(npeRule.internalKey()).isEqualTo("/something");
assertThat(npeRule.template()).isFalse();
assertThat(npeRule.status()).isEqualTo(RuleStatus.BETA);
assertThat(npeRule.toString()).isEqualTo("[repository=findbugs, key=NPE]");
assertThat(rule.key()).isEqualTo("NPE");
assertThat(rule.severity()).isEqualTo(Severity.MAJOR);
assertThat(rule.params()).isEmpty();
- assertThat(rule.engineKey()).isNull();
+ assertThat(rule.internalKey()).isNull();
assertThat(rule.status()).isEqualTo(RuleStatus.defaultStatus());
assertThat(rule.tags()).isEmpty();
}
assertThat(level.hashCode()).isEqualTo(level.hashCode());
}
+ @Test
+ public void sanitize_rule_name() {
+ RuleDefinitions.NewRepository newFindbugs = context.newRepository("findbugs", "java");
+ newFindbugs.newRule("NPE").setName(" \n NullPointer \n ").setHtmlDescription("NPE");
+ newFindbugs.done();
+
+ RuleDefinitions.Rule rule = context.repository("findbugs").rule("NPE");
+ assertThat(rule.name()).isEqualTo("NullPointer");
+ }
+
@Test
public void extend_repository() {
assertThat(context.extendedRepositories()).isEmpty();
}
}
+ @Test
+ public void load_rule_description_from_file() {
+ RuleDefinitions.NewRepository newRepository = context.newRepository("findbugs", "java");
+ newRepository.newRule("NPE").setName("NPE").setHtmlDescription(getClass().getResource("/org/sonar/api/server/rule/RuleDefinitionsTest/sample.html"));
+ newRepository.done();
+
+ RuleDefinitions.Rule rule = context.repository("findbugs").rule("NPE");
+ assertThat(rule.htmlDescription()).isEqualTo("description of rule loaded from file");
+ }
+
+ @Test
+ public void fail_to_load_rule_description_from_file() {
+ RuleDefinitions.NewRepository newRepository = context.newRepository("findbugs", "java");
+ newRepository.newRule("NPE").setName("NPE").setHtmlDescription((URL)null);
+ try {
+ newRepository.done();
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("HTML description of rule [repository=findbugs, key=NPE] is empty");
+ }
+ }
+
@Test
public void fail_if_blank_rule_html_description() {
RuleDefinitions.NewRepository newRepository = context.newRepository("findbugs", "java");
- newRepository.newRule("NPE").setName("NPE").setHtmlDescription(null);
+ newRepository.newRule("NPE").setName("NPE").setHtmlDescription((String)null);
try {
newRepository.done();
fail();
<description>
<![CDATA[Description of Complete]]>
</description>
- <configKey>Checker/TreeWalker/LocalVariableName</configKey>
+ <internalKey>Checker/TreeWalker/LocalVariableName</internalKey>
<severity>BLOCKER</severity>
<cardinality>MULTIPLE</cardinality>
<status>BETA</status>
+ <tag>style</tag>
+ <tag>security</tag>
<param>
<key>tokens</key>
<description>
<![CDATA[Description of Minimal]]>
</description>
</rule>
-</rules>
\ No newline at end of file
+</rules>
--- /dev/null
+description of rule loaded from file
NewRule newRule = newRepository.newRule(rule.getKey());
newRule.setName(ruleName(repository.getKey(), rule));
newRule.setHtmlDescription(ruleDescription(repository.getKey(), rule));
- newRule.setEngineKey(rule.getConfigKey());
+ newRule.setInternalKey(rule.getConfigKey());
newRule.setTemplate(Cardinality.MULTIPLE.equals(rule.getCardinality()));
newRule.setSeverity(rule.getSeverity().toString());
newRule.setStatus(rule.getStatus() == null ? RuleStatus.defaultStatus() : RuleStatus.valueOf(rule.getStatus()));
private RuleDto enableAndInsert(Buffer buffer, SqlSession sqlSession, RuleDefinitions.Rule ruleDef) {
RuleDto ruleDto = new RuleDto()
.setCardinality(ruleDef.template() ? Cardinality.MULTIPLE : Cardinality.SINGLE)
- .setConfigKey(ruleDef.engineKey())
+ .setConfigKey(ruleDef.internalKey())
.setDescription(ruleDef.htmlDescription())
.setLanguage(ruleDef.repository().language())
.setName(ruleDef.name())
dto.setDescription(def.htmlDescription());
changed = true;
}
- if (!StringUtils.equals(dto.getConfigKey(), def.engineKey())) {
- dto.setConfigKey(def.engineKey());
+ if (!StringUtils.equals(dto.getConfigKey(), def.internalKey())) {
+ dto.setConfigKey(def.internalKey());
changed = true;
}
String severity = RulePriority.valueOf(def.severity()).name();
assertThat(rule.name()).isEqualTo("Constant Name");
assertThat(rule.htmlDescription()).isEqualTo("Checks that constant names conform to the specified format");
assertThat(rule.severity()).isEqualTo(Severity.BLOCKER);
- assertThat(rule.engineKey()).isEqualTo("Checker/TreeWalker/ConstantName");
+ assertThat(rule.internalKey()).isEqualTo("Checker/TreeWalker/ConstantName");
assertThat(rule.status()).isEqualTo(RuleStatus.BETA);
assertThat(rule.tags()).isEmpty();
assertThat(rule.params()).hasSize(1);
.setName("One")
.setHtmlDescription("Description of One")
.setSeverity(Severity.BLOCKER)
- .setEngineKey("config1")
+ .setInternalKey("config1")
.setTags("tag1", "tag3", "tag5");
rule1.newParam("param1").setDescription("parameter one").setDefaultValue("default value one");
rule1.newParam("param2").setDescription("parameter two").setDefaultValue("default value two");
.setName("name of " + i)
.setHtmlDescription("description of " + i)
.setSeverity(Severity.BLOCKER)
- .setEngineKey("config1")
+ .setInternalKey("config1")
.setTags("tag1", "tag3", "tag5");
for (int j = 0; j < 20; j++) {
rule.newParam("param" + j);