]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11210 Display organization specific ad hoc rule info
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Thu, 13 Sep 2018 13:10:42 +0000 (15:10 +0200)
committerSonarTech <sonartech@sonarsource.com>
Mon, 24 Sep 2018 18:20:58 +0000 (20:20 +0200)
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OnePredefinedAndAdHocRuleExternalIssuePerLineSensor.java [new file with mode: 0644]
plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDefinitionDto.java
server/sonar-db-dao/src/test/java/org/sonar/db/rule/RuleDbTester.java
server/sonar-server/src/main/java/org/sonar/server/rule/ws/RuleMapper.java
server/sonar-server/src/test/java/org/sonar/server/rule/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/rule/ws/ShowActionTest.java

index cc66f2d9c1d52727adf7714bb25f4522d83cf274..25ee11d763ee2052b12a0fe192bcb8941a80b67c 100644 (file)
@@ -49,7 +49,6 @@ import org.sonar.xoo.rule.OneBlockerIssuePerFileSensor;
 import org.sonar.xoo.rule.OneBugIssuePerLineSensor;
 import org.sonar.xoo.rule.OneDayDebtPerFileSensor;
 import org.sonar.xoo.rule.OneExternalIssuePerLineSensor;
-import org.sonar.xoo.rule.OnePredefinedRuleExternalIssuePerLineSensor;
 import org.sonar.xoo.rule.OneIssueOnDirPerFileSensor;
 import org.sonar.xoo.rule.OneIssuePerDirectorySensor;
 import org.sonar.xoo.rule.OneIssuePerFileSensor;
@@ -57,6 +56,8 @@ import org.sonar.xoo.rule.OneIssuePerLineSensor;
 import org.sonar.xoo.rule.OneIssuePerModuleSensor;
 import org.sonar.xoo.rule.OneIssuePerTestFileSensor;
 import org.sonar.xoo.rule.OneIssuePerUnknownFileSensor;
+import org.sonar.xoo.rule.OnePredefinedAndAdHocRuleExternalIssuePerLineSensor;
+import org.sonar.xoo.rule.OnePredefinedRuleExternalIssuePerLineSensor;
 import org.sonar.xoo.rule.OneVulnerabilityIssuePerModuleSensor;
 import org.sonar.xoo.rule.RandomAccessSensor;
 import org.sonar.xoo.rule.SaveDataTwiceSensor;
