1 | //! Library for applying diagnostic suggestions to source code. |
2 | //! |
3 | //! This is a low-level library. You pass it the [JSON output] from `rustc`, |
4 | //! and you can then use it to apply suggestions to in-memory strings. |
5 | //! This library doesn't execute commands, or read or write from the filesystem. |
6 | //! |
7 | //! If you are looking for the [`cargo fix`] implementation, the core of it is |
8 | //! located in [`cargo::ops::fix`]. |
9 | //! |
10 | //! [`cargo fix`]: https://doc.rust-lang.org/cargo/commands/cargo-fix.html |
11 | //! [`cargo::ops::fix`]: https://github.com/rust-lang/cargo/blob/master/src/cargo/ops/fix.rs |
12 | //! [JSON output]: diagnostics |
13 | //! |
14 | //! The general outline of how to use this library is: |
15 | //! |
16 | //! 1. Call `rustc` and collect the JSON data. |
17 | //! 2. Pass the json data to [`get_suggestions_from_json`]. |
18 | //! 3. Create a [`CodeFix`] with the source of a file to modify. |
19 | //! 4. Call [`CodeFix::apply`] to apply a change. |
20 | //! 5. Call [`CodeFix::finish`] to get the result and write it back to disk. |
21 | |
22 | use std::collections::HashSet; |
23 | use std::ops::Range; |
24 | |
25 | pub mod diagnostics; |
26 | mod error; |
27 | mod replace; |
28 | |
29 | use diagnostics::Diagnostic; |
30 | use diagnostics::DiagnosticSpan; |
31 | pub use error::Error; |
32 | |
33 | /// A filter to control which suggestion should be applied. |
34 | #[derive (Debug, Clone, Copy)] |
35 | pub enum Filter { |
36 | /// For [`diagnostics::Applicability::MachineApplicable`] only. |
37 | MachineApplicableOnly, |
38 | /// Everything is included. YOLO! |
39 | Everything, |
40 | } |
41 | |
42 | /// Collects code [`Suggestion`]s from one or more compiler diagnostic lines. |
43 | /// |
44 | /// Fails if any of diagnostic line `input` is not a valid [`Diagnostic`] JSON. |
45 | /// |
46 | /// * `only` --- only diagnostics with code in a set of error codes would be collected. |
47 | pub fn get_suggestions_from_json<S: ::std::hash::BuildHasher>( |
48 | input: &str, |
49 | only: &HashSet<String, S>, |
50 | filter: Filter, |
51 | ) -> serde_json::error::Result<Vec<Suggestion>> { |
52 | let mut result: Vec = Vec::new(); |
53 | for cargo_msg: , …> as IntoIterator>::Item in serde_json::Deserializer::from_str(input).into_iter::<Diagnostic>() { |
54 | // One diagnostic line might have multiple suggestions |
55 | result.extend(iter:collect_suggestions(&cargo_msg?, only, filter)); |
56 | } |
57 | Ok(result) |
58 | } |
59 | |
60 | #[derive (Debug, Copy, Clone, Hash, PartialEq, Eq)] |
61 | pub struct LinePosition { |
62 | pub line: usize, |
63 | pub column: usize, |
64 | } |
65 | |
66 | impl std::fmt::Display for LinePosition { |
67 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
68 | write!(f, " {}: {}" , self.line, self.column) |
69 | } |
70 | } |
71 | |
72 | #[derive (Debug, Copy, Clone, Hash, PartialEq, Eq)] |
73 | pub struct LineRange { |
74 | pub start: LinePosition, |
75 | pub end: LinePosition, |
76 | } |
77 | |
78 | impl std::fmt::Display for LineRange { |
79 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
80 | write!(f, " {}- {}" , self.start, self.end) |
81 | } |
82 | } |
83 | |
84 | /// An error/warning and possible solutions for fixing it |
85 | #[derive (Debug, Clone, Hash, PartialEq, Eq)] |
86 | pub struct Suggestion { |
87 | pub message: String, |
88 | pub snippets: Vec<Snippet>, |
89 | pub solutions: Vec<Solution>, |
90 | } |
91 | |
92 | /// Solution to a diagnostic item. |
93 | #[derive (Debug, Clone, Hash, PartialEq, Eq)] |
94 | pub struct Solution { |
95 | /// The error message of the diagnostic item. |
96 | pub message: String, |
97 | /// Possible solutions to fix the error. |
98 | pub replacements: Vec<Replacement>, |
99 | } |
100 | |
101 | /// Represents code that will get replaced. |
102 | #[derive (Debug, Clone, Hash, PartialEq, Eq)] |
103 | pub struct Snippet { |
104 | pub file_name: String, |
105 | pub line_range: LineRange, |
106 | pub range: Range<usize>, |
107 | } |
108 | |
109 | /// Represents a replacement of a `snippet`. |
110 | #[derive (Debug, Clone, Hash, PartialEq, Eq)] |
111 | pub struct Replacement { |
112 | /// Code snippet that gets replaced. |
113 | pub snippet: Snippet, |
114 | /// The replacement of the snippet. |
115 | pub replacement: String, |
116 | } |
117 | |
118 | /// Converts a [`DiagnosticSpan`] to a [`Snippet`]. |
119 | fn span_to_snippet(span: &DiagnosticSpan) -> Snippet { |
120 | Snippet { |
121 | file_name: span.file_name.clone(), |
122 | line_range: LineRange { |
123 | start: LinePosition { |
124 | line: span.line_start, |
125 | column: span.column_start, |
126 | }, |
127 | end: LinePosition { |
128 | line: span.line_end, |
129 | column: span.column_end, |
130 | }, |
131 | }, |
132 | range: (span.byte_start as usize)..(span.byte_end as usize), |
133 | } |
134 | } |
135 | |
136 | /// Converts a [`DiagnosticSpan`] into a [`Replacement`]. |
137 | fn collect_span(span: &DiagnosticSpan) -> Option<Replacement> { |
138 | let snippet: Snippet = span_to_snippet(span); |
139 | let replacement: String = span.suggested_replacement.clone()?; |
140 | Some(Replacement { |
141 | snippet, |
142 | replacement, |
143 | }) |
144 | } |
145 | |
146 | /// Collects code [`Suggestion`]s from a single compiler diagnostic line. |
147 | /// |
148 | /// * `only` --- only diagnostics with code in a set of error codes would be collected. |
149 | pub fn collect_suggestions<S: ::std::hash::BuildHasher>( |
150 | diagnostic: &Diagnostic, |
151 | only: &HashSet<String, S>, |
152 | filter: Filter, |
153 | ) -> Option<Suggestion> { |
154 | if !only.is_empty() { |
155 | if let Some(ref code) = diagnostic.code { |
156 | if !only.contains(&code.code) { |
157 | // This is not the code we are looking for |
158 | return None; |
159 | } |
160 | } else { |
161 | // No code, probably a weird builtin warning/error |
162 | return None; |
163 | } |
164 | } |
165 | |
166 | let snippets = diagnostic.spans.iter().map(span_to_snippet).collect(); |
167 | |
168 | let solutions: Vec<_> = diagnostic |
169 | .children |
170 | .iter() |
171 | .filter_map(|child| { |
172 | let replacements: Vec<_> = child |
173 | .spans |
174 | .iter() |
175 | .filter(|span| { |
176 | use crate::diagnostics::Applicability::*; |
177 | use crate::Filter::*; |
178 | |
179 | match (filter, &span.suggestion_applicability) { |
180 | (MachineApplicableOnly, Some(MachineApplicable)) => true, |
181 | (MachineApplicableOnly, _) => false, |
182 | (Everything, _) => true, |
183 | } |
184 | }) |
185 | .filter_map(collect_span) |
186 | .collect(); |
187 | if !replacements.is_empty() { |
188 | Some(Solution { |
189 | message: child.message.clone(), |
190 | replacements, |
191 | }) |
192 | } else { |
193 | None |
194 | } |
195 | }) |
196 | .collect(); |
197 | |
198 | if solutions.is_empty() { |
199 | None |
200 | } else { |
201 | Some(Suggestion { |
202 | message: diagnostic.message.clone(), |
203 | snippets, |
204 | solutions, |
205 | }) |
206 | } |
207 | } |
208 | |
209 | /// Represents a code fix. This doesn't write to disks but is only in memory. |
210 | /// |
211 | /// The general way to use this is: |
212 | /// |
213 | /// 1. Feeds the source of a file to [`CodeFix::new`]. |
214 | /// 2. Calls [`CodeFix::apply`] to apply suggestions to the source code. |
215 | /// 3. Calls [`CodeFix::finish`] to get the "fixed" code. |
216 | pub struct CodeFix { |
217 | data: replace::Data, |
218 | /// Whether or not the data has been modified. |
219 | modified: bool, |
220 | } |
221 | |
222 | impl CodeFix { |
223 | /// Creates a `CodeFix` with the source of a file to modify. |
224 | pub fn new(s: &str) -> CodeFix { |
225 | CodeFix { |
226 | data: replace::Data::new(s.as_bytes()), |
227 | modified: false, |
228 | } |
229 | } |
230 | |
231 | /// Applies a suggestion to the code. |
232 | pub fn apply(&mut self, suggestion: &Suggestion) -> Result<(), Error> { |
233 | for sol in &suggestion.solutions { |
234 | for r in &sol.replacements { |
235 | self.data |
236 | .replace_range(r.snippet.range.clone(), r.replacement.as_bytes())?; |
237 | self.modified = true; |
238 | } |
239 | } |
240 | Ok(()) |
241 | } |
242 | |
243 | /// Gets the result of the "fixed" code. |
244 | pub fn finish(&self) -> Result<String, Error> { |
245 | Ok(String::from_utf8(self.data.to_vec())?) |
246 | } |
247 | |
248 | /// Returns whether or not the data has been modified. |
249 | pub fn modified(&self) -> bool { |
250 | self.modified |
251 | } |
252 | } |
253 | |
254 | /// Applies multiple `suggestions` to the given `code`. |
255 | pub fn apply_suggestions(code: &str, suggestions: &[Suggestion]) -> Result<String, Error> { |
256 | let mut fix: CodeFix = CodeFix::new(code); |
257 | for suggestion: &Suggestion in suggestions.iter().rev() { |
258 | fix.apply(suggestion)?; |
259 | } |
260 | fix.finish() |
261 | } |
262 | |