diff options
14 files changed, 1100 insertions, 34 deletions
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; @@ -180,15 +181,16 @@ public class RuleDto { } @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 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<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 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<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 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<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(); + } +} 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<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 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<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 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<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 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<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 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 = + "<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<String> 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/>"); + + } + +} 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<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 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<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()); + } + +} 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( |