Skip to content

Commit 4728b23

Browse files
authored
fix(amazonq): apply fix removes other issues (#6236)
## Problem Apply fix removes other issues in the same file. This is happening because apply fix command will always replace the entire file contents which triggers a document change event that intersects with all issues in the file. ## Solution Look through the diff hunks and apply them in a range. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 5669e87 commit 4728b23

File tree

5 files changed

+134
-39
lines changed

5 files changed

+134
-39
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "/review: Apply fix removes other issues in the same file."
4+
}

packages/core/src/codewhisperer/activation.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -670,27 +670,28 @@ export async function activate(context: ExtContext): Promise<void> {
670670
function setSubscriptionsForCodeIssues() {
671671
context.extensionContext.subscriptions.push(
672672
vscode.workspace.onDidChangeTextDocument(async (e) => {
673-
// verify the document is something with a finding
674-
for (const issue of SecurityIssueProvider.instance.issues) {
675-
if (issue.filePath === e.document.uri.fsPath) {
676-
disposeSecurityDiagnostic(e)
677-
678-
SecurityIssueProvider.instance.handleDocumentChange(e)
679-
SecurityIssueTreeViewProvider.instance.refresh()
680-
await syncSecurityIssueWebview(context)
681-
682-
toggleIssuesVisibility((issue, filePath) =>
683-
filePath !== e.document.uri.fsPath
684-
? issue.visible
685-
: !detectCommentAboveLine(
686-
e.document,
687-
issue.startLine,
688-
CodeWhispererConstants.amazonqIgnoreNextLine
689-
)
690-
)
691-
break
692-
}
673+
if (e.document.uri.scheme !== 'file') {
674+
return
693675
}
676+
const diagnostics = securityScanRender.securityDiagnosticCollection?.get(e.document.uri)
677+
if (!diagnostics || diagnostics.length === 0) {
678+
return
679+
}
680+
disposeSecurityDiagnostic(e)
681+
682+
SecurityIssueProvider.instance.handleDocumentChange(e)
683+
SecurityIssueTreeViewProvider.instance.refresh()
684+
await syncSecurityIssueWebview(context)
685+
686+
toggleIssuesVisibility((issue, filePath) =>
687+
filePath !== e.document.uri.fsPath
688+
? issue.visible
689+
: !detectCommentAboveLine(
690+
e.document,
691+
issue.startLine,
692+
CodeWhispererConstants.amazonqIgnoreNextLine
693+
)
694+
)
694695
})
695696
)
696697
}

packages/core/src/codewhisperer/commands/basicCommands.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { cancel, confirm } from '../../shared'
6666
import { startCodeFixGeneration } from './startCodeFixGeneration'
6767
import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext'
6868
import path from 'path'
69+
import { parsePatch } from 'diff'
6970

7071
const MessageTimeOut = 5_000
7172

@@ -459,11 +460,21 @@ export const applySecurityFix = Commands.declare(
459460
}
460461

461462
const edit = new vscode.WorkspaceEdit()
462-
edit.replace(
463-
document.uri,
464-
new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end),
465-
updatedContent
466-
)
463+
const diffs = parsePatch(suggestedFix.code)
464+
for (const diff of diffs) {
465+
for (const hunk of [...diff.hunks].reverse()) {
466+
const startLine = document.lineAt(hunk.oldStart - 1)
467+
const endLine = document.lineAt(hunk.oldStart - 1 + hunk.oldLines - 1)
468+
const range = new vscode.Range(startLine.range.start, endLine.range.end)
469+
470+
const newText = updatedContent
471+
.split('\n')
472+
.slice(hunk.newStart - 1, hunk.newStart - 1 + hunk.newLines)
473+
.join('\n')
474+
475+
edit.replace(document.uri, range, newText)
476+
}
477+
}
467478
const isApplied = await vscode.workspace.applyEdit(edit)
468479
if (isApplied) {
469480
void document.save().then((didSave) => {

packages/core/src/codewhisperer/service/securityIssueProvider.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import * as vscode from 'vscode'
7-
import { AggregatedCodeScanIssue, CodeScanIssue, SuggestedFix } from '../models/model'
7+
import { AggregatedCodeScanIssue, CodeScanIssue, CodeScansState, SuggestedFix } from '../models/model'
88
export class SecurityIssueProvider {
99
static #instance: SecurityIssueProvider
1010
public static get instance() {
@@ -25,13 +25,15 @@ export class SecurityIssueProvider {
2525
if (!event.contentChanges || event.contentChanges.length === 0) {
2626
return
2727
}
28-
const { changedRange, lineOffset } = event.contentChanges.reduce(
28+
const { changedRange, changedText, lineOffset } = event.contentChanges.reduce(
2929
(acc, change) => ({
3030
changedRange: acc.changedRange.union(change.range),
31+
changedText: acc.changedText + change.text,
3132
lineOffset: acc.lineOffset + this._getLineOffset(change.range, change.text),
3233
}),
3334
{
3435
changedRange: event.contentChanges[0].range,
36+
changedText: '',
3537
lineOffset: 0,
3638
}
3739
)
@@ -43,18 +45,20 @@ export class SecurityIssueProvider {
4345
return {
4446
...group,
4547
issues: group.issues
46-
.filter(
47-
(issue) =>
48-
// Filter out any modified issues
49-
!changedRange.intersection(
50-
new vscode.Range(
51-
issue.startLine,
52-
event.document.lineAt(issue.startLine)?.range.start.character ?? 0,
53-
issue.endLine,
54-
event.document.lineAt(issue.endLine)?.range.end.character ?? 0
55-
)
56-
)
57-
)
48+
.filter((issue) => {
49+
const range = new vscode.Range(
50+
issue.startLine,
51+
event.document.lineAt(issue.startLine)?.range.start.character ?? 0,
52+
issue.endLine,
53+
event.document.lineAt(issue.endLine - 1)?.range.end.character ?? 0
54+
)
55+
const intersection = changedRange.intersection(range)
56+
return !(
57+
intersection &&
58+
(/\S/.test(changedText) || changedText === '') &&
59+
!CodeScansState.instance.isScansEnabled()
60+
)
61+
})
5862
.map((issue) => {
5963
if (issue.startLine < changedRange.end.line) {
6064
return issue

packages/core/src/test/codewhisperer/commands/basicCommands.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,81 @@ describe('CodeWhisperer-basicCommands', function () {
672672
reasonDesc: 'Failed to apply edit to the workspace.',
673673
})
674674
})
675+
676+
it('should apply the edit at the correct range', async function () {
677+
const fileName = 'sample.py'
678+
const textDocumentMock = createMockDocument(
679+
`from flask import app
680+
681+
682+
@app.route('/')
683+
def execute_input_noncompliant():
684+
from flask import request
685+
module_version = request.args.get("module_version")
686+
# Noncompliant: executes unsanitized inputs.
687+
exec("import urllib%s as urllib" % module_version)
688+
# {/fact}
689+
690+
691+
# {fact rule=code-injection@v1.0 defects=0}
692+
from flask import app
693+
694+
695+
@app.route('/')
696+
def execute_input_compliant():
697+
from flask import request
698+
module_version = request.args.get("module_version")
699+
# Compliant: executes sanitized inputs.
700+
exec("import urllib%d as urllib" % int(module_version))
701+
# {/fact}`,
702+
fileName
703+
)
704+
openTextDocumentMock.resolves(textDocumentMock)
705+
sandbox.stub(vscode.workspace, 'openTextDocument').value(openTextDocumentMock)
706+
707+
sandbox.stub(vscode.WorkspaceEdit.prototype, 'replace').value(replaceMock)
708+
applyEditMock.resolves(true)
709+
sandbox.stub(vscode.workspace, 'applyEdit').value(applyEditMock)
710+
sandbox.stub(diagnosticsProvider, 'removeDiagnostic').value(removeDiagnosticMock)
711+
sandbox.stub(SecurityIssueProvider.instance, 'removeIssue').value(removeIssueMock)
712+
sandbox.stub(vscode.window, 'showTextDocument').value(showTextDocumentMock)
713+
714+
targetCommand = testCommand(applySecurityFix)
715+
codeScanIssue.suggestedFixes = [
716+
{
717+
code: `@@ -6,4 +6,5 @@
718+
from flask import request
719+
module_version = request.args.get("module_version")
720+
# Noncompliant: executes unsanitized inputs.
721+
- exec("import urllib%d as urllib" % int(module_version))
722+
+ __import__("urllib" + module_version)
723+
+#import importlib`,
724+
description: 'dummy',
725+
},
726+
]
727+
await targetCommand.execute(codeScanIssue, fileName, 'webview')
728+
assert.ok(
729+
replaceMock.calledOnceWith(
730+
textDocumentMock.uri,
731+
new vscode.Range(5, 0, 8, 54),
732+
` from flask import request
733+
module_version = request.args.get("module_version")
734+
# Noncompliant: executes unsanitized inputs.
735+
__import__("urllib" + module_version)
736+
#import importlib`
737+
)
738+
)
739+
assert.ok(applyEditMock.calledOnce)
740+
assert.ok(removeDiagnosticMock.calledOnceWith(textDocumentMock.uri, codeScanIssue))
741+
assert.ok(removeIssueMock.calledOnce)
742+
743+
assertTelemetry('codewhisperer_codeScanIssueApplyFix', {
744+
detectorId: codeScanIssue.detectorId,
745+
findingId: codeScanIssue.findingId,
746+
component: 'webview',
747+
result: 'Succeeded',
748+
})
749+
})
675750
})
676751

677752
// describe('generateFix', function () {

0 commit comments

Comments
 (0)