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