Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(amazonq): grouping options for code issues #5314

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ([email protected]()) {
"${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 ([email protected]()) {
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() {

Check warning on line 14 in plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanGroupingStrategyActionGroup.kt

View workflow job for this annotation

GitHub Actions / qodana

Component/Action not registered

Action is not registered in plugin.xml

Check warning

Code scanning / QDJVMC

Component/Action not registered Warning

Action is not registered in plugin.xml
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
Loading