Skip to content

Commit

Permalink
codewhisperer: add markdown content for hover (#3890)
Browse files Browse the repository at this point in the history
* add markdown content for hover

* allow multiple add/deletions in the same diff
  • Loading branch information
ctlai95 authored Oct 12, 2023
1 parent c676f19 commit 94ef1c1
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/codewhisperer/images/severity-critical.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/codewhisperer/images/severity-high.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/codewhisperer/images/severity-info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/codewhisperer/images/severity-low.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/codewhisperer/images/severity-medium.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 18 additions & 4 deletions src/codewhisperer/models/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,35 @@ export interface CodeScanTelemetryEntry {
credentialStartUrl: string | undefined
}

export interface RecommendationDescription {
text: string
markdown: string
}

export interface SuggestedFix {
description: string
code: string
}

export interface RawCodeScanIssue {
filePath: string
startLine: number
endLine: number
title: string
description: {
text: string
markdown: string
}
description: RecommendationDescription
}

export interface CodeScanIssue {
startLine: number
endLine: number
comment: string
title: string
description: RecommendationDescription
detectorId: string
detectorName: string
relatedVulnerabilities: string[]
severity: string
suggestedFixes?: SuggestedFix[]
}

export interface AggregatedCodeScanIssue {
Expand Down
2 changes: 1 addition & 1 deletion src/codewhisperer/service/diagnosticsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function createSecurityDiagnostic(securityIssue: CodeScanIssue) {
const range = new vscode.Range(securityIssue.startLine, 0, securityIssue.endLine, 0)
const securityDiagnostic: vscode.Diagnostic = new vscode.Diagnostic(
range,
securityIssue.comment,
securityIssue.title,
vscode.DiagnosticSeverity.Warning
)
securityDiagnostic.source = 'Detected by CodeWhisperer '
Expand Down
91 changes: 90 additions & 1 deletion src/codewhisperer/service/securityIssueHoverProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import * as vscode from 'vscode'
import { AggregatedCodeScanIssue, CodeScanIssue } from '../models/model'
import globals from '../../shared/extensionGlobals'

export class SecurityIssueHoverProvider implements vscode.HoverProvider {
static #instance: SecurityIssueHoverProvider
Expand Down Expand Up @@ -41,6 +42,94 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider {
}

private _getContent(issue: CodeScanIssue) {
return new vscode.MarkdownString('TBD')
const markdownString = new vscode.MarkdownString()
markdownString.isTrusted = true
markdownString.supportHtml = true
markdownString.supportThemeIcons = true

const [suggestedFix] = issue.suggestedFixes ?? []

if (suggestedFix) {
markdownString.appendMarkdown(
`## Suggested Fix for ${issue.title} ${this._makeSeverityBadge(issue.severity)}\n`
)
} else {
markdownString.appendMarkdown(`## ${issue.title} ${this._makeSeverityBadge(issue.severity)}\n`)
}

markdownString.appendMarkdown(`${issue.description.markdown}\n\n`)

const viewDetailsCommand = vscode.Uri.parse('command:aws.codewhisperer.viewSecurityIssue')
const applyFixCommand = vscode.Uri.parse('command:aws.codewhisperer.applySecurityFix')
markdownString.appendMarkdown(`[$(eye) View Details](${viewDetailsCommand})\n`)

if (suggestedFix) {
markdownString.appendMarkdown(` | [$(wrench) Apply Fix](${applyFixCommand})\n`)
markdownString.appendMarkdown(
`${this._makeCodeBlock(suggestedFix.code, issue.detectorId.split('/').shift())}\n`
)
}

return markdownString
}

private _makeSeverityBadge(severity: string) {
if (!severity) {
return ''
}
return `![${severity}](${vscode.Uri.joinPath(
globals.context.extensionUri,
`src/codewhisperer/images/severity-${severity.toLowerCase()}.svg`
)})`
}

/**
* Creates a markdown string to render a code diff block for a given code block. Lines
* that are highlighted red indicate deletion while lines highlighted in green indicate
* addition. An optional language can be provided for syntax highlighting on lines which are
* not additions or deletions.
*
* @param code The code containing the diff
* @param language The language for syntax highlighting
* @returns The markdown string
*/
private _makeCodeBlock(code: string, language?: string) {
// Add some padding so that each line has the same number of chars
const lines = code.split('\n').slice(1) // Ignore the first line for diff header
const maxLineChars = lines.reduce((acc, curr) => Math.max(acc, curr.length), 0)
const paddedLines = lines.map(line => line.padEnd(maxLineChars + 2))

// Group the lines into sections so consecutive lines of the same type can be placed in
// the same span below
const sections = [paddedLines[0]]
let i = 1
while (i < paddedLines.length) {
if (paddedLines[i][0] === sections[sections.length - 1][0]) {
sections[sections.length - 1] += '\n' + paddedLines[i]
} else {
sections.push(paddedLines[i])
}
i++
}

// Return each section with the correct syntax highlighting and background color
return sections
.map(
section => `
<span class="codicon codicon-none" style="background-color:var(${
section.startsWith('-')
? '--vscode-diffEditor-removedTextBackground'
: section.startsWith('+')
? '--vscode-diffEditor-insertedTextBackground'
: '--vscode-textCodeBlock-background'
});">
\`\`\`${section.startsWith('-') || section.startsWith('+') ? 'diff' : language}
${section}
\`\`\`
</span>`
)
.join('')
}
}
8 changes: 8 additions & 0 deletions src/codewhisperer/service/securityScanHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ function mapToAggregatedList(
: issue.endLine,
endLine: issue.endLine,
comment: `${issue.title.trim()}: ${issue.description.text.trim()}`,
title: issue.title,
description: issue.description,
// TODO: Update this to use the actual values
detectorId: '',
detectorName: '',
relatedVulnerabilities: [],
severity: '',
suggestedFixes: [],
}
}),
}
Expand Down
64 changes: 59 additions & 5 deletions src/test/codewhisperer/service/securityIssueHoverProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,41 @@ import * as vscode from 'vscode'
import { SecurityIssueHoverProvider } from '../../../codewhisperer/service/securityIssueHoverProvider'
import { createMockDocument } from '../testUtil'
import assert from 'assert'
import { CodeScanIssue } from '../../../codewhisperer/models/model'
import sinon from 'sinon'

