-
-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add vulnerability quick-fix + hint #39
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
base: main
Are you sure you want to change the base?
Changes from all commits
abbb343
3afa90f
b451e10
ecacd9c
e17e843
fe1cdde
0f13ef2
dfdbef5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import type { CodeActionContext, CodeActionProvider, Diagnostic, Range, TextDocument } from 'vscode' | ||
| import { CodeAction, CodeActionKind, WorkspaceEdit } from 'vscode' | ||
|
|
||
| const FIXED_VERSION_MESSAGE_PATTERN = / Upgrade to (?<fixedInVersion>\S+) to fix\.$/ | ||
|
|
||
| function getDiagnosticCodeValue(diagnostic: Diagnostic): string | null { | ||
| if (typeof diagnostic.code === 'string') | ||
| return diagnostic.code | ||
|
|
||
| if (typeof diagnostic.code === 'object' && typeof diagnostic.code.value === 'string') | ||
| return diagnostic.code.value | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| function isVulnerabilityDiagnostic(diagnostic: Diagnostic): boolean { | ||
| return getDiagnosticCodeValue(diagnostic) === 'vulnerability' | ||
| } | ||
|
|
||
| function getFixedInVersion(diagnostic: Diagnostic): string | null { | ||
| const fixedInVersionMatch = FIXED_VERSION_MESSAGE_PATTERN.exec(diagnostic.message) | ||
| const fixedInVersion = fixedInVersionMatch?.groups?.fixedInVersion | ||
| return fixedInVersion && fixedInVersion.length > 0 ? fixedInVersion : null | ||
| } | ||
|
|
||
| function createUpdateVersionAction(document: TextDocument, range: Range, fixedInVersion: string): CodeAction { | ||
| const codeAction = new CodeAction(`Update to ${fixedInVersion} to fix vulnerabilities`, CodeActionKind.QuickFix) | ||
| codeAction.isPreferred = true | ||
| const workspaceEdit = new WorkspaceEdit() | ||
| workspaceEdit.replace(document.uri, range, fixedInVersion) | ||
| codeAction.edit = workspaceEdit | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return codeAction | ||
| } | ||
|
|
||
| export class VulnerabilityCodeActionProvider implements CodeActionProvider { | ||
| provideCodeActions(document: TextDocument, _range: Range, context: CodeActionContext): CodeAction[] { | ||
| return context.diagnostics.flatMap((diagnostic) => { | ||
| if (!isVulnerabilityDiagnostic(diagnostic)) | ||
| return [] | ||
|
|
||
| const fixedInVersion = getFixedInVersion(diagnostic) | ||
| if (!fixedInVersion) | ||
| return [] | ||
|
|
||
| return [createUpdateVersionAction(document, diagnostic.range, fixedInVersion)] | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import type { CodeActionContext, Diagnostic, TextDocument } from 'vscode' | ||
| import { describe, expect, it, vi } from 'vitest' | ||
| import { Range, Uri } from 'vscode' | ||
| import { VulnerabilityCodeActionProvider } from '../src/providers/code-actions/vulnerability' | ||
|
|
||
| function createDiagnostic(options: { code: string | { value: string }, message: string }): Diagnostic { | ||
| return { | ||
| code: options.code, | ||
| message: options.message, | ||
| range: new Range(0, 0, 0, 6), | ||
| } as Diagnostic | ||
| } | ||
|
|
||
| function createTextDocument(versionText: string): TextDocument { | ||
| return { | ||
| uri: Uri.parse('file:///package.json'), | ||
| getText: vi.fn(() => versionText), | ||
| } as unknown as TextDocument | ||
| } | ||
|
|
||
| function createCodeActionContext(diagnostics: Diagnostic[]): CodeActionContext { | ||
| return { | ||
| diagnostics, | ||
| triggerKind: 1 as CodeActionContext['triggerKind'], | ||
| only: undefined, | ||
| } | ||
| } | ||
|
|
||
| describe('vulnerability code action provider', () => { | ||
| it('provides a quick fix when vulnerability message includes upgrade version', () => { | ||
| const provider = new VulnerabilityCodeActionProvider() | ||
| const textDocument = createTextDocument('^1.0.0') | ||
|
|
||
| const diagnostic = createDiagnostic({ | ||
| code: { value: 'vulnerability' }, | ||
| message: 'This version has 1 high vulnerability. Upgrade to 1.2.3 to fix.', | ||
| }) | ||
|
|
||
| const codeActions = provider.provideCodeActions( | ||
| textDocument, | ||
| diagnostic.range, | ||
| createCodeActionContext([diagnostic]), | ||
| ) | ||
|
|
||
| expect(codeActions).toEqual([ | ||
|
Check failure on line 45 in tests/vulnerability-code-actions.test.ts
|
||
| expect.objectContaining({ | ||
| title: 'Update to ^1.2.3 to fix vulnerabilities', | ||
| isPreferred: true, | ||
| }), | ||
| ]) | ||
| }) | ||
|
|
||
| it('does not provide a quick fix when vulnerability message has no upgrade target', () => { | ||
| const provider = new VulnerabilityCodeActionProvider() | ||
| const textDocument = createTextDocument('^1.0.0') | ||
|
|
||
| const diagnostic = createDiagnostic({ | ||
| code: { value: 'vulnerability' }, | ||
| message: 'This version has 1 high vulnerability.', | ||
| }) | ||
|
|
||
| const codeActions = provider.provideCodeActions( | ||
| textDocument, | ||
| diagnostic.range, | ||
| createCodeActionContext([diagnostic]), | ||
| ) | ||
|
|
||
| expect(codeActions).toHaveLength(0) | ||
| }) | ||
|
|
||
| it('does not provide a quick fix when current version already matches fixed version', () => { | ||
| const provider = new VulnerabilityCodeActionProvider() | ||
| const textDocument = createTextDocument('~1.2.3') | ||
|
|
||
| const diagnostic = createDiagnostic({ | ||
| code: { value: 'vulnerability' }, | ||
| message: 'This version has 1 high vulnerability. Upgrade to 1.2.3 to fix.', | ||
| }) | ||
|
|
||
| const codeActions = provider.provideCodeActions( | ||
| textDocument, | ||
| diagnostic.range, | ||
| createCodeActionContext([diagnostic]), | ||
| ) | ||
|
|
||
| expect(codeActions).toHaveLength(0) | ||
|
Check failure on line 86 in tests/vulnerability-code-actions.test.ts
|
||
| }) | ||
|
|
||
| it('does not rely on encoded vulnerability code values', () => { | ||
| const provider = new VulnerabilityCodeActionProvider() | ||
| const textDocument = createTextDocument('^1.0.0') | ||
|
|
||
| const diagnostic = createDiagnostic({ | ||
| code: { value: 'vulnerability|1.2.3' }, | ||
| message: 'This version has 1 high vulnerability. Upgrade to 1.2.3 to fix.', | ||
| }) | ||
|
|
||
| const codeActions = provider.provideCodeActions( | ||
| textDocument, | ||
| diagnostic.range, | ||
| createCodeActionContext([diagnostic]), | ||
| ) | ||
|
|
||
| expect(codeActions).toHaveLength(0) | ||
| }) | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.