1 | #![warn (rust_2018_idioms)] |
2 | |
3 | #[macro_use ] |
4 | extern crate log; |
5 | #[cfg (test)] |
6 | #[macro_use ] |
7 | extern crate proptest; |
8 | |
9 | use std::collections::HashSet; |
10 | use std::ops::Range; |
11 | |
12 | use anyhow::Error; |
13 | |
14 | pub mod diagnostics; |
15 | use crate::diagnostics::{Diagnostic, DiagnosticSpan}; |
16 | mod replace; |
17 | |
18 | #[derive (Debug, Clone, Copy)] |
19 | pub enum Filter { |
20 | MachineApplicableOnly, |
21 | Everything, |
22 | } |
23 | |
24 | pub fn get_suggestions_from_json<S: ::std::hash::BuildHasher>( |
25 | input: &str, |
26 | only: &HashSet<String, S>, |
27 | filter: Filter, |
28 | ) -> serde_json::error::Result<Vec<Suggestion>> { |
29 | let mut result: Vec = Vec::new(); |
30 | for cargo_msg: , …> as IntoIterator>::Item in serde_json::Deserializer::from_str(input).into_iter::<Diagnostic>() { |
31 | // One diagnostic line might have multiple suggestions |
32 | result.extend(iter:collect_suggestions(&cargo_msg?, only, filter)); |
33 | } |
34 | Ok(result) |
35 | } |
36 | |
37 | #[derive (Debug, Copy, Clone, Hash, PartialEq, Eq)] |
38 | pub struct LinePosition { |
39 | pub line: usize, |
40 | pub column: usize, |
41 | } |
42 | |
43 | impl std::fmt::Display for LinePosition { |
44 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
45 | write!(f, " {}: {}" , self.line, self.column) |
46 | } |
47 | } |
48 | |
49 | #[derive (Debug, Copy, Clone, Hash, PartialEq, Eq)] |
50 | pub struct LineRange { |
51 | pub start: LinePosition, |
52 | pub end: LinePosition, |
53 | } |
54 | |
55 | impl std::fmt::Display for LineRange { |
56 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
57 | write!(f, " {}- {}" , self.start, self.end) |
58 | } |
59 | } |
60 | |
61 | #[derive (Debug, Clone, Hash, PartialEq, Eq)] |
62 | /// An error/warning and possible solutions for fixing it |
63 | pub struct Suggestion { |
64 | pub message: String, |
65 | pub snippets: Vec<Snippet>, |
66 | pub solutions: Vec<Solution>, |
67 | } |
68 | |
69 | #[derive (Debug, Clone, Hash, PartialEq, Eq)] |
70 | pub struct Solution { |
71 | pub message: String, |
72 | pub replacements: Vec<Replacement>, |
73 | } |
74 | |
75 | #[derive (Debug, Clone, Hash, PartialEq, Eq)] |
76 | pub struct Snippet { |
77 | pub file_name: String, |
78 | pub line_range: LineRange, |
79 | pub range: Range<usize>, |
80 | /// leading surrounding text, text to replace, trailing surrounding text |
81 | /// |
82 | /// This split is useful for higlighting the part that gets replaced |
83 | pub text: (String, String, String), |
84 | } |
85 | |
86 | #[derive (Debug, Clone, Hash, PartialEq, Eq)] |
87 | pub struct Replacement { |
88 | pub snippet: Snippet, |
89 | pub replacement: String, |
90 | } |
91 | |
92 | fn parse_snippet(span: &DiagnosticSpan) -> Option<Snippet> { |
93 | // unindent the snippet |
94 | let indent = span |
95 | .text |
96 | .iter() |
97 | .map(|line| { |
98 | let indent = line |
99 | .text |
100 | .chars() |
101 | .take_while(|&c| char::is_whitespace(c)) |
102 | .count(); |
103 | std::cmp::min(indent, line.highlight_start - 1) |
104 | }) |
105 | .min()?; |
106 | |
107 | let text_slice = span.text[0].text.chars().collect::<Vec<char>>(); |
108 | |
109 | // We subtract `1` because these highlights are 1-based |
110 | // Check the `min` so that it doesn't attempt to index out-of-bounds when |
111 | // the span points to the "end" of the line. For example, a line of |
112 | // "foo\n" with a highlight_start of 5 is intended to highlight *after* |
113 | // the line. This needs to compensate since the newline has been removed |
114 | // from the text slice. |
115 | let start = (span.text[0].highlight_start - 1).min(text_slice.len()); |
116 | let end = (span.text[0].highlight_end - 1).min(text_slice.len()); |
117 | let lead = text_slice[indent..start].iter().collect(); |
118 | let mut body: String = text_slice[start..end].iter().collect(); |
119 | |
120 | for line in span.text.iter().take(span.text.len() - 1).skip(1) { |
121 | body.push(' \n' ); |
122 | body.push_str(&line.text[indent..]); |
123 | } |
124 | let mut tail = String::new(); |
125 | let last = &span.text[span.text.len() - 1]; |
126 | |
127 | // If we get a DiagnosticSpanLine where highlight_end > text.len(), we prevent an 'out of |
128 | // bounds' access by making sure the index is within the array bounds. |
129 | // `saturating_sub` is used in case of an empty file |
130 | let last_tail_index = last.highlight_end.min(last.text.len()).saturating_sub(1); |
131 | let last_slice = last.text.chars().collect::<Vec<char>>(); |
132 | |
133 | if span.text.len() > 1 { |
134 | body.push(' \n' ); |
135 | body.push_str( |
136 | &last_slice[indent..last_tail_index] |
137 | .iter() |
138 | .collect::<String>(), |
139 | ); |
140 | } |
141 | tail.push_str(&last_slice[last_tail_index..].iter().collect::<String>()); |
142 | Some(Snippet { |
143 | file_name: span.file_name.clone(), |
144 | line_range: LineRange { |
145 | start: LinePosition { |
146 | line: span.line_start, |
147 | column: span.column_start, |
148 | }, |
149 | end: LinePosition { |
150 | line: span.line_end, |
151 | column: span.column_end, |
152 | }, |
153 | }, |
154 | range: (span.byte_start as usize)..(span.byte_end as usize), |
155 | text: (lead, body, tail), |
156 | }) |
157 | } |
158 | |
159 | fn collect_span(span: &DiagnosticSpan) -> Option<Replacement> { |
160 | let snippet: Snippet = parse_snippet(span)?; |
161 | let replacement: String = span.suggested_replacement.clone()?; |
162 | Some(Replacement { |
163 | snippet, |
164 | replacement, |
165 | }) |
166 | } |
167 | |
168 | pub fn collect_suggestions<S: ::std::hash::BuildHasher>( |
169 | diagnostic: &Diagnostic, |
170 | only: &HashSet<String, S>, |
171 | filter: Filter, |
172 | ) -> Option<Suggestion> { |
173 | if !only.is_empty() { |
174 | if let Some(ref code) = diagnostic.code { |
175 | if !only.contains(&code.code) { |
176 | // This is not the code we are looking for |
177 | return None; |
178 | } |
179 | } else { |
180 | // No code, probably a weird builtin warning/error |
181 | return None; |
182 | } |
183 | } |
184 | |
185 | let snippets = diagnostic.spans.iter().filter_map(parse_snippet).collect(); |
186 | |
187 | let solutions: Vec<_> = diagnostic |
188 | .children |
189 | .iter() |
190 | .filter_map(|child| { |
191 | let replacements: Vec<_> = child |
192 | .spans |
193 | .iter() |
194 | .filter(|span| { |
195 | use crate::diagnostics::Applicability::*; |
196 | use crate::Filter::*; |
197 | |
198 | match (filter, &span.suggestion_applicability) { |
199 | (MachineApplicableOnly, Some(MachineApplicable)) => true, |
200 | (MachineApplicableOnly, _) => false, |
201 | (Everything, _) => true, |
202 | } |
203 | }) |
204 | .filter_map(collect_span) |
205 | .collect(); |
206 | if !replacements.is_empty() { |
207 | Some(Solution { |
208 | message: child.message.clone(), |
209 | replacements, |
210 | }) |
211 | } else { |
212 | None |
213 | } |
214 | }) |
215 | .collect(); |
216 | |
217 | if solutions.is_empty() { |
218 | None |
219 | } else { |
220 | Some(Suggestion { |
221 | message: diagnostic.message.clone(), |
222 | snippets, |
223 | solutions, |
224 | }) |
225 | } |
226 | } |
227 | |
228 | pub struct CodeFix { |
229 | data: replace::Data, |
230 | } |
231 | |
232 | impl CodeFix { |
233 | pub fn new(s: &str) -> CodeFix { |
234 | CodeFix { |
235 | data: replace::Data::new(data:s.as_bytes()), |
236 | } |
237 | } |
238 | |
239 | pub fn apply(&mut self, suggestion: &Suggestion) -> Result<(), Error> { |
240 | for sol: &Solution in &suggestion.solutions { |
241 | for r: &Replacement in &sol.replacements { |
242 | self.data.replace_range( |
243 | from:r.snippet.range.start, |
244 | up_to_and_including:r.snippet.range.end.saturating_sub(1), |
245 | data:r.replacement.as_bytes(), |
246 | )?; |
247 | } |
248 | } |
249 | Ok(()) |
250 | } |
251 | |
252 | pub fn finish(&self) -> Result<String, Error> { |
253 | Ok(String::from_utf8(self.data.to_vec())?) |
254 | } |
255 | } |
256 | |
257 | pub fn apply_suggestions(code: &str, suggestions: &[Suggestion]) -> Result<String, Error> { |
258 | let mut fix: CodeFix = CodeFix::new(code); |
259 | for suggestion: &Suggestion in suggestions.iter().rev() { |
260 | fix.apply(suggestion)?; |
261 | } |
262 | fix.finish() |
263 | } |
264 | |