]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16397 Store rule description sections in the new DB structure and split when...
authorAurelien Poscia <aurelien.poscia@sonarsource.com>
Thu, 5 May 2022 11:43:29 +0000 (13:43 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 11 May 2022 20:02:59 +0000 (20:02 +0000)
14 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGenerator.java [new file with mode: 0644]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGenerator.java [new file with mode: 0644]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyIssueRuleDescriptionSectionsGenerator.java [new file with mode: 0644]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGenerator.java [new file with mode: 0644]
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolver.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGeneratorTest.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGeneratorTest.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionGeneratorTestData.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolverTest.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorsTest.java [new file with mode: 0644]
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java

index 0530604d30288d9340c4a41e155b1458191b76d4..a353fc695a765849a5f3edef70726aff17225d2f 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.db.rule;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Objects;
 import java.util.Optional;
@@ -179,16 +180,17 @@ public class RuleDto {
     return ruleDescriptionSectionDtos;
   }
 
-  @CheckForNull
-  public RuleDescriptionSectionDto getRuleDescriptionSectionDto(String ruleDescriptionSectionKey) {
-    return findExistingSectionWithSameKey(ruleDescriptionSectionKey).orElse(null);
-  }
-
   @CheckForNull
   public RuleDescriptionSectionDto getDefaultRuleDescriptionSection() {
     return findExistingSectionWithSameKey(DEFAULT_KEY).orElse(null);
   }
 
+  public RuleDto replaceRuleDescriptionSectionDtos(Collection<RuleDescriptionSectionDto> ruleDescriptionSectionDtos) {
+    this.ruleDescriptionSectionDtos.clear();
+    ruleDescriptionSectionDtos.forEach(this::addRuleDescriptionSectionDto);
+    return this;
+  }
+
   public RuleDto addRuleDescriptionSectionDto(RuleDescriptionSectionDto ruleDescriptionSectionDto) {
     checkArgument(sectionWithSameKeyShouldNotExist(ruleDescriptionSectionDto),
       "A section with key %s already exists", ruleDescriptionSectionDto.getKey());
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGenerator.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGenerator.java
new file mode 100644 (file)
index 0000000..1fb1afa
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import java.util.Set;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+
+import static java.util.stream.Collectors.toSet;
+
+public class AdvancedRuleDescriptionSectionsGenerator implements RuleDescriptionSectionsGenerator {
+  private final UuidFactory uuidFactory;
+
+  public AdvancedRuleDescriptionSectionsGenerator(UuidFactory uuidFactory) {
+    this.uuidFactory = uuidFactory;
+  }
+
+  @Override
+  public boolean isGeneratorForRule(RulesDefinition.Rule rule) {
+    return !rule.ruleDescriptionSections().isEmpty();
+  }
+
+  @Override
+  public Set<RuleDescriptionSectionDto> generateSections(RulesDefinition.Rule rule) {
+    return rule.ruleDescriptionSections().stream()
+      .map(section -> RuleDescriptionSectionDto.builder()
+        .uuid(uuidFactory.create())
+        .key(section.getKey())
+        .content(section.getHtmlContent())
+        .build()
+      )
+      .collect(toSet());
+  }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGenerator.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGenerator.java
new file mode 100644 (file)
index 0000000..d0c93c7
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.CheckForNull;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+import org.sonar.markdown.Markdown;
+
+import static java.util.Collections.emptySet;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+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;
+
+public class LegacyHotspotRuleDescriptionSectionsGenerator implements RuleDescriptionSectionsGenerator {
+  private final UuidFactory uuidFactory;
+
+  public LegacyHotspotRuleDescriptionSectionsGenerator(UuidFactory uuidFactory) {
+    this.uuidFactory = uuidFactory;
+  }
+
+  @Override
+  public boolean isGeneratorForRule(RulesDefinition.Rule rule) {
+    return SECURITY_HOTSPOT.equals(rule.type()) && rule.ruleDescriptionSections().isEmpty();
+  }
+
+  @Override
+  public Set<RuleDescriptionSectionDto> generateSections(RulesDefinition.Rule rule) {
+    return getDescriptionInHtml(rule)
+      .map(this::generateSections)
+      .orElse(emptySet());
+  }
+
+  private static Optional<String> getDescriptionInHtml(RulesDefinition.Rule rule) {
+    if (rule.htmlDescription() != null) {
+      return Optional.of(rule.htmlDescription());
+    } else if (rule.markdownDescription() != null) {
+      return Optional.of(Markdown.convertToHtml(rule.markdownDescription()));
+    }
+    return Optional.empty();
+  }
+
+  private Set<RuleDescriptionSectionDto> generateSections(String descriptionInHtml) {
+    String[] split = extractSection("", descriptionInHtml);
+    String remainingText = split[0];
+    String ruleDescriptionSection = split[1];
+
+    split = extractSection("<h2>Exceptions</h2>", remainingText);
+    remainingText = split[0];
+    String exceptions = split[1];
+
+    split = extractSection("<h2>Ask Yourself Whether</h2>", remainingText);
+    remainingText = split[0];
+    String askSection = split[1];
+
+    split = extractSection("<h2>Sensitive Code Example</h2>", remainingText);
+    remainingText = split[0];
+    String sensitiveSection = split[1];
+
+    split = extractSection("<h2>Noncompliant Code Example</h2>", remainingText);
+    remainingText = split[0];
+    String noncompliantSection = split[1];
+
+    split = extractSection("<h2>Recommended Secure Coding Practices</h2>", remainingText);
+    remainingText = split[0];
+    String recommendedSection = split[1];
+
+    split = extractSection("<h2>Compliant Solution</h2>", remainingText);
+    remainingText = split[0];
+    String compliantSection = split[1];
+
+    split = extractSection("<h2>See</h2>", remainingText);
+    remainingText = split[0];
+    String seeSection = split[1];
+
+    RuleDescriptionSectionDto rootSection = createSection(ROOT_CAUSE_SECTION_KEY, ruleDescriptionSection, exceptions, remainingText);
+    RuleDescriptionSectionDto assessSection = createSection(ASSESS_THE_PROBLEM_SECTION_KEY, askSection, sensitiveSection, noncompliantSection);
+    RuleDescriptionSectionDto fixSection = createSection(HOW_TO_FIX_SECTION_KEY, recommendedSection, compliantSection, seeSection);
+
+    return Stream.of(rootSection, assessSection, fixSection)
+      .filter(Objects::nonNull)
+      .collect(Collectors.toSet());
+  }
+
+
+  private static String[] extractSection(String beginning, String description) {
+    String endSection = "<h2>";
+    int beginningIndex = description.indexOf(beginning);
+    if (beginningIndex != -1) {
+      int endIndex = description.indexOf(endSection, beginningIndex + beginning.length());
+      if (endIndex == -1) {
+        endIndex = description.length();
+      }
+      return new String[] {
+        description.substring(0, beginningIndex) + description.substring(endIndex),
+        description.substring(beginningIndex, endIndex)
+      };
+    } else {
+      return new String[] {description, ""};
+    }
+
+  }
+
+  @CheckForNull
+  private RuleDescriptionSectionDto createSection(String key, String... contentPieces) {
+    String content = trimToNull(String.join("", contentPieces));
+    if (content == null) {
+      return null;
+    }
+    return RuleDescriptionSectionDto.builder()
+      .uuid(uuidFactory.create())
+      .key(key)
+      .content(content)
+      .build();
+  }
+
+  @CheckForNull
+  private static String trimToNull(String input) {
+    return input.isEmpty() ? null : input;
+  }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyIssueRuleDescriptionSectionsGenerator.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyIssueRuleDescriptionSectionsGenerator.java
new file mode 100644 (file)
index 0000000..a03bc62
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import java.util.EnumSet;
+import java.util.Set;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
+import static org.sonar.api.rules.RuleType.BUG;
+import static org.sonar.api.rules.RuleType.CODE_SMELL;
+import static org.sonar.api.rules.RuleType.VULNERABILITY;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
+
+public class LegacyIssueRuleDescriptionSectionsGenerator implements RuleDescriptionSectionsGenerator {
+  private static final Set<RuleType> ISSUE_RULE_TYPES = EnumSet.of(CODE_SMELL, BUG, VULNERABILITY);
+
+  private final UuidFactory uuidFactory;
+
+  public LegacyIssueRuleDescriptionSectionsGenerator(UuidFactory uuidFactory) {
+    this.uuidFactory = uuidFactory;
+  }
+
+  @Override
+  public boolean isGeneratorForRule(RulesDefinition.Rule rule) {
+    return ISSUE_RULE_TYPES.contains(rule.type()) && rule.ruleDescriptionSections().isEmpty();
+  }
+
+  @Override
+  public Set<RuleDescriptionSectionDto> generateSections(RulesDefinition.Rule rule) {
+    if (isNotEmpty(rule.htmlDescription())) {
+      return singleton(createDefaultRuleDescriptionSection(uuidFactory.create(), rule.htmlDescription()));
+    } else if (isNotEmpty(rule.markdownDescription())) {
+      return singleton(createDefaultRuleDescriptionSection(uuidFactory.create(), rule.markdownDescription()));
+    }
+    return emptySet();
+  }
+}
index 2e676765dfa6098fb9ccb6230803a7092cb4643e..2d342a401a72de9f876402ebd60f6c1e85e56f14 100644 (file)
@@ -81,7 +81,6 @@ import static org.apache.commons.lang.StringUtils.isNotEmpty;
 import static org.sonar.core.util.stream.MoreCollectors.toList;
 import static org.sonar.core.util.stream.MoreCollectors.toSet;
 import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
 
 /**
  * Register rules at server startup
@@ -100,10 +99,13 @@ public class RegisterRules implements Startable {
   private final WebServerRuleFinder webServerRuleFinder;
   private final UuidFactory uuidFactory;
   private final MetadataIndex metadataIndex;
+  private final RuleDescriptionSectionsGeneratorResolver ruleDescriptionSectionsGeneratorResolver;
+
 
   public RegisterRules(RuleDefinitionsLoader defLoader, QProfileRules qProfileRules, DbClient dbClient, RuleIndexer ruleIndexer,
     ActiveRuleIndexer activeRuleIndexer, Languages languages, System2 system2,
-    WebServerRuleFinder webServerRuleFinder, UuidFactory uuidFactory, MetadataIndex metadataIndex) {
+    WebServerRuleFinder webServerRuleFinder, UuidFactory uuidFactory, MetadataIndex metadataIndex,
+    RuleDescriptionSectionsGeneratorResolver ruleDescriptionSectionsGeneratorResolver) {
     this.defLoader = defLoader;
     this.qProfileRules = qProfileRules;
     this.dbClient = dbClient;
@@ -114,6 +116,7 @@ public class RegisterRules implements Startable {
     this.webServerRuleFinder = webServerRuleFinder;
     this.uuidFactory = uuidFactory;
     this.metadataIndex = metadataIndex;
+    this.ruleDescriptionSectionsGeneratorResolver = ruleDescriptionSectionsGeneratorResolver;
   }
 
   @Override
@@ -394,18 +397,16 @@ public class RegisterRules implements Startable {
       .setIsAdHoc(false)
       .setCreatedAt(system2.now())
       .setUpdatedAt(system2.now());
-    String htmlDescription = ruleDef.htmlDescription();
-    if (isNotEmpty(htmlDescription)) {
-      ruleDto.addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), htmlDescription));
+
+    if (isNotEmpty(ruleDef.htmlDescription())) {
       ruleDto.setDescriptionFormat(Format.HTML);
-    } else {
-      String markdownDescription = ruleDef.markdownDescription();
-      if (isNotEmpty(markdownDescription)) {
-        ruleDto.addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), markdownDescription));
-        ruleDto.setDescriptionFormat(Format.MARKDOWN);
-      }
+    } else if (isNotEmpty(ruleDef.markdownDescription())) {
+      ruleDto.setDescriptionFormat(Format.MARKDOWN);
     }
 
+    generateRuleDescriptionSections(ruleDef)
+      .forEach(ruleDto::addRuleDescriptionSectionDto);
+
     DebtRemediationFunction debtRemediationFunction = ruleDef.debtRemediationFunction();
     if (debtRemediationFunction != null) {
       ruleDto.setDefRemediationFunction(debtRemediationFunction.type().name());
@@ -418,6 +419,11 @@ public class RegisterRules implements Startable {
     return ruleDto;
   }
 
+  private Set<RuleDescriptionSectionDto> generateRuleDescriptionSections(RulesDefinition.Rule ruleDef) {
+    RuleDescriptionSectionsGenerator descriptionSectionGenerator = ruleDescriptionSectionsGeneratorResolver.getRuleDescriptionSectionsGenerator(ruleDef);
+    return descriptionSectionGenerator.generateSections(ruleDef);
+  }
+
   private static Scope toDtoScope(RuleScope scope) {
     switch (scope) {
       case ALL:
@@ -491,28 +497,35 @@ public class RegisterRules implements Startable {
   }
 
   private boolean mergeDescription(RulesDefinition.Rule rule, RuleDto ruleDto) {
-    boolean changed = false;
-
-    String currentDescription = Optional.ofNullable(ruleDto.getDefaultRuleDescriptionSection())
-      .map(RuleDescriptionSectionDto::getContent)
-      .orElse(null);
-
-    String htmlDescription = rule.htmlDescription();
-    String markdownDescription = rule.markdownDescription();
-    if (isDescriptionUpdated(htmlDescription, currentDescription)) {
-      ruleDto.addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), htmlDescription));
+    Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos = generateRuleDescriptionSections(rule);
+    if (ruleDescriptionSectionsUnchanged(ruleDto, newRuleDescriptionSectionDtos)) {
+      return false;
+    }
+    ruleDto.replaceRuleDescriptionSectionDtos(newRuleDescriptionSectionDtos);
+    if (containsHtmlDescription(rule)) {
       ruleDto.setDescriptionFormat(Format.HTML);
-      changed = true;
-    } else if (isDescriptionUpdated(markdownDescription, currentDescription)) {
-      ruleDto.addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), markdownDescription));
+      return true;
+    } else if (isNotEmpty(rule.markdownDescription())) {
       ruleDto.setDescriptionFormat(Format.MARKDOWN);
-      changed = true;
+      return true;
     }
-    return changed;
+    return false;
+  }
+
+  private static boolean containsHtmlDescription(RulesDefinition.Rule rule) {
+    return isNotEmpty(rule.htmlDescription()) || !rule.ruleDescriptionSections().isEmpty();
+  }
+
+  private static boolean ruleDescriptionSectionsUnchanged(RuleDto ruleDto, Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos) {
+    Map<String, String> oldKeysToSections = toMap(ruleDto.getRuleDescriptionSectionDtos());
+    Map<String, String> newKeysToSections = toMap(newRuleDescriptionSectionDtos);
+    return oldKeysToSections.equals(newKeysToSections);
   }
 
-  private static boolean isDescriptionUpdated(@Nullable String description, @Nullable String currentDescription) {
-    return isNotEmpty(description) && !Objects.equals(description, currentDescription);
+  private static Map<String, String> toMap(Set<RuleDescriptionSectionDto> ruleDto) {
+    return ruleDto
+      .stream()
+      .collect(Collectors.toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
   }
 
   private static boolean mergeDebtDefinitions(RulesDefinition.Rule def, RuleDto dto) {
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGenerator.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGenerator.java
new file mode 100644 (file)
index 0000000..b8b0f5d
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import java.util.Set;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+
+public interface RuleDescriptionSectionsGenerator {
+
+  boolean isGeneratorForRule(RulesDefinition.Rule rule);
+
+  Set<RuleDescriptionSectionDto> generateSections(RulesDefinition.Rule rule);
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolver.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolver.java
new file mode 100644 (file)
index 0000000..58c83df
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import java.util.Set;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static java.util.stream.Collectors.toSet;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class RuleDescriptionSectionsGeneratorResolver {
+  private final Set<RuleDescriptionSectionsGenerator> ruleDescriptionSectionsGenerators;
+
+  RuleDescriptionSectionsGeneratorResolver(Set<RuleDescriptionSectionsGenerator> ruleDescriptionSectionsGenerators) {
+    this.ruleDescriptionSectionsGenerators = ruleDescriptionSectionsGenerators;
+  }
+
+  RuleDescriptionSectionsGenerator getRuleDescriptionSectionsGenerator(RulesDefinition.Rule ruleDef) {
+    Set<RuleDescriptionSectionsGenerator> generatorsFound = ruleDescriptionSectionsGenerators.stream()
+      .filter(generator -> generator.isGeneratorForRule(ruleDef))
+      .collect(toSet());
+    checkState(generatorsFound.size() < 2, "More than one rule description section generator found for rule with key %s", ruleDef.key());
+    checkState(!generatorsFound.isEmpty(), "No rule description section generator found for rule with key %s", ruleDef.key());
+    return generatorsFound.iterator().next();
+  }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGeneratorTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGeneratorTest.java
new file mode 100644 (file)
index 0000000..e7f6a02
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import java.util.List;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.api.server.rule.RuleDescriptionSection;
+import org.sonar.api.server.rule.RuleDescriptionSectionBuilder;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+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;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AdvancedRuleDescriptionSectionsGeneratorTest {
+  private static final String UUID_1 = "uuid1";
+  private static final String UUID_2 = "uuid2";
+
+  private static final String HTML_CONTENT = "html content";
+
+  private static final RuleDescriptionSection SECTION_1 = new RuleDescriptionSectionBuilder().sectionKey(HOW_TO_FIX_SECTION_KEY).htmlContent(HTML_CONTENT).build();
+  private static final RuleDescriptionSection SECTION_2 = new RuleDescriptionSectionBuilder().sectionKey(ROOT_CAUSE_SECTION_KEY).htmlContent(HTML_CONTENT + "2").build();
+
+  private static final RuleDescriptionSectionDto EXPECTED_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key(HOW_TO_FIX_SECTION_KEY).content(HTML_CONTENT).build();
+  private static final RuleDescriptionSectionDto EXPECTED_SECTION_2 = RuleDescriptionSectionDto.builder().uuid(UUID_2).key(ROOT_CAUSE_SECTION_KEY)
+    .content(HTML_CONTENT + "2").build();
+
+  @Mock
+  private UuidFactory uuidFactory;
+
+  @Mock
+  private RulesDefinition.Rule rule;
+
+  @InjectMocks
+  private AdvancedRuleDescriptionSectionsGenerator generator;
+
+  @Before
+  public void before() {
+    when(uuidFactory.create()).thenReturn(UUID_1).thenReturn(UUID_2);
+  }
+
+  @Test
+  public void generateSections_whenOneSection_createsOneSections() {
+    when(rule.ruleDescriptionSections()).thenReturn(List.of(SECTION_1));
+
+    Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = generator.generateSections(rule);
+
+    assertThat(ruleDescriptionSectionDtos)
+      .usingRecursiveFieldByFieldElementComparator()
+      .containsOnly(EXPECTED_SECTION_1);
+  }
+
+  @Test
+  public void generateSections_whenTwoSections_createsTwoSections() {
+    when(rule.ruleDescriptionSections()).thenReturn(List.of(SECTION_1, SECTION_2));
+
+    Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = generator.generateSections(rule);
+
+    assertThat(ruleDescriptionSectionDtos)
+      .usingRecursiveFieldByFieldElementComparator()
+      .containsExactlyInAnyOrder(EXPECTED_SECTION_1, EXPECTED_SECTION_2);
+  }
+
+  @Test
+  public void generateSections_whenNoSections_returnsEmptySet() {
+    when(rule.ruleDescriptionSections()).thenReturn(emptyList());
+
+    Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = generator.generateSections(rule);
+
+    assertThat(ruleDescriptionSectionDtos).isEmpty();
+  }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGeneratorTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGeneratorTest.java
new file mode 100644 (file)
index 0000000..6308f48
--- /dev/null
@@ -0,0 +1,239 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+
+import static java.util.stream.Collectors.toMap;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+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;
+
+@RunWith(DataProviderRunner.class)
+public class LegacyHotspotRuleDescriptionSectionsGeneratorTest {
+
+  /*
+   * Bunch of static constant to create rule description.
+   */
+  private static final String DESCRIPTION =
+    "<p>The use of operators pairs ( <code>=+</code>, <code>=-</code> or <code>=!</code> ) where the reversed, single operator was meant (<code>+=</code>,\n"
+      + "<code>-=</code> or <code>!=</code>) will compile and run, but not produce the expected results.</p>\n"
+      + "<p>This rule raises an issue when <code>=+</code>, <code>=-</code>, or <code>=!</code> is used without any spacing between the two operators and when\n"
+      + "there is at least one whitespace character after.</p>\n";
+  private static final String NONCOMPLIANTCODE = "<h2>Noncompliant Code Example</h2>\n" + "<pre>Integer target = -5;\n" + "Integer num = 3;\n" + "\n"
+    + "target =- num;  // Noncompliant; target = -3. Is that really what's meant?\n" + "target =+ num; // Noncompliant; target = 3\n" + "</pre>\n";
+
+  private static final String COMPLIANTCODE =
+    "<h2>Compliant Solution</h2>\n" + "<pre>Integer target = -5;\n" + "Integer num = 3;\n" + "\n" + "target = -num;  // Compliant; intent to assign inverse value of num is clear\n"
+      + "target += num;\n" + "</pre>\n";
+
+  private static final String SEE =
+    "<h2>See</h2>\n" + "<ul>\n" + "  <li> <a href=\"https://cwe.mitre.org/data/definitions/352.html\">MITRE, CWE-352</a> - Cross-Site Request Forgery (CSRF) </li>\n"
+      + "  <li> <a href=\"https://www.owasp.org/index.php/Top_10-2017_A6-Security_Misconfiguration\">OWASP Top 10 2017 Category A6</a> - Security\n" + "  Misconfiguration </li>\n"
+      + "  <li> <a href=\"https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29\">OWASP: Cross-Site Request Forgery</a> </li>\n"
+      + "  <li> <a href=\"https://www.sans.org/top25-software-errors/#cat1\">SANS Top 25</a> - Insecure Interaction Between Components </li>\n"
+      + "  <li> Derived from FindSecBugs rule <a href=\"https://find-sec-bugs.github.io/bugs.htm#SPRING_CSRF_PROTECTION_DISABLED\">SPRING_CSRF_PROTECTION_DISABLED</a> </li>\n"
+      + "  <li> <a href=\"https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#when-to-use-csrf-protection\">Spring Security\n"
+      + "  Official Documentation: When to use CSRF protection</a> </li>\n" + "</ul>\n";
+
+  private static final String RECOMMENTEDCODINGPRACTICE =
+    "<h2>Recommended Secure Coding Practices</h2>\n" + "<ul>\n" + "  <li> activate Spring Security's CSRF protection. </li>\n" + "</ul>\n";
+
+  private static final String ASKATRISK =
+    "<h2>Ask Yourself Whether</h2>\n" + "<ul>\n" + "  <li> Any URLs responding with <code>Access-Control-Allow-Origin: *</code> include sensitive content. </li>\n"
+      + "  <li> Any domains specified in <code>Access-Control-Allow-Origin</code> headers are checked against a whitelist. </li>\n" + "</ul>\n";
+
+  private static final String SENSITIVECODE = "<h2>Sensitive Code Example</h2>\n" + "<pre>\n" + "// === Java Servlet ===\n" + "@Override\n"
+    + "protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n"
+    + "  resp.setHeader(\"Content-Type\", \"text/plain; charset=utf-8\");\n" + "  resp.setHeader(\"Access-Control-Allow-Origin\", \"http://localhost:8080\"); // Questionable\n"
+    + "  resp.setHeader(\"Access-Control-Allow-Credentials\", \"true\"); // Questionable\n" + "  resp.setHeader(\"Access-Control-Allow-Methods\", \"GET\"); // Questionable\n"
+    + "  resp.getWriter().write(\"response\");\n" + "}\n" + "</pre>\n" + "<pre>\n" + "// === Spring MVC Controller annotation ===\n"
+    + "@CrossOrigin(origins = \"http://domain1.com\") // Questionable\n" + "@RequestMapping(\"\")\n" + "public class TestController {\n"
+    + "    public String home(ModelMap model) {\n" + "        model.addAttribute(\"message\", \"ok \");\n" + "        return \"view\";\n" + "    }\n" + "\n"
+    + "    @CrossOrigin(origins = \"http://domain2.com\") // Questionable\n" + "    @RequestMapping(value = \"/test1\")\n" + "    public ResponseEntity&lt;String&gt; test1() {\n"
+    + "        return ResponseEntity.ok().body(\"ok\");\n" + "    }\n" + "}\n" + "</pre>\n";
+
+  private final UuidFactory uuidFactory = mock(UuidFactory.class);
+  private final RulesDefinition.Rule rule = mock(RulesDefinition.Rule.class);
+
+  private final LegacyHotspotRuleDescriptionSectionsGenerator generator = new LegacyHotspotRuleDescriptionSectionsGenerator(uuidFactory);
+
+  @Before
+  public void setUp() {
+    when(rule.htmlDescription()).thenReturn(null);
+    when(rule.markdownDescription()).thenReturn(null);
+  }
+
+  @Test
+  public void parse_returns_all_empty_fields_when_no_description() {
+    when(rule.htmlDescription()).thenReturn(null);
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    assertThat(results).isEmpty();
+  }
+
+  @Test
+  public void parse_returns_all_empty_fields_when_empty_description() {
+    when(rule.htmlDescription()).thenReturn("");
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    assertThat(results).isEmpty();
+  }
+
+  @Test
+  @UseDataProvider("descriptionsWithoutTitles")
+  public void parse_to_risk_description_fields_when_desc_contains_no_section(String description) {
+    when(rule.htmlDescription()).thenReturn(description);
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    assertThat(results).hasSize(1);
+    RuleDescriptionSectionDto uniqueSection = results.iterator().next();
+    assertThat(uniqueSection.getKey()).isEqualTo(ROOT_CAUSE_SECTION_KEY);
+    assertThat(uniqueSection.getContent()).isEqualTo(description);
+  }
+
+  @DataProvider
+  public static Object[][] descriptionsWithoutTitles() {
+    return new Object[][] {{randomAlphabetic(123)}, {"bar\n" + "acme\n" + "foo"}};
+  }
+
+  @Test
+  public void parse_return_null_risk_when_desc_starts_with_ask_yourself_title() {
+    when(rule.htmlDescription()).thenReturn(ASKATRISK + RECOMMENTEDCODINGPRACTICE);
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
+    assertThat(sectionKeyToContent).hasSize(2)
+      .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASKATRISK)
+      .containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENTEDCODINGPRACTICE);
+  }
+
+
+  @Test
+  public void parse_return_null_vulnerable_when_no_ask_yourself_whether_title() {
+    when(rule.htmlDescription()).thenReturn(DESCRIPTION + RECOMMENTEDCODINGPRACTICE);
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
+    assertThat(sectionKeyToContent).hasSize(2)
+      .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
+      .containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENTEDCODINGPRACTICE);
+  }
+
+  @Test
+  public void parse_return_null_fixIt_when_desc_has_no_Recommended_Secure_Coding_Practices_title() {
+    when(rule.htmlDescription()).thenReturn(DESCRIPTION + ASKATRISK);
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
+    assertThat(sectionKeyToContent).hasSize(2)
+      .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
+      .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASKATRISK);
+  }
+
+  @Test
+  public void parse_with_noncompliant_section_not_removed() {
+    when(rule.htmlDescription()).thenReturn(DESCRIPTION + NONCOMPLIANTCODE + COMPLIANTCODE);
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
+    assertThat(sectionKeyToContent).hasSize(3)
+      .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
+      .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, NONCOMPLIANTCODE)
+      .containsEntry(HOW_TO_FIX_SECTION_KEY, COMPLIANTCODE);
+  }
+
+  @Test
+  public void parse_moved_noncompliant_code() {
+    when(rule.htmlDescription()).thenReturn(DESCRIPTION + RECOMMENTEDCODINGPRACTICE + NONCOMPLIANTCODE + SEE);
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
+    assertThat(sectionKeyToContent).hasSize(3)
+      .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
+      .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, NONCOMPLIANTCODE)
+      .containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENTEDCODINGPRACTICE + SEE);
+  }
+
+  @Test
+  public void parse_moved_sensitivecode_code() {
+    when(rule.htmlDescription()).thenReturn(DESCRIPTION + ASKATRISK + RECOMMENTEDCODINGPRACTICE + SENSITIVECODE + SEE);
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
+    assertThat(sectionKeyToContent).hasSize(3)
+      .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
+      .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASKATRISK + SENSITIVECODE)
+      .containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENTEDCODINGPRACTICE + SEE);
+  }
+
+  @Test
+  public void parse_md_rule_description() {
+    String ruleDescription = "This is the custom rule description";
+    String exceptionsContent = "This the exceptions section content";
+    String askContent = "This is the ask section content";
+    String recommendedContent = "This is the recommended section content";
+
+    when(rule.markdownDescription()).thenReturn(ruleDescription + "\n"
+      + "== Exceptions" + "\n"
+      + exceptionsContent + "\n"
+      + "== Ask Yourself Whether" + "\n"
+      + askContent + "\n"
+      + "== Recommended Secure Coding Practices" + "\n"
+      + recommendedContent + "\n");
+
+    Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);
+
+    Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
+    assertThat(sectionKeyToContent).hasSize(3)
+      .containsEntry(ROOT_CAUSE_SECTION_KEY, ruleDescription + "<br/>"
+        + "<h2>Exceptions</h2>"
+        + exceptionsContent + "<br/>")
+      .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY,"<h2>Ask Yourself Whether</h2>"
+        + askContent + "<br/>")
+      .containsEntry(HOW_TO_FIX_SECTION_KEY, "<h2>Recommended Secure Coding Practices</h2>"
+        + recommendedContent + "<br/>");
+
+  }
+
+}
index 895bcc41dabc7ff72bdec28d5d067377c605f307..3aa93a6af1e774cff7102449e1be0c8452a50b02 100644 (file)
@@ -89,6 +89,7 @@ import static org.sonar.api.server.rule.RulesDefinition.NewRule;
 import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10;
 import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.builder;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
 
 @RunWith(DataProviderRunner.class)