@@ -169,6 +170,7 @@ public class XooPlugin implements Plugin {
       context.addExtensions(
         OneExternalIssuePerLineSensor.class,
         OnePredefinedRuleExternalIssuePerLineSensor.class,
+        OnePredefinedAndAdHocRuleExternalIssuePerLineSensor.class,
         SignificantCodeSensor.class);
     }
     if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(7, 3))) {
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OnePredefinedAndAdHocRuleExternalIssuePerLineSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OnePredefinedAndAdHocRuleExternalIssuePerLineSensor.java
new file mode 100644 (file)
index 0000000..b522c29
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.xoo.rule;
+
+import org.sonar.api.batch.fs.FilePredicates;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
+import org.sonar.api.rules.RuleType;
+import org.sonar.xoo.Xoo;
+
+public class OnePredefinedAndAdHocRuleExternalIssuePerLineSensor implements Sensor {
+
+  public static final String ACTIVATE = "sonar.onePredefinedAndAdHocRuleExternalIssuePerLine.activate";
+  private static final String NAME = "One External Issue Per Line With A Predefined And An AdHoc Rule";
+
+  @Override
+  public void describe(SensorDescriptor descriptor) {
+    descriptor
+      .name(NAME)
+      .onlyOnLanguages(Xoo.KEY)
+      .onlyWhenConfiguration(c -> c.getBoolean(ACTIVATE).orElse(false));
+  }
+
+  @Override
+  public void execute(SensorContext context) {
+    FileSystem fs = context.fileSystem();
+    FilePredicates p = fs.predicates();
+    for (InputFile file : fs.inputFiles(p.and(p.hasLanguages(Xoo.KEY), p.hasType(Type.MAIN)))) {
+      createIssues(file, context);
+    }
+  }
+
+  private static void createIssues(InputFile file, SensorContext context) {
+    for (int line = 1; line <= file.lines(); line++) {
+      NewExternalIssue newIssue = context.newExternalIssue();
+      newIssue
+        .engineId(OnePredefinedRuleExternalIssuePerLineSensor.ENGINE_ID)
+        .ruleId(OnePredefinedRuleExternalIssuePerLineSensor.RULE_ID)
+        .at(newIssue.newLocation()
+          .on(file)
+          .at(file.selectLine(line))
+          .message("This issue is generated on each line and the rule is predefined"))
+        .severity(Severity.valueOf(OnePredefinedRuleExternalIssuePerLineSensor.SEVERITY))
+        .remediationEffortMinutes(OnePredefinedRuleExternalIssuePerLineSensor.EFFORT)
+        .type(OnePredefinedRuleExternalIssuePerLineSensor.TYPE)
+        .save();
+
+      // Even if the issue is on a predefined rule, the sensor is declaring an adHoc rule => this info should be ignored
+      context.newAdHocRule()
+        .engineId(OnePredefinedRuleExternalIssuePerLineSensor.ENGINE_ID)
+        .ruleId(OnePredefinedRuleExternalIssuePerLineSensor.RULE_ID)
+        .name("An ad hoc rule")
+        .description("blah blah")
+        .severity(Severity.BLOCKER)
+        .type(RuleType.BUG)
+        .save();
+    }
+  }
+}
index fadb287de47f26d4771436888e252eceef78bf81..f515a6859f7fcc3998a9282958e04e5b251f40dd 100644 (file)
@@ -60,7 +60,7 @@ public class XooPluginTest {
     Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(runtime).build();
     new XooPlugin().define(context);
     assertThat(getExtensions(context))
-      .hasSize(52)
+      .hasSize(53)
       .contains(OneExternalIssuePerLineSensor.class);
   }
 
@@ -70,7 +70,7 @@ public class XooPluginTest {
     Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(runtime).build();
     new XooPlugin().define(context);
     assertThat(getExtensions(context))
-      .hasSize(53)
+      .hasSize(54)
       .contains(OneExternalIssuePerLineSensor.class);
   }
 
index 4d96f20ae2332e9fa9d7f33abbe8177a5f277815..3ada8d5a11fd9ece832d4f38edaff47420340a0c 100644 (file)
@@ -40,12 +40,25 @@ public class RuleDefinitionDto {
   private Integer id;
   private String repositoryKey;
   private String ruleKey;
+
+  /**
+   * Description can be null on external rule, otherwise it should never be null
+   */
   private String description;
+
+  /**
+   * Description format can be null on external rule, otherwise it should never be null
+   */
   private RuleDto.Format descriptionFormat;
   private RuleStatus status;
   private String name;
   private String configKey;
+
+  /**
+   * Severity can be null on external rule, otherwise it should never be null
+   */
   private Integer severity;
+
   private boolean isTemplate;
 
   /**
@@ -128,20 +141,22 @@ public class RuleDefinitionDto {
     return this;
   }
 
+  @CheckForNull
   public String getDescription() {
     return description;
   }
 
-  public RuleDefinitionDto setDescription(String description) {
+  public RuleDefinitionDto setDescription(@Nullable String description) {
     this.description = description;
     return this;
   }
 
+  @CheckForNull
   public RuleDto.Format getDescriptionFormat() {
     return descriptionFormat;
   }
 
-  public RuleDefinitionDto setDescriptionFormat(RuleDto.Format descriptionFormat) {
+  public RuleDefinitionDto setDescriptionFormat(@Nullable RuleDto.Format descriptionFormat) {
     this.descriptionFormat = descriptionFormat;
     return this;
   }
index 3ab4a154a034237d92b357e7213296eb0bb77905..f494dc4d1bbba8ea9d4f6f97ec7283970f130d51 100644 (file)
@@ -94,7 +94,8 @@ public class RuleDbTester {
   }
 
   public RuleParamDto insertRuleParam(RuleDefinitionDto rule) {
-    return insertRuleParam(rule, p -> {});
+    return insertRuleParam(rule, p -> {
+    });
   }
 
   @SafeVarargs
@@ -116,15 +117,6 @@ public class RuleDbTester {
     return ruleDto;
   }
 
-  public RuleDto updateRule(RuleDto ruleDto) {
-    update(ruleDto.getDefinition());
-    RuleMetadataDto metadata = ruleDto.getMetadata();
-    if (metadata.getOrganizationUuid() != null) {
-      db.getDbClient().ruleDao().insertOrUpdate(db.getSession(), metadata.setRuleId(ruleDto.getId()));
-      db.commit();
-    }
-    return ruleDto;
-  }
   /**
    * Create and persist a rule with random values.
    */
@@ -154,7 +146,6 @@ public class RuleDbTester {
     return deprecatedRuleKeyDto;
   }
 
-
   public RuleParamDto insertRuleParam(RuleDto rule) {
     RuleParamDto param = new RuleParamDto();
     param.setRuleId(rule.getId());
index ad20629b98f88d79cb94b1f0d57f1950cc6ecb19..88bef6aa0e380cd7d5e062231f976b777df70312 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.server.rule.ws;
 
 import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -32,6 +31,7 @@ import org.sonar.api.resources.Languages;
 import org.sonar.api.server.debt.DebtRemediationFunction;
 import org.sonar.api.server.debt.internal.DefaultDebtRemediationFunction;
 import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleDto;
 import org.sonar.db.rule.RuleDto.Scope;
 import org.sonar.db.rule.RuleMetadataDto;
 import org.sonar.db.rule.RuleParamDto;
@@ -45,6 +45,7 @@ import org.sonarqube.ws.Rules;
 
 import static java.lang.String.format;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.core.util.stream.MoreCollectors.toList;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_CREATED_AT;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEBT_OVERLOADED;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEBT_REM_FUNCTION;
@@ -73,7 +74,7 @@ import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_TAGS;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_TEMPLATE_KEY;
 
 /**
- * Conversion of {@link org.sonar.db.rule.RuleDto} to {@link org.sonarqube.ws.Rules.Rule}
+ * Conversion of {@link RuleDto} to {@link Rules.Rule}
  */
 public class RuleMapper {
 
@@ -91,23 +92,23 @@ public class RuleMapper {
     return ruleResponse.build();
   }
 
-  public Rules.Rule toWsRule(RuleDefinitionDto ruleDefinitionDto, SearchResult result, Set<String> fieldsToReturn, RuleMetadataDto metadata, Map<String, UserDto> usersByUuid) {
+  public Rules.Rule toWsRule(RuleDefinitionDto ruleDefinition, SearchResult result, Set<String> fieldsToReturn, RuleMetadataDto metadata, Map<String, UserDto> usersByUuid) {
     Rules.Rule.Builder ruleResponse = Rules.Rule.newBuilder();
-    applyRuleDefinition(ruleResponse, ruleDefinitionDto, result, fieldsToReturn);
-    applyRuleMetadata(ruleResponse, metadata, usersByUuid, fieldsToReturn);
-    setDebtRemediationFunctionFields(ruleResponse, ruleDefinitionDto, metadata, fieldsToReturn);
+    applyRuleDefinition(ruleResponse, ruleDefinition, result, fieldsToReturn);
+    applyRuleMetadata(ruleResponse, ruleDefinition, metadata, usersByUuid, fieldsToReturn);
+    setDebtRemediationFunctionFields(ruleResponse, ruleDefinition, metadata, fieldsToReturn);
     return ruleResponse.build();
   }
 
-  public Rules.Rule.Builder applyRuleDefinition(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDefinitionDto, SearchResult result, Set<String> fieldsToReturn) {
+  private Rules.Rule.Builder applyRuleDefinition(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDefinitionDto, SearchResult result, Set<String> fieldsToReturn) {
 
     // Mandatory fields
     ruleResponse.setKey(ruleDefinitionDto.getKey().toString());
     ruleResponse.setType(Common.RuleType.forNumber(ruleDefinitionDto.getType()));
 
     // Optional fields
-    setRepository(ruleResponse, ruleDefinitionDto, fieldsToReturn);
     setName(ruleResponse, ruleDefinitionDto, fieldsToReturn);
+    setRepository(ruleResponse, ruleDefinitionDto, fieldsToReturn);
     setStatus(ruleResponse, ruleDefinitionDto, fieldsToReturn);
     setSysTags(ruleResponse, ruleDefinitionDto, fieldsToReturn);
     setParams(ruleResponse, ruleDefinitionDto, result, fieldsToReturn);
@@ -126,10 +127,45 @@ public class RuleMapper {
     return ruleResponse;
   }
 
-  private void applyRuleMetadata(Rules.Rule.Builder ruleResponse, RuleMetadataDto metadata, Map<String, UserDto> usersByUuid, Set<String> fieldsToReturn) {
+  private void applyRuleMetadata(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDefinition, RuleMetadataDto metadata, Map<String, UserDto> usersByUuid,
+    Set<String> fieldsToReturn) {
     setTags(ruleResponse, metadata, fieldsToReturn);
     setNotesFields(ruleResponse, metadata, usersByUuid, fieldsToReturn);
     setIsRemediationFunctionOverloaded(ruleResponse, metadata, fieldsToReturn);
+    if (ruleDefinition.isAdHoc()) {
+      setAdHocName(ruleResponse, metadata, fieldsToReturn);
+      setAdHocDescription(ruleResponse, metadata, fieldsToReturn);
+      setAdHocSeverity(ruleResponse, metadata, fieldsToReturn);
+      setAdHocType(ruleResponse, metadata);
+    }
+  }
+
+  private static void setAdHocName(Rules.Rule.Builder ruleResponse, RuleMetadataDto metadata, Set<String> fieldsToReturn) {
+    String adHocName = metadata.getAdHocName();
+    if (adHocName != null && shouldReturnField(fieldsToReturn, FIELD_NAME)) {
+      ruleResponse.setName(adHocName);
+    }
+  }
+
+  private void setAdHocDescription(Rules.Rule.Builder ruleResponse, RuleMetadataDto metadata, Set<String> fieldsToReturn) {
+    String adHocDescription = metadata.getAdHocDescription();
+    if (adHocDescription != null && shouldReturnField(fieldsToReturn, FIELD_HTML_DESCRIPTION)) {
+      ruleResponse.setHtmlDesc(macroInterpreter.interpret(adHocDescription));
+    }
+  }
+
+  private static void setAdHocSeverity(Rules.Rule.Builder ruleResponse, RuleMetadataDto metadata, Set<String> fieldsToReturn) {
+    String severity = metadata.getAdHocSeverity();
+    if (shouldReturnField(fieldsToReturn, FIELD_SEVERITY) && severity != null) {
+      ruleResponse.setSeverity(severity);
+    }
+  }
+
+  private static void setAdHocType(Rules.Rule.Builder ruleResponse, RuleMetadataDto metadata) {
+    Integer ruleType = metadata.getAdHocType();
+    if (ruleType != null) {
+      ruleResponse.setType(Common.RuleType.forNumber(ruleType));
+    }
   }
 
   private static void setRepository(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDto, Set<String> fieldsToReturn) {
@@ -158,10 +194,11 @@ public class RuleMapper {
   }
 
   private static void setEffortToFixDescription(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDto, Set<String> fieldsToReturn) {
+    String gapDescription = ruleDto.getGapDescription();
     if ((shouldReturnField(fieldsToReturn, FIELD_EFFORT_TO_FIX_DESCRIPTION) || shouldReturnField(fieldsToReturn, FIELD_GAP_DESCRIPTION))
-      && ruleDto.getGapDescription() != null) {
-      ruleResponse.setEffortToFixDescription(ruleDto.getGapDescription());
-      ruleResponse.setGapDescription(ruleDto.getGapDescription());
+      && gapDescription != null) {
+      ruleResponse.setEffortToFixDescription(gapDescription);
+      ruleResponse.setGapDescription(gapDescription);
     }
   }
 
@@ -250,9 +287,7 @@ public class RuleMapper {
   private static void setParams(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDto, SearchResult searchResult, Set<String> fieldsToReturn) {
     if (shouldReturnField(fieldsToReturn, FIELD_PARAMS)) {
       List<RuleParamDto> ruleParameters = searchResult.getRuleParamsByRuleId().get(ruleDto.getId());
-      ruleResponse.getParamsBuilder().addAllParams(FluentIterable.from(ruleParameters)
-        .transform(RuleParamDtoToWsRuleParam.INSTANCE)
-        .toList());
+      ruleResponse.getParamsBuilder().addAllParams(ruleParameters.stream().map(RuleParamDtoToWsRuleParam.INSTANCE::apply).collect(toList()));
     }
   }
 
@@ -263,10 +298,11 @@ public class RuleMapper {
   }
 
   private void setDescriptionFields(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDto, Set<String> fieldsToReturn) {
+    String description = ruleDto.getDescription();
     if (shouldReturnField(fieldsToReturn, FIELD_HTML_DESCRIPTION)) {
-      String description = ruleDto.getDescription();
-      if (description != null) {
-        switch (ruleDto.getDescriptionFormat()) {
+      RuleDto.Format descriptionFormat = ruleDto.getDescriptionFormat();
+      if (description != null && descriptionFormat != null) {
+        switch (descriptionFormat) {
           case MARKDOWN:
             ruleResponse.setHtmlDesc(macroInterpreter.interpret(Markdown.convertToHtml(description)));
             break;
@@ -274,13 +310,13 @@ public class RuleMapper {
             ruleResponse.setHtmlDesc(macroInterpreter.interpret(description));
             break;
           default:
-            throw new IllegalStateException(format("Rule description format '%s' is unknown for key '%s'", ruleDto.getDescriptionFormat(), ruleDto.getKey().toString()));
+            throw new IllegalStateException(format("Rule description format '%s' is unknown for key '%s'", descriptionFormat, ruleDto.getKey().toString()));
         }
       }
     }
 
-    if (shouldReturnField(fieldsToReturn, FIELD_MARKDOWN_DESCRIPTION) && ruleDto.getDescription() != null) {
-      ruleResponse.setMdDesc(ruleDto.getDescription());
+    if (shouldReturnField(fieldsToReturn, FIELD_MARKDOWN_DESCRIPTION) && description != null) {
+      ruleResponse.setMdDesc(description);
     }
   }
 
@@ -319,8 +355,8 @@ public class RuleMapper {
   }
 
   private void setLanguageName(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDto, Set<String> fieldsToReturn) {
-    if (shouldReturnField(fieldsToReturn, FIELD_LANGUAGE_NAME) && ruleDto.getLanguage() != null) {
-      String languageKey = ruleDto.getLanguage();
+    String languageKey = ruleDto.getLanguage();
+    if (shouldReturnField(fieldsToReturn, FIELD_LANGUAGE_NAME) && languageKey != null) {
       Language language = languages.get(languageKey);
       ruleResponse.setLangName(language == null ? languageKey : language.getName());
     }
@@ -331,7 +367,7 @@ public class RuleMapper {
       ruleResponse.setIsTemplate(ruleDto.isTemplate());
     }
   }
-  
+
   private static void setIsExternal(Rules.Rule.Builder ruleResponse, RuleDefinitionDto ruleDto, Set<String> fieldsToReturn) {
     if (shouldReturnField(fieldsToReturn, FIELD_IS_EXTERNAL)) {
       ruleResponse.setIsExternal(ruleDto.isExternal());
index 6b4b1bf5cebf4812801d3ab2ca0ca822057e9379..23a5f97e48e4b4221a930d0aab446b64c62e48a9 100644 (file)
@@ -628,6 +628,17 @@ public class SearchActionTest {
     assertThat(searchedRule.getTemplateKey()).isEqualTo(templateRule.getRepositoryKey() + ":" + templateRule.getRuleKey());
   }
 
+  @Test
+  public void do_not_return_external_rule() {
+    db.rules().insert(r -> r.setIsExternal(true));
+    indexRules();
+
+    SearchResponse result = ws.newRequest().executeProtobuf(SearchResponse.class);
+
+    assertThat(result.getTotal()).isZero();
+    assertThat(result.getRulesCount()).isZero();
+  }
+
   @Test
   public void search_all_active_rules() {
     OrganizationDto organization = db.organizations().insert();
index a3f3d7b87971e1f3d53ce15946d1daeb38424b75..bdbc2ce3e9be5b78f511a2416f02cf6d6be923b9 100644 (file)
@@ -25,6 +25,8 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.db.DbTester;
 import org.sonar.db.organization.OrganizationDto;
@@ -41,6 +43,7 @@ import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.text.MacroInterpreter;
 import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Rules;
 import org.sonarqube.ws.Rules.Rule;
 import org.sonarqube.ws.Rules.ShowResponse;
@@ -60,6 +63,8 @@ import static org.sonar.server.language.LanguageTesting.newLanguage;
 import static org.sonar.server.rule.ws.ShowAction.PARAM_ACTIVES;
 import static org.sonar.server.rule.ws.ShowAction.PARAM_KEY;
 import static org.sonar.server.rule.ws.ShowAction.PARAM_ORGANIZATION;
+import static org.sonarqube.ws.Common.RuleType.UNKNOWN;
+import static org.sonarqube.ws.Common.RuleType.VULNERABILITY;
 
 public class ShowActionTest {
 
@@ -230,6 +235,7 @@ public class ShowActionTest {
 
     Rule resultRule = result.getRule();
     assertThat(resultRule.getDefaultRemFnType()).isEqualTo("LINEAR_OFFSET");
+    assertThat(resultRule.getDefaultRemFnType()).isEqualTo("LINEAR_OFFSET");
     assertThat(resultRule.getDefaultRemFnGapMultiplier()).isEqualTo("5d");
     assertThat(resultRule.getDefaultRemFnBaseEffort()).isEqualTo("10h");
     assertThat(resultRule.getRemFnType()).isEqualTo("CONSTANT_ISSUE");
@@ -313,6 +319,99 @@ public class ShowActionTest {
     verify(macroInterpreter).interpret("&lt;div&gt;line1<br/>line2&lt;/div&gt;");
   }
 
+  @Test
+  public void show_external_rule() {
+    RuleDefinitionDto externalRule = db.rules().insert(r -> r
+      .setIsExternal(true)
+      .setName("ext rule name"));
+
+    ShowResponse result = ws.newRequest()
+      .setParam(PARAM_KEY, externalRule.getKey().toString())
+      .executeProtobuf(ShowResponse.class);
+
+    Rule resultRule = result.getRule();
+    assertThat(resultRule.getName()).isEqualTo("ext rule name");
+  }
+
+  @Test
+  public void show_adhoc_rule() {
+    OrganizationDto organization = db.organizations().insert();
+    RuleDefinitionDto externalRule = db.rules().insert(r -> r
+      .setIsExternal(true)
+      .setIsAdHoc(true));
+    RuleMetadataDto metadata = db.rules().insertOrUpdateMetadata(externalRule, organization, m -> m
+      .setAdHocName("adhoc name")
+      .setAdHocDescription("<div>desc</div>")
+      .setAdHocSeverity(Severity.BLOCKER)
+      .setAdHocType(RuleType.VULNERABILITY)
+      .setNoteData(null)
+      .setNoteUserUuid(null));
+    doReturn("&lt;div&gt;desc2&lt;/div&gt;").when(macroInterpreter).interpret(metadata.getAdHocDescription());
+
+    ShowResponse result = ws.newRequest()
+      .setParam(PARAM_KEY, externalRule.getKey().toString())
+      .setParam(PARAM_ORGANIZATION, organization.getKey())
+      .executeProtobuf(ShowResponse.class);
+
+    Rule resultRule = result.getRule();
+    assertThat(resultRule)
+      .extracting(Rule::getName, Rule::getHtmlDesc, Rule::getSeverity, Rule::getType)
+      .containsExactlyInAnyOrder("adhoc name", "&lt;div&gt;desc2&lt;/div&gt;", Severity.BLOCKER, VULNERABILITY);
+  }
+
+  @Test
+  public void ignore_predefined_info_on_adhoc_rule() {
+    OrganizationDto organization = db.organizations().insert();
+    RuleDefinitionDto externalRule = db.rules().insert(r -> r
+      .setIsExternal(true)
+      .setIsAdHoc(true)
+      .setName("predefined name")
+      .setDescription("<div>predefined desc</div>")
+      .setSeverity(Severity.BLOCKER)
+      .setType(RuleType.VULNERABILITY));
+    RuleMetadataDto metadata = db.rules().insertOrUpdateMetadata(externalRule, organization, m -> m
+      .setAdHocName("adhoc name")
+      .setAdHocDescription("<div>adhoc desc</div>")
+      .setAdHocSeverity(Severity.MAJOR)
+      .setAdHocType(RuleType.CODE_SMELL)
+      .setNoteData(null)
+      .setNoteUserUuid(null));
+    doReturn("&lt;div&gt;adhoc desc&lt;/div&gt;").when(macroInterpreter).interpret(metadata.getAdHocDescription());
+
+    ShowResponse result = ws.newRequest()
+      .setParam(PARAM_KEY, externalRule.getKey().toString())
+      .setParam(PARAM_ORGANIZATION, organization.getKey())
+      .executeProtobuf(ShowResponse.class);
+
+    Rule resultRule = result.getRule();
+    assertThat(resultRule)
+      .extracting(Rule::getName, Rule::getHtmlDesc, Rule::getSeverity, Rule::getType)
+      .containsExactlyInAnyOrder("adhoc name", "&lt;div&gt;adhoc desc&lt;/div&gt;", Severity.MAJOR, Common.RuleType.CODE_SMELL);
+  }
+
+  @Test
+  public void adhoc_info_are_empty_when_no_metadata() {
+    OrganizationDto organization = db.organizations().insert();
+    RuleDefinitionDto externalRule = db.rules().insert(r -> r
+      .setIsExternal(true)
+      .setIsAdHoc(true)
+      .setName(null)
+      .setDescription(null)
+      .setDescriptionFormat(null)
+      .setSeverity((String) null)
+      .setType(0));
+
+    ShowResponse result = ws.newRequest()
+      .setParam(PARAM_KEY, externalRule.getKey().toString())
+      .setParam(PARAM_ORGANIZATION, organization.getKey())
+      .executeProtobuf(ShowResponse.class);
+
+    Rule resultRule = result.getRule();
+    assertThat(resultRule)
+      .extracting(Rule::hasName, Rule::hasHtmlDesc, Rule::hasSeverity, Rule::getType)
+      .containsExactlyInAnyOrder(false, false, false, UNKNOWN);
+  }
+
   @Test
   public void show_rule_with_activation() {
     OrganizationDto organization = db.organizations().insert();