Skip to content

Commit

Permalink
PLUGINAPI-63 added mappings for impacts to plugin api when creating a…
Browse files Browse the repository at this point in the history
… rule
  • Loading branch information
lukasz-jarocki-sonarsource committed Aug 10, 2023
1 parent 3900fea commit 341a16c
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ public static RuleType convert(Collection<String> tags) {
if (tags.contains(TAG_SECURITY)) {
return RuleType.VULNERABILITY;
}
return RuleType.CODE_SMELL;
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class DefaultNewRule extends RulesDefinition.NewRule {
private String htmlDescription;
private String markdownDescription;
private String internalKey;
private String severity = Severity.MAJOR;
private String severity;
private final Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> defaultImpacts = new EnumMap<>(SoftwareQuality.class);
private boolean template;
private RuleStatus status = RuleStatus.defaultStatus();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

import static java.lang.String.format;
import static java.util.Collections.unmodifiableList;
import static org.sonar.api.rule.Severity.MAJOR;

@Immutable
public class DefaultRule extends RulesDefinition.Rule {
Expand Down Expand Up @@ -79,14 +80,11 @@ public class DefaultRule extends RulesDefinition.Rule {
this.htmlDescription = newRule.htmlDescription();
this.markdownDescription = newRule.markdownDescription();
this.internalKey = newRule.internalKey();
this.severity = newRule.severity();
this.defaultImpacts = Collections.unmodifiableMap(newRule.defaultImpacts());
this.template = newRule.template();
this.status = newRule.status();
this.debtRemediationFunction = newRule.debtRemediationFunction();
this.gapDescription = newRule.gapDescription();
this.scope = newRule.scope() == null ? RuleScope.MAIN : newRule.scope();
this.type = newRule.type() == null ? RuleTagsToTypeConverter.convert(newRule.tags()) : newRule.type();
this.cleanCodeAttribute = newRule.cleanCodeAttribute();
Set<String> tagsBuilder = new TreeSet<>(newRule.tags());
tagsBuilder.removeAll(RuleTagsToTypeConverter.RESERVED_TAGS);
Expand All @@ -101,6 +99,50 @@ public class DefaultRule extends RulesDefinition.Rule {
this.deprecatedRuleKeys = Collections.unmodifiableSet(new TreeSet<>(newRule.deprecatedRuleKeys()));
this.ruleDescriptionSections = newRule.getRuleDescriptionSections();
this.educationPrincipleKeys = Collections.unmodifiableSet(newRule.educationPrincipleKeys());

this.type = determineType(newRule);
this.severity = determineSeverity(newRule);
this.defaultImpacts = determineImpacts(newRule);
}

private static RuleType determineType(DefaultNewRule newRule) {
RuleType type = newRule.type() == null ? RuleTagsToTypeConverter.convert(newRule.tags()) : newRule.type();
if (type != null) {
return type;
}

if (shouldUseBackmapping(newRule)) {
SoftwareQuality softwareQuality = ImpactMapper.getBestImpactForBackmapping(newRule.defaultImpacts()).getKey();
return ImpactMapper.convertToRuleType(softwareQuality);
}
return RuleType.CODE_SMELL;
}

private static String determineSeverity(DefaultNewRule newRule) {
String severity = newRule.severity();
if (severity != null) {
return severity;
}

if (shouldUseBackmapping(newRule)) {
Severity impactSeverity = ImpactMapper.getBestImpactForBackmapping(newRule.defaultImpacts()).getValue();
return ImpactMapper.convertToDeprecatedSeverity(impactSeverity);
}
return MAJOR;
}

private Map<SoftwareQuality, Severity> determineImpacts(DefaultNewRule newRule) {
if (!newRule.defaultImpacts().isEmpty() || type == RuleType.SECURITY_HOTSPOT) {
return Collections.unmodifiableMap(newRule.defaultImpacts());
}
SoftwareQuality softwareQuality = ImpactMapper.convertToSoftwareQuality(type);
Severity impactSeverity = ImpactMapper.convertToImpactSeverity(severity);

return Map.of(softwareQuality, impactSeverity);
}

private static boolean shouldUseBackmapping(DefaultNewRule newRule) {
return newRule.type() == null && newRule.severity() == null && !newRule.defaultImpacts().isEmpty();
}

@Override
Expand Down Expand Up @@ -259,4 +301,5 @@ public int hashCode() {
public String toString() {
return format("[repository=%s, key=%s]", repoKey, key);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Sonar Plugin API
* Copyright (C) 2009-2023 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.api.server.rule.internal;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.sonar.api.issue.impact.Severity;
import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rules.RuleType;

import static org.sonar.api.rule.Severity.BLOCKER;
import static org.sonar.api.rule.Severity.CRITICAL;
import static org.sonar.api.rule.Severity.INFO;
import static org.sonar.api.rule.Severity.MAJOR;
import static org.sonar.api.rule.Severity.MINOR;

/**
* @deprecated since 10.1 This is only used for mapping deprecated types and severities until they are removed
*/
@Deprecated(since = "10.1")
public class ImpactMapper {

static final List<SoftwareQuality> ORDERED_SOFTWARE_QUALITIES = List.of(SoftwareQuality.MAINTAINABILITY,
SoftwareQuality.RELIABILITY, SoftwareQuality.SECURITY);

private ImpactMapper() {
// This class is designed to be static
}

public static SoftwareQuality convertToSoftwareQuality(RuleType ruleType) {
switch (ruleType) {
case CODE_SMELL:
return SoftwareQuality.MAINTAINABILITY;
case BUG:
return SoftwareQuality.RELIABILITY;
case VULNERABILITY:
return SoftwareQuality.SECURITY;
case SECURITY_HOTSPOT:
throw new IllegalStateException("Can not map Security Hotspot to Software Quality");
default:
throw new IllegalStateException("Unknown rule type");
}
}

public static RuleType convertToRuleType(SoftwareQuality softwareQuality) {
switch (softwareQuality) {
case MAINTAINABILITY:
return RuleType.CODE_SMELL;
case RELIABILITY:
return RuleType.BUG;
case SECURITY:
return RuleType.VULNERABILITY;
default:
throw new IllegalStateException("Unknown software quality");
}
}

public static String convertToDeprecatedSeverity(Severity severity) {
switch (severity) {
case HIGH:
return CRITICAL;
case MEDIUM:
return MAJOR;
case LOW:
return MINOR;
default:
throw new IllegalStateException("This severity value " + severity + " is illegal.");
}
}

public static Severity convertToImpactSeverity(String deprecatedSeverity) {
switch (deprecatedSeverity) {
case CRITICAL:
case BLOCKER:
return Severity.HIGH;
case MAJOR:
return Severity.MEDIUM;
case MINOR:
case INFO:
return Severity.LOW;
default:
throw new IllegalStateException("This old severity value " + deprecatedSeverity + " is illegal.");
}
}

/**
* This method is needed for picking the best impact on which we are going to base backmapping (getting type and severity from impact).
* As Impacts like any ordering (there is no "best" impact, all of them are equal) we just need to ensure that our choice is consistent
* to make sure we always pick the same impact when the list (map) of impacts is the same.
*/
public static Map.Entry<SoftwareQuality, Severity> getBestImpactForBackmapping(Map<SoftwareQuality, Severity> impacts) {
return impacts.entrySet()
.stream().min(Comparator.comparing(i -> ORDERED_SOFTWARE_QUALITIES.indexOf(i.getKey())))
.orElseThrow(() -> new IllegalArgumentException("There is no impact to choose from."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ public void type_is_vulnerability_if_has_tag_security() {
}

@Test
public void default_is_code_smell() {
assertThat(convert(asList("clumsy", "spring"))).isEqualTo(RuleType.CODE_SMELL);
assertThat(convert(Collections.emptyList())).isEqualTo(RuleType.CODE_SMELL);
public void default_is_null() {
assertThat(convert(asList("clumsy", "spring"))).isNull();
assertThat(convert(Collections.emptyList())).isNull();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,19 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
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;

public class DefaultRuleTest {

private static final RuleDescriptionSection RULE_DESCRIPTION_SECTION = new RuleDescriptionSectionBuilder().sectionKey("section_key").htmlContent("html desc").build();
private static final RuleDescriptionSection RULE_DESCRIPTION_SECTION_2 = new RuleDescriptionSectionBuilder().sectionKey("section_key_2").htmlContent("html desc 2").build();

private DefaultRepository repo = mock(DefaultRepository.class);

@Test
public void getters() {
DefaultRepository repo = mock(DefaultRepository.class);
DefaultNewRule rule = new DefaultNewRule("plugin", "repo", "key");

rule.setScope(RuleScope.MAIN);
Expand Down Expand Up @@ -92,6 +96,80 @@ public void getters() {
assertThat(defaultRule.educationPrincipleKeys()).containsOnly("principle_key1", "principle_key2", "principle_key3");
}

@Test
public void constructor_impact_is_set_when_type_and_severity_is_null() {
DefaultNewRule rule = new DefaultNewRule("plugin", "repo", "key");

DefaultRule defaultRule = new DefaultRule(repo, rule);

assertThat(defaultRule.type()).isEqualTo(CODE_SMELL);
assertThat(defaultRule.severity()).isEqualTo(org.sonar.api.rule.Severity.MAJOR);
assertThat(defaultRule.defaultImpacts()).containsEntry(SoftwareQuality.MAINTAINABILITY, Severity.MEDIUM);
}

@Test
public void constructor_type_and_severity_are_set_when_impact_is_defined() {
DefaultNewRule rule = new DefaultNewRule("plugin", "repo", "key");
rule.addDefaultImpact(SoftwareQuality.SECURITY, Severity.LOW);

DefaultRule defaultRule = new DefaultRule(repo, rule);

assertThat(defaultRule.type()).isEqualTo(VULNERABILITY);
assertThat(defaultRule.severity()).isEqualTo(org.sonar.api.rule.Severity.MINOR);
assertThat(defaultRule.defaultImpacts()).containsEntry(SoftwareQuality.SECURITY, Severity.LOW);
}

@Test
public void constructor_impact_is_mapped_when_type_and_severity_are_set() {
DefaultNewRule rule = new DefaultNewRule("plugin", "repo", "key");
rule.setSeverity(org.sonar.api.rule.Severity.CRITICAL);
rule.setType(BUG);

DefaultRule defaultRule = new DefaultRule(repo, rule);

assertThat(defaultRule.type()).isEqualTo(BUG);
assertThat(defaultRule.severity()).isEqualTo(org.sonar.api.rule.Severity.CRITICAL);
assertThat(defaultRule.defaultImpacts()).containsEntry(SoftwareQuality.RELIABILITY, Severity.HIGH);
}

@Test
public void constructor_impact_is_mapped_and_severity_is_major_when_only_type_is_set() {
DefaultNewRule rule = new DefaultNewRule("plugin", "repo", "key");
rule.setType(VULNERABILITY);

DefaultRule defaultRule = new DefaultRule(repo, rule);

assertThat(defaultRule.type()).isEqualTo(VULNERABILITY);
assertThat(defaultRule.severity()).isEqualTo(org.sonar.api.rule.Severity.MAJOR);
assertThat(defaultRule.defaultImpacts()).containsEntry(SoftwareQuality.SECURITY, Severity.MEDIUM);
}

@Test
public void constructor_severity_is_set_to_major_when_type_and_impact_are_defined() {
DefaultNewRule rule = new DefaultNewRule("plugin", "repo", "key");
rule.setType(VULNERABILITY);
rule.addDefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.LOW);

DefaultRule defaultRule = new DefaultRule(repo, rule);

assertThat(defaultRule.type()).isEqualTo(VULNERABILITY);
assertThat(defaultRule.severity()).isEqualTo(org.sonar.api.rule.Severity.MAJOR);
assertThat(defaultRule.defaultImpacts()).containsEntry(SoftwareQuality.MAINTAINABILITY, Severity.LOW);
}

@Test
public void constructor_type_is_set_to_code_smell_when_severity_and_impact_are_defined() {
DefaultNewRule rule = new DefaultNewRule("plugin", "repo", "key");
rule.addDefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.MEDIUM);
rule.setSeverity(org.sonar.api.rule.Severity.BLOCKER);

DefaultRule defaultRule = new DefaultRule(repo, rule);

assertThat(defaultRule.type()).isEqualTo(CODE_SMELL);
assertThat(defaultRule.severity()).isEqualTo(org.sonar.api.rule.Severity.BLOCKER);
assertThat(defaultRule.defaultImpacts()).containsEntry(SoftwareQuality.MAINTAINABILITY, Severity.MEDIUM);
}

@Test
public void to_string() {
DefaultRepository repo = mock(DefaultRepository.class);
Expand Down
Loading

0 comments on commit 341a16c

Please sign in to comment.