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;
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());
--- /dev/null
+/*
+ * 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());
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
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
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;
this.webServerRuleFinder = webServerRuleFinder;
this.uuidFactory = uuidFactory;
this.metadataIndex = metadataIndex;
+ this.ruleDescriptionSectionsGeneratorResolver = ruleDescriptionSectionsGeneratorResolver;
}
@Override
.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());
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:
}
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) {
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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/>");
+
+ }
+
+}
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)
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
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();
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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");
+ }
+
+}
--- /dev/null
+/*
+ * 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());
+ }
+
+}
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;
addIfStartupLeaderAndPluginsChanged(
RegisterMetrics.class,
RegisterQualityGates.class,
+ RuleDescriptionSectionsGeneratorResolver.class,
+ AdvancedRuleDescriptionSectionsGenerator.class,
+ LegacyHotspotRuleDescriptionSectionsGenerator.class,
+ LegacyIssueRuleDescriptionSectionsGenerator.class,
RegisterRules.class,
BuiltInQProfileLoader.class);
addIfStartupLeader(