diff options
Diffstat (limited to 'editors/code/src/utils')
-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 | 299 | ||||
-rw-r--r-- | editors/code/src/utils/diagnostics/vscode.ts | 14 |
4 files changed, 0 insertions, 457 deletions
diff --git a/editors/code/src/utils/diagnostics/SuggestedFix.ts b/editors/code/src/utils/diagnostics/SuggestedFix.ts deleted file mode 100644 index 6e660bb61..000000000 --- a/editors/code/src/utils/diagnostics/SuggestedFix.ts +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
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 deleted file mode 100644 index 57c9856cf..000000000 --- a/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts +++ /dev/null | |||
@@ -1,77 +0,0 @@ | |||
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 deleted file mode 100644 index 1f0c0d3e4..000000000 --- a/editors/code/src/utils/diagnostics/rust.ts +++ /dev/null | |||
@@ -1,299 +0,0 @@ | |||
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 | export interface RustDiagnosticSpanMacroExpansion { | ||
14 | span: RustDiagnosticSpan; | ||
15 | macro_decl_name: string; | ||
16 | def_site_span?: RustDiagnosticSpan; | ||
17 | } | ||
18 | |||
19 | // Reference: | ||
20 | // https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs | ||
21 | export interface RustDiagnosticSpan { | ||
22 | line_start: number; | ||
23 | line_end: number; | ||
24 | column_start: number; | ||
25 | column_end: number; | ||
26 | is_primary: boolean; | ||
27 | file_name: string; | ||
28 | label?: string; | ||
29 | expansion?: RustDiagnosticSpanMacroExpansion; | ||
30 | suggested_replacement?: string; | ||
31 | suggestion_applicability?: SuggestionApplicability; | ||
32 | } | ||
33 | |||
34 | export interface RustDiagnostic { | ||
35 | spans: RustDiagnosticSpan[]; | ||
36 | rendered: string; | ||
37 | message: string; | ||
38 | level: string; | ||
39 | code?: { | ||
40 | code: string; | ||
41 | }; | ||
42 | children: RustDiagnostic[]; | ||
43 | } | ||
44 | |||
45 | export interface MappedRustDiagnostic { | ||
46 | location: vscode.Location; | ||
47 | diagnostic: vscode.Diagnostic; | ||
48 | suggestedFixes: SuggestedFix[]; | ||
49 | } | ||
50 | |||
51 | interface MappedRustChildDiagnostic { | ||
52 | related?: vscode.DiagnosticRelatedInformation; | ||
53 | suggestedFix?: SuggestedFix; | ||
54 | messageLine?: string; | ||
55 | } | ||
56 | |||
57 | /** | ||
58 | * Converts a Rust level string to a VsCode severity | ||
59 | */ | ||
60 | function mapLevelToSeverity(s: string): vscode.DiagnosticSeverity { | ||
61 | if (s === 'error') { | ||
62 | return vscode.DiagnosticSeverity.Error; | ||
63 | } | ||
64 | if (s.startsWith('warn')) { | ||
65 | return vscode.DiagnosticSeverity.Warning; | ||
66 | } | ||
67 | return vscode.DiagnosticSeverity.Information; | ||
68 | } | ||
69 | |||
70 | /** | ||
71 | * Check whether a file name is from macro invocation | ||
72 | */ | ||
73 | function isFromMacro(fileName: string): boolean { | ||
74 | return fileName.startsWith('<') && fileName.endsWith('>'); | ||
75 | } | ||
76 | |||
77 | /** | ||
78 | * Converts a Rust macro span to a VsCode location recursively | ||
79 | */ | ||
80 | function mapMacroSpanToLocation( | ||
81 | spanMacro: RustDiagnosticSpanMacroExpansion, | ||
82 | ): vscode.Location | undefined { | ||
83 | if (!isFromMacro(spanMacro.span.file_name)) { | ||
84 | return mapSpanToLocation(spanMacro.span); | ||
85 | } | ||
86 | |||
87 | if (spanMacro.span.expansion) { | ||
88 | return mapMacroSpanToLocation(spanMacro.span.expansion); | ||
89 | } | ||
90 | |||
91 | return; | ||
92 | } | ||
93 | |||
94 | /** | ||
95 | * Converts a Rust span to a VsCode location | ||
96 | */ | ||
97 | function mapSpanToLocation(span: RustDiagnosticSpan): vscode.Location { | ||
98 | if (isFromMacro(span.file_name) && span.expansion) { | ||
99 | const macroLoc = mapMacroSpanToLocation(span.expansion); | ||
100 | if (macroLoc) { | ||
101 | return macroLoc; | ||
102 | } | ||
103 | } | ||
104 | |||
105 | const fileName = path.join(vscode.workspace.rootPath || '', span.file_name); | ||
106 | const fileUri = vscode.Uri.file(fileName); | ||
107 | |||
108 | const range = new vscode.Range( | ||
109 | new vscode.Position(span.line_start - 1, span.column_start - 1), | ||
110 | new vscode.Position(span.line_end - 1, span.column_end - 1), | ||
111 | ); | ||
112 | |||
113 | return new vscode.Location(fileUri, range); | ||
114 | } | ||
115 | |||
116 | /** | ||
117 | * Converts a secondary Rust span to a VsCode related information | ||
118 | * | ||
119 | * If the span is unlabelled this will return `undefined`. | ||
120 | */ | ||
121 | function mapSecondarySpanToRelated( | ||
122 | span: RustDiagnosticSpan, | ||
123 | ): vscode.DiagnosticRelatedInformation | undefined { | ||
124 | if (!span.label) { | ||
125 | // Nothing to label this with | ||
126 | return; | ||
127 | } | ||
128 | |||
129 | const location = mapSpanToLocation(span); | ||
130 | return new vscode.DiagnosticRelatedInformation(location, span.label); | ||
131 | } | ||
132 | |||
133 | /** | ||
134 | * Determines if diagnostic is related to unused code | ||
135 | */ | ||
136 | function isUnusedOrUnnecessary(rd: RustDiagnostic): boolean { | ||
137 | if (!rd.code) { | ||
138 | return false; | ||
139 | } | ||
140 | |||
141 | return [ | ||
142 | 'dead_code', | ||
143 | 'unknown_lints', | ||
144 | 'unreachable_code', | ||
145 | 'unused_attributes', | ||
146 | 'unused_imports', | ||
147 | 'unused_macros', | ||
148 | 'unused_variables', | ||
149 | ].includes(rd.code.code); | ||
150 | } | ||
151 | |||
152 | /** | ||
153 | * Determines if diagnostic is related to deprecated code | ||
154 | */ | ||
155 | function isDeprecated(rd: RustDiagnostic): boolean { | ||
156 | if (!rd.code) { | ||
157 | return false; | ||
158 | } | ||
159 | |||
160 | return ['deprecated'].includes(rd.code.code); | ||
161 | } | ||
162 | |||
163 | /** | ||
164 | * Converts a Rust child diagnostic to a VsCode related information | ||
165 | * | ||
166 | * This can have three outcomes: | ||
167 | * | ||
168 | * 1. If this is no primary span this will return a `noteLine` | ||
169 | * 2. If there is a primary span with a suggested replacement it will return a | ||
170 | * `codeAction`. | ||
171 | * 3. If there is a primary span without a suggested replacement it will return | ||
172 | * a `related`. | ||
173 | */ | ||
174 | function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic { | ||
175 | const span = rd.spans.find(s => s.is_primary); | ||
176 | |||
177 | if (!span) { | ||
178 | // `rustc` uses these spanless children as a way to print multi-line | ||
179 | // messages | ||
180 | return { messageLine: rd.message }; | ||
181 | } | ||
182 | |||
183 | // If we have a primary span use its location, otherwise use the parent | ||
184 | const location = mapSpanToLocation(span); | ||
185 | |||
186 | // We need to distinguish `null` from an empty string | ||
187 | if (span && typeof span.suggested_replacement === 'string') { | ||
188 | // Include our replacement in the title unless it's empty | ||
189 | const title = span.suggested_replacement | ||
190 | ? `${rd.message}: \`${span.suggested_replacement}\`` | ||
191 | : rd.message; | ||
192 | |||
193 | return { | ||
194 | suggestedFix: new SuggestedFix( | ||
195 | title, | ||
196 | location, | ||
197 | span.suggested_replacement, | ||
198 | span.suggestion_applicability, | ||
199 | ), | ||
200 | }; | ||
201 | } else { | ||
202 | const related = new vscode.DiagnosticRelatedInformation( | ||
203 | location, | ||
204 | rd.message, | ||
205 | ); | ||
206 | |||
207 | return { related }; | ||
208 | } | ||
209 | } | ||
210 | |||
211 | /** | ||
212 | * Converts a Rust root diagnostic to VsCode form | ||
213 | * | ||
214 | * This flattens the Rust diagnostic by: | ||
215 | * | ||
216 | * 1. Creating a `vscode.Diagnostic` with the root message and primary span. | ||
217 | * 2. Adding any labelled secondary spans to `relatedInformation` | ||
218 | * 3. Categorising child diagnostics as either `SuggestedFix`es, | ||
219 | * `relatedInformation` or additional message lines. | ||
220 | * | ||
221 | * If the diagnostic has no primary span this will return `undefined` | ||
222 | */ | ||
223 | export function mapRustDiagnosticToVsCode( | ||
224 | rd: RustDiagnostic, | ||
225 | ): MappedRustDiagnostic | undefined { | ||
226 | const primarySpan = rd.spans.find(s => s.is_primary); | ||
227 | if (!primarySpan) { | ||
228 | return; | ||
229 | } | ||
230 | |||
231 | const location = mapSpanToLocation(primarySpan); | ||
232 | const secondarySpans = rd.spans.filter(s => !s.is_primary); | ||
233 | |||
234 | const severity = mapLevelToSeverity(rd.level); | ||
235 | let primarySpanLabel = primarySpan.label; | ||
236 | |||
237 | const vd = new vscode.Diagnostic(location.range, rd.message, severity); | ||
238 | |||
239 | let source = 'rustc'; | ||
240 | let code = rd.code && rd.code.code; | ||
241 | if (code) { | ||
242 | // See if this is an RFC #2103 scoped lint (e.g. from Clippy) | ||
243 | const scopedCode = code.split('::'); | ||
244 | if (scopedCode.length === 2) { | ||
245 | [source, code] = scopedCode; | ||
246 | } | ||
247 | } | ||
248 | |||
249 | vd.source = source; | ||
250 | vd.code = code; | ||
251 | vd.relatedInformation = []; | ||
252 | vd.tags = []; | ||
253 | |||
254 | for (const secondarySpan of secondarySpans) { | ||
255 | const related = mapSecondarySpanToRelated(secondarySpan); | ||
256 | if (related) { | ||
257 | vd.relatedInformation.push(related); | ||
258 | } | ||
259 | } | ||
260 | |||
261 | const suggestedFixes = []; | ||
262 | for (const child of rd.children) { | ||
263 | const { related, suggestedFix, messageLine } = mapRustChildDiagnostic( | ||
264 | child, | ||
265 | ); | ||
266 | |||
267 | if (related) { | ||
268 | vd.relatedInformation.push(related); | ||
269 | } | ||
270 | if (suggestedFix) { | ||
271 | suggestedFixes.push(suggestedFix); | ||
272 | } | ||
273 | if (messageLine) { | ||
274 | vd.message += `\n${messageLine}`; | ||
275 | |||
276 | // These secondary messages usually duplicate the content of the | ||
277 | // primary span label. | ||
278 | primarySpanLabel = undefined; | ||
279 | } | ||
280 | } | ||
281 | |||
282 | if (primarySpanLabel) { | ||
283 | vd.message += `\n${primarySpanLabel}`; | ||
284 | } | ||
285 | |||
286 | if (isUnusedOrUnnecessary(rd)) { | ||
287 | vd.tags.push(vscode.DiagnosticTag.Unnecessary); | ||
288 | } | ||
289 | |||
290 | if (isDeprecated(rd)) { | ||
291 | vd.tags.push(vscode.DiagnosticTag.Deprecated); | ||
292 | } | ||
293 | |||
294 | return { | ||
295 | location, | ||
296 | diagnostic: vd, | ||
297 | suggestedFixes, | ||
298 | }; | ||
299 | } | ||
diff --git a/editors/code/src/utils/diagnostics/vscode.ts b/editors/code/src/utils/diagnostics/vscode.ts deleted file mode 100644 index f4a5450e2..000000000 --- a/editors/code/src/utils/diagnostics/vscode.ts +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
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 | } | ||