From fcb80c0ade78102214df316476cf8c15e0e819d3 Mon Sep 17 00:00:00 2001 From: Aurelien Poscia Date: Thu, 5 May 2022 13:43:29 +0200 Subject: [PATCH] SONAR-16397 Store rule description sections in the new DB structure and split when necessary --- .../main/java/org/sonar/db/rule/RuleDto.java | 12 +- ...ancedRuleDescriptionSectionsGenerator.java | 53 ++++ ...tspotRuleDescriptionSectionsGenerator.java | 146 +++++++++++ ...IssueRuleDescriptionSectionsGenerator.java | 60 +++++ .../org/sonar/server/rule/RegisterRules.java | 69 +++-- .../RuleDescriptionSectionsGenerator.java | 32 +++ ...eDescriptionSectionsGeneratorResolver.java | 44 ++++ ...dRuleDescriptionSectionsGeneratorTest.java | 101 ++++++++ ...tRuleDescriptionSectionsGeneratorTest.java | 239 ++++++++++++++++++ .../sonar/server/rule/RegisterRulesTest.java | 15 +- .../RuleDescriptionGeneratorTestData.java | 135 ++++++++++ ...criptionSectionsGeneratorResolverTest.java | 76 ++++++ ...RuleDescriptionSectionsGeneratorsTest.java | 144 +++++++++++ .../platformlevel/PlatformLevelStartup.java | 8 + 14 files changed, 1100 insertions(+), 34 deletions(-) create mode 100644 server/sonar-webserver-core/src/main/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGenerator.java create mode 100644 server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGenerator.java create mode 100644 server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyIssueRuleDescriptionSectionsGenerator.java create mode 100644 server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGenerator.java create mode 100644 server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolver.java create mode 100644 server/sonar-webserver-core/src/test/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGeneratorTest.java create mode 100644 server/sonar-webserver-core/src/test/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGeneratorTest.java create mode 100644 server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionGeneratorTestData.java create mode 100644 server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolverTest.java create mode 100644 server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorsTest.java diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java index 0530604d302..a353fc695a7 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java @@ -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 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 index 00000000000..1fb1afaf8e2 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGenerator.java @@ -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 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 index 00000000000..d0c93c7fdbc --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGenerator.java @@ -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 generateSections(RulesDefinition.Rule rule) { + return getDescriptionInHtml(rule) + .map(this::generateSections) + .orElse(emptySet()); + } + + private static Optional 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 generateSections(String descriptionInHtml) { + String[] split = extractSection("", descriptionInHtml); + String remainingText = split[0]; + String ruleDescriptionSection = split[1]; + + split = extractSection("

Exceptions

", remainingText); + remainingText = split[0]; + String exceptions = split[1]; + + split = extractSection("

Ask Yourself Whether

", remainingText); + remainingText = split[0]; + String askSection = split[1]; + + split = extractSection("

Sensitive Code Example

", remainingText); + remainingText = split[0]; + String sensitiveSection = split[1]; + + split = extractSection("

Noncompliant Code Example

", remainingText); + remainingText = split[0]; + String noncompliantSection = split[1]; + + split = extractSection("

Recommended Secure Coding Practices

", remainingText); + remainingText = split[0]; + String recommendedSection = split[1]; + + split = extractSection("

Compliant Solution

", remainingText); + remainingText = split[0]; + String compliantSection = split[1]; + + split = extractSection("

See

", 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 = "

"; + 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 index 00000000000..a03bc629647 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyIssueRuleDescriptionSectionsGenerator.java @@ -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 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 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(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java index 2e676765dfa..2d342a401a7 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java @@ -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 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 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 newRuleDescriptionSectionDtos) { + Map oldKeysToSections = toMap(ruleDto.getRuleDescriptionSectionDtos()); + Map 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 toMap(Set 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 index 00000000000..b8b0f5d96a2 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGenerator.java @@ -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 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 index 00000000000..58c83df4ed5 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolver.java @@ -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 ruleDescriptionSectionsGenerators; + + RuleDescriptionSectionsGeneratorResolver(Set ruleDescriptionSectionsGenerators) { + this.ruleDescriptionSectionsGenerators = ruleDescriptionSectionsGenerators; + } + + RuleDescriptionSectionsGenerator getRuleDescriptionSectionsGenerator(RulesDefinition.Rule ruleDef) { + Set 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 index 00000000000..e7f6a0229d9 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGeneratorTest.java @@ -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 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 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 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 index 00000000000..6308f481229 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGeneratorTest.java @@ -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 = + "

The use of operators pairs ( =+, =- or =! ) where the reversed, single operator was meant (+=,\n" + + "-= or !=) will compile and run, but not produce the expected results.

\n" + + "

This rule raises an issue when =+, =-, or =! is used without any spacing between the two operators and when\n" + + "there is at least one whitespace character after.

\n"; + private static final String NONCOMPLIANTCODE = "

Noncompliant Code Example

\n" + "
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" + "
\n"; + + private static final String COMPLIANTCODE = + "

Compliant Solution

\n" + "
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" + "
\n"; + + private static final String SEE = + "

See

\n" + "\n"; + + private static final String RECOMMENTEDCODINGPRACTICE = + "

Recommended Secure Coding Practices

\n" + "
    \n" + "
  • activate Spring Security's CSRF protection.
  • \n" + "
\n"; + + private static final String ASKATRISK = + "

Ask Yourself Whether

\n" + "
    \n" + "
  • Any URLs responding with Access-Control-Allow-Origin: * include sensitive content.
  • \n" + + "
  • Any domains specified in Access-Control-Allow-Origin headers are checked against a whitelist.
  • \n" + "
\n"; + + private static final String SENSITIVECODE = "

Sensitive Code Example

\n" + "
\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" + "
\n" + "
\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<String> test1() {\n"
+    + "        return ResponseEntity.ok().body(\"ok\");\n" + "    }\n" + "}\n" + "
\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 results = generator.generateSections(rule); + + assertThat(results).isEmpty(); + } + + @Test + public void parse_returns_all_empty_fields_when_empty_description() { + when(rule.htmlDescription()).thenReturn(""); + + Set 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 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 results = generator.generateSections(rule); + + Map 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 results = generator.generateSections(rule); + + Map 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 results = generator.generateSections(rule); + + Map 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 results = generator.generateSections(rule); + + Map 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 results = generator.generateSections(rule); + + Map 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 results = generator.generateSections(rule); + + Map 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 results = generator.generateSections(rule); + + Map sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent)); + assertThat(sectionKeyToContent).hasSize(3) + .containsEntry(ROOT_CAUSE_SECTION_KEY, ruleDescription + "
" + + "

Exceptions

" + + exceptionsContent + "
") + .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY,"

Ask Yourself Whether

" + + askContent + "
") + .containsEntry(HOW_TO_FIX_SECTION_KEY, "

Recommended Secure Coding Practices

" + + recommendedContent + "
"); + + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java index 895bcc41dab..3aa93a6af1e 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java @@ -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 index 00000000000..3013de77b59 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionGeneratorTestData.java @@ -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 ruleDescriptionSections; + private final RuleDescriptionSectionGeneratorIdentifier expectedGenerator; + private final Set expectedRuleDescriptionSectionsDto; + + private RuleDescriptionGeneratorTestData(RuleType ruleType, @Nullable String htmlDescription,@Nullable String markdownDescription, List ruleDescriptionSections, + RuleDescriptionSectionGeneratorIdentifier expectedGenerator, Set 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 getRuleDescriptionSections() { + return ruleDescriptionSections; + } + + RuleDescriptionSectionGeneratorIdentifier getExpectedGenerator() { + return expectedGenerator; + } + + public Set 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 ruleDescriptionSections = new ArrayList<>(); + private Set 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 index 00000000000..33ad1dd1ca7 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolverTest.java @@ -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 index 00000000000..9687ab4f7a1 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorsTest.java @@ -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 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 idToGenerator = ImmutableMap.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()); + } + +} diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java index eb4f64bd212..54ceaf5a4c1 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java @@ -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( -- 2.39.5