harper_core/linting/
suggestion.rs1use std::fmt::Display;
2
3use is_macro::Is;
4use serde::{Deserialize, Serialize};
5
6use crate::Span;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Is, PartialEq, Eq, Hash)]
10pub enum Suggestion {
11 ReplaceWith(Vec<char>),
13 InsertAfter(Vec<char>),
15 Remove,
17}
18
19impl Suggestion {
20 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 pub fn replace_with_match_case(mut value: Vec<char>, template: &[char]) -> Self {
31 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 pub fn apply(&self, span: Span<char>, source: &mut Vec<char>) {
55 match self {
56 Self::ReplaceWith(chars) => {
57 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}