Skip to content

Commit 72d8c34

Browse files
authored
feat(amazonq): apply fix without save aws#4855
Problem Scans can now run without saving the file to disk, but "Apply Fix" still saves the file to disk. Solution - Use WorkspaceEdit to apply fix - If auto-scans is enabled, immediately remove the issue from hover and diagnostics after fixing it
1 parent ebb43e8 commit 72d8c34

File tree

4 files changed

+120
-57
lines changed

4 files changed

+120
-57
lines changed

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727
} from '../util/customizationUtil'
2828
import { applyPatch } from 'diff'
2929
import { closeSecurityIssueWebview, showSecurityIssueWebview } from '../views/securityIssue/securityIssueWebview'
30-
import { fsCommon } from '../../srcShared/fs'
3130
import { Mutable } from '../../shared/utilities/tsUtils'
3231
import { CodeWhispererSource } from './types'
3332
import { FeatureConfigProvider } from '../service/featureConfigProvider'
@@ -36,6 +35,9 @@ import { Auth, AwsConnection } from '../../auth'
3635
import { once } from '../../shared/utilities/functionUtils'
3736
import { isTextEditor } from '../../shared/utilities/editorUtilities'
3837
import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands'
38+
import { removeDiagnostic } from '../service/diagnosticsProvider'
39+
import { SecurityIssueHoverProvider } from '../service/securityIssueHoverProvider'
40+
import { SecurityIssueCodeActionProvider } from '../service/securityIssueCodeActionProvider'
3941

