1#![warn(rust_2018_idioms)]
2
3#[macro_use]
4extern crate log;
5#[cfg(test)]
6#[macro_use]
7extern crate proptest;
8
9use std::collections::HashSet;
10use std::ops::Range;
11
12use anyhow::Error;
13
14pub mod diagnostics;
15use crate::diagnostics::{Diagnostic, DiagnosticSpan};
16mod replace;
17
18#[derive(Debug, Clone, Copy)]
19pub enum Filter {
20 MachineApplicableOnly,
21 Everything,
22}
23
24pub 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)]
38pub struct LinePosition {
39 pub line: usize,
40 pub column: usize,
41}
42
43impl 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)]
50pub struct LineRange {
51 pub start: LinePosition,
52 pub end: LinePosition,
53}
54
55impl 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
63pub 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)]
70pub struct Solution {
71 pub message: String,
72 pub replacements: Vec<Replacement>,
73}
74
75#[derive(Debug, Clone, Hash, PartialEq, Eq)]
76pub 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)]
87pub struct Replacement {
88 pub snippet: Snippet,
89 pub replacement: String,
90}
91
92fn 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
159fn 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
168pub 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
228pub struct CodeFix {
229 data: replace::Data,
230}
231
232impl 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
257pub 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