rustfix/
lib.rs

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//! > This crate is maintained by the Cargo team, primarily for use by Cargo and Rust compiler test suite
23//! > and not intended for external use (except as a transitive dependency). This
24//! > crate may make major changes to its APIs or be deprecated without warning.
25
26use std::collections::HashSet;
27use std::ops::Range;
28
29pub mod diagnostics;
30mod error;
31mod replace;
32
33use diagnostics::Diagnostic;
34use diagnostics::DiagnosticSpan;
35pub use error::Error;
36
37/// A filter to control which suggestion should be applied.
38#[derive(Debug, Clone, Copy)]
39pub enum Filter {
40    /// For [`diagnostics::Applicability::MachineApplicable`] only.
41    MachineApplicableOnly,
42    /// Everything is included. YOLO!
43    Everything,
44}
45
46/// Collects code [`Suggestion`]s from one or more compiler diagnostic lines.
47///
48/// Fails if any of diagnostic line `input` is not a valid [`Diagnostic`] JSON.
49///
50/// * `only` --- only diagnostics with code in a set of error codes would be collected.
51pub fn get_suggestions_from_json<S: ::std::hash::BuildHasher>(
52    input: &str,
53    only: &HashSet<String, S>,
54    filter: Filter,
55) -> serde_json::error::Result<Vec<Suggestion>> {
56    let mut result = Vec::new();
57    for cargo_msg in serde_json::Deserializer::from_str(input).into_iter::<Diagnostic>() {
58        // One diagnostic line might have multiple suggestions
59        result.extend(collect_suggestions(&cargo_msg?, only, filter));
60    }
61    Ok(result)
62}
63
64#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
65pub struct LinePosition {
66    pub line: usize,
67    pub column: usize,
68}
69
70impl std::fmt::Display for LinePosition {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "{}:{}", self.line, self.column)
73    }
74}
75
76#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
77pub struct LineRange {
78    pub start: LinePosition,
79    pub end: LinePosition,
80}
81
82impl std::fmt::Display for LineRange {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}-{}", self.start, self.end)
85    }
86}
87
88/// An error/warning and possible solutions for fixing it
89#[derive(Debug, Clone, Hash, PartialEq, Eq)]
90pub struct Suggestion {
91    pub message: String,
92    pub snippets: Vec<Snippet>,
93    pub solutions: Vec<Solution>,
94}
95
96/// Solution to a diagnostic item.
97#[derive(Debug, Clone, Hash, PartialEq, Eq)]
98pub struct Solution {
99    /// The error message of the diagnostic item.
100    pub message: String,
101    /// Possible solutions to fix the error.
102    pub replacements: Vec<Replacement>,
103}
104
105/// Represents code that will get replaced.
106#[derive(Debug, Clone, Hash, PartialEq, Eq)]
107pub struct Snippet {
108    pub file_name: String,
109    pub line_range: LineRange,
110    pub range: Range<usize>,
111}
112
113/// Represents a replacement of a `snippet`.
114#[derive(Debug, Clone, Hash, PartialEq, Eq)]
115pub struct Replacement {
116    /// Code snippet that gets replaced.
117    pub snippet: Snippet,
118    /// The replacement of the snippet.
119    pub replacement: String,
120}
121
122/// Converts a [`DiagnosticSpan`] to a [`Snippet`].
123fn span_to_snippet(span: &DiagnosticSpan) -> Snippet {
124    Snippet {
125        file_name: span.file_name.clone(),
126        line_range: LineRange {
127            start: LinePosition {
128                line: span.line_start,
129                column: span.column_start,
130            },
131            end: LinePosition {
132                line: span.line_end,
133                column: span.column_end,
134            },
135        },
136        range: (span.byte_start as usize)..(span.byte_end as usize),
137    }
138}
139
140/// Converts a [`DiagnosticSpan`] into a [`Replacement`].
141fn collect_span(span: &DiagnosticSpan) -> Option<Replacement> {
142    let snippet = span_to_snippet(span);
143    let replacement = span.suggested_replacement.clone()?;
144    Some(Replacement {
145        snippet,
146        replacement,
147    })
148}
149
150/// Collects code [`Suggestion`]s from a single compiler diagnostic line.
151///
152/// * `only` --- only diagnostics with code in a set of error codes would be collected.
153pub fn collect_suggestions<S: ::std::hash::BuildHasher>(
154    diagnostic: &Diagnostic,
155    only: &HashSet<String, S>,
156    filter: Filter,
157) -> Option<Suggestion> {
158    if !only.is_empty() {
159        if let Some(ref code) = diagnostic.code {
160            if !only.contains(&code.code) {
161                // This is not the code we are looking for
162                return None;
163            }
164        } else {
165            // No code, probably a weird builtin warning/error
166            return None;
167        }
168    }
169
170    let solutions: Vec<_> = diagnostic
171        .children
172        .iter()
173        .filter_map(|child| {
174            let replacements: Vec<_> = child
175                .spans
176                .iter()
177                .filter(|span| {
178                    use crate::diagnostics::Applicability::*;
179                    use crate::Filter::*;
180
181                    match (filter, &span.suggestion_applicability) {
182                        (MachineApplicableOnly, Some(MachineApplicable)) => true,
183                        (MachineApplicableOnly, _) => false,
184                        (Everything, _) => true,
185                    }
186                })
187                .filter_map(collect_span)
188                .collect();
189            if !replacements.is_empty() {
190                Some(Solution {
191                    message: child.message.clone(),
192                    replacements,
193                })
194            } else {
195                None
196            }
197        })
198        .collect();
199
200    if solutions.is_empty() {
201        None
202    } else {
203        Some(Suggestion {
204            message: diagnostic.message.clone(),
205            snippets: diagnostic.spans.iter().map(span_to_snippet).collect(),
206            solutions,
207        })
208    }
209}
210
211/// Represents a code fix. This doesn't write to disks but is only in memory.
212///
213/// The general way to use this is:
214///
215/// 1. Feeds the source of a file to [`CodeFix::new`].
216/// 2. Calls [`CodeFix::apply`] to apply suggestions to the source code.
217/// 3. Calls [`CodeFix::finish`] to get the "fixed" code.
218#[derive(Clone)]
219pub struct CodeFix {
220    data: replace::Data,
221    /// Whether or not the data has been modified.
222    modified: bool,
223}
224
225impl CodeFix {
226    /// Creates a `CodeFix` with the source of a file to modify.
227    pub fn new(s: &str) -> CodeFix {
228        CodeFix {
229            data: replace::Data::new(s.as_bytes()),
230            modified: false,
231        }
232    }
233
234    /// Applies a suggestion to the code.
235    pub fn apply(&mut self, suggestion: &Suggestion) -> Result<(), Error> {
236        for solution in &suggestion.solutions {
237            for r in &solution.replacements {
238                self.data
239                    .replace_range(r.snippet.range.clone(), r.replacement.as_bytes())
240                    .inspect_err(|_| self.data.restore())?;
241            }
242        }
243        self.data.commit();
244        self.modified = true;
245        Ok(())
246    }
247
248    /// Applies an individual solution from a [`Suggestion`].
249    pub fn apply_solution(&mut self, solution: &Solution) -> Result<(), Error> {
250        for r in &solution.replacements {
251            self.data
252                .replace_range(r.snippet.range.clone(), r.replacement.as_bytes())
253                .inspect_err(|_| self.data.restore())?;
254        }
255        self.data.commit();
256        self.modified = true;
257        Ok(())
258    }
259
260    /// Gets the result of the "fixed" code.
261    pub fn finish(&self) -> Result<String, Error> {
262        Ok(String::from_utf8(self.data.to_vec())?)
263    }
264
265    /// Returns whether or not the data has been modified.
266    pub fn modified(&self) -> bool {
267        self.modified
268    }
269}
270
271/// Applies multiple `suggestions` to the given `code`, handling certain conflicts automatically.
272///
273/// If a replacement in a suggestion exactly matches a replacement of a previously applied solution,
274/// that entire suggestion will be skipped without generating an error.
275/// This is currently done to alleviate issues like rust-lang/rust#51211,
276/// although it may be removed if that's fixed deeper in the compiler.
277///
278/// The intent of this design is that the overall application process
279/// should repeatedly apply non-conflicting suggestions then rëevaluate the result,
280/// looping until either there are no more suggestions to apply or some budget is exhausted.
281pub fn apply_suggestions(code: &str, suggestions: &[Suggestion]) -> Result<String, Error> {
282    let mut fix = CodeFix::new(code);
283    for suggestion in suggestions.iter().rev() {
284        fix.apply(suggestion).or_else(|err| match err {
285            Error::AlreadyReplaced {
286                is_identical: true, ..
287            } => Ok(()),
288            _ => Err(err),
289        })?;
290    }
291    fix.finish()
292}