4042
export const toggleCodeSuggestions = Commands.declare(
4143
{ id: 'aws.amazonq.toggleCodeSuggestion', compositeKey: { 1: 'source' } },
@@ -343,14 +345,23 @@ export const applySecurityFix = Commands.declare(
343345
throw Error('Failed to get updated content from applying diff patch')
344346
}
345347

346-
// saving the document text if not save
347-
const isSaved = await document.save()
348-
if (!isSaved) {
349-
throw Error('Failed to save editor text changes into the file.')
348+
const edit = new vscode.WorkspaceEdit()
349+
edit.replace(
350+
document.uri,
351+
new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end),
352+
updatedContent
353+
)
354+
const isApplied = await vscode.workspace.applyEdit(edit)
355+
if (!isApplied) {
356+
throw Error('Failed to apply edit to the workspace.')
357+
}
358+
359+
if (CodeScansState.instance.isScansEnabled()) {
360+
removeDiagnostic(document.uri, issue)
361+
SecurityIssueHoverProvider.instance.removeIssue(document.uri, issue)
362+
SecurityIssueCodeActionProvider.instance.removeIssue(document.uri, issue)
350363
}
351364

352-
// writing the patch applied version of document into the file
353-
await fsCommon.writeFile(filePath, updatedContent)
354365
await closeSecurityIssueWebview(issue.findingId)
355366
} catch (err) {
356367
getLogger().error(`Apply fix command failed. ${err}`)

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,19 @@ function getLineOffset(range: vscode.Range, text: string) {
128128
const changedLines = text.split('\n').length
129129
return changedLines - originLines
130130
}
131+
132+
export function removeDiagnostic(uri: vscode.Uri, issue: CodeScanIssue) {
133+
const currentSecurityDiagnostics = securityScanRender.securityDiagnosticCollection?.get(uri)
134+
if (currentSecurityDiagnostics) {
135+
const newSecurityDiagnostics = currentSecurityDiagnostics.filter(diagnostic => {
136+
return (
137+
typeof diagnostic.code !== 'string' &&
138+
typeof diagnostic.code !== 'number' &&
139+
diagnostic.code?.value !== issue.detectorId &&
140+
diagnostic.message !== issue.title &&
141+
diagnostic.range !== new vscode.Range(issue.startLine, 0, issue.endLine, 0)
142+
)
143+
})
144+
securityScanRender.securityDiagnosticCollection?.set(uri, newSecurityDiagnostics)
145+
}
146+
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import * as vscode from 'vscode'
7-
import { AggregatedCodeScanIssue, CodeScansState } from '../models/model'
7+
import { AggregatedCodeScanIssue, CodeScanIssue, CodeScansState } from '../models/model'
88
export abstract class SecurityIssueProvider {
99
private _issues: AggregatedCodeScanIssue[] = []
1010
public get issues() {
@@ -64,4 +64,16 @@ export abstract class SecurityIssueProvider {
6464
const changedLines = text.split('\n').length
6565
return changedLines - originLines
6666
}
67+
68+
public removeIssue(uri: vscode.Uri, issue: CodeScanIssue) {
69+
this._issues = this._issues.map(group => {
70+
if (group.filePath !== uri.fsPath) {
71+
return group
72+
}
73+
return {
74+
...group,
75+
issues: group.issues.filter(i => i.findingId !== issue.findingId),
76+
}
77+
})
78+
}
6779
}

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

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ import { AuthUtil } from '../../../codewhisperer/util/authUtil'
2828
import { getTestWindow } from '../../shared/vscode/window'
2929
import { ExtContext } from '../../../shared/extensions'
3030
import { get, set } from '../../../codewhisperer/util/commonUtil'
31-
import { MockDocument } from '../../fake/fakeDocument'
32-
import { FileSystemCommon } from '../../../srcShared/fs'
3331
import { getLogger } from '../../../shared/logger/logger'
3432
import {
3533
createAutoScans,
@@ -55,6 +53,9 @@ import { cwQuickPickSource } from '../../../codewhisperer/commands/types'
5553
import { isTextEditor } from '../../../shared/utilities/editorUtilities'
5654
import { refreshStatusBar } from '../../../codewhisperer/service/inlineCompletionService'
5755
import { focusAmazonQPanel } from '../../../codewhispererChat/commands/registerCommands'
56+
import * as diagnosticsProvider from '../../../codewhisperer/service/diagnosticsProvider'
57+
import { SecurityIssueHoverProvider } from '../../../codewhisperer/service/securityIssueHoverProvider'
58+
import { SecurityIssueCodeActionProvider } from '../../../codewhisperer/service/securityIssueCodeActionProvider'
5859

5960
describe('CodeWhisperer-basicCommands', function () {
6061
let targetCommand: Command<any> & vscode.Disposable
@@ -392,15 +393,19 @@ describe('CodeWhisperer-basicCommands', function () {
392393

393394
describe('applySecurityFix', function () {
394395
let sandbox: sinon.SinonSandbox
395-
let saveStub: sinon.SinonStub
396396
let openTextDocumentMock: sinon.SinonStub
397-
let writeFileMock: sinon.SinonStub
397+
let replaceMock: sinon.SinonStub
398+
let applyEditMock: sinon.SinonStub
399+
let removeDiagnosticMock: sinon.SinonStub
400+
let removeIssueMock: sinon.SinonStub
398401

399402
beforeEach(function () {
400403
sandbox = sinon.createSandbox()
401-
saveStub = sinon.stub()
402404
openTextDocumentMock = sinon.stub()
403-
writeFileMock = sinon.stub()
405+
replaceMock = sinon.stub()
406+
applyEditMock = sinon.stub()
407+
removeDiagnosticMock = sinon.stub()
408+
removeIssueMock = sinon.stub()
404409
})
405410

406411
afterEach(function () {
@@ -409,14 +414,17 @@ describe('CodeWhisperer-basicCommands', function () {
409414

410415
it('should call applySecurityFix command successfully', async function () {
411416
const fileName = 'sample.py'
412-
saveStub.resolves(true)
413-
const textDocumentMock = new MockDocument('first line\n second line\n fourth line', fileName, saveStub)
417+
const textDocumentMock = createMockDocument('first line\n second line\n fourth line', fileName)
414418

415419
openTextDocumentMock.resolves(textDocumentMock)
416420
sandbox.stub(vscode.workspace, 'openTextDocument').value(openTextDocumentMock)
417421

418-
writeFileMock.resolves(true)
419-
sinon.stub(FileSystemCommon.prototype, 'writeFile').value(writeFileMock)
422+
sandbox.stub(vscode.WorkspaceEdit.prototype, 'replace').value(replaceMock)
423+
applyEditMock.resolves(true)
424+
sandbox.stub(vscode.workspace, 'applyEdit').value(applyEditMock)
425+
sandbox.stub(diagnosticsProvider, 'removeDiagnostic').value(removeDiagnosticMock)
426+
sandbox.stub(SecurityIssueHoverProvider.instance, 'removeIssue').value(removeIssueMock)
427+
sandbox.stub(SecurityIssueCodeActionProvider.instance, 'removeIssue').value(removeIssueMock)
420428

421429
targetCommand = testCommand(applySecurityFix)
422430
const codeScanIssue = createCodeScanIssue({
@@ -428,8 +436,16 @@ describe('CodeWhisperer-basicCommands', function () {
428436
],
429437
})
430438
await targetCommand.execute(codeScanIssue, fileName, 'hover')
431-
assert.ok(saveStub.calledOnce)
432-
assert.ok(writeFileMock.calledOnceWith(fileName, 'first line\n third line\n fourth line'))
439+
assert.ok(
440+
replaceMock.calledOnceWith(
441+
textDocumentMock.uri,
442+
new vscode.Range(0, 0, 2, 12),
443+
'first line\n third line\n fourth line'
444+
)
445+
)
446+
assert.ok(applyEditMock.calledOnce)
447+
assert.ok(removeDiagnosticMock.calledOnceWith(textDocumentMock.uri, codeScanIssue))
448+
assert.ok(removeIssueMock.calledTwice)
433449

434450
assertTelemetry('codewhisperer_codeScanIssueApplyFix', {
435451
detectorId: codeScanIssue.detectorId,
@@ -439,83 +455,91 @@ describe('CodeWhisperer-basicCommands', function () {
439455
})
440456
})
441457

442-
it('handles patch failure', async function () {
443-
const textDocumentMock = createMockDocument()
458+
it('should call applySecurityFix command successfully but not remove issues if auto-scans is disabled', async function () {
459+
const fileName = 'sample.py'
460+
const textDocumentMock = createMockDocument('first line\n second line\n fourth line', fileName)
444461

445462
openTextDocumentMock.resolves(textDocumentMock)
446-
447463
sandbox.stub(vscode.workspace, 'openTextDocument').value(openTextDocumentMock)
448464

465+
sandbox.stub(vscode.WorkspaceEdit.prototype, 'replace').value(replaceMock)
466+
applyEditMock.resolves(true)
467+
sandbox.stub(vscode.workspace, 'applyEdit').value(applyEditMock)
468+
sandbox.stub(diagnosticsProvider, 'removeDiagnostic').value(removeDiagnosticMock)
469+
sandbox.stub(SecurityIssueHoverProvider.instance, 'removeIssue').value(removeIssueMock)
470+
sandbox.stub(SecurityIssueCodeActionProvider.instance, 'removeIssue').value(removeIssueMock)
471+
await CodeScansState.instance.setScansEnabled(false)
472+
449473
targetCommand = testCommand(applySecurityFix)
450474
const codeScanIssue = createCodeScanIssue({
451475
suggestedFixes: [
452476
{
453-
code: '@@ -1,1 -1,1 @@\n-mock\n+line5',
454-
description: 'dummy',
477+
description: 'fix',
478+
code: '@@ -1,3 +1,3 @@\n first line\n- second line\n+ third line\n fourth line',
455479
},
456480
],
457481
})
458-
await targetCommand.execute(codeScanIssue, 'test.py', 'webview')
482+
await targetCommand.execute(codeScanIssue, fileName, 'hover')
483+
assert.ok(
484+
replaceMock.calledOnceWith(
485+
textDocumentMock.uri,
486+
new vscode.Range(0, 0, 2, 12),
487+
'first line\n third line\n fourth line'
488+
)
489+
)
490+
assert.ok(applyEditMock.calledOnce)
491+
assert.ok(removeDiagnosticMock.notCalled)
492+
assert.ok(removeIssueMock.notCalled)
459493

460-
assert.strictEqual(getTestWindow().shownMessages[0].message, 'Failed to apply suggested code fix.')
461494
assertTelemetry('codewhisperer_codeScanIssueApplyFix', {
462495
detectorId: codeScanIssue.detectorId,
463496
findingId: codeScanIssue.findingId,
464-
component: 'webview',
465-
result: 'Failed',
466-
reason: 'Error: Failed to get updated content from applying diff patch',
497+
component: 'hover',
498+
result: 'Succeeded',
467499
})
468500
})
469501

470-
it('handles document save failure', async function () {
471-
const fileName = 'sample.py'
472-
saveStub.resolves(false)
473-
const textDocumentMock = new MockDocument('first line\n second line\n fourth line', fileName, saveStub)
502+
it('handles patch failure', async function () {
503+
const textDocumentMock = createMockDocument()
474504

475505
openTextDocumentMock.resolves(textDocumentMock)
476506

477507
sandbox.stub(vscode.workspace, 'openTextDocument').value(openTextDocumentMock)
478-
const loggerStub = sinon.stub(getLogger(), 'error')
479508

480509
targetCommand = testCommand(applySecurityFix)
481510
const codeScanIssue = createCodeScanIssue({
482511
suggestedFixes: [
483512
{
484-
description: 'fix',
485-
code: '@@ -1,3 +1,3 @@\n first line\n- second line\n+ third line\n fourth line',
513+
code: '@@ -1,1 -1,1 @@\n-mock\n+line5',
514+
description: 'dummy',
486515
},
487516
],
488517
})
489-
await targetCommand.execute(codeScanIssue, fileName, 'quickfix')
518+
await targetCommand.execute(codeScanIssue, 'test.py', 'webview')
490519

491-
assert.ok(saveStub.calledOnce)
492-
assert.ok(loggerStub.calledOnce)
493-
const actual = loggerStub.getCall(0).args[0]
494-
assert.strictEqual(
495-
actual,
496-
'Apply fix command failed. Error: Failed to save editor text changes into the file.'
497-
)
520+
assert.strictEqual(getTestWindow().shownMessages[0].message, 'Failed to apply suggested code fix.')
498521
assertTelemetry('codewhisperer_codeScanIssueApplyFix', {
499522
detectorId: codeScanIssue.detectorId,
500523
findingId: codeScanIssue.findingId,
501-
component: 'quickfix',
524+
component: 'webview',
502525
result: 'Failed',
503-
reason: 'Error: Failed to save editor text changes into the file.',
526+
reason: 'Error: Failed to get updated content from applying diff patch',
504527
})
505528
})
506529

507-
it('handles document write failure', async function () {
530+
it('handles apply edit failure', async function () {
508531
const fileName = 'sample.py'
509-
saveStub.resolves(true)
510-
const textDocumentMock = new MockDocument('first line\n second line\n fourth line', fileName, saveStub)
532+
const textDocumentMock = createMockDocument('first line\n second line\n fourth line', fileName)
511533

512534
openTextDocumentMock.resolves(textDocumentMock)
513-
writeFileMock.rejects('Error: Writing to file failed.')
514535

515536
sandbox.stub(vscode.workspace, 'openTextDocument').value(openTextDocumentMock)
516-
sinon.stub(FileSystemCommon.prototype, 'writeFile').value(writeFileMock)
517537
const loggerStub = sinon.stub(getLogger(), 'error')
518538

539+
sinon.stub(vscode.WorkspaceEdit.prototype, 'replace').value(replaceMock)
540+
applyEditMock.resolves(false)
541+
sinon.stub(vscode.workspace, 'applyEdit').value(applyEditMock)
542+
519543
targetCommand = testCommand(applySecurityFix)
520544
const codeScanIssue = createCodeScanIssue({
521545
suggestedFixes: [
@@ -525,18 +549,18 @@ describe('CodeWhisperer-basicCommands', function () {
525549
},
526550
],
527551
})
528-
await targetCommand.execute(codeScanIssue, fileName, 'hover')
552+
await targetCommand.execute(codeScanIssue, fileName, 'quickfix')
529553

530-
assert.ok(saveStub.calledOnce)
554+
assert.ok(replaceMock.calledOnce)
531555
assert.ok(loggerStub.calledOnce)
532556
const actual = loggerStub.getCall(0).args[0]
533-
assert.strictEqual(actual, 'Apply fix command failed. Error: Writing to file failed.')
557+
assert.strictEqual(actual, 'Apply fix command failed. Error: Failed to apply edit to the workspace.')
534558
assertTelemetry('codewhisperer_codeScanIssueApplyFix', {
535559
detectorId: codeScanIssue.detectorId,
536560
findingId: codeScanIssue.findingId,
537-
component: 'hover',
561+
component: 'quickfix',
538562
result: 'Failed',
539-
reason: 'Error: Writing to file failed.',
563+
reason: 'Error: Failed to apply edit to the workspace.',
540564
})
541565
})
542566
})

0 commit comments

Comments
 (0)