@@ -125,12 +126,23 @@ public class RegisterRulesTest {
   private RuleIndexer ruleIndexer;
   private ActiveRuleIndexer activeRuleIndexer;
   private RuleIndex ruleIndex;
+  private RuleDescriptionSectionsGenerator ruleDescriptionSectionsGenerator = mock(RuleDescriptionSectionsGenerator.class);
+  private RuleDescriptionSectionsGeneratorResolver resolver = mock(RuleDescriptionSectionsGeneratorResolver.class);
 
   @Before
   public void before() {
     ruleIndexer = new RuleIndexer(es.client(), dbClient);
     ruleIndex = new RuleIndex(es.client(), system);
     activeRuleIndexer = new ActiveRuleIndexer(dbClient, es.client());
+    when(resolver.getRuleDescriptionSectionsGenerator(any())).thenReturn(ruleDescriptionSectionsGenerator);
+    when(ruleDescriptionSectionsGenerator.generateSections(any())).thenAnswer(answer -> {
+      RulesDefinition.Rule rule = answer.getArgument(0, RulesDefinition.Rule.class);
+      String description = rule.htmlDescription() == null ? rule.markdownDescription() : rule.htmlDescription();
+      return Set.of(builder().uuid(UuidFactoryFast.getInstance().create()).key("default").content(description).build());
+    });
+
+
+    when(ruleDescriptionSectionsGenerator.isGeneratorForRule(any())).thenReturn(true);
   }
 
   @Test
@@ -989,7 +1001,8 @@ public class RegisterRulesTest {
     when(languages.get(any())).thenReturn(mock(Language.class));
     reset(webServerRuleFinder);
 
-    RegisterRules task = new RegisterRules(loader, qProfileRules, dbClient, ruleIndexer, activeRuleIndexer, languages, system, webServerRuleFinder, uuidFactory, metadataIndex);
+    RegisterRules task = new RegisterRules(loader, qProfileRules, dbClient, ruleIndexer, activeRuleIndexer, languages, system, webServerRuleFinder, uuidFactory, metadataIndex,
+      resolver);
     task.start();
     // Execute a commit to refresh session state as the task is using its own session
     db.getSession().commit();
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionGeneratorTestData.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionGeneratorTestData.java
new file mode 100644 (file)
index 0000000..3013de7
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.StringJoiner;
+import javax.annotation.Nullable;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.rule.RuleDescriptionSection;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+
+class RuleDescriptionGeneratorTestData {
+  enum RuleDescriptionSectionGeneratorIdentifier {
+    LEGACY_ISSUE, LEGACY_HOTSPOT, ADVANCED_RULE;
+  }
+
+  private final RuleType ruleType;
+  private final String htmlDescription;
+
+  private final String markdownDescription;
+  private final List<RuleDescriptionSection> ruleDescriptionSections;
+  private final RuleDescriptionSectionGeneratorIdentifier expectedGenerator;
+  private final Set<RuleDescriptionSectionDto> expectedRuleDescriptionSectionsDto;
+
+  private RuleDescriptionGeneratorTestData(RuleType ruleType, @Nullable String htmlDescription,@Nullable String markdownDescription, List<RuleDescriptionSection> ruleDescriptionSections,
+    RuleDescriptionSectionGeneratorIdentifier expectedGenerator, Set<RuleDescriptionSectionDto> expectedRuleDescriptionSectionsDto) {
+    this.ruleType = ruleType;
+    this.htmlDescription = htmlDescription;
+    this.markdownDescription = markdownDescription;
+    this.ruleDescriptionSections = ruleDescriptionSections;
+    this.expectedGenerator = expectedGenerator;
+    this.expectedRuleDescriptionSectionsDto = expectedRuleDescriptionSectionsDto;
+  }
+
+  public RuleType getRuleType() {
+    return ruleType;
+  }
+
+  String getHtmlDescription() {
+    return htmlDescription;
+  }
+
+  String getMarkdownDescription() {
+    return markdownDescription;
+  }
+
+  List<RuleDescriptionSection> getRuleDescriptionSections() {
+    return ruleDescriptionSections;
+  }
+
+  RuleDescriptionSectionGeneratorIdentifier getExpectedGenerator() {
+    return expectedGenerator;
+  }
+
+  public Set<RuleDescriptionSectionDto> getExpectedRuleDescriptionSectionsDto() {
+    return expectedRuleDescriptionSectionsDto;
+  }
+
+  static RuleDescriptionGeneratorTestDataBuilder aRuleOfType(RuleType ruleType) {
+    return new RuleDescriptionGeneratorTestDataBuilder(ruleType);
+  }
+
+  @Override
+  public String toString() {
+    return new StringJoiner(", ")
+      .add(ruleType.name())
+      .add(htmlDescription == null ? "html present" : "html absent")
+      .add(markdownDescription == null ? "md present" : "md absent")
+      .add(String.valueOf(ruleDescriptionSections.size()))
+      .add("generator=" + expectedGenerator)
+      .toString();
+  }
+
+  public static final class RuleDescriptionGeneratorTestDataBuilder {
+    private final RuleType ruleType;
+    private String htmlDescription;
+    private String markdownDescription;
+    private List<RuleDescriptionSection> ruleDescriptionSections = new ArrayList<>();
+    private Set<RuleDescriptionSectionDto> expectedRuleDescriptionSectionsDto = new HashSet<>();
+    private RuleDescriptionSectionGeneratorIdentifier expectedGenerator;
+
+    private RuleDescriptionGeneratorTestDataBuilder(RuleType ruleType) {
+      this.ruleType = ruleType;
+    }
+
+    RuleDescriptionGeneratorTestDataBuilder html(@Nullable String htmlDescription) {
+      this.htmlDescription = htmlDescription;
+      return this;
+    }
+
+    RuleDescriptionGeneratorTestDataBuilder md(@Nullable String markdownDescription) {
+      this.markdownDescription = markdownDescription;
+      return this;
+    }
+
+    RuleDescriptionGeneratorTestDataBuilder addSection(RuleDescriptionSection ruleDescriptionSection) {
+      this.ruleDescriptionSections.add(ruleDescriptionSection);
+      return this;
+    }
+
+    RuleDescriptionGeneratorTestDataBuilder expectedGenerator(RuleDescriptionSectionGeneratorIdentifier generatorToUse) {
+      this.expectedGenerator = generatorToUse;
+      return this;
+    }
+
+    RuleDescriptionGeneratorTestDataBuilder addExpectedSection(RuleDescriptionSectionDto sectionDto) {
+      this.expectedRuleDescriptionSectionsDto.add(sectionDto);
+      return this;
+    }
+
+    RuleDescriptionGeneratorTestData build() {
+      return new RuleDescriptionGeneratorTestData(ruleType, htmlDescription, markdownDescription, ruleDescriptionSections, expectedGenerator, expectedRuleDescriptionSectionsDto);
+    }
+  }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolverTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolverTest.java
new file mode 100644 (file)
index 0000000..33ad1dd
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RuleDescriptionSectionsGeneratorResolverTest {
+
+  private static final String RULE_KEY = "RULE_KEY";
+
+  @Mock
+  private RuleDescriptionSectionsGenerator generator1;
+  @Mock
+  private RuleDescriptionSectionsGenerator generator2;
+  @Mock
+  private RulesDefinition.Rule rule;
+
+  private RuleDescriptionSectionsGeneratorResolver resolver;
+
+  @Before
+  public void setUp() {
+    resolver = new RuleDescriptionSectionsGeneratorResolver(Set.of(generator1, generator2));
+    when(rule.key()).thenReturn(RULE_KEY);
+  }
+
+  @Test
+  public void getRuleDescriptionSectionsGenerator_returnsTheCorrectGenerator() {
+    when(generator2.isGeneratorForRule(rule)).thenReturn(true);
+    assertThat(resolver.getRuleDescriptionSectionsGenerator(rule)).isEqualTo(generator2);
+  }
+
+  @Test
+  public void getRuleDescriptionSectionsGenerator_whenNoGeneratorFound_throwsWithCorrectMessage() {
+    assertThatIllegalStateException()
+      .isThrownBy(() ->  resolver.getRuleDescriptionSectionsGenerator(rule))
+      .withMessage("No rule description section generator found for rule with key RULE_KEY");
+  }
+
+  @Test
+  public void getRuleDescriptionSectionsGenerator_whenMoreThanOneGeneratorFound_throwsWithCorrectMessage() {
+    when(generator1.isGeneratorForRule(rule)).thenReturn(true);
+    when(generator2.isGeneratorForRule(rule)).thenReturn(true);
+    assertThatIllegalStateException()
+      .isThrownBy(() ->  resolver.getRuleDescriptionSectionsGenerator(rule))
+      .withMessage("More than one rule description section generator found for rule with key RULE_KEY");
+  }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorsTest.java
new file mode 100644 (file)
index 0000000..9687ab4
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.server.rule;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.sonar.api.server.rule.RuleDescriptionSection;
+import org.sonar.api.server.rule.RuleDescriptionSectionBuilder;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+import org.sonar.server.rule.RuleDescriptionGeneratorTestData.RuleDescriptionSectionGeneratorIdentifier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.rules.RuleType.BUG;
+import static org.sonar.api.rules.RuleType.CODE_SMELL;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.api.rules.RuleType.VULNERABILITY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
+import static org.sonar.server.rule.RuleDescriptionGeneratorTestData.RuleDescriptionSectionGeneratorIdentifier.ADVANCED_RULE;
+import static org.sonar.server.rule.RuleDescriptionGeneratorTestData.RuleDescriptionSectionGeneratorIdentifier.LEGACY_HOTSPOT;
+import static org.sonar.server.rule.RuleDescriptionGeneratorTestData.RuleDescriptionSectionGeneratorIdentifier.LEGACY_ISSUE;
+import static org.sonar.server.rule.RuleDescriptionGeneratorTestData.aRuleOfType;
+
+@RunWith(Parameterized.class)
+public class RuleDescriptionSectionsGeneratorsTest {
+
+  private static final String KEY_1 = "KEY";
+  private static final String KEY_2 = "KEY_2";
+  private static final String UUID_1 = "uuid1";
+  private static final String UUID_2 = "uuid2";
+
+  private static final String HTML_CONTENT = "html content";
+  private static final String MD_CONTENT = "md content balblab";
+
+  private static final RuleDescriptionSection SECTION_1 = new RuleDescriptionSectionBuilder().sectionKey(KEY_1).htmlContent(HTML_CONTENT).build();
+  private static final RuleDescriptionSection SECTION_2 = new RuleDescriptionSectionBuilder().sectionKey(KEY_2).htmlContent(HTML_CONTENT).build();
+
+  private static final RuleDescriptionSectionDto DEFAULT_HTML_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key("default").content(HTML_CONTENT).build();
+  private static final RuleDescriptionSectionDto DEFAULT_HTML_HOTSPOT_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key(ROOT_CAUSE_SECTION_KEY).content(HTML_CONTENT).build();
+  private static final RuleDescriptionSectionDto DEFAULT_MD_HOTSPOT_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key(ROOT_CAUSE_SECTION_KEY).content(MD_CONTENT).build();
+  private static final RuleDescriptionSectionDto DEFAULT_MD_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key("default").content(MD_CONTENT).build();
+  private static final RuleDescriptionSectionDto HTML_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key(KEY_1).content(HTML_CONTENT).build();
+  private static final RuleDescriptionSectionDto HTML_SECTION_2 = RuleDescriptionSectionDto.builder().uuid(UUID_2).key(KEY_2).content(HTML_CONTENT).build();
+
+  @Parameterized.Parameters(name = "{index} = {0}")
+  public static List<RuleDescriptionGeneratorTestData> testData() {
+    return Arrays.asList(
+      // ISSUES
+      aRuleOfType(BUG).html(null).md(null).expectedGenerator(LEGACY_ISSUE).build(),
+      aRuleOfType(BUG).html(HTML_CONTENT).md(null).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_HTML_SECTION_1).build(),
+      aRuleOfType(BUG).html(null).md(MD_CONTENT).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_MD_SECTION_1).build(),
+      aRuleOfType(BUG).html(HTML_CONTENT).md(MD_CONTENT).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_HTML_SECTION_1).build(),
+      aRuleOfType(CODE_SMELL).html(HTML_CONTENT).md(MD_CONTENT).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_HTML_SECTION_1).build(),
+      aRuleOfType(VULNERABILITY).html(HTML_CONTENT).md(MD_CONTENT).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_HTML_SECTION_1).build(),
+      // HOTSPOT
+      aRuleOfType(SECURITY_HOTSPOT).html(null).md(null).expectedGenerator(LEGACY_HOTSPOT).build(),
+      aRuleOfType(SECURITY_HOTSPOT).html(HTML_CONTENT).md(null).expectedGenerator(LEGACY_HOTSPOT).addExpectedSection(DEFAULT_HTML_HOTSPOT_SECTION_1).build(),
+      aRuleOfType(SECURITY_HOTSPOT).html(null).md(MD_CONTENT).expectedGenerator(LEGACY_HOTSPOT).addExpectedSection(DEFAULT_MD_HOTSPOT_SECTION_1).build(),
+      aRuleOfType(SECURITY_HOTSPOT).html(HTML_CONTENT).md(MD_CONTENT).expectedGenerator(LEGACY_HOTSPOT).addExpectedSection(DEFAULT_HTML_HOTSPOT_SECTION_1).build(),
+      // ADVANCED RULES
+      aRuleOfType(BUG).html(null).md(null).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
+      aRuleOfType(BUG).html(HTML_CONTENT).md(null).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
+      aRuleOfType(BUG).html(null).md(MD_CONTENT).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
+      aRuleOfType(BUG).html(HTML_CONTENT).md(MD_CONTENT).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
+      aRuleOfType(BUG).html(HTML_CONTENT).md(MD_CONTENT).addSection(SECTION_1).addSection(SECTION_2).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1)
+        .addExpectedSection(
+          HTML_SECTION_2).build(),
+      aRuleOfType(SECURITY_HOTSPOT).html(null).md(null).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
+      aRuleOfType(SECURITY_HOTSPOT).html(HTML_CONTENT).md(null).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
+      aRuleOfType(SECURITY_HOTSPOT).html(null).md(MD_CONTENT).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
+      aRuleOfType(SECURITY_HOTSPOT).html(HTML_CONTENT).md(MD_CONTENT).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build()
+    );
+  }
+
+  private final UuidFactory uuidFactory = mock(UuidFactory.class);
+  private final RulesDefinition.Rule rule = mock(RulesDefinition.Rule.class);
+
+  private final RuleDescriptionGeneratorTestData testData;
+
+  private final RuleDescriptionSectionsGenerator advancedRuleDescriptionSectionsGenerator = new AdvancedRuleDescriptionSectionsGenerator(uuidFactory);
+  private final RuleDescriptionSectionsGenerator legacyHotspotRuleDescriptionSectionsGenerator = new LegacyHotspotRuleDescriptionSectionsGenerator(uuidFactory);
+  private final RuleDescriptionSectionsGenerator legacyIssueRuleDescriptionSectionsGenerator = new LegacyIssueRuleDescriptionSectionsGenerator(uuidFactory);
+
+  Map<RuleDescriptionSectionGeneratorIdentifier, RuleDescriptionSectionsGenerator> idToGenerator = ImmutableMap.<RuleDescriptionSectionGeneratorIdentifier, RuleDescriptionSectionsGenerator>builder()
+    .put(ADVANCED_RULE, advancedRuleDescriptionSectionsGenerator)
+    .put(LEGACY_HOTSPOT, legacyHotspotRuleDescriptionSectionsGenerator)
+    .put(LEGACY_ISSUE, legacyIssueRuleDescriptionSectionsGenerator)
+    .build();
+
+  public RuleDescriptionSectionsGeneratorsTest(RuleDescriptionGeneratorTestData testData) {
+    this.testData = testData;
+  }
+
+  @Before
+  public void before() {
+    when(uuidFactory.create()).thenReturn(UUID_1).thenReturn(UUID_2);
+    when(rule.htmlDescription()).thenReturn(testData.getHtmlDescription());
+    when(rule.markdownDescription()).thenReturn(testData.getMarkdownDescription());
+    when(rule.ruleDescriptionSections()).thenReturn(testData.getRuleDescriptionSections());
+    when(rule.type()).thenReturn(testData.getRuleType());
+  }
+
+  @Test
+  public void scenario() {
+    assertThat(advancedRuleDescriptionSectionsGenerator.isGeneratorForRule(rule)).isEqualTo(ADVANCED_RULE.equals(testData.getExpectedGenerator()));
+    assertThat(legacyHotspotRuleDescriptionSectionsGenerator.isGeneratorForRule(rule)).isEqualTo(LEGACY_HOTSPOT.equals(testData.getExpectedGenerator()));
+    assertThat(legacyIssueRuleDescriptionSectionsGenerator.isGeneratorForRule(rule)).isEqualTo(LEGACY_ISSUE.equals(testData.getExpectedGenerator()));
+
+    generateAndVerifySectionsContent(idToGenerator.get(testData.getExpectedGenerator()));
+  }
+
+  private void generateAndVerifySectionsContent(RuleDescriptionSectionsGenerator advancedRuleDescriptionSectionsGenerator) {
+    assertThat(advancedRuleDescriptionSectionsGenerator.generateSections(rule))
+      .usingRecursiveFieldByFieldElementComparator()
+      .containsExactlyInAnyOrderElementsOf(testData.getExpectedRuleDescriptionSectionsDto());
+  }
+
+}
index eb4f64bd212e95efa6c7a23ba75d02612fe596d0..54ceaf5a4c168de227ce574a0e05a3b324812d8b 100644 (file)
@@ -37,7 +37,11 @@ import org.sonar.server.qualityprofile.builtin.BuiltInQProfileLoader;
 import org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdateImpl;
 import org.sonar.server.qualityprofile.builtin.BuiltInQualityProfilesUpdateListener;
 import org.sonar.server.qualityprofile.RegisterQualityProfiles;
+import org.sonar.server.rule.AdvancedRuleDescriptionSectionsGenerator;
+import org.sonar.server.rule.LegacyHotspotRuleDescriptionSectionsGenerator;
+import org.sonar.server.rule.LegacyIssueRuleDescriptionSectionsGenerator;
 import org.sonar.server.rule.RegisterRules;
+import org.sonar.server.rule.RuleDescriptionSectionsGeneratorResolver;
 import org.sonar.server.rule.WebServerRuleFinder;
 import org.sonar.server.startup.GeneratePluginIndex;
 import org.sonar.server.startup.RegisterMetrics;
@@ -65,6 +69,10 @@ public class PlatformLevelStartup extends PlatformLevel {
     addIfStartupLeaderAndPluginsChanged(
       RegisterMetrics.class,
       RegisterQualityGates.class,
+      RuleDescriptionSectionsGeneratorResolver.class,
+      AdvancedRuleDescriptionSectionsGenerator.class,
+      LegacyHotspotRuleDescriptionSectionsGenerator.class,
+      LegacyIssueRuleDescriptionSectionsGenerator.class,
       RegisterRules.class,
       BuiltInQProfileLoader.class);
     addIfStartupLeader(