| 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 | |