aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/utils/rust_diagnostics.ts
diff options
context:
space:
mode:
authorRyan Cumming <[email protected]>2019-06-24 22:40:06 +0100
committerRyan Cumming <[email protected]>2019-06-25 12:16:04 +0100
commit6d6cb25cf46a2327d6cb2278385763abfa7a95a0 (patch)
tree334f256dd02766fd4103abc485ff62918a27e6f9 /editors/code/src/utils/rust_diagnostics.ts
parent364ac9b9468e1930f39e0dddc454b2eb7d68f360 (diff)
Rich mapping of cargo watch output
Currently we depend on the ASCII rendering string that `rustc` provides to populate Visual Studio Code's diagnostic. This has a number of shortcomings: 1. It's not a very good use of space in the error list 2. We can't jump to secondary spans (e.g. where a called function is defined) 3. We can't use Code Actions aka Quick Fix This moves all of the low-level parsing and mapping to a `rust_diagnostics.ts`. This uses some heuristics to map Rust diagnostics to VsCode: 1. As before, the Rust diagnostic message and primary span is used for the root diagnostic. However, we now just use the message instead of the rendered version. 2. Every secondary span is converted to "related information". This shows as child in the error list and can be jumped to. 3. Every child diagnostic is categorised in to three buckets: 1. If they have no span they're treated as another line of the root messages 2. If they have replacement text they're treated as a Code Action 3. If they have a span but no replacement text they're treated as related information (same as secondary spans).
Diffstat (limited to 'editors/code/src/utils/rust_diagnostics.ts')
-rw-r--r--editors/code/src/utils/rust_diagnostics.ts220
1 files changed, 220 insertions, 0 deletions
diff --git a/editors/code/src/utils/rust_diagnostics.ts b/editors/code/src/utils/rust_diagnostics.ts
new file mode 100644
index 000000000..7d8cc0e0b
--- /dev/null
+++ b/editors/code/src/utils/rust_diagnostics.ts
@@ -0,0 +1,220 @@
1import * as path from 'path';
2import * as vscode from 'vscode';
3
4// Reference:
5// https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs
6export interface RustDiagnosticSpan {
7 line_start: number;
8 line_end: number;
9 column_start: number;
10 column_end: number;
11 is_primary: boolean;
12 file_name: string;
13 label?: string;
14 suggested_replacement?: string;
15 suggestion_applicability?:
16 | 'MachineApplicable'
17 | 'HasPlaceholders'
18 | 'MaybeIncorrect'
19 | 'Unspecified';
20}
21
22export interface RustDiagnostic {
23 spans: RustDiagnosticSpan[];
24 rendered: string;
25 message: string;
26 level: string;
27 code?: {
28 code: string;
29 };
30 children: RustDiagnostic[];
31}
32
33export interface MappedRustDiagnostic {
34 location: vscode.Location;
35 diagnostic: vscode.Diagnostic;
36 codeActions: vscode.CodeAction[];
37}
38
39interface MappedRustChildDiagnostic {
40 related?: vscode.DiagnosticRelatedInformation;
41 codeAction?: vscode.CodeAction;
42 messageLine?: string;
43}
44
45/**
46 * Converts a Rust level string to a VsCode severity
47 */
48function mapLevelToSeverity(s: string): vscode.DiagnosticSeverity {
49 if (s === 'error') {
50 return vscode.DiagnosticSeverity.Error;
51 }
52 if (s.startsWith('warn')) {
53 return vscode.DiagnosticSeverity.Warning;
54 }
55 return vscode.DiagnosticSeverity.Information;
56}
57
58/**
59 * Converts a Rust span to a VsCode location
60 */
61function mapSpanToLocation(span: RustDiagnosticSpan): vscode.Location {
62 const fileName = path.join(vscode.workspace.rootPath!, span.file_name);
63 const fileUri = vscode.Uri.file(fileName);
64
65 const range = new vscode.Range(
66 new vscode.Position(span.line_start - 1, span.column_start - 1),
67 new vscode.Position(span.line_end - 1, span.column_end - 1)
68 );
69
70 return new vscode.Location(fileUri, range);
71}
72
73/**
74 * Converts a secondary Rust span to a VsCode related information
75 *
76 * If the span is unlabelled this will return `undefined`.
77 */
78function mapSecondarySpanToRelated(
79 span: RustDiagnosticSpan
80): vscode.DiagnosticRelatedInformation | undefined {
81 if (!span.label) {
82 // Nothing to label this with
83 return;
84 }
85
86 const location = mapSpanToLocation(span);
87 return new vscode.DiagnosticRelatedInformation(location, span.label);
88}
89
90/**
91 * Determines if diagnostic is related to unused code
92 */
93function isUnusedOrUnnecessary(rd: RustDiagnostic): boolean {
94 if (!rd.code) {
95 return false;
96 }
97
98 const { code } = rd.code;
99 return code.startsWith('unused_') || code === 'dead_code';
100}
101
102/**
103 * Converts a Rust child diagnostic to a VsCode related information
104 *
105 * This can have three outcomes:
106 *
107 * 1. If this is no primary span this will return a `noteLine`
108 * 2. If there is a primary span with a suggested replacement it will return a
109 * `codeAction`.
110 * 3. If there is a primary span without a suggested replacement it will return
111 * a `related`.
112 */
113function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
114 const span = rd.spans.find(s => s.is_primary);
115
116 if (!span) {
117 // `rustc` uses these spanless children as a way to print multi-line
118 // messages
119 return { messageLine: rd.message };
120 }
121
122 // If we have a primary span use its location, otherwise use the parent
123 const location = mapSpanToLocation(span);
124
125 // We need to distinguish `null` from an empty string
126 if (span && typeof span.suggested_replacement === 'string') {
127 const edit = new vscode.WorkspaceEdit();
128 edit.replace(location.uri, location.range, span.suggested_replacement);
129
130 // Include our replacement in the label unless it's empty
131 const title = span.suggested_replacement
132 ? `${rd.message}: \`${span.suggested_replacement}\``
133 : rd.message;
134
135 const codeAction = new vscode.CodeAction(
136 title,
137 vscode.CodeActionKind.QuickFix
138 );
139
140 codeAction.edit = edit;
141 codeAction.isPreferred =
142 span.suggestion_applicability === 'MachineApplicable';
143
144 return { codeAction };
145 } else {
146 const related = new vscode.DiagnosticRelatedInformation(
147 location,
148 rd.message
149 );
150
151 return { related };
152 }
153}
154
155/**
156 * Converts a Rust root diagnostic to VsCode form
157 *
158 * This flattens the Rust diagnostic by:
159 *
160 * 1. Creating a `vscode.Diagnostic` with the root message and primary span.
161 * 2. Adding any labelled secondary spans to `relatedInformation`
162 * 3. Categorising child diagnostics as either Quick Fix actions,
163 * `relatedInformation` or additional message lines.
164 *
165 * If the diagnostic has no primary span this will return `undefined`
166 */
167export function mapRustDiagnosticToVsCode(
168 rd: RustDiagnostic
169): MappedRustDiagnostic | undefined {
170 const codeActions = [];
171
172 const primarySpan = rd.spans.find(s => s.is_primary);
173 if (!primarySpan) {
174 return;
175 }
176
177 const location = mapSpanToLocation(primarySpan);
178 const secondarySpans = rd.spans.filter(s => !s.is_primary);
179
180 const severity = mapLevelToSeverity(rd.level);
181
182 const vd = new vscode.Diagnostic(location.range, rd.message, severity);
183
184 vd.source = 'rustc';
185 vd.code = rd.code ? rd.code.code : undefined;
186 vd.relatedInformation = [];
187
188 for (const secondarySpan of secondarySpans) {
189 const related = mapSecondarySpanToRelated(secondarySpan);
190 if (related) {
191 vd.relatedInformation.push(related);
192 }
193 }
194
195 for (const child of rd.children) {
196 const { related, codeAction, messageLine } = mapRustChildDiagnostic(
197 child
198 );
199
200 if (related) {
201 vd.relatedInformation.push(related);
202 }
203 if (codeAction) {
204 codeActions.push(codeAction);
205 }
206 if (messageLine) {
207 vd.message += `\n${messageLine}`;
208 }
209 }
210
211 if (isUnusedOrUnnecessary(rd)) {
212 vd.tags = [vscode.DiagnosticTag.Unnecessary];
213 }
214
215 return {
216 location,
217 diagnostic: vd,
218 codeActions
219 };
220}