From: Zipeng WU Date: Thu, 5 May 2022 12:52:04 +0000 (+0200) Subject: SONAR-16364 Update Rule API to support multiple description sections X-Git-Tag: 9.5.0.56709~158 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=41af7cd13eed44de4c3941669bdb5968347f454a;p=sonarqube.git SONAR-16364 Update Rule API to support multiple description sections --- diff --git a/build.gradle b/build.gradle index de436745c67..59bd075924c 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleForIndexingDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleForIndexingDto.java index e7d7aa4e43b..50b92cd20c1 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleForIndexingDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleForIndexingDto.java @@ -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 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; + } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/RuleDescriptionFormatter.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/RuleDescriptionFormatter.java index 2de9e1fca83..13db9a72a63 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/rule/RuleDescriptionFormatter.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/RuleDescriptionFormatter.java @@ -20,49 +20,93 @@ 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 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 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 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 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 ruleDescriptionSectionDtos, - String ruleKey, RuleDto.Format descriptionFormat) { - Optional ruleDescriptionSectionDto = findDefaultDescription(ruleDescriptionSectionDtos); - return ruleDescriptionSectionDto - .map(ruleDescriptionSection -> toHtml(ruleKey, descriptionFormat, ruleDescriptionSection)) - .orElse(null); + RuleType ruleType, RuleDto.Format descriptionFormat) { + if (ruleDescriptionSectionDtos.isEmpty()) { + return null; + } + Map 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 findDefaultDescription(Collection ruleDescriptionSectionDtos) { - return ruleDescriptionSectionDtos.stream() - .filter(RuleDescriptionSectionDto::isDefault) - .collect(toOptional()); + private static String concatHtmlSections(Map sectionKeyToHtml, RuleType ruleType) { + Map 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("

") + .append(titleMapping.get(sectionKey)) + .append("

") + .append(sectionKeyToHtml.get(sectionKey)) + .append("
"); + } + } + 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(); } } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/rule/RuleDescriptionFormatterTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/rule/RuleDescriptionFormatterTest.java index 7c0879424c0..260bb5ed4f6 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/rule/RuleDescriptionFormatterTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/rule/RuleDescriptionFormatterTest.java @@ -20,10 +20,14 @@ 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("md description"); } @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, "
Root is Root
"); + var section2 = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "
This is not a problem
"); + var section3 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "
I don't want to fix
"); + 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( + "

What's the risk ?

" + + "
Root is Root

" + + "

Assess the risk

" + + "
This is not a problem

" + + "

How can you fix it ?

" + + "
I don't want to fix

" + ); + } + @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(); + } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java index 759bd3cc839..d9ea20f6388 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java @@ -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); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleMapper.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleMapper.java index 7346ffdb6a8..21202d076db 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleMapper.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleMapper.java @@ -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 fieldsToReturn, Map> 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 usersByUuid, Set fieldsToReturn) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java index 06590ea3460..15260041edf 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RulesWsParameters.java @@ -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 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 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); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java index 151c4147f26..8fa9a6bd490 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java @@ -170,6 +170,11 @@ public class SearchAction implements RulesWsAction { "
  • \"debtRemFnOffset\" becomes \"remFnBaseEffort\"
  • " + "
  • \"defaultDebtRemFnOffset\" becomes \"defaultRemFnBaseEffort\"
  • " + "
  • \"debtOverloaded\" becomes \"remFnOverloaded\"
  • " + + "
    " + + "Since 9.5 :" + + "
      " + + "
    • the field \"htmlDesc\" has been deprecated.
    • " + + "
    • the field \"descriptionSections\" has been added.
    • " + "
    ") .setResponseExample(getClass().getResource("search-example.json")) .setSince("4.4") diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java index ab6db925ab9..a41e6a2b4f2 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java @@ -73,7 +73,12 @@ public class ShowAction implements RulesWsAction { "
  • \"defaultDebtRemFnOffset\" becomes \"defaultRemFnBaseEffort\"
  • " + "
  • \"debtOverloaded\" becomes \"remFnOverloaded\"
  • " + "" + - "In 7.1, the field 'scope' has been added.") + "In 7.1, the field 'scope' has been added.
    " + + "Since 9.5 :" + + "
      " + + "
    • the field \"htmlDesc\" has been deprecated.
    • " + + "
    • the field \"descriptionSections\" has been added.
    • " + + "
    ") .setSince("4.2") .setResponseExample(Resources.getResource(getClass(), "show-example.json")) .setHandler(this); diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/search-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/search-example.json index b6b8279303d..23772081574 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/search-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/search-example.json @@ -21,6 +21,16 @@ "scope": "MAIN", "isExternal": false, "type": "CODE_SMELL", + "descriptionSections": [ + { + "key": "Why is this an issue ?", + "content": "

    Unnecessary imports should be removed

    " + }, + { + "key": "How to fix it ?", + "content": "

    Recommended Secure Coding Practices

    • activate Spring Security's CSRF protection.
    " + } + ], "params": [ { "key": "max", diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/show-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/show-example.json index 8664d1509fa..6e96d7bb8df 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/show-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/show-example.json @@ -22,6 +22,16 @@ "scope": "MAIN", "isExternal": false, "type": "CODE_SMELL", + "descriptionSections": [ + { + "key": "Why is this an issue ?", + "content": "

    Unnecessary imports should be removed

    " + }, + { + "key": "How to fix it ?", + "content": "

    Recommended Secure Coding Practices

    • activate Spring Security's CSRF protection.
    " + } + ], "params": [ { "key": "max", diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/ShowActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/ShowActionTest.java index 3e06e45ce86..00d6bb05a42 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/ShowActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/ShowActionTest.java @@ -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", "<div>desc2</div>", 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, "
    Root is Root
    "); + var section2 = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "
    This is not a problem
    "); + var section3 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "
    I don't want to fix
    "); + + 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( + "

    What's the risk ?

    " + + "
    Root is Root

    " + + "

    Assess the risk

    " + + "
    This is not a problem

    " + + "

    How can you fix it ?

    " + + "
    I don't want to fix

    " + ); + + assertThat(resultRule.getMdDesc()) + .contains( + "

    What's the risk ?

    " + + "
    Root is Root

    " + + "

    Assess the risk

    " + + "
    This is not a problem

    " + + "

    How can you fix it ?

    " + + "
    I don't want to fix

    "); + + assertThat(resultRule.getDescriptionSectionsList()) + .extracting(Rule.DescriptionSection::getKey, Rule.DescriptionSection::getContent) + .containsExactlyInAnyOrder( + tuple(ROOT_CAUSE_SECTION_KEY, "
    Root is Root
    "), + tuple(ASSESS_THE_PROBLEM_SECTION_KEY, "
    This is not a problem
    "), + tuple(HOW_TO_FIX_SECTION_KEY, "
    I don't want to fix
    ")); + } + + @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("toto is toto"); + assertThat(resultRule.getMdDesc()).contains("*toto is toto*"); + + assertThat(resultRule.getDescriptionSectionsList()) + .extracting(Rule.DescriptionSection::getKey, Rule.DescriptionSection::getContent) + .contains(tuple(DEFAULT_KEY, "toto is toto")); + } + @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; + } } diff --git a/sonar-ws/src/main/protobuf/ws-rules.proto b/sonar-ws/src/main/protobuf/ws-rules.proto index 719e7b7e326..76ee26ae340 100644 --- a/sonar-ws/src/main/protobuf/ws-rules.proto +++ b/sonar-ws/src/main/protobuf/ws-rules.proto @@ -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;