Skip to main content

harper_core/linting/
suggestion.rs

1use std::{
2    borrow::Borrow,
3    fmt::{Debug, Display},
4};
5
6use is_macro::Is;
7use serde::{Deserialize, Serialize};
8
9use crate::{Span, case};
10
11/// A suggested edit that could resolve a [`Lint`](super::Lint).
12#[derive(Clone, Serialize, Deserialize, Is, PartialEq, Eq, Hash)]
13pub enum Suggestion {
14    /// Replace the offending text with a specific character sequence.
15    ReplaceWith(Vec<char>),
16    /// Insert the provided characters _after_ the offending text.
17    InsertAfter(Vec<char>),
18    /// Remove the offending text.
19    Remove,
20}
21
22impl Suggestion {
23    /// Variant of [`Self::replace_with_match_case`] that accepts a static string.
24    pub fn replace_with_match_case_str(
25        value: &str,
26        template: impl IntoIterator<Item = impl Borrow<char>>,
27    ) -> Self {
28        Self::replace_with_match_case(value.chars().collect(), template)
29    }
30
31    /// Construct an instance of [`Self::ReplaceWith`], but make the content match the case of the
32    /// provided template.
33    ///
34    /// For example, if we want to replace "You're" with "You are", we can provide "you are" and
35    /// "You're".
36    pub fn replace_with_match_case(
37        value: Vec<char>,
38        template: impl IntoIterator<Item = impl Borrow<char>>,
39    ) -> Self {
40        Self::ReplaceWith(case::copy_casing(template, value).to_vec())
41    }
42
43    /// Apply a suggestion to a given text.
44    pub fn apply(&self, span: Span<char>, source: &mut Vec<char>) {
45        match self {
46            Self::ReplaceWith(chars) => {
47                // Avoid allocation if possible
48                if chars.len() == span.len() {
49                    for (index, c) in chars.iter().enumerate() {
50                        source[index + span.start] = *c
51                    }
52                } else {
53                    let popped = source.split_off(span.start);
54
55                    source.extend(chars);
56                    source.extend(popped.into_iter().skip(span.len()));
57                }
58            }
59            Self::Remove => {
60                for i in span.end..source.len() {
61                    source[i - span.len()] = source[i];
62                }
63
64                source.truncate(source.len() - span.len());
65            }
66            Self::InsertAfter(chars) => {
67                let popped = source.split_off(span.end);
68                source.extend(chars);
69                source.extend(popped);
70            }
71        }
72    }
73}
74
75impl Display for Suggestion {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            Suggestion::ReplaceWith(with) => {
79                write!(f, "Replace with: “{}”", with.iter().collect::<String>())
80            }
81            Suggestion::InsertAfter(with) => {
82                write!(f, "Insert “{}”", with.iter().collect::<String>())
83            }
84            Suggestion::Remove => write!(f, "Remove error"),
85        }
86    }
87}
88
89// To make debug output more readable.
90// The default debug implementation for Vec<char> isn't ideal in this scenario, as it prints
91// characters one at a time, line by line.
92impl Debug for Suggestion {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        <Self as Display>::fmt(self, f)
95    }
96}
97
98pub trait SuggestionCollectionExt {
99    fn to_replace_suggestions(
100        self,
101        case_template: impl IntoIterator<Item = impl Borrow<char>> + Clone,
102    ) -> impl Iterator<Item = Suggestion>;
103}
104
105impl<I, T> SuggestionCollectionExt for I
106where
107    I: IntoIterator<Item = T>,
108    T: AsRef<str>,
109{
110    fn to_replace_suggestions(
111        self,
112        case_template: impl IntoIterator<Item = impl Borrow<char>> + Clone,
113    ) -> impl Iterator<Item = Suggestion> {
114        self.into_iter().map(move |s| {
115            Suggestion::replace_with_match_case_str(s.as_ref(), case_template.clone())
116        })
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::Span;
123
124    use super::Suggestion;
125
126    #[test]
127    fn insert_comma_after() {
128        let source = "This is a test";
129        let mut source_chars = source.chars().collect();
130        let sug = Suggestion::InsertAfter(vec![',']);
131        sug.apply(Span::new(0, 4), &mut source_chars);
132
133        assert_eq!(source_chars, "This, is a test".chars().collect::<Vec<_>>());
134    }
135
136    #[test]
137    fn suggestion_your_match_case() {
138        let template: Vec<_> = "You're".chars().collect();
139        let value: Vec<_> = "you are".chars().collect();
140
141        let correct = "You are".chars().collect();
142
143        assert_eq!(
144            Suggestion::replace_with_match_case(value, &template),
145            Suggestion::ReplaceWith(correct)
146        )
147    }
148
149    #[test]
150    fn issue_1065() {
151        let template: Vec<_> = "Stack Overflow".chars().collect();
152        let value: Vec<_> = "stackoverflow".chars().collect();
153
154        let correct = "StackOverflow".chars().collect();
155
156        assert_eq!(
157            Suggestion::replace_with_match_case(value, &template),
158            Suggestion::ReplaceWith(correct)
159        )
160    }
161}