const makeIssue = (overrides?: Partial<CodeScanIssue>): CodeScanIssue => ({
startLine: 0,
endLine: 0,
comment: 'comment',
title: 'title',
description: {
text: 'description',
markdown: 'description',
},
detectorId: 'language/cool-detector@v1.0',
detectorName: 'detectorName',
relatedVulnerabilities: [],
severity: 'High',
suggestedFixes: [
{ description: 'fix', code: '@@ -1,1 +1,1 @@\nfirst line\n-second line\n+third line\nfourth line' },
],
...overrides,
})

describe('securityIssueHoverProvider', () => {
describe('providerHover', () => {
it('should return hover for each issue for the current position', () => {
sinon.stub(vscode.Uri, 'joinPath').callsFake(() => vscode.Uri.parse('myPath'))

const securityIssueHoverProvider = new SecurityIssueHoverProvider()
const mockDocument = createMockDocument('def two_sum(nums, target):\nfor', 'test.py', 'python')
securityIssueHoverProvider.issues = [
{
filePath: mockDocument.fileName,
issues: [
{ startLine: 0, endLine: 1, comment: 'issue on this line' },
{ startLine: 0, endLine: 1, comment: 'some other issue' },
makeIssue({ startLine: 0, endLine: 1 }),
makeIssue({ startLine: 0, endLine: 1, suggestedFixes: [] }),
],
},
]
Expand All @@ -27,8 +50,39 @@ describe('securityIssueHoverProvider', () => {
const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token)

assert.strictEqual(actual.contents.length, 2)
assert.strictEqual((actual.contents[0] as vscode.MarkdownString).value, 'TBD')
assert.strictEqual((actual.contents[1] as vscode.MarkdownString).value, 'TBD')
assert.strictEqual(
(actual.contents[0] as vscode.MarkdownString).value,
'## Suggested Fix for title ![High](file:///myPath)\n' +
'description\n\n' +
'[$(eye) View Details](command:aws.codewhisperer.viewSecurityIssue)\n' +
' | [$(wrench) Apply Fix](command:aws.codewhisperer.applySecurityFix)\n\n' +
'<span class="codicon codicon-none" style="background-color:var(--vscode-textCodeBlock-background);">\n\n' +
'```language\n' +
'first line \n' +
'```\n\n' +
'</span>\n' +
'<span class="codicon codicon-none" style="background-color:var(--vscode-diffEditor-removedTextBackground);">\n\n' +
'```diff\n' +
'-second line \n' +
'```\n\n' +
'</span>\n' +
'<span class="codicon codicon-none" style="background-color:var(--vscode-diffEditor-insertedTextBackground);">\n\n' +
'```diff\n' +
'+third line \n' +
'```\n\n' +
'</span>\n' +
'<span class="codicon codicon-none" style="background-color:var(--vscode-textCodeBlock-background);">\n\n' +
'```language\n' +
'fourth line \n' +
'```\n\n' +
'</span>\n'
)
assert.strictEqual(
(actual.contents[1] as vscode.MarkdownString).value,
'## title ![High](file:///myPath)\n' +
'description\n\n' +
'[$(eye) View Details](command:aws.codewhisperer.viewSecurityIssue)\n'
)
})

it('should return empty contents if there is no issue on the current position', () => {
Expand All @@ -37,7 +91,7 @@ describe('securityIssueHoverProvider', () => {
securityIssueHoverProvider.issues = [
{
filePath: mockDocument.fileName,
issues: [{ startLine: 0, endLine: 1, comment: 'issue on this line' }],
issues: [makeIssue({ startLine: 0, endLine: 1 })],
},
]

Expand Down
10 changes: 10 additions & 0 deletions src/test/codewhisperer/views/securityPanelViewProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ const codeScanIssue: CodeScanIssue[] = [
startLine: 0,
endLine: 4,
comment: 'foo',
title: 'bar',
description: {
text: 'foo',
markdown: 'foo',
},
detectorId: '',
detectorName: '',
relatedVulnerabilities: [],
severity: 'low',
suggestedFixes: [],
},
]

Expand Down

0 comments on commit 94ef1c1

Please sign in to comment.