harper_core/linting/
suggestion.rs

1use std::fmt::Display;
2
3use is_macro::Is;
4use serde::{Deserialize, Serialize};
5
6use crate::Span;
7
8/// A suggested edit that could resolve a [`Lint`](super::Lint).
9#[derive(Debug, Clone, Serialize, Deserialize, Is, PartialEq, Eq, Hash)]
10pub enum Suggestion {
11    /// Replace the offending text with a specific character sequence.
12    ReplaceWith(Vec<char>),
13    /// Insert the provided characters _after_ the offending text.
14    InsertAfter(Vec<char>),
15    /// Remove the offending text.
16    Remove,
17}
18
19impl Suggestion {
20    /// Variant of [`Self::replace_with_match_case`] that accepts a static string.
21    pub fn replace_with_match_case_str(value: &'static str, template: &[char]) -> Self {
22        Self::replace_with_match_case(value.chars().collect(), template)
23    }
24
25    /// Construct an instance of [`Self::ReplaceWith`], but make the content match the case of the
26    /// provided template.
27    ///
28    /// For example, if we want to replace "You're" with "You are", we can provide "you are" and
29    /// "You're".
30    pub fn replace_with_match_case(mut value: Vec<char>, template: &[char]) -> Self {
31        // If the value is longer than the template, use this.
32        let template_term = [template.last().copied().unwrap_or('l')]
33            .into_iter()
34            .cycle();
35
36        for (v, t) in value.iter_mut().filter(|v| v.is_alphabetic()).zip(
37            template
38                .iter()
39                .filter(|v| v.is_alphabetic())
40                .copied()
41                .chain(template_term),
42        ) {
43            if t.is_uppercase() {
44                *v = v.to_ascii_uppercase();
45            } else {
46                *v = v.to_ascii_lowercase();
47            }
48        }
49
50        Self::ReplaceWith(value)
51    }
52
53    /// Apply a suggestion to a given text.
54    pub fn apply(&self, span: Span<char>, source: &mut Vec<char>) {
55        match self {
56            Self::ReplaceWith(chars) => {
57                // Avoid allocation if possible
58                if chars.len() == span.len() {
59                    for (index, c) in chars.iter().enumerate() {
60                        source[index + span.start] = *c
61                    }
62                } else {
63                    let popped = source.split_off(span.start);
64
65                    source.extend(chars);
66                    source.extend(popped.into_iter().skip(span.len()));
67                }
68            }
69            Self::Remove => {
70                for i in span.end..source.len() {
71                    source[i - span.len()] = source[i];
72                }
73
74                source.truncate(source.len() - span.len());
75            }
76            Self::InsertAfter(chars) => {
77                let popped = source.split_off(span.end);
78                source.extend(chars);
79                source.extend(popped);
80            }
81        }
82    }
83}
84
85impl Display for Suggestion {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match self {
88            Suggestion::ReplaceWith(with) => {
89                write!(f, "Replace with: “{}”", with.iter().collect::<String>())
90            }
91            Suggestion::InsertAfter(with) => {
92                write!(f, "Insert “{}”", with.iter().collect::<String>())
93            }
94            Suggestion::Remove => write!(f, "Remove error"),
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use crate::Span;
102
103    use super::Suggestion;
104
105    #[test]
106    fn insert_comma_after() {
107        let source = "This is a test";
108        let mut source_chars = source.chars().collect();
109        let sug = Suggestion::InsertAfter(vec![',']);
110        sug.apply(Span::new(0, 4), &mut source_chars);
111
112        assert_eq!(source_chars, "This, is a test".chars().collect::<Vec<_>>());
113    }
114
115    #[test]
116    fn suggestion_your_match_case() {
117        let template: Vec<_> = "You're".chars().collect();
118        let value: Vec<_> = "you are".chars().collect();
119
120        let correct = "You are".chars().collect();
121
122        assert_eq!(
123            Suggestion::replace_with_match_case(value, &template),
124            Suggestion::ReplaceWith(correct)
125        )
126    }
127
128    #[test]
129    fn issue_1065() {
130        let template: Vec<_> = "Stack Overflow".chars().collect();
131        let value: Vec<_> = "stackoverflow".chars().collect();
132
133        let correct = "StackOverflow".chars().collect();
134
135        assert_eq!(
136            Suggestion::replace_with_match_case(value, &template),
137            Suggestion::ReplaceWith(correct)
138        )
139    }
140}