Skip to content

Commit 5fa889b

Browse files
authored
feat(amazonq): grouping options for code issues (#5314)
1 parent 5470418 commit 5fa889b

File tree

6 files changed

+109
-3
lines changed

6 files changed

+109
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "Amazon Q /review: Code issues can now be grouped by severity or file location."
4+
}

plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml

+5
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@
114114

115115

116116
<group id="aws.toolkit.codewhisperer.toolbar.security">
117+
<group id="codewhisperer.toolbar.security.group" icon="AllIcons.Actions.GroupBy" text="Group" popup="true">
118+
<separator text="Group By"/>
119+
<group id="CodeWhispererCodeScanGroupBy"
120+
class="software.aws.toolkits.jetbrains.services.codewhisperer.codescan.actions.CodeWhispererCodeScanGroupingStrategyActionGroup" text="Group"/>
121+
</group>
117122
<group id="codewhisperer.toolbar.security.filter" icon="AllIcons.General.Filter" text="Filter" popup="true">
118123
<separator text="Severity"/>
119124
<group id="CodeWhispererCodeScanFilterGroup"

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanManager.kt

+41
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners
7171
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners.CodeWhispererCodeScanFileListener
7272
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.CodeScanSessionConfig
7373
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.AmazonQCodeReviewGitUtils.isInsideWorkTree
74+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueGroupingStrategy
7475
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueSeverity
7576
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
7677
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.overlaps
@@ -138,8 +139,10 @@ class CodeWhispererCodeScanManager(val project: Project) {
138139
IssueSeverity.LOW.displayName to DefaultMutableTreeNode(IssueSeverity.LOW.displayName),
139140
IssueSeverity.INFO.displayName to DefaultMutableTreeNode(IssueSeverity.INFO.displayName)
140141
)
142+
private val fileNodeLookup = mutableMapOf<VirtualFile, DefaultMutableTreeNode>()
141143
private val scanNodesLookup = mutableMapOf<VirtualFile, MutableList<DefaultMutableTreeNode>>()
142144
private val selectedSeverityValues = IssueSeverity.entries.associate { it.displayName to true }.toMutableMap()
145+
private var selectedGroupingStrategy = IssueGroupingStrategy.SEVERITY
143146

144147
private val documentListener = CodeWhispererCodeScanDocumentListener(project)
145148
private val editorMouseListener = CodeWhispererCodeScanEditorMouseMotionListener(project)
@@ -281,6 +284,12 @@ class CodeWhispererCodeScanManager(val project: Project) {
281284
updateCodeScanIssuesTree()
282285
}
283286

287+
fun getGroupingStrategySelected() = selectedGroupingStrategy
288+
fun setGroupingStrategySelected(groupingStrategy: IssueGroupingStrategy) {
289+
selectedGroupingStrategy = groupingStrategy
290+
updateCodeScanIssuesTree()
291+
}
292+
284293
/**
285294
* Returns true if there are any code scan issues.
286295
*/
@@ -868,7 +877,18 @@ class CodeWhispererCodeScanManager(val project: Project) {
868877
node.removeAllChildren()
869878
}
870879
}
880+
synchronized(fileNodeLookup) {
881+
fileNodeLookup.clear()
882+
}
871883

