diff --git a/.changes/next-release/feature-6565a79c-90f3-4d49-abb6-ca4d5c0ff679.json b/.changes/next-release/feature-6565a79c-90f3-4d49-abb6-ca4d5c0ff679.json new file mode 100644 index 0000000000..fe31575409 --- /dev/null +++ b/.changes/next-release/feature-6565a79c-90f3-4d49-abb6-ca4d5c0ff679.json @@ -0,0 +1,4 @@ +{ + "type" : "feature", + "description" : "Amazon Q /review: Code issues can now be grouped by severity or file location." +} \ No newline at end of file diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml b/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml index dcfb006872..d0594e1d4c 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml +++ b/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml @@ -114,6 +114,11 @@ + + + + () private val scanNodesLookup = mutableMapOf>() private val selectedSeverityValues = IssueSeverity.entries.associate { it.displayName to true }.toMutableMap() + private var selectedGroupingStrategy = IssueGroupingStrategy.SEVERITY private val documentListener = CodeWhispererCodeScanDocumentListener(project) private val editorMouseListener = CodeWhispererCodeScanEditorMouseMotionListener(project) @@ -281,6 +284,12 @@ class CodeWhispererCodeScanManager(val project: Project) { updateCodeScanIssuesTree() } + fun getGroupingStrategySelected() = selectedGroupingStrategy + fun setGroupingStrategySelected(groupingStrategy: IssueGroupingStrategy) { + selectedGroupingStrategy = groupingStrategy + updateCodeScanIssuesTree() + } + /** * Returns true if there are any code scan issues. */ @@ -868,7 +877,18 @@ class CodeWhispererCodeScanManager(val project: Project) { node.removeAllChildren() } } + synchronized(fileNodeLookup) { + fileNodeLookup.clear() + } + return if (selectedGroupingStrategy == IssueGroupingStrategy.SEVERITY) { + createCodeScanIssuesTreeBySeverity(codeScanIssues) + } else { + createCodeScanIssuesTreeByFileLocation(codeScanIssues) + } + } + + private fun createCodeScanIssuesTreeBySeverity(codeScanIssues: List): DefaultMutableTreeNode { severityNodeLookup.forEach { (severity, node) -> if (selectedSeverityValues[severity] == true) { synchronized(codeScanTreeNodeRoot) { @@ -890,6 +910,27 @@ class CodeWhispererCodeScanManager(val project: Project) { return codeScanTreeNodeRoot } + private fun createCodeScanIssuesTreeByFileLocation(codeScanIssues: List): DefaultMutableTreeNode { + codeScanIssues.forEach { issue -> + val fileNode = synchronized(fileNodeLookup) { + fileNodeLookup.getOrPut(issue.file) { + val node = DefaultMutableTreeNode(issue.file) + synchronized(codeScanTreeNodeRoot) { + codeScanTreeNodeRoot.add(node) + } + node + } + } + + val scanNode = DefaultMutableTreeNode(issue) + fileNode.add(scanNode) + scanNodesLookup.getOrPut(issue.file) { + mutableListOf() + }.add(scanNode) + } + return codeScanTreeNodeRoot + } + private fun checkIssueCodeSnippet(codeSnippet: List, startLine: Int, endLine: Int, documentLines: List): Boolean = try { codeSnippet .asSequence() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt index 3bb9a755c0..afb17958af 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt @@ -20,6 +20,7 @@ import com.intellij.util.ui.JBUI import icons.AwsIcons import kotlinx.coroutines.CoroutineScope import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners.CodeWhispererCodeScanTreeMouseListener +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueGroupingStrategy import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueSeverity import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue @@ -47,6 +48,8 @@ import javax.swing.tree.TreePath */ internal class CodeWhispererCodeScanResultsView(private val project: Project, private val defaultScope: CoroutineScope) : JPanel(BorderLayout()) { + private fun isGroupedBySeverity() = CodeWhispererCodeScanManager.getInstance(project).getGroupingStrategySelected() == IssueGroupingStrategy.SEVERITY + private val codeScanTree: Tree = Tree().apply { isRootVisible = false CodeWhispererCodeScanTreeMouseListener(project).installOn(this) @@ -62,6 +65,9 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr } private fun expandItems() { + if (!isGroupedBySeverity()) { + return + } val criticalTreePath = TreePath(arrayOf(codeScanTree.model.root, codeScanTree.model.getChild(codeScanTree.model.root, 0))) val highTreePath = TreePath(arrayOf(codeScanTree.model.root, codeScanTree.model.getChild(codeScanTree.model.root, 1))) codeScanTree.expandPath(criticalTreePath) @@ -326,7 +332,7 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr return actionManager.createActionToolbar(ACTION_PLACE, group, false) } - private class ColoredTreeCellRenderer : TreeCellRenderer { + private inner class ColoredTreeCellRenderer : TreeCellRenderer { private fun getSeverityIcon(severity: String): Icon? = when (severity) { IssueSeverity.LOW.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_LOW IssueSeverity.MEDIUM.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_MEDIUM @@ -359,7 +365,11 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr } is CodeWhispererCodeScanIssue -> { val cellText = obj.title.trimEnd('.') - val cellDescription = "${obj.file.name} ${obj.displayTextRange()}" + val cellDescription = if (this@CodeWhispererCodeScanResultsView.isGroupedBySeverity()) { + "${obj.file.name} ${obj.displayTextRange()}" + } else { + obj.displayTextRange() + } if (obj.isInvalid) { cell.text = message("codewhisperer.codescan.scan_recommendation_invalid", obj.title, cellDescription, INACTIVE_TEXT_COLOR) cell.toolTipText = message("codewhisperer.codescan.scan_recommendation_invalid.tooltip_text") @@ -367,7 +377,11 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr } else { cell.text = message("codewhisperer.codescan.scan_recommendation", cellText, cellDescription, INACTIVE_TEXT_COLOR) cell.toolTipText = cellText - cell.icon = obj.issueSeverity.icon + cell.icon = if (this@CodeWhispererCodeScanResultsView.isGroupedBySeverity()) { + obj.issueSeverity.icon + } else { + getSeverityIcon(obj.severity) + } } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanGroupingStrategyActionGroup.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanGroupingStrategyActionGroup.kt new file mode 100644 index 0000000000..2974006904 --- /dev/null +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanGroupingStrategyActionGroup.kt @@ -0,0 +1,37 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.actions + +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.ex.CheckboxAction +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueGroupingStrategy + +class CodeWhispererCodeScanGroupingStrategyActionGroup : ActionGroup() { + override fun getChildren(e: AnActionEvent?): Array = IssueGroupingStrategy.entries.map { GroupByAction(it) }.toTypedArray() + + private class GroupByAction(private val groupingStrategy: IssueGroupingStrategy) : CheckboxAction() { + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + override fun isSelected(event: AnActionEvent): Boolean { + val project = event.project ?: return false + return CodeWhispererCodeScanManager.getInstance(project).getGroupingStrategySelected() == groupingStrategy + } + + override fun setSelected(event: AnActionEvent, state: Boolean) { + val project = event.project ?: return + if (state) { + CodeWhispererCodeScanManager.getInstance(project).setGroupingStrategySelected(groupingStrategy) + } + } + + override fun update(e: AnActionEvent) { + super.update(e) + e.presentation.text = groupingStrategy.displayName + } + } +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt index ff1c0c72c6..ad92cde831 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt @@ -73,6 +73,11 @@ enum class IssueSeverity(val displayName: String) { INFO("Info"), } +enum class IssueGroupingStrategy(val displayName: String) { + SEVERITY("Severity"), + FILE_LOCATION("File Location"), +} + fun getCodeScanIssueDetailsHtml( issue: CodeWhispererCodeScanIssue, display: CodeScanIssueDetailsDisplayType,