Skip to content

Commit d7aaad9

Browse files
authored
Merge branch 'main' into dogusata/add-description-to-feedback-form
2 parents 9c632b5 + 5fa889b commit d7aaad9

File tree

10 files changed

+278
-84
lines changed

10 files changed

+278
-84
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "Amazon Q: Prevent IndexOutOfBoundsException by adding boundary checks for invalid range markers (#5187)"
4+
}
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/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt

+32-22
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ import kotlinx.coroutines.CoroutineScope
1616
import kotlinx.coroutines.Job
1717
import kotlinx.coroutines.delay
1818
import kotlinx.coroutines.launch
19-
import software.amazon.awssdk.core.exception.SdkServiceException
2019
import software.amazon.awssdk.services.codewhispererruntime.model.GetTestGenerationResponse
2120
import software.amazon.awssdk.services.codewhispererruntime.model.Range
2221
import software.amazon.awssdk.services.codewhispererruntime.model.StartTestGenerationResponse
2322
import software.amazon.awssdk.services.codewhispererruntime.model.TargetCode
2423
import software.amazon.awssdk.services.codewhispererruntime.model.TestGenerationJobStatus
2524
import software.amazon.awssdk.services.codewhispererstreaming.model.ExportContext
2625
import software.amazon.awssdk.services.codewhispererstreaming.model.ExportIntent
26+
import software.aws.toolkits.core.utils.Waiters.waitUntil
2727
import software.aws.toolkits.core.utils.debug
2828
import software.aws.toolkits.core.utils.error
2929
import software.aws.toolkits.core.utils.getLogger
@@ -58,6 +58,7 @@ import java.io.ByteArrayOutputStream
5858
import java.io.File
5959
import java.io.IOException
6060
import java.nio.file.Paths
61+
import java.time.Duration
6162
import java.time.Instant
6263
import java.util.concurrent.atomic.AtomicBoolean
6364
import java.util.zip.ZipInputStream
@@ -109,29 +110,38 @@ class CodeWhispererUTGChatManager(val project: Project, private val cs: Coroutin
109110

110111
// 2nd API call: StartTestGeneration
111112
val startTestGenerationResponse = try {
112-
startTestGeneration(
113-
uploadId = createUploadUrlResponse.uploadId(),
114-
targetCode = listOf(
115-
TargetCode.builder()
116-
.relativeTargetPath(codeTestResponseContext.currentFileRelativePath.toString())
117-
.targetLineRangeList(
118-
if (selectionRange != null) {
119-
listOf(
120-
selectionRange
113+
var response: StartTestGenerationResponse? = null
114+
115+
waitUntil(
116+
succeedOn = { response?.sdkHttpResponse()?.statusCode() == 200 },
117+
maxDuration = Duration.ofSeconds(1), // 1 second timeout
118+
) {
119+
try {
120+
response = startTestGeneration(
121+
uploadId = createUploadUrlResponse.uploadId(),
122+
targetCode = listOf(
123+
TargetCode.builder()
124+
.relativeTargetPath(codeTestResponseContext.currentFileRelativePath.toString())
125+
.targetLineRangeList(
126+
if (selectionRange != null) {
127+
listOf(selectionRange)
128+
} else {
129+
emptyList()
130+
}
121131
)
122-
} else {
123-
emptyList()
124-
}
125-
)
126-
.build()
127-
),
128-
userInput = prompt
129-
)
130-
} catch (e: Exception) {
131-
val statusCode = when {
132-
e is SdkServiceException -> e.statusCode()
133-
else -> 400
132+
.build()
133+
),
134+
userInput = prompt
135+
)
136+
delay(200)
137+
response?.testGenerationJob() != null
138+
} catch (e: Exception) {
139+
throw e
140+
}
134141
}
142+
143+
response ?: throw RuntimeException("Failed to start test generation")
144+
} catch (e: Exception) {
135145
LOG.error(e) { "Unexpected error while creating test generation job" }
136146
val errorMessage = getTelemetryErrorMessage(e, CodeWhispererConstants.FeatureName.TEST_GENERATION)
137147
throw CodeTestException(

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,

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererUserModificationTracker.kt

+16-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.assertj.core.util.VisibleForTesting
1919
import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
2020
import software.aws.toolkits.core.utils.debug
2121
import software.aws.toolkits.core.utils.getLogger
22+
import software.aws.toolkits.core.utils.warn
2223
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
2324
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
2425
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
@@ -154,14 +155,28 @@ class CodeWhispererUserModificationTracker(private val project: Project) : Dispo
154155
private fun emitTelemetryOnSuggestion(acceptedSuggestion: AcceptedSuggestionEntry) {
155156
val file = acceptedSuggestion.vFile
156157

157-
if (file == null || (!file.isValid)) {
158+
if (file == null || (!file.isValid) || !acceptedSuggestion.range.isValid) {
158159
sendModificationTelemetry(acceptedSuggestion, null)
159160
sendUserModificationTelemetryToServiceAPI(acceptedSuggestion)
160161
} else {
161162
// Will remove this later when we truly don't need toolkit user modification telemetry anymore
162163
val document = runReadAction {
163164
FileDocumentManager.getInstance().getDocument(file)
164165
}
166+
val start = acceptedSuggestion.range.startOffset
167+
val end = acceptedSuggestion.range.endOffset
168+
if (document != null) {
169+
if (start < 0 || end < start || end > document.textLength) {
170+
LOG.warn {
171+
"Invalid range for suggestion ${acceptedSuggestion.requestId}: " +
172+
"start=$start, end=$end, docLength=${document.textLength}"
173+
}
174+
sendModificationTelemetry(acceptedSuggestion, null)
175+
sendUserModificationTelemetryToServiceAPI(acceptedSuggestion)
176+
return
177+
}
178+
}
179+
165180
val currentString = document?.getText(
166181
TextRange(acceptedSuggestion.range.startOffset, acceptedSuggestion.range.endOffset)
167182
)

0 commit comments

Comments
 (0)