884+
return if (selectedGroupingStrategy == IssueGroupingStrategy.SEVERITY) {
885+
createCodeScanIssuesTreeBySeverity(codeScanIssues)
886+
} else {
887+
createCodeScanIssuesTreeByFileLocation(codeScanIssues)
888+
}
889+
}
890+
891+
private fun createCodeScanIssuesTreeBySeverity(codeScanIssues: List<CodeWhispererCodeScanIssue>): DefaultMutableTreeNode {
872892
severityNodeLookup.forEach { (severity, node) ->
873893
if (selectedSeverityValues[severity] == true) {
874894
synchronized(codeScanTreeNodeRoot) {
@@ -890,6 +910,27 @@ class CodeWhispererCodeScanManager(val project: Project) {
890910
return codeScanTreeNodeRoot
891911
}
892912

913+
private fun createCodeScanIssuesTreeByFileLocation(codeScanIssues: List<CodeWhispererCodeScanIssue>): DefaultMutableTreeNode {
914+
codeScanIssues.forEach { issue ->
915+
val fileNode = synchronized(fileNodeLookup) {
916+
fileNodeLookup.getOrPut(issue.file) {
917+
val node = DefaultMutableTreeNode(issue.file)
918+
synchronized(codeScanTreeNodeRoot) {
919+
codeScanTreeNodeRoot.add(node)
920+
}
921+
node
922+
}
923+
}
924+
925+
val scanNode = DefaultMutableTreeNode(issue)
926+
fileNode.add(scanNode)
927+
scanNodesLookup.getOrPut(issue.file) {
928+
mutableListOf()
929+
}.add(scanNode)
930+
}
931+
return codeScanTreeNodeRoot
932+
}
933+
893934
private fun checkIssueCodeSnippet(codeSnippet: List<CodeLine>, startLine: Int, endLine: Int, documentLines: List<String>): Boolean = try {
894935
codeSnippet
895936
.asSequence()

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt

+17-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.intellij.util.ui.JBUI
2020
import icons.AwsIcons
2121
import kotlinx.coroutines.CoroutineScope
2222
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners.CodeWhispererCodeScanTreeMouseListener
23+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueGroupingStrategy
2324
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueSeverity
2425
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig
2526
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue
@@ -47,6 +48,8 @@ import javax.swing.tree.TreePath
4748
*/
4849
internal class CodeWhispererCodeScanResultsView(private val project: Project, private val defaultScope: CoroutineScope) : JPanel(BorderLayout()) {
4950

51+
private fun isGroupedBySeverity() = CodeWhispererCodeScanManager.getInstance(project).getGroupingStrategySelected() == IssueGroupingStrategy.SEVERITY
52+
5053
private val codeScanTree: Tree = Tree().apply {
5154
isRootVisible = false
5255
CodeWhispererCodeScanTreeMouseListener(project).installOn(this)
@@ -62,6 +65,9 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr
6265
}
6366

6467
private fun expandItems() {
68+
if (!isGroupedBySeverity()) {
69+
return
70+
}
6571
val criticalTreePath = TreePath(arrayOf(codeScanTree.model.root, codeScanTree.model.getChild(codeScanTree.model.root, 0)))
6672
val highTreePath = TreePath(arrayOf(codeScanTree.model.root, codeScanTree.model.getChild(codeScanTree.model.root, 1)))
6773
codeScanTree.expandPath(criticalTreePath)
@@ -326,7 +332,7 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr
326332
return actionManager.createActionToolbar(ACTION_PLACE, group, false)
327333
}
328334

329-
private class ColoredTreeCellRenderer : TreeCellRenderer {
335+
private inner class ColoredTreeCellRenderer : TreeCellRenderer {
330336
private fun getSeverityIcon(severity: String): Icon? = when (severity) {
331337
IssueSeverity.LOW.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_LOW
332338
IssueSeverity.MEDIUM.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_MEDIUM
@@ -359,15 +365,23 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr
359365
}
360366
is CodeWhispererCodeScanIssue -> {
361367
val cellText = obj.title.trimEnd('.')
362-
val cellDescription = "${obj.file.name} ${obj.displayTextRange()}"
368+
val cellDescription = if (this@CodeWhispererCodeScanResultsView.isGroupedBySeverity()) {
369+
"${obj.file.name} ${obj.displayTextRange()}"
370+
} else {
371+
obj.displayTextRange()
372+
}
363373
if (obj.isInvalid) {
364374
cell.text = message("codewhisperer.codescan.scan_recommendation_invalid", obj.title, cellDescription, INACTIVE_TEXT_COLOR)
365375
cell.toolTipText = message("codewhisperer.codescan.scan_recommendation_invalid.tooltip_text")
366376
cell.icon = AllIcons.General.Information
367377
} else {
368378
cell.text = message("codewhisperer.codescan.scan_recommendation", cellText, cellDescription, INACTIVE_TEXT_COLOR)
369379
cell.toolTipText = cellText
370-
cell.icon = obj.issueSeverity.icon
380+
cell.icon = if (this@CodeWhispererCodeScanResultsView.isGroupedBySeverity()) {
381+
obj.issueSeverity.icon
382+
} else {
383+
getSeverityIcon(obj.severity)
384+
}
371385
}
372386
}
373387
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.actions
5+
6+
import com.intellij.openapi.actionSystem.ActionGroup
7+
import com.intellij.openapi.actionSystem.ActionUpdateThread
8+
import com.intellij.openapi.actionSystem.AnAction
9+
import com.intellij.openapi.actionSystem.AnActionEvent
10+
import com.intellij.openapi.actionSystem.ex.CheckboxAction
11+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager
12+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueGroupingStrategy
13+
14+
class CodeWhispererCodeScanGroupingStrategyActionGroup : ActionGroup() {
15+
override fun getChildren(e: AnActionEvent?): Array<out AnAction> = IssueGroupingStrategy.entries.map { GroupByAction(it) }.toTypedArray()
16+
17+
private class GroupByAction(private val groupingStrategy: IssueGroupingStrategy) : CheckboxAction() {
18+
override fun getActionUpdateThread() = ActionUpdateThread.BGT
19+
20+
override fun isSelected(event: AnActionEvent): Boolean {
21+
val project = event.project ?: return false
22+
return CodeWhispererCodeScanManager.getInstance(project).getGroupingStrategySelected() == groupingStrategy
23+
}
24+
25+
override fun setSelected(event: AnActionEvent, state: Boolean) {
26+
val project = event.project ?: return
27+
if (state) {
28+
CodeWhispererCodeScanManager.getInstance(project).setGroupingStrategySelected(groupingStrategy)
29+
}
30+
}
31+
32+
override fun update(e: AnActionEvent) {
33+
super.update(e)
34+
e.presentation.text = groupingStrategy.displayName
35+
}
36+
}
37+
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt

+5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ enum class IssueSeverity(val displayName: String) {
7373
INFO("Info"),
7474
}
7575

76+
enum class IssueGroupingStrategy(val displayName: String) {
77+
SEVERITY("Severity"),
78+
FILE_LOCATION("File Location"),
79+
}
80+
7681
fun getCodeScanIssueDetailsHtml(
7782
issue: CodeWhispererCodeScanIssue,
7883
display: CodeScanIssueDetailsDisplayType,

0 commit comments

Comments
 (0)