aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/utils/diagnostics/rust.ts
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/utils/diagnostics/rust.ts')
-rw-r--r--editors/code/src/utils/diagnostics/rust.ts235
1 files changed, 235 insertions, 0 deletions
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 @@
1import * as path from 'path';
2import * as vscode from 'vscode';
3
4import SuggestedFix from './SuggestedFix';
5
6export 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
15export 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
27export 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
38export interface MappedRustDiagnostic {
39 location: vscode.Location;
40 diagnostic: vscode.Diagnostic;
41 suggestedFixes: SuggestedFix[];
42}
43
44interface 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 */
53function 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 */
66function 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 */
83function 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 */
98function 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 */
124function 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 */
173export 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}