aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/utils/rust_diagnostics.ts
diff options
context:
space:
mode:
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}