Skip to content

Commit

Permalink
feat(amazonq): grouping options for code issues (#5314)
Browse files Browse the repository at this point in the history
  • Loading branch information
ctlai95 authored Feb 4, 2025
1 parent 5470418 commit 5fa889b
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "feature",
"description" : "Amazon Q /review: Code issues can now be grouped by severity or file location."
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@


<group id="aws.toolkit.codewhisperer.toolbar.security">
<group id="codewhisperer.toolbar.security.group" icon="AllIcons.Actions.GroupBy" text="Group" popup="true">
<separator text="Group By"/>
<group id="CodeWhispererCodeScanGroupBy"
class="software.aws.toolkits.jetbrains.services.codewhisperer.codescan.actions.CodeWhispererCodeScanGroupingStrategyActionGroup" text="Group"/>
</group>
<group id="codewhisperer.toolbar.security.filter" icon="AllIcons.General.Filter" text="Filter" popup="true">
<separator text="Severity"/>
<group id="CodeWhispererCodeScanFilterGroup"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners.CodeWhispererCodeScanFileListener
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.CodeScanSessionConfig
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.AmazonQCodeReviewGitUtils.isInsideWorkTree
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.credentials.CodeWhispererClientAdaptor
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.overlaps
Expand Down Expand Up @@ -138,8 +139,10 @@ class CodeWhispererCodeScanManager(val project: Project) {
IssueSeverity.LOW.displayName to DefaultMutableTreeNode(IssueSeverity.LOW.displayName),
IssueSeverity.INFO.displayName to DefaultMutableTreeNode(IssueSeverity.INFO.displayName)
)
private val fileNodeLookup = mutableMapOf<VirtualFile, DefaultMutableTreeNode>()
private val scanNodesLookup = mutableMapOf<VirtualFile, MutableList<DefaultMutableTreeNode>>()
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)
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<CodeWhispererCodeScanIssue>): DefaultMutableTreeNode {
severityNodeLookup.forEach { (severity, node) ->
if (selectedSeverityValues[severity] == true) {
synchronized(codeScanTreeNodeRoot) {
Expand All @@ -890,6 +910,27 @@ class CodeWhispererCodeScanManager(val project: Project) {
return codeScanTreeNodeRoot
}

private fun createCodeScanIssuesTreeByFileLocation(codeScanIssues: List<CodeWhispererCodeScanIssue>): 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<CodeLine>, startLine: Int, endLine: Int, documentLines: List<String>): Boolean = try {
codeSnippet
.asSequence()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -359,15 +365,23 @@ 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")
cell.icon = AllIcons.General.Information
} 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)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<out AnAction> = 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 5fa889b

Please sign in to comment.