Skip to main content

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