]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16364 Update Rule API to support multiple description sections
authorZipeng WU <zipeng.wu@sonarsource.com>
Thu, 5 May 2022 12:52:04 +0000 (14:52 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 9 May 2022 20:02:59 +0000 (20:02 +0000)
13 files changed:
build.gradle
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleForIndexingDto.java
server/sonar-server-common/src/main/java/org/sonar/server/rule/RuleDescriptionFormatter.java
server/sonar-server-common/src/test/java/org/sonar/server/rule/RuleDescriptionFormatterTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleMapper.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/search-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/show-example.json
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/ShowActionTest.java
sonar-ws/src/main/protobuf/ws-rules.proto

index de436745c6764cdaef1fef15abdc64b7a8c0c554..59bd075924c7740a5bd2417e9dc82af3ca24d434 100644 (file)
@@ -172,7 +172,7 @@ subprojects {
       dependency 'org.sonarsource.kotlin:sonar-kotlin-plugin:2.9.0.1147'
       dependency 'org.sonarsource.slang:sonar-ruby-plugin:1.9.0.3429'
       dependency 'org.sonarsource.slang:sonar-scala-plugin:1.9.0.3429'
-      dependency 'org.sonarsource.api.plugin:sonar-plugin-api:9.5.0.71'
+      dependency 'org.sonarsource.api.plugin:sonar-plugin-api:9.6.1.114'
       dependency 'org.sonarsource.xml:sonar-xml-plugin:2.5.0.3376'
       dependency 'org.sonarsource.iac:sonar-iac-plugin:1.7.0.2012'
       dependency 'org.sonarsource.text:sonar-text-plugin:1.0.0.120'
index e7d7aa4e43b9d13fd585e3b5ce307e7eed9bf744..50b92cd20c1a5357b835632b81e1d8798579c2bf 100644 (file)
@@ -47,10 +47,11 @@ public class RuleForIndexingDto {
   private String internalKey;
   private String language;
   private boolean isExternal;
+
   private int type;
+
   private long createdAt;
   private long updatedAt;
-
   private Set<RuleDescriptionSectionDto> ruleDescriptionSectionsDtos = new HashSet<>();
 
   @VisibleForTesting
@@ -200,4 +201,8 @@ public class RuleForIndexingDto {
   public void setTemplateRepository(String templateRepository) {
     this.templateRepository = templateRepository;
   }
+
+  public void setType(int type) {
+    this.type = type;
+  }
 }
index 2de9e1fca838e695c3b222220e0838172461b1ab..13db9a72a63b4f27c0bf2944c866c253e4b6ba34 100644 (file)
 package org.sonar.server.rule;
 
 import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import javax.annotation.CheckForNull;
+import org.sonar.api.rules.RuleType;
 import org.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.markdown.Markdown;
 
 import static com.google.common.collect.MoreCollectors.toOptional;
-import static java.lang.String.format;
+import static java.util.stream.Collectors.toMap;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.INTRODUCTION_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.RESOURCES_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
+import static org.sonar.db.rule.RuleDto.Format.MARKDOWN;
 
 public class RuleDescriptionFormatter {
 
+  public static final List<String> SECTION_KEYS = List.of(
+    INTRODUCTION_SECTION_KEY,
+    ROOT_CAUSE_SECTION_KEY,
+    ASSESS_THE_PROBLEM_SECTION_KEY,
+    HOW_TO_FIX_SECTION_KEY,
+    RESOURCES_SECTION_KEY);
+
+  public static final Map<String, String> HOTSPOT_SECTION_TITLES = Map.of(
+    ROOT_CAUSE_SECTION_KEY, "What's the risk ?",
+    ASSESS_THE_PROBLEM_SECTION_KEY, "Assess the risk",
+    HOW_TO_FIX_SECTION_KEY, "How can you fix it ?"
+  );
+
+  public static final Map<String, String> RULE_SECTION_TITLES = Map.of(
+    ROOT_CAUSE_SECTION_KEY, "Why is this an issue ?",
+    HOW_TO_FIX_SECTION_KEY, "How to fix it ?",
+    RESOURCES_SECTION_KEY, "Resources"
+  );
+
   private RuleDescriptionFormatter() { /* static helpers */ }
 
+  @CheckForNull
   public static String getDescriptionAsHtml(RuleDto ruleDto) {
     if (ruleDto.getDescriptionFormat() == null) {
       return null;
     }
     Collection<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = ruleDto.getRuleDescriptionSectionDtos();
-    return retrieveDescription(ruleDescriptionSectionDtos, ruleDto.getRuleKey(), Objects.requireNonNull(ruleDto.getDescriptionFormat()));
+    return retrieveDescription(ruleDescriptionSectionDtos, RuleType.valueOf(ruleDto.getType()), Objects.requireNonNull(ruleDto.getDescriptionFormat()));
   }
 
+  @CheckForNull
   private static String retrieveDescription(Collection<RuleDescriptionSectionDto> ruleDescriptionSectionDtos,
-    String ruleKey, RuleDto.Format descriptionFormat) {
-    Optional<RuleDescriptionSectionDto> ruleDescriptionSectionDto = findDefaultDescription(ruleDescriptionSectionDtos);
-    return ruleDescriptionSectionDto
-      .map(ruleDescriptionSection -> toHtml(ruleKey, descriptionFormat, ruleDescriptionSection))
-      .orElse(null);
+    RuleType ruleType, RuleDto.Format descriptionFormat) {
+    if (ruleDescriptionSectionDtos.isEmpty()) {
+      return null;
+    }
+    Map<String, String> sectionKeyToHtml = ruleDescriptionSectionDtos.stream()
+      .collect(toMap(RuleDescriptionSectionDto::getKey, section -> toHtml(descriptionFormat, section)));
+    if (sectionKeyToHtml.containsKey(DEFAULT_KEY)) {
+      return sectionKeyToHtml.get(DEFAULT_KEY);
+    } else {
+      return concatHtmlSections(sectionKeyToHtml, ruleType);
+    }
   }
 
-  private static Optional<RuleDescriptionSectionDto> findDefaultDescription(Collection<RuleDescriptionSectionDto> ruleDescriptionSectionDtos) {
-    return ruleDescriptionSectionDtos.stream()
-      .filter(RuleDescriptionSectionDto::isDefault)
-      .collect(toOptional());
+  private static String concatHtmlSections(Map<String, String> sectionKeyToHtml, RuleType ruleType) {
+    Map<String, String> titleMapping = ruleType.equals(RuleType.SECURITY_HOTSPOT) ? HOTSPOT_SECTION_TITLES : RULE_SECTION_TITLES;
+    var builder = new StringBuilder();
+    for (String sectionKey : SECTION_KEYS) {
+      if (sectionKeyToHtml.containsKey(sectionKey)) {
+        builder.append("<h2>")
+          .append(titleMapping.get(sectionKey))
+          .append("</h2>")
+          .append(sectionKeyToHtml.get(sectionKey))
+          .append("<br/>");
+      }
+    }
+    return builder.toString();
   }
 
-  private static String toHtml(String ruleKey, RuleDto.Format descriptionFormat, RuleDescriptionSectionDto ruleDescriptionSectionDto) {
-    switch (descriptionFormat) {
-      case MARKDOWN:
-        return Markdown.convertToHtml(ruleDescriptionSectionDto.getContent());
-      case HTML:
-        return ruleDescriptionSectionDto.getContent();
-      default:
-        throw new IllegalStateException(format("Rule description section format '%s' is unknown for rule key '%s'", descriptionFormat, ruleKey));
+  private static String toHtml(RuleDto.Format descriptionFormat, RuleDescriptionSectionDto ruleDescriptionSectionDto) {
+    if (MARKDOWN.equals(descriptionFormat)) {
+      return Markdown.convertToHtml(ruleDescriptionSectionDto.getContent());
+    } else {
+      return ruleDescriptionSectionDto.getContent();
     }
   }
 
index 7c0879424c0be2a0ec0a58ca3afd92544db3a50d..260bb5ed4f69ecdfa2816f23a1fd946304e4ff47 100644 (file)
 package org.sonar.server.rule;
 
 import org.junit.Test;
+import org.sonar.api.rules.RuleType;
 import org.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.RuleDto;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
 
 public class RuleDescriptionFormatterTest {
@@ -33,21 +37,43 @@ public class RuleDescriptionFormatterTest {
 
   @Test
   public void getMarkdownDescriptionAsHtml() {
-    RuleDto rule = new RuleDto().setDescriptionFormat(RuleDto.Format.MARKDOWN).addRuleDescriptionSectionDto(MARKDOWN_SECTION);
+    RuleDto rule = new RuleDto().setDescriptionFormat(RuleDto.Format.MARKDOWN).addRuleDescriptionSectionDto(MARKDOWN_SECTION).setType(RuleType.BUG);
     String html = RuleDescriptionFormatter.getDescriptionAsHtml(rule);
     assertThat(html).isEqualTo("<strong>md</strong> <code>description</code>");
   }
 
   @Test
   public void getHtmlDescriptionAsIs() {
-    RuleDto rule = new RuleDto().setDescriptionFormat(RuleDto.Format.HTML).addRuleDescriptionSectionDto(HTML_SECTION);
+    RuleDto rule = new RuleDto().setDescriptionFormat(RuleDto.Format.HTML).addRuleDescriptionSectionDto(HTML_SECTION).setType(RuleType.BUG);
     String html = RuleDescriptionFormatter.getDescriptionAsHtml(rule);
     assertThat(html).isEqualTo(HTML_SECTION.getContent());
   }
 
+  @Test
+  public void concatHtmlDescriptionSections() {
+    var section1 = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, "<div>Root is Root</div>");
+    var section2 = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "<div>This is not a problem</div>");
+    var section3 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "<div>I don't want to fix</div>");
+    RuleDto rule = new RuleDto().setDescriptionFormat(RuleDto.Format.HTML)
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .addRuleDescriptionSectionDto(section1)
+      .addRuleDescriptionSectionDto(section2)
+      .addRuleDescriptionSectionDto(section3);
+    String html = RuleDescriptionFormatter.getDescriptionAsHtml(rule);
+    assertThat(html)
+      .contains(
+        "<h2>What's the risk ?</h2>"
+          + "<div>Root is Root</div><br/>"
+          + "<h2>Assess the risk</h2>"
+          + "<div>This is not a problem</div><br/>"
+          + "<h2>How can you fix it ?</h2>"
+          + "<div>I don't want to fix</div><br/>"
+      );
+  }
+
   @Test
   public void handleEmptyDescription() {
-    RuleDto rule = new RuleDto().setDescriptionFormat(RuleDto.Format.HTML);
+    RuleDto rule = new RuleDto().setDescriptionFormat(RuleDto.Format.HTML).setType(RuleType.BUG);
     String result = RuleDescriptionFormatter.getDescriptionAsHtml(rule);
     assertThat(result).isNull();
   }
@@ -55,9 +81,12 @@ public class RuleDescriptionFormatterTest {
   @Test
   public void handleNullDescriptionFormat() {
     RuleDescriptionSectionDto sectionWithNullFormat = createDefaultRuleDescriptionSection("uuid", "whatever");
-    RuleDto rule = new RuleDto().addRuleDescriptionSectionDto(sectionWithNullFormat);
+    RuleDto rule = new RuleDto().addRuleDescriptionSectionDto(sectionWithNullFormat).setType(RuleType.BUG);
     String result = RuleDescriptionFormatter.getDescriptionAsHtml(rule);
     assertThat(result).isNull();
   }
 
+  private static RuleDescriptionSectionDto createRuleDescriptionSection(String key, String content) {
+    return RuleDescriptionSectionDto.builder().key(key).content(content).build();
+  }
 }
index 759bd3cc8392b759b2fdca874ce44da3b370e80c..d9ea20f6388993b3f12038ef770f19bb8028e2d9 100644 (file)
@@ -163,17 +163,17 @@ public class ShowAction implements HotspotsWsAction {
     responseBuilder.setCanChangeStatus(hotspotWsSupport.canChangeStatus(components.getProject()));
   }
 
-  private static void formatRule(ShowWsResponse.Builder responseBuilder, RuleDto ruleDefinitionDto) {
-    SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(ruleDefinitionDto.getSecurityStandards());
+  private static void formatRule(ShowWsResponse.Builder responseBuilder, RuleDto ruleDto) {
+    SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(ruleDto.getSecurityStandards());
     SecurityStandards.SQCategory sqCategory = securityStandards.getSqCategory();
 
     Hotspots.Rule.Builder ruleBuilder = Hotspots.Rule.newBuilder()
-      .setKey(ruleDefinitionDto.getKey().toString())
-      .setName(nullToEmpty(ruleDefinitionDto.getName()))
+      .setKey(ruleDto.getKey().toString())
+      .setName(nullToEmpty(ruleDto.getName()))
       .setSecurityCategory(sqCategory.getKey())
       .setVulnerabilityProbability(sqCategory.getVulnerability().name());
 
-    HotspotRuleDescription hotspotRuleDescription = HotspotRuleDescription.from(ruleDefinitionDto);
+    HotspotRuleDescription hotspotRuleDescription = HotspotRuleDescription.from(ruleDto);
     hotspotRuleDescription.getVulnerable().ifPresent(ruleBuilder::setVulnerabilityDescription);
     hotspotRuleDescription.getRisk().ifPresent(ruleBuilder::setRiskDescription);
     hotspotRuleDescription.getFixIt().ifPresent(ruleBuilder::setFixRecommendations);
index 7346ffdb6a84fe0a810f542c9f00a559671658bf..21202d076db626ec4a80e7a986301d8bcefdad24 100644 (file)
@@ -23,10 +23,12 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import org.sonar.api.resources.Language;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.rule.RuleKey;
@@ -50,12 +52,14 @@ import org.sonarqube.ws.Rules;
 import static java.util.stream.Collectors.joining;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
 import static org.sonar.core.util.stream.MoreCollectors.toList;
+import static org.sonar.db.rule.RuleDto.Format.MARKDOWN;
 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;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEFAULT_DEBT_REM_FUNCTION;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEFAULT_REM_FUNCTION;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEPRECATED_KEYS;
+import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DESCRIPTION_SECTIONS;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_EFFORT_TO_FIX_DESCRIPTION;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_GAP_DESCRIPTION;
 import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_HTML_DESCRIPTION;
@@ -106,32 +110,32 @@ public class RuleMapper {
     return ruleResponse.build();
   }
 
-  private Rules.Rule.Builder applyRuleDefinition(Rules.Rule.Builder ruleResponse, RuleDto ruleDefinitionDto, SearchResult result,
+  private Rules.Rule.Builder applyRuleDefinition(Rules.Rule.Builder ruleResponse, RuleDto ruleDto, SearchResult result,
     Set<String> fieldsToReturn, Map<String, List<DeprecatedRuleKeyDto>> deprecatedRuleKeysByRuleUuid) {
 
     // Mandatory fields
-    ruleResponse.setKey(ruleDefinitionDto.getKey().toString());
-    ruleResponse.setType(Common.RuleType.forNumber(ruleDefinitionDto.getType()));
+    ruleResponse.setKey(ruleDto.getKey().toString());
+    ruleResponse.setType(Common.RuleType.forNumber(ruleDto.getType()));
 
     // Optional fields
-    setName(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setRepository(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setStatus(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setSysTags(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setParams(ruleResponse, ruleDefinitionDto, result, fieldsToReturn);
-    setCreatedAt(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setDescriptionFields(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setSeverity(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setInternalKey(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setLanguage(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setLanguageName(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setIsTemplate(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setIsExternal(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setTemplateKey(ruleResponse, ruleDefinitionDto, result, fieldsToReturn);
-    setDefaultDebtRemediationFunctionFields(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setEffortToFixDescription(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setScope(ruleResponse, ruleDefinitionDto, fieldsToReturn);
-    setDeprecatedKeys(ruleResponse, ruleDefinitionDto, fieldsToReturn, deprecatedRuleKeysByRuleUuid);
+    setName(ruleResponse, ruleDto, fieldsToReturn);
+    setRepository(ruleResponse, ruleDto, fieldsToReturn);
+    setStatus(ruleResponse, ruleDto, fieldsToReturn);
+    setSysTags(ruleResponse, ruleDto, fieldsToReturn);
+    setParams(ruleResponse, ruleDto, result, fieldsToReturn);
+    setCreatedAt(ruleResponse, ruleDto, fieldsToReturn);
+    setDescriptionFields(ruleResponse, ruleDto, fieldsToReturn);
+    setSeverity(ruleResponse, ruleDto, fieldsToReturn);
+    setInternalKey(ruleResponse, ruleDto, fieldsToReturn);
+    setLanguage(ruleResponse, ruleDto, fieldsToReturn);
+    setLanguageName(ruleResponse, ruleDto, fieldsToReturn);
+    setIsTemplate(ruleResponse, ruleDto, fieldsToReturn);
+    setIsExternal(ruleResponse, ruleDto, fieldsToReturn);
+    setTemplateKey(ruleResponse, ruleDto, result, fieldsToReturn);
+    setDefaultDebtRemediationFunctionFields(ruleResponse, ruleDto, fieldsToReturn);
+    setEffortToFixDescription(ruleResponse, ruleDto, fieldsToReturn);
+    setScope(ruleResponse, ruleDto, fieldsToReturn);
+    setDeprecatedKeys(ruleResponse, ruleDto, fieldsToReturn, deprecatedRuleKeysByRuleUuid);
     return ruleResponse;
   }
 
@@ -330,17 +334,30 @@ public class RuleMapper {
       }
     }
 
-    if (shouldReturnField(fieldsToReturn, FIELD_MARKDOWN_DESCRIPTION)
-      && !ruleDto.getRuleDescriptionSectionDtos().isEmpty()) {
-      String description = concatenateSectionTemporaryForSonar16302(ruleDto);
-      ruleResponse.setMdDesc(description);
+    if (shouldReturnField(fieldsToReturn, FIELD_DESCRIPTION_SECTIONS)) {
+      for (var section : ruleDto.getRuleDescriptionSectionDtos()) {
+        ruleResponse.addDescriptionSectionsBuilder()
+          .setKey(section.getKey())
+          .setContent(retrieveDescriptionContent(ruleDto.getDescriptionFormat(), section))
+          .build();
+      }
+    }
+
+    if (shouldReturnField(fieldsToReturn, FIELD_MARKDOWN_DESCRIPTION)) {
+      if (MARKDOWN.equals(ruleDto.getDescriptionFormat())) {
+        Optional.ofNullable(ruleDto.getDefaultRuleDescriptionSection())
+          .map(RuleDescriptionSectionDto::getContent)
+          .ifPresent(ruleResponse::setMdDesc);
+      } else {
+        ruleResponse.setMdDesc(ruleResponse.getHtmlDesc());
+      }
     }
   }
 
-  private static String concatenateSectionTemporaryForSonar16302(RuleDto ruleDto) {
-    return ruleDto.getRuleDescriptionSectionDtos().stream()
-      .map(RuleDescriptionSectionDto::getContent)
-      .collect(joining());
+  private static String retrieveDescriptionContent(@Nullable RuleDto.Format format, RuleDescriptionSectionDto sectionDto) {
+    return MARKDOWN.equals(format) ?
+      Markdown.convertToHtml(sectionDto.getContent()) :
+      sectionDto.getContent();
   }
 
   private void setNotesFields(Rules.Rule.Builder ruleResponse, RuleMetadataDto ruleDto, Map<String, UserDto> usersByUuid, Set<String> fieldsToReturn) {
index 06590ea3460b79213196f2a28149068a2f5c4a02..15260041edfd6bcfd630fc923c1a6aea95920cd9 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.rule.ws;
 
-import com.google.common.collect.ImmutableSet;
 import java.util.Set;
 
 public class RulesWsParameters {
@@ -61,6 +60,8 @@ public class RulesWsParameters {
   public static final String FIELD_LANGUAGE_NAME = "langName";
   public static final String FIELD_HTML_DESCRIPTION = "htmlDesc";
   public static final String FIELD_MARKDOWN_DESCRIPTION = "mdDesc";
+
+  public static final String FIELD_DESCRIPTION_SECTIONS = "descriptionSections";
   public static final String FIELD_NOTE_LOGIN = "noteLogin";
   public static final String FIELD_MARKDOWN_NOTE = "mdNote";
   public static final String FIELD_HTML_NOTE = "htmlNote";
@@ -103,9 +104,9 @@ public class RulesWsParameters {
 
   public static final String FIELD_DEPRECATED_KEYS = "deprecatedKeys";
 
-  public static final Set<String> OPTIONAL_FIELDS = ImmutableSet.of(FIELD_REPO, FIELD_NAME, FIELD_CREATED_AT, FIELD_UPDATED_AT, FIELD_SEVERITY, FIELD_STATUS, FIELD_INTERNAL_KEY,
+  public static final Set<String> OPTIONAL_FIELDS = Set.of(FIELD_REPO, FIELD_NAME, FIELD_CREATED_AT, FIELD_UPDATED_AT, FIELD_SEVERITY, FIELD_STATUS, FIELD_INTERNAL_KEY,
     FIELD_IS_EXTERNAL, FIELD_IS_TEMPLATE, FIELD_TEMPLATE_KEY, FIELD_TAGS, FIELD_SYSTEM_TAGS, FIELD_LANGUAGE, FIELD_LANGUAGE_NAME, FIELD_HTML_DESCRIPTION,
-    FIELD_MARKDOWN_DESCRIPTION, FIELD_NOTE_LOGIN, FIELD_MARKDOWN_NOTE, FIELD_HTML_NOTE,
+    FIELD_MARKDOWN_DESCRIPTION, FIELD_DESCRIPTION_SECTIONS, FIELD_NOTE_LOGIN, FIELD_MARKDOWN_NOTE, FIELD_HTML_NOTE,
     FIELD_DEFAULT_DEBT_REM_FUNCTION, FIELD_EFFORT_TO_FIX_DESCRIPTION, FIELD_DEBT_OVERLOADED, FIELD_DEBT_REM_FUNCTION,
     FIELD_DEFAULT_REM_FUNCTION, FIELD_GAP_DESCRIPTION, FIELD_REM_FUNCTION_OVERLOADED, FIELD_REM_FUNCTION,
     FIELD_PARAMS, FIELD_ACTIVES, FIELD_SCOPE, FIELD_DEPRECATED_KEYS);
index 151c4147f2630a13f51352ce3ef9e901c2b0e1a1..8fa9a6bd4900625a05ba59b7983ba6a7ec7fa673 100644 (file)
@@ -170,6 +170,11 @@ public class SearchAction implements RulesWsAction {
         "<li>\"debtRemFnOffset\" becomes \"remFnBaseEffort\"</li>" +
         "<li>\"defaultDebtRemFnOffset\" becomes \"defaultRemFnBaseEffort\"</li>" +
         "<li>\"debtOverloaded\" becomes \"remFnOverloaded\"</li>" +
+        "</ul><br/>" +
+        "Since 9.5 :" +
+        "<ul>" +
+        "<li>the field \"htmlDesc\" has been deprecated.</li>" +
+        "<li>the field \"descriptionSections\" has been added.</li>" +
         "</ul>")
       .setResponseExample(getClass().getResource("search-example.json"))
       .setSince("4.4")
index ab6db925ab9e34ee387358c9679947aca6363af8..a41e6a2b4f2752f1cf9eb8f38b8c330f1f963d27 100644 (file)
@@ -73,7 +73,12 @@ public class ShowAction implements RulesWsAction {
         "<li>\"defaultDebtRemFnOffset\" becomes \"defaultRemFnBaseEffort\"</li>" +
         "<li>\"debtOverloaded\" becomes \"remFnOverloaded\"</li>" +
         "</ul>" +
-        "In 7.1, the field 'scope' has been added.")
+        "In 7.1, the field 'scope' has been added.<br/>" +
+        "Since 9.5 :" +
+        "<ul>" +
+        "<li>the field \"htmlDesc\" has been deprecated.</li>" +
+        "<li>the field \"descriptionSections\" has been added.</li>" +
+        "</ul>")
       .setSince("4.2")
       .setResponseExample(Resources.getResource(getClass(), "show-example.json"))
       .setHandler(this);
index b6b8279303dd975f9d47d71c8e8755ffb52e57e9..237720815748e0f477b1a2ab92ad7d01a816a9ed 100644 (file)
       "scope": "MAIN",
       "isExternal": false,
       "type": "CODE_SMELL",
+      "descriptionSections": [
+        {
+          "key": "Why is this an issue ?",
+          "content": "<h3 class=\"page-title coding-rules-detail-header\"><big>Unnecessary imports should be removed</big></h3>"
+        },
+        {
+          "key": "How to fix it ?",
+          "content": "<h2>Recommended Secure Coding Practices</h2><ul><li> activate Spring Security's CSRF protection. </li></ul>"
+        }
+      ],
       "params": [
         {
           "key": "max",
index 8664d1509fa17672a3f14e612083f563040c1b13..6e96d7bb8dff9ea22bf23dc1aee4ccf078c1cbda 100644 (file)
     "scope": "MAIN",
     "isExternal": false,
     "type": "CODE_SMELL",
+    "descriptionSections": [
+        {
+            "key": "Why is this an issue ?",
+            "content": "<h3 class=\"page-title coding-rules-detail-header\"><big>Unnecessary imports should be removed</big></h3>"
+        },
+        {
+            "key": "How to fix it ?",
+            "content": "<h2>Recommended Secure Coding Practices</h2><ul><li> activate Spring Security's CSRF protection. </li></ul>"
+        }
+    ],
     "params": [
         {
             "key": "max",
index 3e06e45ce8647d305cc37d43cbf2c4bf8ad93ace..00d6bb05a421da89ea1a83a3709144b1428ccab0 100644 (file)
@@ -33,6 +33,7 @@ import org.sonar.db.DbTester;
 import org.sonar.db.qualityprofile.ActiveRuleDto;
 import org.sonar.db.qualityprofile.ActiveRuleParamDto;
 import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.db.rule.RuleMetadataDto;
 import org.sonar.db.rule.RuleParamDto;
@@ -51,9 +52,15 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
 import static org.sonar.db.rule.RuleDto.Format.MARKDOWN;
 import static org.sonar.db.rule.RuleTesting.newCustomRule;
+import static org.sonar.db.rule.RuleTesting.newRuleWithoutDescriptionSection;
 import static org.sonar.db.rule.RuleTesting.newTemplateRule;
 import static org.sonar.db.rule.RuleTesting.setTags;
 import static org.sonar.server.language.LanguageTesting.newLanguage;
@@ -339,6 +346,74 @@ public class ShowActionTest {
       .containsExactlyInAnyOrder("adhoc name", "&lt;div&gt;desc2&lt;/div&gt;", Severity.BLOCKER, VULNERABILITY);
   }
 
+  @Test
+  public void show_rule_desc_sections() {
+    when(macroInterpreter.interpret(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
+
+    var section1 = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, "<div>Root is Root</div>");
+    var section2 = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "<div>This is not a problem</div>");
+    var section3 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "<div>I don't want to fix</div>");
+
+    RuleDto rule = createRuleWithDescriptionSections(section1, section2, section3);
+    rule.setType(RuleType.SECURITY_HOTSPOT);
+    db.rules().insert(rule);
+
+    ShowResponse result = ws.newRequest()
+      .setParam(PARAM_KEY, rule.getKey().toString())
+      .executeProtobuf(ShowResponse.class);
+
+    Rule resultRule = result.getRule();
+    assertThat(resultRule.getHtmlDesc())
+      .contains(
+        "<h2>What's the risk ?</h2>"
+          + "<div>Root is Root</div><br/>"
+          + "<h2>Assess the risk</h2>"
+          + "<div>This is not a problem</div><br/>"
+          + "<h2>How can you fix it ?</h2>"
+          + "<div>I don't want to fix</div><br/>"
+      );
+
+    assertThat(resultRule.getMdDesc())
+      .contains(
+        "<h2>What's the risk ?</h2>"
+          + "<div>Root is Root</div><br/>"
+          + "<h2>Assess the risk</h2>"
+          + "<div>This is not a problem</div><br/>"
+          + "<h2>How can you fix it ?</h2>"
+          + "<div>I don't want to fix</div><br/>");
+
+    assertThat(resultRule.getDescriptionSectionsList())
+      .extracting(Rule.DescriptionSection::getKey, Rule.DescriptionSection::getContent)
+      .containsExactlyInAnyOrder(
+        tuple(ROOT_CAUSE_SECTION_KEY, "<div>Root is Root</div>"),
+        tuple(ASSESS_THE_PROBLEM_SECTION_KEY, "<div>This is not a problem</div>"),
+        tuple(HOW_TO_FIX_SECTION_KEY, "<div>I don't want to fix</div>"));
+  }
+
+  @Test
+  public void show_rule_markdown_description() {
+    when(macroInterpreter.interpret(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
+
+    var section = createRuleDescriptionSection("default", "*toto is toto*");
+
+    RuleDto rule = createRuleWithDescriptionSections(section);
+    rule.setDescriptionFormat(MARKDOWN);
+    db.rules().insert(rule);
+
+    ShowResponse result = ws.newRequest()
+      .setParam(PARAM_KEY, rule.getKey().toString())
+      .executeProtobuf(ShowResponse.class);
+
+    Rule resultRule = result.getRule();
+
+    assertThat(resultRule.getHtmlDesc()).contains("<strong>toto is toto</strong>");
+    assertThat(resultRule.getMdDesc()).contains("*toto is toto*");
+
+    assertThat(resultRule.getDescriptionSectionsList())
+      .extracting(Rule.DescriptionSection::getKey, Rule.DescriptionSection::getContent)
+      .contains(tuple(DEFAULT_KEY, "<strong>toto is toto</strong>"));
+  }
+
   @Test
   public void ignore_predefined_info_on_adhoc_rule() {
     RuleDto externalRule = db.rules().insert(r -> r
@@ -444,4 +519,15 @@ public class ShowActionTest {
         tuple("actives", false));
   }
 
+  private RuleDescriptionSectionDto createRuleDescriptionSection(String key, String content) {
+    return RuleDescriptionSectionDto.builder().uuid(uuidFactory.create()).key(key).content(content).build();
+  }
+
+  private RuleDto createRuleWithDescriptionSections(RuleDescriptionSectionDto... sections) {
+    var rule = newRuleWithoutDescriptionSection();
+    for (var section : sections) {
+      rule.addRuleDescriptionSectionDto(section);
+    }
+    return rule;
+  }
 }
index 719e7b7e3266538e24d7995fe393b5ce3bed02c3..76ee26ae340c246d96b2ca1f7fe84452445dd78c 100644 (file)
@@ -71,7 +71,7 @@ message Rule {
   optional string repo = 2;
   optional string name = 3;
   optional string createdAt = 4;
-  optional string htmlDesc = 5;
+  optional string htmlDesc = 5 [deprecated=true];
   optional string htmlNote = 6;
   optional string mdDesc = 7;
   optional string mdNote = 8;
@@ -122,6 +122,12 @@ message Rule {
   optional sonarqube.ws.commons.RuleScope scope = 46;
   optional bool isExternal = 47;
   optional DeprecatedKeys deprecatedKeys = 48;
+  repeated DescriptionSection descriptionSections = 49;
+
+  message DescriptionSection {
+    required string key = 1;
+    required string content = 2;
+  }
 
   message Params {
     repeated Param params = 1;