diff options
Diffstat (limited to 'editors/code/src/utils/diagnostics')
-rw-r--r-- | editors/code/src/utils/diagnostics/SuggestedFix.ts | 67 | ||||
-rw-r--r-- | editors/code/src/utils/diagnostics/SuggestedFixCollection.ts | 77 | ||||
-rw-r--r-- | editors/code/src/utils/diagnostics/rust.ts | 235 | ||||
-rw-r--r-- | editors/code/src/utils/diagnostics/vscode.ts | 14 |
4 files changed, 393 insertions, 0 deletions
diff --git a/editors/code/src/utils/diagnostics/SuggestedFix.ts b/editors/code/src/utils/diagnostics/SuggestedFix.ts new file mode 100644 index 000000000..b1be2a225 --- /dev/null +++ b/editors/code/src/utils/diagnostics/SuggestedFix.ts | |||
@@ -0,0 +1,67 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | import { SuggestionApplicability } from './rust'; | ||
4 | |||
5 | /** | ||
6 | * Model object for text replacements suggested by the Rust compiler | ||
7 | * | ||
8 | * This is an intermediate form between the raw `rustc` JSON and a | ||
9 | * `vscode.CodeAction`. It's optimised for the use-cases of | ||
10 | * `SuggestedFixCollection`. | ||
11 | */ | ||
12 | export default class SuggestedFix { | ||
13 | public readonly title: string; | ||
14 | public readonly location: vscode.Location; | ||
15 | public readonly replacement: string; | ||
16 | public readonly applicability: SuggestionApplicability; | ||
17 | |||
18 | /** | ||
19 | * Diagnostics this suggested fix could resolve | ||
20 | */ | ||
21 | public diagnostics: vscode.Diagnostic[]; | ||
22 | |||
23 | constructor( | ||
24 | title: string, | ||
25 | location: vscode.Location, | ||
26 | replacement: string, | ||
27 | applicability: SuggestionApplicability = SuggestionApplicability.Unspecified | ||
28 | ) { | ||
29 | this.title = title; | ||
30 | this.location = location; | ||
31 | this.replacement = replacement; | ||
32 | this.applicability = applicability; | ||
33 | this.diagnostics = []; | ||
34 | } | ||
35 | |||
36 | /** | ||
37 | * Determines if this suggested fix is equivalent to another instance | ||
38 | */ | ||
39 | public isEqual(other: SuggestedFix): boolean { | ||
40 | return ( | ||
41 | this.title === other.title && | ||
42 | this.location.range.isEqual(other.location.range) && | ||
43 | this.replacement === other.replacement && | ||
44 | this.applicability === other.applicability | ||
45 | ); | ||
46 | } | ||
47 | |||
48 | /** | ||
49 | * Converts this suggested fix to a VS Code Quick Fix code action | ||
50 | */ | ||
51 | public toCodeAction(): vscode.CodeAction { | ||
52 | const codeAction = new vscode.CodeAction( | ||
53 | this.title, | ||
54 | vscode.CodeActionKind.QuickFix | ||
55 | ); | ||
56 | |||
57 | const edit = new vscode.WorkspaceEdit(); | ||
58 | edit.replace(this.location.uri, this.location.range, this.replacement); | ||
59 | codeAction.edit = edit; | ||
60 | |||
61 | codeAction.isPreferred = | ||
62 | this.applicability === SuggestionApplicability.MachineApplicable; | ||
63 | |||
64 | codeAction.diagnostics = [...this.diagnostics]; | ||
65 | return codeAction; | ||
66 | } | ||
67 | } | ||
diff --git a/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts b/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts new file mode 100644 index 000000000..132ce12f8 --- /dev/null +++ b/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import SuggestedFix from './SuggestedFix'; | ||
3 | |||
4 | /** | ||
5 | * Collection of suggested fixes across multiple documents | ||
6 | * | ||
7 | * This stores `SuggestedFix` model objects and returns them via the | ||
8 | * `vscode.CodeActionProvider` interface. | ||
9 | */ | ||
10 | export default class SuggestedFixCollection | ||
11 | implements vscode.CodeActionProvider { | ||
12 | public static PROVIDED_CODE_ACTION_KINDS = [vscode.CodeActionKind.QuickFix]; | ||
13 | |||
14 | /** | ||
15 | * Map of document URI strings to suggested fixes | ||
16 | */ | ||
17 | private suggestedFixes: Map<string, SuggestedFix[]>; | ||
18 | |||
19 | constructor() { | ||
20 | this.suggestedFixes = new Map(); | ||
21 | } | ||
22 | |||
23 | /** | ||
24 | * Clears all suggested fixes across all documents | ||
25 | */ | ||
26 | public clear(): void { | ||
27 | this.suggestedFixes = new Map(); | ||
28 | } | ||
29 | |||
30 | /** | ||
31 | * Adds a suggested fix for the given diagnostic | ||
32 | * | ||
33 | * Some suggested fixes will appear in multiple diagnostics. For example, | ||
34 | * forgetting a `mut` on a variable will suggest changing the delaration on | ||
35 | * every mutable usage site. If the suggested fix has already been added | ||
36 | * this method will instead associate the existing fix with the new | ||
37 | * diagnostic. | ||
38 | */ | ||
39 | public addSuggestedFixForDiagnostic( | ||
40 | suggestedFix: SuggestedFix, | ||
41 | diagnostic: vscode.Diagnostic | ||
42 | ): void { | ||
43 | const fileUriString = suggestedFix.location.uri.toString(); | ||
44 | const fileSuggestions = this.suggestedFixes.get(fileUriString) || []; | ||
45 | |||
46 | const existingSuggestion = fileSuggestions.find(s => | ||
47 | s.isEqual(suggestedFix) | ||
48 | ); | ||
49 | |||
50 | if (existingSuggestion) { | ||
51 | // The existing suggestion also applies to this new diagnostic | ||
52 | existingSuggestion.diagnostics.push(diagnostic); | ||
53 | } else { | ||
54 | // We haven't seen this suggestion before | ||
55 | suggestedFix.diagnostics.push(diagnostic); | ||
56 | fileSuggestions.push(suggestedFix); | ||
57 | } | ||
58 | |||
59 | this.suggestedFixes.set(fileUriString, fileSuggestions); | ||
60 | } | ||
61 | |||
62 | /** | ||
63 | * Filters suggested fixes by their document and range and converts them to | ||
64 | * code actions | ||
65 | */ | ||
66 | public provideCodeActions( | ||
67 | document: vscode.TextDocument, | ||
68 | range: vscode.Range | ||
69 | ): vscode.CodeAction[] { | ||
70 | const documentUriString = document.uri.toString(); | ||
71 | |||
72 | const suggestedFixes = this.suggestedFixes.get(documentUriString); | ||
73 | return (suggestedFixes || []) | ||
74 | .filter(({ location }) => location.range.intersection(range)) | ||
75 | .map(suggestedEdit => suggestedEdit.toCodeAction()); | ||
76 | } | ||
77 | } | ||
diff --git a/editors/code/src/utils/diagnostics/rust.ts b/editors/code/src/utils/diagnostics/rust.ts new file mode 100644 index 000000000..d16576eb1 --- /dev/null +++ b/editors/code/src/utils/diagnostics/rust.ts | |||
@@ -0,0 +1,235 @@ | |||
1 | import * as path from 'path'; | ||
2 | import * as vscode from 'vscode'; | ||
3 | |||
4 | import SuggestedFix from './SuggestedFix'; | ||
5 | |||
6 | export enum SuggestionApplicability { | ||
7 | MachineApplicable = 'MachineApplicable', | ||
8 | HasPlaceholders = 'HasPlaceholders', | ||
9 | MaybeIncorrect = 'MaybeIncorrect', | ||
10 | Unspecified = 'Unspecified' | ||
11 | } | ||
12 | |||
13 | // Reference: | ||
14 | // https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs | ||
15 | export interface RustDiagnosticSpan { | ||
16 | line_start: number; | ||
17 | line_end: number; | ||
18 | column_start: number; | ||
19 | column_end: number; | ||
20 | is_primary: boolean; | ||
21 | file_name: string; | ||
22 | label?: string; | ||
23 | suggested_replacement?: string; | ||
24 | suggestion_applicability?: SuggestionApplicability; | ||
25 | } | ||
26 | |||
27 | export interface RustDiagnostic { | ||
28 | spans: RustDiagnosticSpan[]; | ||
29 | rendered: string; | ||
30 | message: string; | ||
31 | level: string; | ||
32 | code?: { | ||
33 | code: string; | ||
34 | }; | ||
35 | children: RustDiagnostic[]; | ||
36 | } | ||
37 | |||
38 | export interface MappedRustDiagnostic { | ||
39 | location: vscode.Location; | ||
40 | diagnostic: vscode.Diagnostic; | ||
41 | suggestedFixes: SuggestedFix[]; | ||
42 | } | ||
43 | |||
44 | interface MappedRustChildDiagnostic { | ||
45 | related?: vscode.DiagnosticRelatedInformation; | ||
46 | suggestedFix?: SuggestedFix; | ||
47 | messageLine?: string; | ||
48 | } | ||
49 | |||
50 | /** | ||
51 | * Converts a Rust level string to a VsCode severity | ||
52 | */ | ||
53 | function mapLevelToSeverity(s: string): vscode.DiagnosticSeverity { | ||
54 | if (s === 'error') { | ||
55 | return vscode.DiagnosticSeverity.Error; | ||
56 | } | ||
57 | if (s.startsWith('warn')) { | ||
58 | return vscode.DiagnosticSeverity.Warning; | ||
59 | } | ||
60 | return vscode.DiagnosticSeverity.Information; | ||
61 | } | ||
62 | |||
63 | /** | ||
64 | * Converts a Rust span to a VsCode location | ||
65 | */ | ||
66 | function mapSpanToLocation(span: RustDiagnosticSpan): vscode.Location { | ||
67 | const fileName = path.join(vscode.workspace.rootPath!, span.file_name); | ||
68 | const fileUri = vscode.Uri.file(fileName); | ||
69 | |||
70 | const range = new vscode.Range( | ||
71 | new vscode.Position(span.line_start - 1, span.column_start - 1), | ||
72 | new vscode.Position(span.line_end - 1, span.column_end - 1) | ||
73 | ); | ||
74 | |||
75 | return new vscode.Location(fileUri, range); | ||
76 | } | ||
77 | |||
78 | /** | ||
79 | * Converts a secondary Rust span to a VsCode related information | ||
80 | * | ||
81 | * If the span is unlabelled this will return `undefined`. | ||
82 | */ | ||
83 | function mapSecondarySpanToRelated( | ||
84 | span: RustDiagnosticSpan | ||
85 | ): vscode.DiagnosticRelatedInformation | undefined { | ||
86 | if (!span.label) { | ||
87 | // Nothing to label this with | ||
88 | return; | ||
89 | } | ||
90 | |||
91 | const location = mapSpanToLocation(span); | ||
92 | return new vscode.DiagnosticRelatedInformation(location, span.label); | ||
93 | } | ||
94 | |||
95 | /** | ||
96 | * Determines if diagnostic is related to unused code | ||
97 | */ | ||
98 | function isUnusedOrUnnecessary(rd: RustDiagnostic): boolean { | ||
99 | if (!rd.code) { | ||
100 | return false; | ||
101 | } | ||
102 | |||
103 | return [ | ||
104 | 'dead_code', | ||
105 | 'unknown_lints', | ||
106 | 'unused_attributes', | ||
107 | 'unused_imports', | ||
108 | 'unused_macros', | ||
109 | 'unused_variables' | ||
110 | ].includes(rd.code.code); | ||
111 | } | ||
112 | |||
113 | /** | ||
114 | * Converts a Rust child diagnostic to a VsCode related information | ||
115 | * | ||
116 | * This can have three outcomes: | ||
117 | * | ||
118 | * 1. If this is no primary span this will return a `noteLine` | ||
119 | * 2. If there is a primary span with a suggested replacement it will return a | ||
120 | * `codeAction`. | ||
121 | * 3. If there is a primary span without a suggested replacement it will return | ||
122 | * a `related`. | ||
123 | */ | ||
124 | function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic { | ||
125 | const span = rd.spans.find(s => s.is_primary); | ||
126 | |||
127 | if (!span) { | ||
128 | // `rustc` uses these spanless children as a way to print multi-line | ||
129 | // messages | ||
130 | return { messageLine: rd.message }; | ||
131 | } | ||
132 | |||
133 | // If we have a primary span use its location, otherwise use the parent | ||
134 | const location = mapSpanToLocation(span); | ||
135 | |||
136 | // We need to distinguish `null` from an empty string | ||
137 | if (span && typeof span.suggested_replacement === 'string') { | ||
138 | // Include our replacement in the title unless it's empty | ||
139 | const title = span.suggested_replacement | ||
140 | ? `${rd.message}: \`${span.suggested_replacement}\`` | ||
141 | : rd.message; | ||
142 | |||
143 | return { | ||
144 | suggestedFix: new SuggestedFix( | ||
145 | title, | ||
146 | location, | ||
147 | span.suggested_replacement, | ||
148 | span.suggestion_applicability | ||
149 | ) | ||
150 | }; | ||
151 | } else { | ||
152 | const related = new vscode.DiagnosticRelatedInformation( | ||
153 | location, | ||
154 | rd.message | ||
155 | ); | ||
156 | |||
157 | return { related }; | ||
158 | } | ||
159 | } | ||
160 | |||
161 | /** | ||
162 | * Converts a Rust root diagnostic to VsCode form | ||
163 | * | ||
164 | * This flattens the Rust diagnostic by: | ||
165 | * | ||
166 | * 1. Creating a `vscode.Diagnostic` with the root message and primary span. | ||
167 | * 2. Adding any labelled secondary spans to `relatedInformation` | ||
168 | * 3. Categorising child diagnostics as either `SuggestedFix`es, | ||
169 | * `relatedInformation` or additional message lines. | ||
170 | * | ||
171 | * If the diagnostic has no primary span this will return `undefined` | ||
172 | */ | ||
173 | export function mapRustDiagnosticToVsCode( | ||
174 | rd: RustDiagnostic | ||
175 | ): MappedRustDiagnostic | undefined { | ||
176 | const primarySpan = rd.spans.find(s => s.is_primary); | ||
177 | if (!primarySpan) { | ||
178 | return; | ||
179 | } | ||
180 | |||
181 | const location = mapSpanToLocation(primarySpan); | ||
182 | const secondarySpans = rd.spans.filter(s => !s.is_primary); | ||
183 | |||
184 | const severity = mapLevelToSeverity(rd.level); | ||
185 | |||
186 | const vd = new vscode.Diagnostic(location.range, rd.message, severity); | ||
187 | |||
188 | let source = 'rustc'; | ||
189 | let code = rd.code && rd.code.code; | ||
190 | if (code) { | ||
191 | // See if this is an RFC #2103 scoped lint (e.g. from Clippy) | ||
192 | const scopedCode = code.split('::'); | ||
193 | if (scopedCode.length === 2) { | ||
194 | [source, code] = scopedCode; | ||
195 | } | ||
196 | } | ||
197 | |||
198 | vd.source = source; | ||
199 | vd.code = code; | ||
200 | vd.relatedInformation = []; | ||
201 | |||
202 | for (const secondarySpan of secondarySpans) { | ||
203 | const related = mapSecondarySpanToRelated(secondarySpan); | ||
204 | if (related) { | ||
205 | vd.relatedInformation.push(related); | ||
206 | } | ||
207 | } | ||
208 | |||
209 | const suggestedFixes = []; | ||
210 | for (const child of rd.children) { | ||
211 | const { related, suggestedFix, messageLine } = mapRustChildDiagnostic( | ||
212 | child | ||
213 | ); | ||
214 | |||
215 | if (related) { | ||
216 | vd.relatedInformation.push(related); | ||
217 | } | ||
218 | if (suggestedFix) { | ||
219 | suggestedFixes.push(suggestedFix); | ||
220 | } | ||
221 | if (messageLine) { | ||
222 | vd.message += `\n${messageLine}`; | ||
223 | } | ||
224 | } | ||
225 | |||
226 | if (isUnusedOrUnnecessary(rd)) { | ||
227 | vd.tags = [vscode.DiagnosticTag.Unnecessary]; | ||
228 | } | ||
229 | |||
230 | return { | ||
231 | location, | ||
232 | diagnostic: vd, | ||
233 | suggestedFixes | ||
234 | }; | ||
235 | } | ||
diff --git a/editors/code/src/utils/diagnostics/vscode.ts b/editors/code/src/utils/diagnostics/vscode.ts new file mode 100644 index 000000000..d8b85b720 --- /dev/null +++ b/editors/code/src/utils/diagnostics/vscode.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | /** Compares two `vscode.Diagnostic`s for equality */ | ||
4 | export function areDiagnosticsEqual( | ||
5 | left: vscode.Diagnostic, | ||
6 | right: vscode.Diagnostic | ||
7 | ): boolean { | ||
8 | return ( | ||
9 | left.source === right.source && | ||
10 | left.severity === right.severity && | ||
11 | left.range.isEqual(right.range) && | ||
12 | left.message === right.message | ||
13 | ); | ||
14 | } | ||