@@ -21,6 +21,7 @@ package org.sonar.db.rule; | |||
import com.google.common.base.Splitter; | |||
import com.google.common.collect.ImmutableSet; | |||
import java.util.Collection; | |||
import java.util.HashSet; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
@@ -179,16 +180,17 @@ public class RuleDto { | |||
return ruleDescriptionSectionDtos; | |||
} | |||
@CheckForNull | |||
public RuleDescriptionSectionDto getRuleDescriptionSectionDto(String ruleDescriptionSectionKey) { | |||
return findExistingSectionWithSameKey(ruleDescriptionSectionKey).orElse(null); | |||
} | |||
@CheckForNull | |||
public RuleDescriptionSectionDto getDefaultRuleDescriptionSection() { | |||
return findExistingSectionWithSameKey(DEFAULT_KEY).orElse(null); | |||
} | |||
public RuleDto replaceRuleDescriptionSectionDtos(Collection<RuleDescriptionSectionDto> ruleDescriptionSectionDtos) { | |||
this.ruleDescriptionSectionDtos.clear(); | |||
ruleDescriptionSectionDtos.forEach(this::addRuleDescriptionSectionDto); | |||
return this; | |||
} | |||
public RuleDto addRuleDescriptionSectionDto(RuleDescriptionSectionDto ruleDescriptionSectionDto) { | |||
checkArgument(sectionWithSameKeyShouldNotExist(ruleDescriptionSectionDto), | |||
"A section with key %s already exists", ruleDescriptionSectionDto.getKey()); |
@@ -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()); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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) { |
@@ -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); | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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/>"); | |||
} | |||
} |
@@ -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(); |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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( |