dotenv_analyzer/fix/
mod.rs

1use dotenv_core::LineEntry;
2
3use crate::{LintKind, Warning};
4
5mod duplicated_key;
6mod ending_blank_line;
7mod extra_blank_line;
8mod incorrect_delimiter;
9mod key_without_value;
10mod leading_character;
11mod lowercase_key;
12mod quote_character;
13mod space_character;
14mod substitution_key;
15mod trailing_whitespace;
16mod unordered_key;
17mod value_without_quotes;
18
19trait Fix {
20    fn name(&self) -> LintKind;
21
22    fn fix_warnings(&self, warning_lines: &[usize], lines: &mut Vec<LineEntry>) -> Option<usize> {
23        let mut count: usize = 0;
24        for line_number in warning_lines {
25            let line = lines
26                .iter_mut()
27                .find(|entry| entry.number == *line_number)?;
28            if self.fix_line(line).is_some() {
29                count += 1;
30            }
31        }
32
33        Some(count)
34    }
35
36    fn fix_line(&self, _line: &mut LineEntry) -> Option<()> {
37        None
38    }
39
40    fn is_mandatory(&self) -> bool {
41        false
42    }
43}
44
45// Fix order matters
46fn fixlist() -> Vec<Box<dyn Fix>> {
47    vec![
48        // At first we run the fixers that handle a single line entry (they use default
49        // implementation of the fix_warnings() function)
50        Box::<key_without_value::KeyWithoutValueFixer>::default(),
51        Box::<lowercase_key::LowercaseKeyFixer>::default(),
52        Box::<space_character::SpaceCharacterFixer>::default(),
53        Box::<trailing_whitespace::TrailingWhitespaceFixer>::default(),
54        Box::<leading_character::LeadingCharacterFixer>::default(),
55        Box::<value_without_quotes::ValueWithoutQuotesFixer>::default(),
56        Box::<quote_character::QuoteCharacterFixer>::default(),
57        Box::<incorrect_delimiter::IncorrectDelimiterFixer>::default(),
58        Box::<extra_blank_line::ExtraBlankLineFixer>::default(),
59        Box::<substitution_key::SubstitutionKeyFixer>::default(),
60        // Then we should run the fixers that handle the line entry collection at whole
61        Box::<unordered_key::UnorderedKeyFixer>::default(),
62        Box::<duplicated_key::DuplicatedKeyFixer>::default(),
63        Box::<ending_blank_line::EndingBlankLineFixer>::default(),
64    ]
65}
66
67pub fn fix(warnings: &[Warning], lines: &mut Vec<LineEntry>, skip_checks: &[LintKind]) -> usize {
68    if warnings.is_empty() {
69        return 0;
70    }
71    let mut fixes = fixlist();
72
73    // Skip fixes for checks in --skip argument (globally)
74    fixes.retain(|f| !skip_checks.contains(&f.name()));
75
76    let mut count = 0;
77    for fixer in fixes {
78        // We can optimize it: create check_name:warnings map in advance
79        let warning_lines: Vec<usize> = warnings
80            .iter()
81            .filter(|w| *w.check_name() == fixer.name())
82            .map(|w| w.line_number())
83            .collect();
84
85        // Some fixers are mandatory because previous fixers can spawn warnings for them
86        if fixer.is_mandatory() || !warning_lines.is_empty() {
87            match fixer.fix_warnings(&warning_lines, lines) {
88                Some(fixer_count) => count += fixer_count,
89                None => {
90                    return 0;
91                }
92            }
93        }
94    }
95
96    // Removes extra blank lines
97    lines.retain(|l| !l.is_deleted);
98
99    count
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::tests::*;
106
107    #[test]
108    fn run_with_empty_warnings_test() {
109        let mut lines = vec![line_entry(1, 2, "A=B"), blank_line_entry(2, 2)];
110
111        assert_eq!(0, fix(&[], &mut lines, &[]));
112    }
113
114    #[test]
115    fn run_with_fixable_warning_test() {
116        let mut lines = vec![
117            line_entry(1, 3, "A=B"),
118            line_entry(2, 3, "c=d"),
119            blank_line_entry(3, 3),
120        ];
121        let warnings = [Warning::new(
122            lines[1].number,
123            LintKind::LowercaseKey,
124            "The c key should be in uppercase",
125        )];
126
127        assert_eq!(1, fix(&warnings, &mut lines, &[]));
128        assert_eq!("C=d", lines[1].raw_string);
129    }
130
131    #[test]
132    fn run_when_lines_do_not_fit_numbers_test() {
133        let mut lines = vec![
134            line_entry(1, 3, "a=B"),
135            line_entry(4, 3, "c=D"),
136            blank_line_entry(3, 3),
137        ];
138        let warnings = [
139            Warning::new(
140                lines[0].number,
141                LintKind::LowercaseKey,
142                "The a key should be in uppercase",
143            ),
144            Warning::new(
145                lines[1].number,
146                LintKind::LowercaseKey,
147                "The c key should be in uppercase",
148            ),
149        ];
150
151        assert_eq!(2, fix(&warnings, &mut lines, &[]));
152    }
153
154    #[test]
155    fn new_warnings_after_fix_test() {
156        let mut lines = vec![
157            line_entry(1, 5, "A1=1"),
158            line_entry(2, 5, "A2=2"),
159            line_entry(3, 5, "a0=0"),
160            line_entry(4, 5, "a2=2"),
161            blank_line_entry(5, 5),
162        ];
163        let warnings = [
164            Warning::new(
165                lines[2].number,
166                LintKind::LowercaseKey,
167                "The a0 key should be in uppercase",
168            ),
169            Warning::new(
170                lines[3].number,
171                LintKind::LowercaseKey,
172                "The a2 key should be in uppercase",
173            ),
174        ];
175
176        assert_eq!(2, fix(&warnings, &mut lines, &[]));
177        assert_eq!("A0=0", lines[0].raw_string);
178        assert_eq!("A1=1", lines[1].raw_string);
179        assert_eq!("A2=2", lines[2].raw_string);
180        assert_eq!("# A2=2", lines[3].raw_string);
181        assert_eq!("\n", lines[4].raw_string);
182    }
183
184    #[test]
185    fn skip_duplicated_key() {
186        let mut lines = vec![
187            line_entry(1, 5, "A1=1"),
188            line_entry(2, 5, "A2=2"),
189            line_entry(3, 5, "a0=0"),
190            line_entry(4, 5, "a2=2"),
191            blank_line_entry(5, 5),
192        ];
193        let warnings = [
194            Warning::new(
195                lines[2].number,
196                LintKind::LowercaseKey,
197                "The a0 key should be in uppercase",
198            ),
199            Warning::new(
200                lines[3].number,
201                LintKind::LowercaseKey,
202                "The a2 key should be in uppercase",
203            ),
204        ];
205
206        assert_eq!(2, fix(&warnings, &mut lines, &[LintKind::DuplicatedKey]));
207        assert_eq!("A0=0", lines[0].raw_string);
208        assert_eq!("A1=1", lines[1].raw_string);
209        assert_eq!("A2=2", lines[2].raw_string);
210        assert_eq!("A2=2", lines[3].raw_string);
211        assert_eq!("\n", lines[4].raw_string);
212    }
213
214    #[test]
215    fn skip_unordered_key() {
216        let mut lines = vec![
217            line_entry(1, 5, "A1=1"),
218            line_entry(2, 5, "A2=2"),
219            line_entry(3, 5, "a0=0"),
220            line_entry(4, 5, "a2=2"),
221            blank_line_entry(5, 5),
222        ];
223        let warnings = [
224            Warning::new(
225                lines[2].number,
226                LintKind::LowercaseKey,
227                "The a0 key should be in uppercase",
228            ),
229            Warning::new(
230                lines[3].number,
231                LintKind::LowercaseKey,
232                "The a2 key should be in uppercase",
233            ),
234        ];
235
236        assert_eq!(2, fix(&warnings, &mut lines, &[LintKind::UnorderedKey]));
237        assert_eq!("A1=1", lines[0].raw_string);
238        assert_eq!("A2=2", lines[1].raw_string);
239        assert_eq!("A0=0", lines[2].raw_string);
240        assert_eq!("# A2=2", lines[3].raw_string);
241        assert_eq!("\n", lines[4].raw_string);
242    }
243}