diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76a724dd02..ba92009986 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -sonarlint-core = "10.7.1.79146" +sonarlint-core = "10.9.0.79477" sonar-java = "8.5.0.37199" sonar-javascript = "10.16.0.27621" diff --git a/src/main/java/org/sonarlint/intellij/SonarLintIcons.kt b/src/main/java/org/sonarlint/intellij/SonarLintIcons.kt index 9e2a4fb541..9494651c1a 100644 --- a/src/main/java/org/sonarlint/intellij/SonarLintIcons.kt +++ b/src/main/java/org/sonarlint/intellij/SonarLintIcons.kt @@ -82,12 +82,42 @@ object SonarLintIcons { @JvmField val FOCUS = getIcon("/images/focus.svg") + private val BUG_ICONS = mapOf( + IssueSeverity.BLOCKER to getIcon("/images/bug/bugBlocker.svg"), + IssueSeverity.CRITICAL to getIcon("/images/bug/bugHigh.svg"), + IssueSeverity.MAJOR to getIcon("/images/bug/bugMedium.svg"), + IssueSeverity.MINOR to getIcon("/images/bug/bugLow.svg"), + IssueSeverity.INFO to getIcon("/images/bug/bugInfo.svg") + ) + + private val CODE_SMELL_ICONS = mapOf( + IssueSeverity.BLOCKER to getIcon("/images/codeSmell/codeSmellBlocker.svg"), + IssueSeverity.CRITICAL to getIcon("/images/codeSmell/codeSmellHigh.svg"), + IssueSeverity.MAJOR to getIcon("/images/codeSmell/codeSmellMedium.svg"), + IssueSeverity.MINOR to getIcon("/images/codeSmell/codeSmellLow.svg"), + IssueSeverity.INFO to getIcon("/images/codeSmell/codeSmellInfo.svg") + ) + + private val VULNERABILITY_ICONS = mapOf( + IssueSeverity.BLOCKER to getIcon("/images/vulnerability/vulnerabilityBlocker.svg"), + IssueSeverity.CRITICAL to getIcon("/images/vulnerability/vulnerabilityHigh.svg"), + IssueSeverity.MAJOR to getIcon("/images/vulnerability/vulnerabilityMedium.svg"), + IssueSeverity.MINOR to getIcon("/images/vulnerability/vulnerabilityLow.svg"), + IssueSeverity.INFO to getIcon("/images/vulnerability/vulnerabilityInfo.svg") + ) + + private val PROBABILITY_ICONS = mapOf( + VulnerabilityProbability.HIGH to getIcon("/images/hotspot/hotspotHigh.svg"), + VulnerabilityProbability.MEDIUM to getIcon("/images/hotspot/hotspotMedium.svg"), + VulnerabilityProbability.LOW to getIcon("/images/hotspot/hotspotLow.svg") + ) + private val SEVERITY_ICONS = mapOf( - IssueSeverity.BLOCKER to getIcon("/images/severity/blocker.svg"), - IssueSeverity.CRITICAL to getIcon("/images/severity/critical.svg"), - IssueSeverity.INFO to getIcon("/images/severity/info.svg"), - IssueSeverity.MAJOR to getIcon("/images/severity/major.svg"), - IssueSeverity.MINOR to getIcon("/images/severity/minor.svg") + IssueSeverity.BLOCKER to getIcon("/images/impact/blocker.svg"), + IssueSeverity.CRITICAL to getIcon("/images/impact/high.svg"), + IssueSeverity.MAJOR to getIcon("/images/impact/medium.svg"), + IssueSeverity.MINOR to getIcon("/images/impact/low.svg"), + IssueSeverity.INFO to getIcon("/images/impact/info.svg") ) private val IMPACT_ICONS = mapOf( @@ -98,17 +128,46 @@ object SonarLintIcons { ImpactSeverity.INFO to getIcon("/images/impact/info.svg") ) - private val TYPE_ICONS = mapOf( - RuleType.BUG to getIcon("/images/type/bug.svg"), - RuleType.CODE_SMELL to getIcon("/images/type/codeSmell.svg"), - RuleType.VULNERABILITY to getIcon("/images/type/vulnerability.svg"), - RuleType.SECURITY_HOTSPOT to getIcon("/images/type/hotspot.svg") + val backgroundColorsByVulnerabilityProbability = mapOf( + VulnerabilityProbability.HIGH to JBColor(Color(254, 243, 242), Color(253, 162, 155, 20)), + VulnerabilityProbability.MEDIUM to JBColor(Color(255, 240, 235), Color(254, 150, 75, 20)), + VulnerabilityProbability.LOW to JBColor(Color(252, 245, 228), Color(250, 220, 121, 20)) ) - private val PROBABILITY_ICONS = mapOf( - VulnerabilityProbability.HIGH to getIcon("/images/type/hotspotHigh.svg"), - VulnerabilityProbability.MEDIUM to getIcon("/images/type/hotspotMedium.svg"), - VulnerabilityProbability.LOW to getIcon("/images/type/hotspotLow.svg") + val fontColorsByVulnerabilityProbability = mapOf( + VulnerabilityProbability.HIGH to JBColor(Color(180, 35, 24), Color(253, 162, 155)), + VulnerabilityProbability.MEDIUM to JBColor(Color(147, 55, 13), Color(254, 150, 75)), + VulnerabilityProbability.LOW to JBColor(Color(140, 94, 30), Color(250, 220, 121)) + ) + + val borderColorsByVulnerabilityProbability = mapOf( + VulnerabilityProbability.HIGH to JBColor(Color(217, 44, 32), Color(253, 162, 155)), + VulnerabilityProbability.MEDIUM to JBColor(Color(254, 150, 75), Color(254, 150, 75)), + VulnerabilityProbability.LOW to JBColor(Color(250, 220, 121), Color(250, 220, 121)) + ) + + val backgroundColorsBySeverity = mapOf( + IssueSeverity.BLOCKER to JBColor(Color(254, 228, 226), Color(128, 27, 20, 20)), + IssueSeverity.CRITICAL to JBColor(Color(254, 243, 242), Color(253, 162, 155, 20)), + IssueSeverity.MAJOR to JBColor(Color(255, 240, 235), Color(254, 150, 75, 20)), + IssueSeverity.MINOR to JBColor(Color(252, 245, 228), Color(250, 220, 121, 20)), + IssueSeverity.INFO to JBColor(Color(245, 251, 255), Color(143, 202, 234, 20)) + ) + + val fontColorsBySeverity = mapOf( + IssueSeverity.BLOCKER to JBColor(Color(128, 27, 20), Color(249, 112, 102)), + IssueSeverity.CRITICAL to JBColor(Color(180, 35, 24), Color(253, 162, 155)), + IssueSeverity.MAJOR to JBColor(Color(147, 55, 13), Color(254, 150, 75)), + IssueSeverity.MINOR to JBColor(Color(140, 94, 30), Color(250, 220, 121)), + IssueSeverity.INFO to JBColor(Color(49, 107, 146), Color(143, 202, 234)) + ) + + val borderColorsBySeverity = mapOf( + IssueSeverity.BLOCKER to JBColor(Color(128, 27, 20), Color(249, 112, 102)), + IssueSeverity.CRITICAL to JBColor(Color(217, 44, 32), Color(253, 162, 155)), + IssueSeverity.MAJOR to JBColor(Color(254, 150, 75), Color(254, 150, 75)), + IssueSeverity.MINOR to JBColor(Color(250, 220, 121), Color(250, 220, 121)), + IssueSeverity.INFO to JBColor(Color(143, 202, 234), Color(143, 202, 234)) ) val backgroundColorsByImpact = mapOf( @@ -144,11 +203,6 @@ object SonarLintIcons { return SEVERITY_ICONS[severity]!! } - @JvmStatic - fun type(type: RuleType): Icon { - return TYPE_ICONS[type]!! - } - @JvmStatic fun impact(impact: ImpactSeverity): Icon { return IMPACT_ICONS[impact]!! @@ -163,4 +217,15 @@ object SonarLintIcons { fun hotspotTypeWithProbability(vulnerabilityProbability: VulnerabilityProbability): Icon { return PROBABILITY_ICONS[vulnerabilityProbability]!! } + + @JvmStatic + fun getIconForTypeAndSeverity(type: RuleType, severity: IssueSeverity): Icon { + return when (type) { + RuleType.BUG -> BUG_ICONS[severity]!! + RuleType.CODE_SMELL -> CODE_SMELL_ICONS[severity]!! + RuleType.VULNERABILITY -> VULNERABILITY_ICONS[severity]!! + RuleType.SECURITY_HOTSPOT -> throw UnsupportedOperationException("Security Hotspots do not support severity") + } + } + } diff --git a/src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt b/src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt index 5664968fc9..04e48cad3f 100644 --- a/src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt +++ b/src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt @@ -515,7 +515,7 @@ object SonarLintIntelliJClient : SonarLintRpcClientDelegate { } val connection = connectionOpt.get() return if (connection.token != null) { - Either.forLeft(TokenDto(connection.token)) + Either.forLeft(TokenDto(connection.token!!)) } else { Either.forRight(UsernamePasswordDto(connection.login, connection.password)) } diff --git a/src/main/java/org/sonarlint/intellij/actions/ReopenIssueAction.kt b/src/main/java/org/sonarlint/intellij/actions/ReopenIssueAction.kt index 1d8d87be0c..5d20f4a6d4 100644 --- a/src/main/java/org/sonarlint/intellij/actions/ReopenIssueAction.kt +++ b/src/main/java/org/sonarlint/intellij/actions/ReopenIssueAction.kt @@ -69,7 +69,7 @@ class ReopenIssueAction(private var issue: LiveIssue? = null) : AbstractSonarAct var serverKey: String? = null if (issue is LiveIssue) { - serverKey = issue.getServerKey() ?: issue.id.toString() + serverKey = issue.getServerKey() ?: issue.getId().toString() } else if (issue is LocalTaintVulnerability) { serverKey = issue.key() } diff --git a/src/main/java/org/sonarlint/intellij/config/global/rules/RuleConfigurationPanel.java b/src/main/java/org/sonarlint/intellij/config/global/rules/RuleConfigurationPanel.java index e47636a2dc..f28599350b 100644 --- a/src/main/java/org/sonarlint/intellij/config/global/rules/RuleConfigurationPanel.java +++ b/src/main/java/org/sonarlint/intellij/config/global/rules/RuleConfigurationPanel.java @@ -46,6 +46,7 @@ import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBLoadingPanel; import com.intellij.ui.components.JBPanel; +import com.intellij.ui.components.JBPanelWithEmptyText; import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.JBTextArea; import com.intellij.ui.components.fields.ExpandableTextField; @@ -139,6 +140,7 @@ public class RuleConfigurationPanel implements Disposable, ConfigurationPanel(new BorderLayout()); + private void createUIComponents() { + rulePanel = new JBPanelWithEmptyText(new BorderLayout()); + rulePanel.withEmptyText(EMPTY_HTML); rulePanel.setBorder(JBUI.Borders.emptyLeft(5)); ruleHeaderPanel = new RuleHeaderPanel(this); @@ -429,17 +432,27 @@ protected void hyperlinkActivated(HyperlinkEvent e) { labelPanel.add(configureRuleLabel); labelPanel.add(ruleServerLabel); panel.add(labelPanel, BorderLayout.NORTH); - return panel; } private void initOptionsAndDescriptionPanel() { myParamsPanel.removeAll(); ruleDescription.removeAll(); - ruleHeaderPanel.showMessage(EMPTY_HTML); + setEmptyDisplay(EMPTY_HTML); myParamsPanel.validate(); myParamsPanel.repaint(); } + private void setEmptyDisplay(String msg) { + rulePanel.withEmptyText(msg); + ruleHeaderPanel.setVisible(false); + ruleDescription.setVisible(false); + } + + private void unsetEmptyDisplay() { + ruleHeaderPanel.setVisible(true); + ruleDescription.setVisible(true); + } + private JScrollPane initTreeScrollPane() { // create tree table model = new RulesTreeTableModel(new RulesTreeNode.Root()); @@ -494,9 +507,8 @@ private void updateParamsAndDescriptionPanel() { if (singleNode != null) { updateParamsAndDescriptionPanel(singleNode); } else { - ruleHeaderPanel.showMessage("Multiple rules are selected."); + setEmptyDisplay("Multiple rules are selected."); myParamsPanel.removeAll(); - } } else { initOptionsAndDescriptionPanel(); @@ -507,8 +519,8 @@ private void updateParamsAndDescriptionPanel() { } private void updateParamsAndDescriptionPanel(RulesTreeNode.Rule singleNode) { - ruleHeaderPanel.updateForRuleConfiguration(singleNode.getKey(), singleNode.type(), singleNode.severity(), singleNode.attribute(), - singleNode.impacts()); + ruleHeaderPanel.updateForRuleConfiguration(singleNode.attribute(), singleNode.impacts(), singleNode.getKey()); + unsetEmptyDisplay(); var fileType = RuleLanguages.Companion.findFileTypeByRuleLanguage(singleNode.language()); runOnPooledThread(project, () -> diff --git a/src/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeNode.java b/src/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeNode.java index a502ea42d9..f5548444ae 100644 --- a/src/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeNode.java +++ b/src/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeNode.java @@ -32,9 +32,7 @@ import org.sonarsource.sonarlint.core.client.utils.ImpactSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDefinitionDto; -import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; -import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; public abstract class RulesTreeNode extends DefaultMutableTreeNode { @@ -114,7 +112,7 @@ public Rule(RuleDefinitionDto details, boolean activated, Map no this.details = details; this.activated = activated; this.nonDefaultParams = new HashMap<>(nonDefaultParams); - var highestQualityImpact = details.getDefaultImpacts().stream().max(Comparator.comparing(ImpactDto::getImpactSeverity)); + var highestQualityImpact = details.getSoftwareImpacts().stream().max(Comparator.comparing(ImpactDto::getImpactSeverity)); this.highestQuality = highestQualityImpact.map(ImpactDto::getSoftwareQuality).orElse(null); this.highestImpact = highestQualityImpact.map(ImpactDto::getImpactSeverity).map(ImpactSeverity::fromDto).orElse(null); } @@ -137,15 +135,7 @@ public CleanCodeAttribute attribute() { } public List impacts() { - return details.getDefaultImpacts(); - } - - public IssueSeverity severity() { - return details.getSeverity(); - } - - public RuleType type() { - return details.getType(); + return details.getSoftwareImpacts(); } public Language language() { diff --git a/src/main/java/org/sonarlint/intellij/config/project/ModuleBindingPanel.kt b/src/main/java/org/sonarlint/intellij/config/project/ModuleBindingPanel.kt index 1281e40eb6..0eef1c6944 100644 --- a/src/main/java/org/sonarlint/intellij/config/project/ModuleBindingPanel.kt +++ b/src/main/java/org/sonarlint/intellij/config/project/ModuleBindingPanel.kt @@ -108,7 +108,7 @@ class ModuleBindingPanel(private val project: Project, currentConnectionSupplier projectKeyTextField.emptyText.text = "Input project key or search one" val projectKeyLabel = JLabel("Project key:") projectKeyLabel.labelFor = projectKeyTextField - val insets = JBUI.insets(2, 0, 0, 0) + val insets = JBUI.insetsTop(2) moduleBindingDetailsPanel.add( projectKeyLabel, GridBagConstraints( 0, 0, 1, 1, 0.0, 0.0, diff --git a/src/main/java/org/sonarlint/intellij/core/BackendService.kt b/src/main/java/org/sonarlint/intellij/core/BackendService.kt index cd5dbf0f2b..25759d3d42 100644 --- a/src/main/java/org/sonarlint/intellij/core/BackendService.kt +++ b/src/main/java/org/sonarlint/intellij/core/BackendService.kt @@ -136,12 +136,12 @@ import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SslConfigu import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.TelemetryClientConstantAttributesDto import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.AddIssueCommentParams import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ChangeIssueStatusParams +import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.GetEffectiveIssueDetailsParams +import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.GetEffectiveIssueDetailsResponse import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ReopenIssueParams import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ReopenIssueResponse import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ResolutionStatus import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.GetNewCodeDefinitionParams -import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetEffectiveRuleDetailsParams -import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetEffectiveRuleDetailsResponse import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetStandaloneRuleDescriptionParams import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetStandaloneRuleDescriptionResponse import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ListAllStandaloneRulesDefinitionsResponse @@ -411,7 +411,7 @@ class BackendService : Disposable { return System.getProperty(propertyName)?.let { try { Duration.ofMinutes(it.toLong()) - } catch (e: NumberFormatException) { + } catch (_: NumberFormatException) { try { Duration.parse(it) } catch (d: DateTimeParseException) { @@ -445,7 +445,7 @@ class BackendService : Disposable { } fun getAllProjects(server: ServerConnection): CompletableFuture { - val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token)) } + val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token!!)) } ?: Either.forRight(UsernamePasswordDto(server.login, server.password)) val params: GetAllProjectsParams = if (server.isSonarCloud) { GetAllProjectsParams(TransientSonarCloudConnectionDto(server.organizationKey, credentials)) @@ -623,16 +623,10 @@ class BackendService : Disposable { } } - fun getActiveRuleDetails(module: Module, ruleKey: String, contextKey: String?): CompletableFuture { + fun getIssueDetails(module: Module, issueId: UUID): CompletableFuture { val moduleId = moduleId(module) return requestFromBackend { - it.rulesService.getEffectiveRuleDetails( - GetEffectiveRuleDetailsParams( - moduleId, - ruleKey, - contextKey - ) - ) + it.issueService.getEffectiveIssueDetails(GetEffectiveIssueDetailsParams(moduleId, issueId)) } } @@ -741,7 +735,7 @@ class BackendService : Disposable { } fun checkSmartNotificationsSupported(server: ServerConnection): CompletableFuture { - val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token)) } + val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token!!)) } ?: Either.forRight(UsernamePasswordDto(server.login, server.password)) val params: CheckSmartNotificationsSupportedParams = if (server.isSonarCloud) { CheckSmartNotificationsSupportedParams(TransientSonarCloudConnectionDto(server.organizationKey, credentials)) @@ -752,7 +746,7 @@ class BackendService : Disposable { } fun validateConnection(server: ServerConnection): CompletableFuture { - val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token)) } + val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token!!)) } ?: Either.forRight(UsernamePasswordDto(server.login, server.password)) val params: ValidateConnectionParams = if (server.isSonarCloud) { ValidateConnectionParams(TransientSonarCloudConnectionDto(server.organizationKey, credentials)) @@ -763,14 +757,14 @@ class BackendService : Disposable { } fun listUserOrganizations(server: ServerConnection): CompletableFuture { - val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token)) } + val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token!!)) } ?: Either.forRight(UsernamePasswordDto(server.login, server.password)) val params = ListUserOrganizationsParams(credentials) return requestFromBackend { it.connectionService.listUserOrganizations(params) } } fun getOrganization(server: ServerConnection, organizationKey: String): CompletableFuture { - val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token)) } + val credentials: Either = server.token?.let { Either.forLeft(TokenDto(server.token!!)) } ?: Either.forRight(UsernamePasswordDto(server.login, server.password)) val params = GetOrganizationParams(credentials, organizationKey) return requestFromBackend { it.connectionService.getOrganization(params) } @@ -881,7 +875,7 @@ class BackendService : Disposable { } .thenApplyAsync { response -> response.fileStatuses.filterValues { it.isExcluded }.keys.mapNotNull { filesByUri[it] } } .join() - } catch (e: CancellationException) { + } catch (_: CancellationException) { SonarLintConsole.get(module.project).debug("The request to retrieve file exclusions has been canceled") emptyList() } catch (e: Exception) { @@ -921,19 +915,20 @@ class BackendService : Disposable { event.filter { it.type != ModuleFileEvent.Type.DELETED }.mapNotNull { val relativePath = getRelativePathForAnalysis(module, it.virtualFile) ?: return@mapNotNull null val forcedLanguage = contributedLanguages[it.virtualFile]?.let { fl -> Language.valueOf(fl.name) } - val uri = VirtualFileUtils.toURI(it.virtualFile) - computeReadActionSafely(it.virtualFile, module.project) { - ClientFileDto( - uri, - Paths.get(relativePath), - moduleId, - isTestSources(it.virtualFile, module.project), - it.getEncoding(module.project).toString(), - Paths.get(it.virtualFile.path), - if (FileUtilRt.isTooLarge(it.virtualFile.length)) null else getFileContent(it.virtualFile), - forcedLanguage, - true - ) + VirtualFileUtils.toURI(it.virtualFile)?.let { uri -> + computeReadActionSafely(it.virtualFile, module.project) { + ClientFileDto( + uri, + Paths.get(relativePath), + moduleId, + isTestSources(it.virtualFile, module.project), + it.getEncoding(module.project).toString(), + Paths.get(it.virtualFile.path), + if (FileUtilRt.isTooLarge(it.virtualFile.length)) null else getFileContent(it.virtualFile), + forcedLanguage, + true + ) + } } } } diff --git a/src/main/java/org/sonarlint/intellij/finding/Finding.kt b/src/main/java/org/sonarlint/intellij/finding/Finding.kt index 88a84dcc89..07f9b7d15a 100644 --- a/src/main/java/org/sonarlint/intellij/finding/Finding.kt +++ b/src/main/java/org/sonarlint/intellij/finding/Finding.kt @@ -20,32 +20,26 @@ package org.sonarlint.intellij.finding import com.intellij.openapi.vfs.VirtualFile +import java.util.UUID import org.sonarsource.sonarlint.core.client.utils.CleanCodeAttribute import org.sonarsource.sonarlint.core.client.utils.ImpactSeverity import org.sonarsource.sonarlint.core.client.utils.SoftwareQuality +import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType interface Finding { - fun getCleanCodeAttribute(): CleanCodeAttribute? - - fun getImpacts(): Map + fun getId(): UUID + fun getCleanCodeAttribute(): CleanCodeAttribute? + fun getImpacts(): List fun getHighestQuality(): SoftwareQuality? - fun getHighestImpact(): ImpactSeverity? - fun getServerKey(): String? - fun getRuleKey(): String - - fun getType(): RuleType - + fun getType(): RuleType? fun getRuleDescriptionContextKey(): String? - fun file(): VirtualFile? - fun isValid(): Boolean - fun isOnNewCode(): Boolean fun isResolved(): Boolean diff --git a/src/main/java/org/sonarlint/intellij/finding/Issue.kt b/src/main/java/org/sonarlint/intellij/finding/Issue.kt index 422dc2e24b..eab15abc09 100644 --- a/src/main/java/org/sonarlint/intellij/finding/Issue.kt +++ b/src/main/java/org/sonarlint/intellij/finding/Issue.kt @@ -19,10 +19,7 @@ */ package org.sonarlint.intellij.finding -import java.util.UUID - interface Issue : Finding { - fun getId(): UUID fun resolve() fun reopen() } diff --git a/src/main/java/org/sonarlint/intellij/finding/LiveFinding.java b/src/main/java/org/sonarlint/intellij/finding/LiveFinding.java index 933a37bd51..a18cac363f 100644 --- a/src/main/java/org/sonarlint/intellij/finding/LiveFinding.java +++ b/src/main/java/org/sonarlint/intellij/finding/LiveFinding.java @@ -26,22 +26,22 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.DocumentUtil; import java.time.Instant; +import java.util.Collections; +import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.client.utils.CleanCodeAttribute; import org.sonarsource.sonarlint.core.client.utils.ImpactSeverity; import org.sonarsource.sonarlint.core.client.utils.SoftwareQuality; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; -import static org.apache.commons.codec.digest.DigestUtils.md5Hex; import static org.sonarlint.intellij.common.ui.ReadActionUtils.computeReadActionSafely; public abstract class LiveFinding implements Finding { @@ -52,24 +52,21 @@ public abstract class LiveFinding implements Finding { private final Module module; private final RangeMarker range; private final VirtualFile virtualFile; - private final String textRangeHashString; private final String message; private final String ruleKey; private final boolean isOnNewCode; - private final FindingContext context; private final List quickFixes; private final String ruleDescriptionContextKey; private final CleanCodeAttribute cleanCodeAttribute; - private final Map impacts; - // tracked fields (mutable) - private IssueSeverity severity; + private final List impacts; + private final IssueSeverity severity; + private final SoftwareQuality highestQuality; + private final ImpactSeverity highestImpact; private Instant introductionDate; private String serverFindingKey; private boolean resolved; - private SoftwareQuality highestQuality = null; - private ImpactSeverity highestImpact = null; protected LiveFinding(Module module, RaisedFindingDto finding, VirtualFile virtualFile, @Nullable RangeMarker range, @Nullable FindingContext context, List quickFixes) { @@ -79,7 +76,6 @@ protected LiveFinding(Module module, RaisedFindingDto finding, VirtualFile virtu this.range = range; this.message = finding.getPrimaryMessage(); this.ruleKey = finding.getRuleKey(); - this.severity = finding.getSeverity(); this.virtualFile = virtualFile; this.uid = UID_GEN.getAndIncrement(); this.context = context; @@ -89,26 +85,29 @@ protected LiveFinding(Module module, RaisedFindingDto finding, VirtualFile virtu this.isOnNewCode = finding.isOnNewCode(); this.resolved = finding.isResolved(); - this.cleanCodeAttribute = CleanCodeAttribute.fromDto(finding.getCleanCodeAttribute()); - this.impacts = finding.getImpacts().stream().map(e -> Map.entry(SoftwareQuality.fromDto(e.getSoftwareQuality()), ImpactSeverity.fromDto(e.getImpactSeverity()))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - var highestQualityImpact = getImpacts().entrySet().stream().max(Map.Entry.comparingByValue()); - this.highestQuality = highestQualityImpact.map(Map.Entry::getKey).orElse(null); - this.highestImpact = highestQualityImpact.map(Map.Entry::getValue).orElse(null); - - if (range != null) { - var document = computeReadActionSafely(virtualFile, range::getDocument); - if (document != null) { - var lineContent = document.getText(new TextRange(range.getStartOffset(), range.getEndOffset())); - this.textRangeHashString = md5Hex(lineContent.replaceAll("[\\s]", "")); + if (finding.getSeverityMode().isLeft()) { + this.severity = finding.getSeverityMode().getLeft().getSeverity(); + this.cleanCodeAttribute = null; + this.impacts = Collections.emptyList(); + this.highestQuality = null; + this.highestImpact = null; + } else { + this.severity = null; + this.cleanCodeAttribute = CleanCodeAttribute.fromDto(finding.getSeverityMode().getRight().getCleanCodeAttribute()); + this.impacts = finding.getSeverityMode().getRight().getImpacts(); + // Is empty for Security Hotspots + if (!impacts.isEmpty()) { + var highestQualityImpact = Collections.max(impacts, Comparator.comparing(ImpactDto::getImpactSeverity)); + this.highestQuality = SoftwareQuality.fromDto(highestQualityImpact.getSoftwareQuality()); + this.highestImpact = ImpactSeverity.fromDto(highestQualityImpact.getImpactSeverity()); } else { - this.textRangeHashString = null; + this.highestQuality = null; + this.highestImpact = null; } - } else { - this.textRangeHashString = null; } } + @NotNull public UUID getId() { return getBackendId(); } @@ -117,7 +116,6 @@ public Module getModule() { return module; } - @CheckForNull public UUID getBackendId() { return backendId; } @@ -135,10 +133,6 @@ public String getMessage() { return message; } - public String getTextRangeHashString() { - return textRangeHashString; - } - @NotNull @Override public String getRuleKey() { @@ -187,6 +181,7 @@ public Project project() { return module.getProject(); } + @Nullable public IssueSeverity getUserSeverity() { return severity; } @@ -205,6 +200,7 @@ public boolean isResolved() { return resolved; } + @Nullable @Override public CleanCodeAttribute getCleanCodeAttribute() { return this.cleanCodeAttribute; @@ -212,7 +208,7 @@ public CleanCodeAttribute getCleanCodeAttribute() { @NotNull @Override - public Map getImpacts() { + public List getImpacts() { return this.impacts; } @@ -237,7 +233,7 @@ public List quickFixes() { return quickFixes; } - @org.jetbrains.annotations.Nullable + @Nullable @Override public String getRuleDescriptionContextKey() { return ruleDescriptionContextKey; @@ -247,11 +243,13 @@ public boolean isOnNewCode() { return isOnNewCode; } + @Nullable @Override public SoftwareQuality getHighestQuality() { return highestQuality; } + @Nullable @Override public ImpactSeverity getHighestImpact() { return highestImpact; diff --git a/src/main/java/org/sonarlint/intellij/finding/hotspot/LiveSecurityHotspot.java b/src/main/java/org/sonarlint/intellij/finding/hotspot/LiveSecurityHotspot.java index b171bb41d0..330802ca38 100644 --- a/src/main/java/org/sonarlint/intellij/finding/hotspot/LiveSecurityHotspot.java +++ b/src/main/java/org/sonarlint/intellij/finding/hotspot/LiveSecurityHotspot.java @@ -24,17 +24,15 @@ import com.intellij.openapi.vfs.VirtualFile; import java.util.Collections; import java.util.List; -import java.util.Map; import javax.annotation.Nullable; import org.jetbrains.annotations.NotNull; import org.sonarlint.intellij.finding.FindingContext; import org.sonarlint.intellij.finding.LiveFinding; import org.sonarlint.intellij.finding.QuickFix; import org.sonarsource.sonarlint.core.client.utils.CleanCodeAttribute; -import org.sonarsource.sonarlint.core.client.utils.ImpactSeverity; -import org.sonarsource.sonarlint.core.client.utils.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.VulnerabilityProbability; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; @@ -77,8 +75,8 @@ public CleanCodeAttribute getCleanCodeAttribute() { @NotNull @Override - public Map getImpacts() { - return Collections.emptyMap(); + public List getImpacts() { + return Collections.emptyList(); } @NotNull diff --git a/src/main/java/org/sonarlint/intellij/finding/issue/LiveIssue.java b/src/main/java/org/sonarlint/intellij/finding/issue/LiveIssue.java index 759fd6fba8..b5005965f8 100644 --- a/src/main/java/org/sonarlint/intellij/finding/issue/LiveIssue.java +++ b/src/main/java/org/sonarlint/intellij/finding/issue/LiveIssue.java @@ -23,8 +23,8 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.vfs.VirtualFile; import java.util.List; +import javax.annotation.CheckForNull; import javax.annotation.Nullable; -import org.jetbrains.annotations.NotNull; import org.sonarlint.intellij.finding.FindingContext; import org.sonarlint.intellij.finding.Issue; import org.sonarlint.intellij.finding.LiveFinding; @@ -34,6 +34,7 @@ public class LiveIssue extends LiveFinding implements Issue { + @Nullable private final RuleType type; public LiveIssue(Module module, RaisedIssueDto issue, VirtualFile virtualFile, List quickFixes) { @@ -42,10 +43,14 @@ public LiveIssue(Module module, RaisedIssueDto issue, VirtualFile virtualFile, L public LiveIssue(Module module, RaisedIssueDto issue, VirtualFile virtualFile, @Nullable RangeMarker range, @Nullable FindingContext context, List quickFixes) { super(module, issue, virtualFile, range, context, quickFixes); - this.type = issue.getType(); + if (issue.getSeverityMode().isLeft()) { + this.type = issue.getSeverityMode().getLeft().getType(); + } else { + this.type = null; + } } - @NotNull + @CheckForNull @Override public RuleType getType() { return type; diff --git a/src/main/java/org/sonarlint/intellij/finding/issue/vulnerabilities/LocalTaintVulnerability.kt b/src/main/java/org/sonarlint/intellij/finding/issue/vulnerabilities/LocalTaintVulnerability.kt index 83a21c8fd9..626ed1e54e 100644 --- a/src/main/java/org/sonarlint/intellij/finding/issue/vulnerabilities/LocalTaintVulnerability.kt +++ b/src/main/java/org/sonarlint/intellij/finding/issue/vulnerabilities/LocalTaintVulnerability.kt @@ -21,6 +21,7 @@ package org.sonarlint.intellij.finding.issue.vulnerabilities import com.intellij.openapi.module.Module import java.time.Instant +import java.util.Collections import java.util.UUID import org.sonarlint.intellij.finding.Flow import org.sonarlint.intellij.finding.Issue @@ -29,6 +30,7 @@ import org.sonarlint.intellij.finding.issue.LiveIssue import org.sonarsource.sonarlint.core.client.utils.CleanCodeAttribute import org.sonarsource.sonarlint.core.client.utils.ImpactSeverity import org.sonarsource.sonarlint.core.client.utils.SoftwareQuality +import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType @@ -36,15 +38,18 @@ import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType class LocalTaintVulnerability(val module: Module?, private val primaryLocation: Location, val flows: List, private val remoteTaintVulnerability: TaintVulnerabilityDto, private var resolved: Boolean): Issue { - private val highestQuality = getImpacts().entries.stream().max(java.util.Map.Entry.comparingByValue()).map { it.key }.orElse(null) - private val highestImpact = getImpacts().entries.stream().max(java.util.Map.Entry.comparingByValue()).map { it.value }.orElse(null) + private val highestQuality = + SoftwareQuality.fromDto(Collections.max(getImpacts(), Comparator.comparing(ImpactDto::getImpactSeverity)).softwareQuality) + private val highestImpact = + ImpactSeverity.fromDto(Collections.max(getImpacts(), Comparator.comparing(ImpactDto::getImpactSeverity)).impactSeverity) fun rangeMarker() = primaryLocation.range override fun file() = primaryLocation.file fun key(): String = remoteTaintVulnerability.sonarServerKey fun message(): String = remoteTaintVulnerability.message ?: "" fun creationDate(): Instant = remoteTaintVulnerability.introductionDate - fun severity(): IssueSeverity = remoteTaintVulnerability.severity + fun severity(): IssueSeverity? = + if (remoteTaintVulnerability.severityMode.isLeft) remoteTaintVulnerability.severityMode.left.severity else null override fun getId(): UUID = remoteTaintVulnerability.id override fun resolve() { resolved = true @@ -54,15 +59,19 @@ class LocalTaintVulnerability(val module: Module?, private val primaryLocation: resolved = false } - override fun getCleanCodeAttribute(): CleanCodeAttribute? = remoteTaintVulnerability.cleanCodeAttribute?.let { CleanCodeAttribute.valueOf(it.name) } - override fun getImpacts(): Map = remoteTaintVulnerability.impacts.mapKeys { SoftwareQuality.valueOf(it.key.name) }.mapValues { ImpactSeverity.valueOf(it.value.name) } + override fun getCleanCodeAttribute(): CleanCodeAttribute? = + if (remoteTaintVulnerability.severityMode.isRight) CleanCodeAttribute.fromDto(remoteTaintVulnerability.severityMode.right.cleanCodeAttribute) else null + + override fun getImpacts(): List = + if (remoteTaintVulnerability.severityMode.isRight) remoteTaintVulnerability.severityMode.right.impacts else emptyList() override fun getHighestQuality(): SoftwareQuality? = highestQuality override fun getHighestImpact(): ImpactSeverity? = highestImpact override fun isResolved() = resolved override fun getServerKey() = key() override fun getRuleKey(): String = remoteTaintVulnerability.ruleKey - override fun getType(): RuleType = remoteTaintVulnerability.type + override fun getType(): RuleType? = + if (remoteTaintVulnerability.severityMode.isLeft) remoteTaintVulnerability.severityMode.left.type else null override fun isValid() = file()?.isValid == true && rangeMarker()?.isValid == true override fun isOnNewCode() = remoteTaintVulnerability.isOnNewCode diff --git a/src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java b/src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java index 8d26b05dfb..33560970e9 100644 --- a/src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java +++ b/src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java @@ -217,7 +217,7 @@ public void updateOnSelect(@Nullable LiveFinding issue, Show } runOnUiThread(project, - () -> findingDetailsPanel.showServerOnlyIssue(showFinding.getModule(), showFinding.getFile(), showFinding.getRuleKey(), rangeMarker, showFinding.getFlows(), + () -> findingDetailsPanel.showServerOnlyIssue(showFinding.getModule(), showFinding.getFile(), showFinding.getFindingKey(), rangeMarker, showFinding.getFlows(), showFinding.getFlowMessage())); }); } diff --git a/src/main/java/org/sonarlint/intellij/ui/FindingDetailsPanel.kt b/src/main/java/org/sonarlint/intellij/ui/FindingDetailsPanel.kt index a055e7a6d1..6d35f912cb 100644 --- a/src/main/java/org/sonarlint/intellij/ui/FindingDetailsPanel.kt +++ b/src/main/java/org/sonarlint/intellij/ui/FindingDetailsPanel.kt @@ -26,6 +26,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.JBTabbedPane +import java.util.UUID import org.sonarlint.intellij.common.util.SonarLintUtils import org.sonarlint.intellij.editor.EditorDecorator import org.sonarlint.intellij.finding.Flow @@ -58,7 +59,7 @@ class FindingDetailsPanel(private val project: Project, parentDisposable: Dispos val model = flowsTreeBuilder.createModel() flowsTree = FlowsTree(project, model) flowsTreeBuilder.clearFlows() - flowsTree.emptyText.setText("No $findingKindText selected") + flowsTree.emptyText.text = "No $findingKindText selected" } private fun createTabs(parentDisposable: Disposable) { @@ -73,24 +74,32 @@ class FindingDetailsPanel(private val project: Project, parentDisposable: Dispos } fun show(liveFinding: LiveFinding) { - rulePanel.setSelectedFinding(liveFinding.module, liveFinding, liveFinding.getRuleKey(), liveFinding.getRuleDescriptionContextKey()) + rulePanel.setSelectedFinding(liveFinding.module, liveFinding, liveFinding.getId()) flowsTreeBuilder.populateForFinding(liveFinding) SonarLintUtils.getService(project, EditorDecorator::class.java).highlightFinding(liveFinding) - flowsTree.emptyText.setText("Selected $findingKindText doesn't have flows") + flowsTree.emptyText.text = "Selected $findingKindText doesn't have flows" flowsTree.expandAll() } - fun showServerOnlyIssue(module: Module, file: VirtualFile, ruleKey: String, range: RangeMarker, flows: MutableList, flowMessage: String) { - rulePanel.setSelectedFinding(module, null, ruleKey, null) + fun showServerOnlyIssue( + module: Module, + file: VirtualFile, + issueKey: String, + range: RangeMarker, + flows: MutableList, + flowMessage: String + ) { + val issueId = UUID.fromString(issueKey) + rulePanel.setSelectedFinding(module, null, issueId) flowsTreeBuilder.populateForFinding(file, range, flowMessage, flows) SonarLintUtils.getService(project, EditorDecorator::class.java).highlightRange(range) - flowsTree.emptyText.setText("Selected $findingKindText doesn't have flows") + flowsTree.emptyText.text = "Selected $findingKindText doesn't have flows" flowsTree.expandAll() } fun clear() { flowsTreeBuilder.clearFlows() - flowsTree.emptyText.setText("No $findingKindText selected") + flowsTree.emptyText.text = "No $findingKindText selected" rulePanel.clear() } diff --git a/src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.kt b/src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.kt index c9056f46c2..9ee6955f2d 100644 --- a/src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.kt +++ b/src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.kt @@ -42,6 +42,7 @@ import com.intellij.util.ui.UIUtil import java.awt.BorderLayout import java.awt.GridBagConstraints import java.awt.GridBagLayout +import java.util.UUID import java.util.concurrent.TimeUnit import javax.swing.JEditorPane import javax.swing.event.HyperlinkEvent @@ -64,7 +65,7 @@ import org.sonarlint.intellij.ui.ruledescription.RuleDescriptionPanel import org.sonarlint.intellij.ui.ruledescription.RuleHeaderPanel import org.sonarlint.intellij.ui.ruledescription.RuleLanguages import org.sonarlint.intellij.util.runOnPooledThread -import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.EffectiveRuleDetailsDto +import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.EffectiveIssueDetailsDto private const val RULE_CONFIG_LINK_PREFIX = "#rule:" @@ -80,8 +81,7 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL private val securityHotspotHeaderMessage = JEditorPane() private val ruleDetailsLoader = RuleDetailsLoader() private var finding: Finding? = null - private var ruleDetails: EffectiveRuleDetailsDto? = null - private var ruleKey: String? = null + private var issueDetails: EffectiveIssueDetailsDto? = null init { mainPanel.add(topPanel.apply { @@ -121,7 +121,7 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL LafManagerListener { runOnUiThread(project) { updateUiComponents() } }) } - private data class RuleDetailsLoaderState(val lastModule: Module?, val lastFindingRuleKey: String?, val lastContextKey: String?) + private data class RuleDetailsLoaderState(val lastModule: Module?, val lastIssueId: UUID?) /** * To avoid useless calls to the backend/server, we cache parameters that may produce different rule details. @@ -129,14 +129,14 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL */ inner class RuleDetailsLoader { - private var state = RuleDetailsLoaderState(null, null, null) + private var state = RuleDetailsLoaderState(null, null) fun clearState() { - state = RuleDetailsLoaderState(null, null, null) + state = RuleDetailsLoaderState(null, null) } - fun updateActiveRuleDetailsIfNeeded(module: Module, ruleKey: String, ruleDescriptionContextKey: String?) { - val newState = RuleDetailsLoaderState(module, ruleKey, ruleDescriptionContextKey) + fun updateActiveRuleDetailsIfNeeded(module: Module, issueId: UUID) { + val newState = RuleDetailsLoaderState(module, issueId) if (state == newState) { // Still force a refresh of the UI, as some other fields of the finding may be different runOnUiThread(project) { @@ -150,15 +150,15 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL override fun run(progressIndicator: ProgressIndicator) { runOnPooledThread(project) { SonarLintUtils.getService(BackendService::class.java) - .getActiveRuleDetails(module, ruleKey, ruleDescriptionContextKey) + .getIssueDetails(module, issueId) .orTimeout(30, TimeUnit.SECONDS) .handle { response, error -> stopLoading() - ruleDetails = if (error != null) { + issueDetails = if (error != null) { SonarLintConsole.get(project).error("Cannot get rule description", error) null } else { - response.details() + response.details } runOnUiThread(project) { updateUiComponents() @@ -178,32 +178,29 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL private fun clearValues() { finding = null - ruleKey = null - ruleDetails = null + issueDetails = null ruleDetailsLoader.clearState() } - fun setSelectedFinding(module: Module, finding: Finding?, ruleKey: String, ruleDescriptionContextKey: String?) { + fun setSelectedFinding(module: Module, finding: Finding?, findingId: UUID) { this.finding = finding - this.ruleKey = ruleKey - ruleDetailsLoader.updateActiveRuleDetailsIfNeeded(module, ruleKey, ruleDescriptionContextKey) + ruleDetailsLoader.updateActiveRuleDetailsIfNeeded(module, findingId) } private fun updateUiComponents() { ApplicationManager.getApplication().assertIsDispatchThread() val finding = this.finding - val ruleKey = this.ruleKey - val ruleDetails = this.ruleDetails - if (ruleDetails != null && (finding != null || ruleKey != null)) { + val issueDetails = this.issueDetails + if (issueDetails != null && finding != null) { disableEmptyDisplay(true) - finding?.let { updateHeader(finding, finding.getRuleKey(), ruleDetails) } ?: ruleKey?.let { updateHeader(null, ruleKey, ruleDetails) } + updateHeader(finding, issueDetails) descriptionPanel.removeAll() - val fileType = RuleLanguages.findFileTypeByRuleLanguage(ruleDetails.language) - ruleDetails.description.map( + val fileType = RuleLanguages.findFileTypeByRuleLanguage(issueDetails.language) + issueDetails.description.map( { monolithDescription -> descriptionPanel.addMonolith(monolithDescription, fileType) }, { withSections -> descriptionPanel.addSections(withSections, fileType) } ) - updateParams(ruleDetails) + updateParams(issueDetails) } else { val errorLoadingRuleDetails = finding != null descriptionPanel.removeAll() @@ -213,12 +210,12 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL } } - private fun updateHeader(finding: Finding?, ruleKey: String, ruleDescription: EffectiveRuleDetailsDto) { - ruleNameLabel.text = StringEscapeUtils.escapeHtml4(ruleDescription.name) + private fun updateHeader(finding: Finding?, issueDetails: EffectiveIssueDetailsDto) { + ruleNameLabel.text = StringEscapeUtils.escapeHtml4(issueDetails.name) ruleNameLabel.setCopyable(true) securityHotspotHeaderMessage.isVisible = finding is LiveSecurityHotspot when (finding) { - null -> headerPanel.updateForServerIssue(ruleDescription, ruleKey) + null -> headerPanel.updateForServerIssue(issueDetails) is LiveSecurityHotspot -> { val serverConnection = SonarLintUtils.getService(project, ProjectBindingManager::class.java).serverConnection @@ -246,9 +243,10 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL } SwingHelper.setHtml(securityHotspotHeaderMessage, htmlStringBuilder.toString(), JBUI.CurrentTheme.ContextHelp.FOREGROUND) - headerPanel.updateForSecurityHotspot(project, ruleDescription.key, ruleDescription.type, finding) + headerPanel.updateForSecurityHotspot(project, issueDetails.ruleKey, finding) } - else -> headerPanel.updateForIssue(project, ruleDescription.type, ruleDescription.severity, finding as Issue) + + else -> headerPanel.updateForIssue(project, issueDetails, finding as Issue) } } @@ -271,25 +269,25 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL return """$text""" } - private fun updateParams(ruleDescription: EffectiveRuleDetailsDto) { - if (ruleDescription.params.isNotEmpty()) { - populateParamPanel(ruleDescription) + private fun updateParams(issueDetails: EffectiveIssueDetailsDto) { + if (issueDetails.params.isNotEmpty()) { + populateParamPanel(issueDetails) } else { paramsPanel.isVisible = false } } - private fun populateParamPanel(ruleDetails: EffectiveRuleDetailsDto) { + private fun populateParamPanel(issueDetails: EffectiveIssueDetailsDto) { paramsPanel.isVisible = true paramsPanel.apply { removeAll() val constraints = GridBagConstraints() constraints.anchor = GridBagConstraints.BASELINE_LEADING constraints.gridy = 0 - for (param in ruleDetails.params) { + for (param in issueDetails.params) { val paramDefaultValue = param.defaultValue val defaultValue = paramDefaultValue ?: "(none)" - val currentValue = Settings.getGlobalSettings().getRuleParamValue(ruleDetails.key, param.name).orElse(defaultValue) + val currentValue = Settings.getGlobalSettings().getRuleParamValue(issueDetails.ruleKey, param.name).orElse(defaultValue) constraints.gridx = 0 constraints.fill = GridBagConstraints.HORIZONTAL constraints.insets.right = UIUtil.DEFAULT_HGAP @@ -320,7 +318,8 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL isFocusable = false border = null SwingHelper.setHtml( - this, """Parameter values can be set in Rule Settings. + this, + """Parameter values can be set in Rule Settings. | In connected mode, server side configuration overrides local settings.""".trimMargin(), UIUtil.getLabelForeground() ) }, constraints) @@ -329,7 +328,7 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL private class RuleConfigHyperLinkListener(private val project: Project) : BrowserHyperlinkListener() { - public override fun hyperlinkActivated(e: HyperlinkEvent) { + override fun hyperlinkActivated(e: HyperlinkEvent) { if (e.description.startsWith(RULE_CONFIG_LINK_PREFIX)) { openRuleSettings(e.description.substringAfter(RULE_CONFIG_LINK_PREFIX)) return @@ -346,6 +345,7 @@ class SonarLintRulePanel(private val project: Project, parent: Disposable) : JBL } private fun disableEmptyDisplay(state: Boolean) { + ruleNameLabel.isVisible = state topPanel.isVisible = state descriptionPanel.isVisible = state paramsPanel.isVisible = state diff --git a/src/main/java/org/sonarlint/intellij/ui/nodes/IssueNode.java b/src/main/java/org/sonarlint/intellij/ui/nodes/IssueNode.java index c040ba452b..29f9c70627 100644 --- a/src/main/java/org/sonarlint/intellij/ui/nodes/IssueNode.java +++ b/src/main/java/org/sonarlint/intellij/ui/nodes/IssueNode.java @@ -88,24 +88,24 @@ private void doRender(TreeCellRenderer renderer) { } } else { var severity = issue.getUserSeverity(); + var type = issue.getType(); + Icon typeIcon = null; + var typeStr = ""; var severityText = ""; - Icon severityIcon = null; - if (severity != null) { + if (severity != null && type != null) { + typeIcon = SonarLintIcons.getIconForTypeAndSeverity(type, severity); + typeStr = type.toString().replace('_', ' ').toLowerCase(Locale.ENGLISH); severityText = StringUtil.capitalize(severity.toString().toLowerCase(Locale.ENGLISH)); - severityIcon = SonarLintIcons.severity(severity); } - var type = issue.getType(); - var typeIcon = SonarLintIcons.type(type); - var typeStr = type.toString().replace('_', ' ').toLowerCase(Locale.ENGLISH); if (issue.getServerKey() != null && serverConnection.isPresent()) { var connection = serverConnection.get(); renderer.setIconToolTip(severityText + " " + typeStr + " already detected by " + connection.getProductName() + " analysis"); - setIcon(renderer, new CompoundIcon(CompoundIcon.Axis.X_AXIS, gap, connection.getProductIcon(), typeIcon, severityIcon)); + setIcon(renderer, new CompoundIcon(CompoundIcon.Axis.X_AXIS, gap, connection.getProductIcon(), typeIcon)); } else { renderer.setIconToolTip(severityText + " " + typeStr); var serverIconEmptySpace = SonarLintIcons.ICON_SONARQUBE_16.getIconWidth() + gap; - setIcon(renderer, new OffsetIcon(serverIconEmptySpace, new CompoundIcon(CompoundIcon.Axis.X_AXIS, gap, typeIcon, severityIcon))); + setIcon(renderer, new OffsetIcon(serverIconEmptySpace, new CompoundIcon(CompoundIcon.Axis.X_AXIS, gap, typeIcon))); } } diff --git a/src/main/java/org/sonarlint/intellij/ui/ruledescription/RuleHeaderPanel.kt b/src/main/java/org/sonarlint/intellij/ui/ruledescription/RuleHeaderPanel.kt index f8bbf44e5b..b877877401 100644 --- a/src/main/java/org/sonarlint/intellij/ui/ruledescription/RuleHeaderPanel.kt +++ b/src/main/java/org/sonarlint/intellij/ui/ruledescription/RuleHeaderPanel.kt @@ -44,7 +44,11 @@ import javax.swing.JPanel import javax.swing.SwingConstants import org.sonarlint.intellij.SonarLintIcons import org.sonarlint.intellij.SonarLintIcons.backgroundColorsByImpact +import org.sonarlint.intellij.SonarLintIcons.backgroundColorsBySeverity +import org.sonarlint.intellij.SonarLintIcons.backgroundColorsByVulnerabilityProbability import org.sonarlint.intellij.SonarLintIcons.borderColorsByImpact +import org.sonarlint.intellij.SonarLintIcons.borderColorsBySeverity +import org.sonarlint.intellij.SonarLintIcons.borderColorsByVulnerabilityProbability import org.sonarlint.intellij.actions.MarkAsResolvedAction.Companion.canBeMarkedAsResolved import org.sonarlint.intellij.actions.MarkAsResolvedAction.Companion.openMarkAsResolvedDialogAsync import org.sonarlint.intellij.actions.ReopenIssueAction.Companion.canBeReopened @@ -58,7 +62,7 @@ import org.sonarlint.intellij.util.SonarGotItTooltipsUtils import org.sonarsource.sonarlint.core.client.utils.CleanCodeAttribute import org.sonarsource.sonarlint.core.client.utils.ImpactSeverity import org.sonarsource.sonarlint.core.client.utils.SoftwareQuality -import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.EffectiveRuleDetailsDto +import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.EffectiveIssueDetailsDto import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType @@ -73,10 +77,7 @@ class RuleHeaderPanel(private val parent: Disposable) : JBPanel private val wrappedPanel = JBPanel>(WrapLayout(FlowLayout.LEFT)) private val attributePanel = RoundedPanelWithBackgroundColor(JBColor(Gray._236, Gray._72)) private val qualityLabels = LinkedList() - private val ruleTypeIcon = JBLabel() - private val ruleTypeLabel = JBLabel() - private val ruleSeverityIcon = JBLabel() - private val ruleSeverityLabel = JBLabel() + private var severityLabel: RoundedPanelWithBackgroundColor? = null private val hotspotVulnerabilityLabel = JBLabel("Review priority: ") private val hotspotVulnerabilityValueLabel = JBLabel() private val ruleKeyLabel = JBLabel() @@ -92,11 +93,8 @@ class RuleHeaderPanel(private val parent: Disposable) : JBPanel fun clear() { attributePanel.removeAll() qualityLabels.clear() - ruleTypeIcon.icon = null - ruleTypeLabel.text = "" + severityLabel = null ruleKeyLabel.text = "" - ruleSeverityIcon.icon = null - ruleSeverityLabel.text = "" hotspotVulnerabilityLabel.isVisible = false hotspotVulnerabilityValueLabel.text = "" hotspotVulnerabilityValueLabel.border = BorderFactory.createEmptyBorder() @@ -107,18 +105,15 @@ class RuleHeaderPanel(private val parent: Disposable) : JBPanel } fun updateForRuleConfiguration( - ruleKey: String, type: RuleType, severity: IssueSeverity, - attribute: CleanCodeAttribute?, qualities: List, + cleanCodeAttribute: CleanCodeAttribute, + impacts: List, + ruleKey: String ) { - clear() - updateServerCommonFields(type, attribute, qualities, ruleKey) - updateRuleSeverity(severity) + updateCommonFieldsInMQRMode(cleanCodeAttribute, impacts, ruleKey) } - fun updateForIssue(project: Project, type: RuleType, severity: IssueSeverity, issue: Issue) { - clear() - updateCommonFields(type, issue.getCleanCodeAttribute(), issue.getImpacts(), issue.getRuleKey()) - updateRuleSeverity(severity) + fun updateForIssue(project: Project, issueDetails: EffectiveIssueDetailsDto, issue: Issue) { + updateCommonFields(issueDetails) if (canBeReopened(project, issue)) { changeStatusButton.isVisible = true @@ -137,22 +132,25 @@ class RuleHeaderPanel(private val parent: Disposable) : JBPanel } } - fun updateForServerIssue(ruleDescription: EffectiveRuleDetailsDto, ruleKey: String) { - clear() - updateServerCommonFields(ruleDescription.type, ruleDescription.cleanCodeAttribute?.let { CleanCodeAttribute.fromDto(it) }, ruleDescription.defaultImpacts, ruleKey) - updateRuleSeverity(ruleDescription.severity) - } - - private fun updateRuleSeverity(severity: IssueSeverity) { - ruleSeverityIcon.icon = SonarLintIcons.severity(severity) - ruleSeverityLabel.text = cleanCapitalized(severity.toString()) - ruleSeverityLabel.setCopyable(true) + fun updateForServerIssue(issueDetails: EffectiveIssueDetailsDto) { + updateCommonFields(issueDetails) } - fun updateForSecurityHotspot(project: Project, ruleKey: String, type: RuleType, securityHotspot: LiveSecurityHotspot) { + fun updateForSecurityHotspot(project: Project, ruleKey: String, securityHotspot: LiveSecurityHotspot) { clear() - updateCommonFields(type, securityHotspot.getCleanCodeAttribute(), securityHotspot.getImpacts(), ruleKey) - ruleTypeIcon.icon = SonarLintIcons.hotspotTypeWithProbability(securityHotspot.vulnerabilityProbability) + severityLabel = + RoundedPanelWithBackgroundColor( + backgroundColorsByVulnerabilityProbability[securityHotspot.vulnerabilityProbability], + borderColorsByVulnerabilityProbability[securityHotspot.vulnerabilityProbability] + ).apply { + add(JBLabel().apply { icon = SonarLintIcons.hotspotTypeWithProbability(securityHotspot.vulnerabilityProbability) }) + add(JBLabel(cleanCapitalized(RuleType.SECURITY_HOTSPOT.toString())).apply { + foreground = SonarLintIcons.fontColorsByVulnerabilityProbability[securityHotspot.vulnerabilityProbability] + }) + } + ruleKeyLabel.text = ruleKey + ruleKeyLabel.setCopyable(true) + organizeHeader(false) hotspotVulnerabilityLabel.isVisible = true hotspotVulnerabilityValueLabel.apply { text = securityHotspot.vulnerabilityProbability.name @@ -170,72 +168,73 @@ class RuleHeaderPanel(private val parent: Disposable) : JBPanel } } - private fun updateServerCommonFields(type: RuleType, attribute: CleanCodeAttribute?, qualities: List, ruleKey: String) { - val newCctEnabled = attribute != null && qualities.isNotEmpty() - if (newCctEnabled) { - val attributeLabel = JBLabel("" + cleanCapitalized(attribute!!.label) + " issue | Not " + clean(attribute.toString()) + "
") - attributePanel.apply { - add(attributeLabel) - toolTipText = "Clean Code attributes are characteristics code needs to have to be considered clean." - } - qualities.forEach { - val impactSeverity = ImpactSeverity.fromDto(it.impactSeverity) - val cleanImpact = impactSeverity.label - val cleanQuality = SoftwareQuality.fromDto(it.softwareQuality).label - val qualityPanel = RoundedPanelWithBackgroundColor(SonarLintIcons.backgroundColorsByImpact[impactSeverity]).apply { - toolTipText = "Issues found for this rule will have a $cleanImpact impact on the $cleanQuality of your software." - } - qualityPanel.add(JBLabel(cleanCapitalized(it.softwareQuality.toString())).apply { - foreground = SonarLintIcons.fontColorsByImpact[impactSeverity] - }) - qualityPanel.add(JBLabel().apply { icon = SonarLintIcons.impact(impactSeverity) }) - qualityLabels.add(qualityPanel) - } + private fun updateCommonFields(issueDetails: EffectiveIssueDetailsDto) { + if (issueDetails.severityDetails.isLeft) { + val mqrMode = issueDetails.severityDetails.left + updateCommonFieldsInStandardMode(mqrMode.severity, mqrMode.type, issueDetails.ruleKey) } else { - ruleTypeIcon.icon = SonarLintIcons.type(type) - ruleTypeLabel.text = cleanCapitalized(type.toString()) - ruleTypeLabel.setCopyable(true) + val standardMode = issueDetails.severityDetails.right + updateCommonFieldsInMQRMode( + CleanCodeAttribute.fromDto(standardMode.cleanCodeAttribute), + standardMode.impacts, + issueDetails.ruleKey + ) + } + } + + private fun updateCommonFieldsInStandardMode( + severity: IssueSeverity, + type: RuleType, + ruleKey: String + ) { + clear() + severityLabel = RoundedPanelWithBackgroundColor(backgroundColorsBySeverity[severity], borderColorsBySeverity[severity]).apply { + add(JBLabel().apply { icon = SonarLintIcons.getIconForTypeAndSeverity(type, severity) }) + add(JBLabel(cleanCapitalized(type.toString())).apply { + foreground = SonarLintIcons.fontColorsBySeverity[severity] + }) + add(JBLabel().apply { icon = SonarLintIcons.severity(severity) }) } ruleKeyLabel.text = ruleKey ruleKeyLabel.setCopyable(true) - organizeHeader(newCctEnabled) + organizeHeader(false) } - private fun updateCommonFields(type: RuleType, attribute: CleanCodeAttribute?, qualities: Map, ruleKey: String) { - val newCctEnabled = attribute != null && qualities.isNotEmpty() - if (newCctEnabled) { - val attributeLabel = JBLabel("" + cleanCapitalized(attribute!!.category.label) + " issue | " + clean(attribute.label) + "
") - attributePanel.apply { - add(attributeLabel) - toolTipText = "Clean Code attributes are characteristics code needs to have to be considered clean." - } - qualities.entries.forEach { - val cleanImpact = cleanCapitalized(it.value.label) - val cleanQuality = cleanCapitalized(it.key.label) - val qualityPanel = - RoundedPanelWithBackgroundColor(backgroundColorsByImpact[it.value], borderColorsByImpact[it.value]).apply { + private fun updateCommonFieldsInMQRMode( + cleanCodeAttribute: CleanCodeAttribute, + impacts: List, + ruleKey: String + ) { + clear() + val attributeLabel = + JBLabel("" + cleanCapitalized(cleanCodeAttribute.category.label) + " issue | " + cleanCodeAttribute.label + "
") + attributePanel.apply { + add(attributeLabel) + toolTipText = "Clean Code attributes are characteristics code needs to have to be considered clean." + } + impacts.forEach { + val impactSeverity = ImpactSeverity.fromDto(it.impactSeverity) + val cleanImpact = impactSeverity.label + val cleanQuality = SoftwareQuality.fromDto(it.softwareQuality).label + val qualityPanel = + RoundedPanelWithBackgroundColor(backgroundColorsByImpact[impactSeverity], borderColorsByImpact[impactSeverity]).apply { toolTipText = "Issues found for this rule will have a $cleanImpact impact on the $cleanQuality of your software." } - qualityPanel.add(JBLabel(cleanCapitalized(it.key.toString())).apply { - foreground = SonarLintIcons.fontColorsByImpact[it.value] - }) - qualityPanel.add(JBLabel().apply { icon = SonarLintIcons.impact(it.value) }) - qualityLabels.add(qualityPanel) - } - } else { - ruleTypeIcon.icon = SonarLintIcons.type(type) - ruleTypeLabel.text = cleanCapitalized(type.toString()) - ruleTypeLabel.setCopyable(true) + qualityPanel.add(JBLabel(cleanCapitalized(it.softwareQuality.toString())).apply { + foreground = SonarLintIcons.fontColorsByImpact[impactSeverity] + }) + qualityPanel.add(JBLabel().apply { icon = SonarLintIcons.impact(impactSeverity) }) + qualityLabels.add(qualityPanel) } ruleKeyLabel.text = ruleKey ruleKeyLabel.setCopyable(true) - organizeHeader(newCctEnabled) + organizeHeader(true) } - private fun organizeHeader(newCct: Boolean) { - if (newCct) { + private fun organizeHeader(isMQRMode: Boolean) { + if (isMQRMode) { wrappedPanel.add(attributePanel.apply { border = BorderFactory.createEmptyBorder(0, 0, 0, 15) }) qualityLabels.forEach { wrappedPanel.add(it) } @@ -243,12 +242,7 @@ class RuleHeaderPanel(private val parent: Disposable) : JBPanel SonarGotItTooltipsUtils.showCleanCodeToolTip(wrappedPanel, parent) } } else { - wrappedPanel.add(ruleTypeIcon) - wrappedPanel.add(ruleTypeLabel.apply { - border = JBUI.Borders.emptyRight(0) - }) - wrappedPanel.add(ruleSeverityIcon) - wrappedPanel.add(ruleSeverityLabel) + wrappedPanel.add(severityLabel) wrappedPanel.add(hotspotVulnerabilityLabel) wrappedPanel.add(hotspotVulnerabilityValueLabel.apply { font = JBFont.label().asBold() @@ -264,15 +258,12 @@ class RuleHeaderPanel(private val parent: Disposable) : JBPanel changeStatusPanel.add(changeStatusButton) wrappedPanel.add(changeStatusPanel) - if (newCct) { + if (isMQRMode) { wrappedPanel.add(learnMore) } add(wrappedPanel, BorderLayout.CENTER) - } - - fun showMessage(msg: String) { - clear() - ruleTypeLabel.text = msg + revalidate() + repaint() } private fun cleanCapitalized(txt: String): String { diff --git a/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/TaintVulnerabilitiesPanel.kt b/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/TaintVulnerabilitiesPanel.kt index 9d59c6e28f..c1384a1079 100644 --- a/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/TaintVulnerabilitiesPanel.kt +++ b/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/TaintVulnerabilitiesPanel.kt @@ -159,7 +159,7 @@ class TaintVulnerabilitiesPanel(private val project: Project) : SimpleToolWindow private fun centeredLabel(textLabel: String, actionText: String?, action: AnAction?): JBPanelWithEmptyText { val labelPanel = JBPanelWithEmptyText(HorizontalLayout(5)) val text = labelPanel.emptyText - text.setText(textLabel) + text.text = textLabel if (action != null && actionText != null) { text.appendLine( actionText, SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES @@ -333,7 +333,7 @@ class TaintVulnerabilitiesPanel(private val project: Project) : SimpleToolWindow rulePanel.clear() highlighting.removeHighlights() } else { - issue.module?.let { module -> rulePanel.setSelectedFinding(module, issue, issue.getRuleKey(), issue.getRuleDescriptionContextKey()) } + issue.module?.let { module -> rulePanel.setSelectedFinding(module, issue, issue.getId()) } ?: rulePanel.clear() } } @@ -375,8 +375,9 @@ class TaintVulnerabilitiesPanel(private val project: Project) : SimpleToolWindow } findModuleForFile(showFinding.file, project)?.let { + val taintId = UUID.fromString(showFinding.findingKey) runOnUiThread(project) { - rulePanel.setSelectedFinding(it, null, showFinding.ruleKey, null) + rulePanel.setSelectedFinding(it, null, taintId) getService(project, EditorDecorator::class.java).highlightRange(rangeMarker) } } diff --git a/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/tree/render/LocalTaintVulnerabilityRenderer.kt b/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/tree/render/LocalTaintVulnerabilityRenderer.kt index aa02cc55fe..4eefe63ed0 100644 --- a/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/tree/render/LocalTaintVulnerabilityRenderer.kt +++ b/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/tree/render/LocalTaintVulnerabilityRenderer.kt @@ -35,11 +35,12 @@ import org.sonarlint.intellij.util.DateUtils object LocalTaintVulnerabilityRenderer : NodeRenderer { override fun render(renderer: TreeCellRenderer, node: LocalTaintVulnerability) { - val toolTipText: String + var toolTipText: String? = null val gap = if (JBUIScale.isUsrHiDPI) 8 else 4 - if (node.getCleanCodeAttribute() != null && node.getImpacts().isNotEmpty()) { - val (quality, impact) = node.getImpacts().maxByOrNull { it.value }!! + if (node.getHighestQuality() != null && node.getHighestImpact() != null) { + val quality = node.getHighestQuality() + val impact = node.getHighestImpact() val impactText = StringUtil.capitalize(impact.toString().lowercase()) val qualityText = quality.toString().lowercase() toolTipText = "$impactText $qualityText" @@ -48,25 +49,28 @@ object LocalTaintVulnerabilityRenderer : NodeRenderer { CompoundIcon( CompoundIcon.Axis.X_AXIS, gap, - SonarLintIcons.impact(impact)) + SonarLintIcons.impact(impact!!) + ) ) } else { - val severity = StringUtil.capitalize(node.severity().toString().lowercase(Locale.ENGLISH)) - val type = node.getType() - val typeStr = type.toString().replace('_', ' ').lowercase(Locale.ENGLISH) - toolTipText = "$severity $typeStr" - setIcon( - renderer, - CompoundIcon( - CompoundIcon.Axis.X_AXIS, - gap, - SonarLintIcons.type(type), - SonarLintIcons.severity(node.severity()) + if (node.severity() != null && node.getType() != null) { + val severity = StringUtil.capitalize(node.severity().toString().lowercase(Locale.ENGLISH)) + val type = node.getType() + val typeStr = type.toString().replace('_', ' ').lowercase(Locale.ENGLISH) + toolTipText = "$severity $typeStr" + setIcon( + renderer, + CompoundIcon( + CompoundIcon.Axis.X_AXIS, + gap, + SonarLintIcons.getIconForTypeAndSeverity(type!!, node.severity()!!), + SonarLintIcons.severity(node.severity()!!) + ) ) - ) + } } - renderer.setIconToolTip(toolTipText) + toolTipText?.let { renderer.setIconToolTip(it) } renderer.append(issueCoordinates(node), SimpleTextAttributes.GRAY_ATTRIBUTES) renderer.toolTipText = "Double click to open location" diff --git a/src/main/java/org/sonarlint/intellij/util/VirtualFileUtils.kt b/src/main/java/org/sonarlint/intellij/util/VirtualFileUtils.kt index 32503d5c7e..381d6c5a13 100644 --- a/src/main/java/org/sonarlint/intellij/util/VirtualFileUtils.kt +++ b/src/main/java/org/sonarlint/intellij/util/VirtualFileUtils.kt @@ -41,7 +41,7 @@ object VirtualFileUtils { } else { null } - } catch (e: URISyntaxException) { + } catch (_: URISyntaxException) { getService(GlobalLogOutput::class.java).log("Could not transform ${file.url} to URI", ClientLogOutput.Level.DEBUG) null } diff --git a/src/main/resources/images/type/bug.svg b/src/main/resources/images/bug/bugBlocker.svg similarity index 51% rename from src/main/resources/images/type/bug.svg rename to src/main/resources/images/bug/bugBlocker.svg index a621172443..ba3baab3ea 100644 --- a/src/main/resources/images/type/bug.svg +++ b/src/main/resources/images/bug/bugBlocker.svg @@ -20,6 +20,10 @@ --> - - + + diff --git a/src/main/resources/images/bug/bugHigh.svg b/src/main/resources/images/bug/bugHigh.svg new file mode 100644 index 0000000000..e22d0e6a6b --- /dev/null +++ b/src/main/resources/images/bug/bugHigh.svg @@ -0,0 +1,29 @@ + + + + + diff --git a/src/main/resources/images/bug/bugInfo.svg b/src/main/resources/images/bug/bugInfo.svg new file mode 100644 index 0000000000..d4b19c4776 --- /dev/null +++ b/src/main/resources/images/bug/bugInfo.svg @@ -0,0 +1,29 @@ + + + + + diff --git a/src/main/resources/images/bug/bugLow.svg b/src/main/resources/images/bug/bugLow.svg new file mode 100644 index 0000000000..e13d3e542a --- /dev/null +++ b/src/main/resources/images/bug/bugLow.svg @@ -0,0 +1,29 @@ + + + + + diff --git a/src/main/resources/images/bug/bugMedium.svg b/src/main/resources/images/bug/bugMedium.svg new file mode 100644 index 0000000000..d023ef45b7 --- /dev/null +++ b/src/main/resources/images/bug/bugMedium.svg @@ -0,0 +1,29 @@ + + + + + diff --git a/src/main/resources/images/type/codeSmell.svg b/src/main/resources/images/codeSmell/codeSmellBlocker.svg similarity index 58% rename from src/main/resources/images/type/codeSmell.svg rename to src/main/resources/images/codeSmell/codeSmellBlocker.svg index 0b5f3b45a6..ee9aaa273d 100644 --- a/src/main/resources/images/type/codeSmell.svg +++ b/src/main/resources/images/codeSmell/codeSmellBlocker.svg @@ -20,5 +20,7 @@ --> - + diff --git a/src/main/resources/images/codeSmell/codeSmellHigh.svg b/src/main/resources/images/codeSmell/codeSmellHigh.svg new file mode 100644 index 0000000000..ec50dbc349 --- /dev/null +++ b/src/main/resources/images/codeSmell/codeSmellHigh.svg @@ -0,0 +1,26 @@ + + + + diff --git a/src/main/resources/images/codeSmell/codeSmellInfo.svg b/src/main/resources/images/codeSmell/codeSmellInfo.svg new file mode 100644 index 0000000000..bfc25aa721 --- /dev/null +++ b/src/main/resources/images/codeSmell/codeSmellInfo.svg @@ -0,0 +1,26 @@ + + + + diff --git a/src/main/resources/images/codeSmell/codeSmellLow.svg b/src/main/resources/images/codeSmell/codeSmellLow.svg new file mode 100644 index 0000000000..e94281d502 --- /dev/null +++ b/src/main/resources/images/codeSmell/codeSmellLow.svg @@ -0,0 +1,26 @@ + + + + diff --git a/src/main/resources/images/codeSmell/codeSmellMedium.svg b/src/main/resources/images/codeSmell/codeSmellMedium.svg new file mode 100644 index 0000000000..a0d9d98952 --- /dev/null +++ b/src/main/resources/images/codeSmell/codeSmellMedium.svg @@ -0,0 +1,26 @@ + + + + diff --git a/src/main/resources/images/hotspot/hotspotHigh.svg b/src/main/resources/images/hotspot/hotspotHigh.svg new file mode 100644 index 0000000000..eef3fd5953 --- /dev/null +++ b/src/main/resources/images/hotspot/hotspotHigh.svg @@ -0,0 +1,26 @@ + + + + diff --git a/src/main/resources/images/type/hotspotLow.svg b/src/main/resources/images/hotspot/hotspotLow.svg similarity index 64% rename from src/main/resources/images/type/hotspotLow.svg rename to src/main/resources/images/hotspot/hotspotLow.svg index 2d65ef82b8..40fc012755 100644 --- a/src/main/resources/images/type/hotspotLow.svg +++ b/src/main/resources/images/hotspot/hotspotLow.svg @@ -20,5 +20,7 @@ --> - + diff --git a/src/main/resources/images/hotspot/hotspotMedium.svg b/src/main/resources/images/hotspot/hotspotMedium.svg new file mode 100644 index 0000000000..52c4d05f71 --- /dev/null +++ b/src/main/resources/images/hotspot/hotspotMedium.svg @@ -0,0 +1,26 @@ + + + + diff --git a/src/main/resources/images/type/vulnerability.svg b/src/main/resources/images/vulnerability/vulnerabilityBlocker.svg similarity index 69% rename from src/main/resources/images/type/vulnerability.svg rename to src/main/resources/images/vulnerability/vulnerabilityBlocker.svg index b52c729898..e7c8ca9111 100644 --- a/src/main/resources/images/type/vulnerability.svg +++ b/src/main/resources/images/vulnerability/vulnerabilityBlocker.svg @@ -20,5 +20,7 @@ --> - + diff --git a/src/main/resources/images/type/hotspotMedium.svg b/src/main/resources/images/vulnerability/vulnerabilityHigh.svg similarity index 65% rename from src/main/resources/images/type/hotspotMedium.svg rename to src/main/resources/images/vulnerability/vulnerabilityHigh.svg index c7132c9a80..c5dd6e90dc 100644 --- a/src/main/resources/images/type/hotspotMedium.svg +++ b/src/main/resources/images/vulnerability/vulnerabilityHigh.svg @@ -20,5 +20,7 @@ --> - + diff --git a/src/main/resources/images/type/hotspot.svg b/src/main/resources/images/vulnerability/vulnerabilityInfo.svg similarity index 65% rename from src/main/resources/images/type/hotspot.svg rename to src/main/resources/images/vulnerability/vulnerabilityInfo.svg index 422c167cfe..ad5d069fc7 100644 --- a/src/main/resources/images/type/hotspot.svg +++ b/src/main/resources/images/vulnerability/vulnerabilityInfo.svg @@ -20,5 +20,7 @@ --> - + diff --git a/src/main/resources/images/type/hotspotHigh.svg b/src/main/resources/images/vulnerability/vulnerabilityLow.svg similarity index 65% rename from src/main/resources/images/type/hotspotHigh.svg rename to src/main/resources/images/vulnerability/vulnerabilityLow.svg index 2cec5f7fae..94a1b9b186 100644 --- a/src/main/resources/images/type/hotspotHigh.svg +++ b/src/main/resources/images/vulnerability/vulnerabilityLow.svg @@ -20,5 +20,7 @@ --> - + diff --git a/src/main/resources/images/vulnerability/vulnerabilityMedium.svg b/src/main/resources/images/vulnerability/vulnerabilityMedium.svg new file mode 100644 index 0000000000..6ef9712c38 --- /dev/null +++ b/src/main/resources/images/vulnerability/vulnerabilityMedium.svg @@ -0,0 +1,26 @@ + + + + diff --git a/src/test/java/org/sonarlint/intellij/SonarLintIconsTest.kt b/src/test/java/org/sonarlint/intellij/SonarLintIconsTest.kt index 13ea4e5032..b9d4e6b2a0 100644 --- a/src/test/java/org/sonarlint/intellij/SonarLintIconsTest.kt +++ b/src/test/java/org/sonarlint/intellij/SonarLintIconsTest.kt @@ -21,11 +21,11 @@ package org.sonarlint.intellij import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.sonarlint.intellij.SonarLintIcons.getIconForTypeAndSeverity import org.sonarlint.intellij.SonarLintIcons.hotspotTypeWithProbability import org.sonarlint.intellij.SonarLintIcons.impact import org.sonarlint.intellij.SonarLintIcons.severity import org.sonarlint.intellij.SonarLintIcons.toDisabled -import org.sonarlint.intellij.SonarLintIcons.type import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.VulnerabilityProbability import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType @@ -47,8 +47,12 @@ class SonarLintIconsTest { @Test fun testTypes() { - for (value in RuleType.values()) { - assertThat(type(value)).isNotNull + for (type in RuleType.values()) { + for (severity in IssueSeverity.values()) { + if (type != RuleType.SECURITY_HOTSPOT) { + assertThat(getIconForTypeAndSeverity(type, severity)).isNotNull + } + } } for (value in VulnerabilityProbability.values()) { assertThat(hotspotTypeWithProbability(value)).isNotNull diff --git a/src/test/java/org/sonarlint/intellij/SonarLintTestUtils.java b/src/test/java/org/sonarlint/intellij/SonarLintTestUtils.java index bf539a4e43..a8eb6ca8ac 100644 --- a/src/test/java/org/sonarlint/intellij/SonarLintTestUtils.java +++ b/src/test/java/org/sonarlint/intellij/SonarLintTestUtils.java @@ -25,8 +25,10 @@ import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; +import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; +import org.sonarsource.sonarlint.core.rpc.protocol.common.StandardModeDetails; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -50,6 +52,7 @@ public static RaisedIssueDto createIssue(int id) { var issue = mock(RaisedIssueDto.class); when(issue.getRuleKey()).thenReturn(Integer.toString(id)); when(issue.getPrimaryMessage()).thenReturn("issue " + id); + when(issue.getSeverityMode()).thenReturn(Either.forLeft(new StandardModeDetails(IssueSeverity.MAJOR, RuleType.BUG))); when(issue.getSeverity()).thenReturn(IssueSeverity.MAJOR); when(issue.getType()).thenReturn(RuleType.BUG); when(issue.getCleanCodeAttribute()).thenReturn(CleanCodeAttribute.COMPLETE); diff --git a/src/test/java/org/sonarlint/intellij/config/global/rules/RulesTreeNodeTests.java b/src/test/java/org/sonarlint/intellij/config/global/rules/RulesTreeNodeTests.java index b4ba4aff64..252bba0045 100644 --- a/src/test/java/org/sonarlint/intellij/config/global/rules/RulesTreeNodeTests.java +++ b/src/test/java/org/sonarlint/intellij/config/global/rules/RulesTreeNodeTests.java @@ -20,12 +20,15 @@ package org.sonarlint.intellij.config.global.rules; import java.util.HashMap; +import java.util.List; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDefinitionDto; -import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; +import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; +import org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; -import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; +import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -38,8 +41,9 @@ void getters_rule() { when(details.getName()).thenReturn("name"); when(details.getKey()).thenReturn("key"); when(details.isActiveByDefault()).thenReturn(true); - when(details.getSeverity()).thenReturn(IssueSeverity.MAJOR); - when(details.getType()).thenReturn(RuleType.BUG); + when(details.getSoftwareImpacts()) + .thenReturn(List.of(new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM), new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.BLOCKER))); + when(details.getCleanCodeAttribute()).thenReturn(CleanCodeAttribute.CONVENTIONAL); when(details.getLanguage()).thenReturn(Language.JAVA); var node = new RulesTreeNode.Rule(details, false, new HashMap<>()); @@ -48,8 +52,8 @@ void getters_rule() { assertThat(node).hasToString("name"); assertThat(node.getDefaultActivation()).isTrue(); assertThat(node.isNonDefault()).isTrue(); - assertThat(node.severity()).isEqualTo(IssueSeverity.MAJOR); - assertThat(node.type()).isEqualTo(RuleType.BUG); + assertThat(node.getHighestImpact()).isEqualTo(org.sonarsource.sonarlint.core.client.utils.ImpactSeverity.BLOCKER); + assertThat(node.getHighestQuality()).isEqualTo(SoftwareQuality.RELIABILITY); assertThat(node.language()).isEqualTo(Language.JAVA); } diff --git a/src/test/java/org/sonarlint/intellij/config/global/rules/RulesTreeTableModelTests.java b/src/test/java/org/sonarlint/intellij/config/global/rules/RulesTreeTableModelTests.java index bc110e6d99..c305c76a19 100644 --- a/src/test/java/org/sonarlint/intellij/config/global/rules/RulesTreeTableModelTests.java +++ b/src/test/java/org/sonarlint/intellij/config/global/rules/RulesTreeTableModelTests.java @@ -31,10 +31,7 @@ import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDefinitionDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; -import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttributeCategory; import org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity; -import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; -import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; import static org.assertj.core.api.Assertions.assertThat; @@ -56,11 +53,8 @@ class RulesTreeTableModelTests { @BeforeEach void setUp() { - when(ruleDetails.getType()).thenReturn(RuleType.BUG); - when(ruleDetails.getSeverity()).thenReturn(IssueSeverity.MAJOR); when(ruleDetails.getCleanCodeAttribute()).thenReturn(CleanCodeAttribute.CONVENTIONAL); - when(ruleDetails.getCleanCodeAttributeCategory()).thenReturn(CleanCodeAttributeCategory.CONSISTENT); - when(ruleDetails.getDefaultImpacts()).thenReturn(List.of(new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM))); + when(ruleDetails.getSoftwareImpacts()).thenReturn(List.of(new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM))); when(ruleDetails.getKey()).thenReturn("key"); when(ruleDetails.isActiveByDefault()).thenReturn(false); root.add(lang); diff --git a/src/test/java/org/sonarlint/intellij/finding/issue/IssueFixtures.kt b/src/test/java/org/sonarlint/intellij/finding/issue/IssueFixtures.kt index e46c828601..88615aae1a 100644 --- a/src/test/java/org/sonarlint/intellij/finding/issue/IssueFixtures.kt +++ b/src/test/java/org/sonarlint/intellij/finding/issue/IssueFixtures.kt @@ -31,8 +31,10 @@ import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.QuickFixDto import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.TextEditDto import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute +import org.sonarsource.sonarlint.core.rpc.protocol.common.Either import org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity +import org.sonarsource.sonarlint.core.rpc.protocol.common.MQRModeDetails import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality import org.sonarsource.sonarlint.core.rpc.protocol.common.TextRangeDto @@ -48,12 +50,21 @@ fun aLiveIssue( return liveIssue } -fun aRawIssue(textRange: TextRangeDto?) = - RaisedIssueDto( +fun aRawIssue(textRange: TextRangeDto?): RaisedIssueDto { + return RaisedIssueDto( UUID.randomUUID(), "serverKey", "rule:key", "message", + Either.forRight( + MQRModeDetails( + CleanCodeAttribute.COMPLETE, listOf( + ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH), + ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH), + ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH) + ) + ) + ), IssueSeverity.INFO, RuleType.BUG, CleanCodeAttribute.COMPLETE, @@ -70,6 +81,7 @@ fun aRawIssue(textRange: TextRangeDto?) = mutableListOf(), null ) +} private fun toTextRange(rangeMarker: RangeMarker?): TextRangeDto? { return rangeMarker?.let { diff --git a/src/test/java/org/sonarlint/intellij/mediumtests/StandaloneModeMediumTests.kt b/src/test/java/org/sonarlint/intellij/mediumtests/StandaloneModeMediumTests.kt index a92fabd7d5..e31d9ce1a7 100644 --- a/src/test/java/org/sonarlint/intellij/mediumtests/StandaloneModeMediumTests.kt +++ b/src/test/java/org/sonarlint/intellij/mediumtests/StandaloneModeMediumTests.kt @@ -55,8 +55,6 @@ import org.sonarlint.intellij.fs.VirtualFileEvent import org.sonarlint.intellij.trigger.TriggerType import org.sonarlint.intellij.util.SonarLintAppUtils import org.sonarlint.intellij.util.getDocument -import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity -import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileEvent class StandaloneModeMediumTests : AbstractSonarLintLightTests() { @@ -92,15 +90,21 @@ class StandaloneModeMediumTests : AbstractSonarLintLightTests() { { it.getRuleKey() }, { it.message }, { it.getType() }, + { it.getCleanCodeAttribute() }, { it.userSeverity }, + { it.getHighestImpact() }, + { it.getHighestQuality() }, { issue -> issue.range?.let { Pair(it.startOffset, it.endOffset) } } ) .containsExactly( tuple( "xml:S1778", "Remove all characters located before \" var issue = mock(RaisedIssueDto.class); when(issue.getRuleKey()).thenReturn(rule); + when(issue.getSeverityMode()).thenReturn(Either.forRight(new MQRModeDetails(CleanCodeAttribute.CONVENTIONAL, impacts))); when(issue.getType()).thenReturn(RuleType.BUG); when(issue.getCleanCodeAttribute()).thenReturn(CleanCodeAttribute.CONVENTIONAL); when(issue.getImpacts()).thenReturn(impacts); diff --git a/src/test/java/org/sonarlint/intellij/ui/tree/SecurityHotspotTreeModelBuilderTests.java b/src/test/java/org/sonarlint/intellij/ui/tree/SecurityHotspotTreeModelBuilderTests.java index 24694af2f7..18fdc8fd1c 100644 --- a/src/test/java/org/sonarlint/intellij/ui/tree/SecurityHotspotTreeModelBuilderTests.java +++ b/src/test/java/org/sonarlint/intellij/ui/tree/SecurityHotspotTreeModelBuilderTests.java @@ -48,8 +48,10 @@ import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.VulnerabilityProbability; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; +import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; +import org.sonarsource.sonarlint.core.rpc.protocol.common.StandardModeDetails; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -251,6 +253,7 @@ private LiveSecurityHotspot mockSecurityHotspot(String path, int startOffset, St when(virtualFile.isValid()).thenReturn(true); var issue = mock(RaisedHotspotDto.class); + when(issue.getSeverityMode()).thenReturn(Either.forLeft(new StandardModeDetails(IssueSeverity.BLOCKER, RuleType.SECURITY_HOTSPOT))); when(issue.getSeverity()).thenReturn(IssueSeverity.BLOCKER); when(issue.getRuleKey()).thenReturn(rule); when(issue.getVulnerabilityProbability()).thenReturn(vulnerability);