aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/utils')
-rw-r--r--editors/code/src/utils/diagnostics/SuggestedFix.ts67
-rw-r--r--editors/code/src/utils/diagnostics/SuggestedFixCollection.ts77
-rw-r--r--editors/code/src/utils/diagnostics/rust.ts299
-rw-r--r--editors/code/src/utils/diagnostics/vscode.ts14
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 @@
1import * as vscode from 'vscode';
2
3import { 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 */
12export 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 @@
1import * as vscode from 'vscode';
2import 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 */
10export 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 @@
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
13export 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
21export 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
34export 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
45export interface MappedRustDiagnostic {
46 location: vscode.Location;
47 diagnostic: vscode.Diagnostic;
48 suggestedFixes: SuggestedFix[];
49}
50
51interface 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 */
60function 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 */
73function 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 */
80function 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 */
97function 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 */
121function 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 */
136function 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 */
155function 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 */
174function 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 */
223export 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 @@
1import * as vscode from 'vscode';
2
3/** Compares two `vscode.Diagnostic`s for equality */
4export 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}