cargo_cargofmt/formatting/
blank_lines.rs

1use crate::toml::TokenKind;
2use crate::toml::TomlToken;
3
4/// Assumptions:
5/// - newlines normalized
6/// - trailing spaces trimmed
7#[tracing::instrument]
8pub fn constrain_blank_lines(tokens: &mut crate::toml::TomlTokens<'_>, min: usize, max: usize) {
9    let mut depth = 0;
10    let mut indices = crate::toml::TokenIndices::new();
11    while let Some(mut i) = indices.next_index(tokens) {
12        match tokens.tokens[i].kind {
13            TokenKind::StdTableOpen | TokenKind::ArrayTableOpen => {}
14            TokenKind::ArrayOpen | TokenKind::InlineTableOpen => {
15                depth += 1;
16            }
17            TokenKind::StdTableClose | TokenKind::ArrayTableClose => {}
18            TokenKind::ArrayClose | TokenKind::InlineTableClose => {
19                depth -= 1;
20            }
21            TokenKind::SimpleKey => {}
22            TokenKind::KeySep => {}
23            TokenKind::KeyValSep => {}
24            TokenKind::Scalar => {}
25            TokenKind::ValueSep => {}
26            TokenKind::Whitespace => {}
27            TokenKind::Comment => {}
28            TokenKind::Newline if i == 0 => {
29                tokens.tokens.remove(0);
30                indices.set_next_index(0);
31            }
32            TokenKind::Newline => {
33                let blank_i = i + 1;
34                if blank_i < tokens.len() {
35                    let actual_newline_count = tokens.tokens[blank_i..]
36                        .iter()
37                        .take_while(|t| t.kind == TokenKind::Newline)
38                        .count();
39                    let is_trailing_newline = i + 1 == tokens.tokens.len();
40                    let constrained_newline_count =
41                        if is_trailing_newline || depth != 0 || after_table_header(tokens, i) {
42                            0
43                        } else {
44                            actual_newline_count.clamp(min, max)
45                        };
46                    if let Some(remove_count) =
47                        actual_newline_count.checked_sub(constrained_newline_count)
48                    {
49                        tokens.tokens.splice(blank_i..blank_i + remove_count, []);
50                    } else if let Some(add_count) =
51                        constrained_newline_count.checked_sub(actual_newline_count)
52                    {
53                        tokens
54                            .tokens
55                            .splice(blank_i..blank_i, (0..add_count).map(|_| TomlToken::NL));
56                    }
57                    i = blank_i + constrained_newline_count - 1;
58                    indices.set_next_index(i + 1);
59                }
60            }
61            TokenKind::Error => {}
62        }
63    }
64}
65
66fn after_table_header(tokens: &crate::toml::TomlTokens<'_>, i: usize) -> bool {
67    // Assumption: `i` is the newline after a table close
68    let Some(prev_nl_i) = (0..i)
69        .rev()
70        .find(|i| tokens.tokens[*i].kind == TokenKind::Newline)
71    else {
72        return false;
73    };
74
75    let after_header = tokens.tokens[prev_nl_i..i]
76        .iter()
77        .any(|t| matches!(t.kind, TokenKind::StdTableOpen | TokenKind::ArrayTableOpen));
78    if !after_header {
79        return false;
80    }
81
82    for token in &tokens.tokens[i..] {
83        match token.kind {
84            TokenKind::StdTableOpen | TokenKind::ArrayTableOpen => {
85                // empty tables don't count
86                return false;
87            }
88            TokenKind::SimpleKey => {
89                break;
90            }
91            _ => {}
92        }
93    }
94
95    true
96}
97
98#[cfg(test)]
99mod test {
100    use snapbox::assert_data_eq;
101    use snapbox::str;
102    use snapbox::IntoData;
103
104    #[track_caller]
105    fn valid(input: &str, min: usize, max: usize, expected: impl IntoData) {
106        let mut tokens = crate::toml::TomlTokens::parse(input);
107        super::constrain_blank_lines(&mut tokens, min, max);
108        let actual = tokens.to_string();
109
110        assert_data_eq!(&actual, expected);
111
112        let (_, errors) = toml::de::DeTable::parse_recoverable(&actual);
113        if !errors.is_empty() {
114            use std::fmt::Write as _;
115            let mut result = String::new();
116            writeln!(&mut result, "---").unwrap();
117            for error in errors {
118                writeln!(&mut result, "{error}").unwrap();
119                writeln!(&mut result, "---").unwrap();
120            }
121            panic!("failed to parse\n---\n{actual}\n{result}");
122        }
123    }
124
125    #[test]
126    fn empty_no_blank() {
127        valid("", 0, 0, str![]);
128    }
129
130    #[test]
131    fn empty_many_blanks() {
132        valid("", 3, 10, str![]);
133    }
134
135    #[test]
136    fn single_key_value_no_blank() {
137        valid("a = 5", 0, 0, str!["a = 5"]);
138    }
139
140    #[test]
141    fn single_key_value_many_blanks() {
142        valid("a = 5", 3, 10, str!["a = 5"]);
143    }
144
145    #[test]
146    fn remove_blank_lines() {
147        valid(
148            "
149
150a = 5
151
152
153b = 6
154
155
156# comment 
157
158
159# comment
160
161
162c = 7
163
164
165[d]
166
167
168e = 10
169
170
171f = [
172
173
174  1,
175
176
177  2,
178
179]
180
181
182g = { a = 1, b = 2 }
183
184
185",
186            0,
187            0,
188            str![[r#"
189a = 5
190b = 6
191# comment 
192# comment
193c = 7
194[d]
195e = 10
196f = [
197  1,
198  2,
199]
200g = { a = 1, b = 2 }
201
202"#]],
203        );
204    }
205
206    #[test]
207    fn compact_blank_lines() {
208        valid(
209            "
210
211a = 5
212
213
214b = 6
215
216
217# comment 
218
219
220# comment
221
222
223c = 7
224
225
226[d]
227
228
229e = 10
230
231
232f = [
233
234
235  1,
236
237
238  2,
239
240
241]
242
243
244g = { a = 1, b = 2 }
245
246
247",
248            1,
249            1,
250            str![[r#"
251a = 5
252
253b = 6
254
255# comment 
256
257# comment
258
259c = 7
260
261[d]
262e = 10
263
264f = [
265  1,
266  2,
267]
268
269g = { a = 1, b = 2 }
270
271
272"#]],
273        );
274    }
275
276    #[test]
277    fn add_blank_lines() {
278        valid(
279            "a = 5
280b = 6
281# comment 
282# comment
283c = 7
284[d]
285e = 10
286f = [
287  1,
288  2,
289]
290g = { a = 1, b = 2 }",
291            2,
292            2,
293            str![[r#"
294a = 5
295
296
297b = 6
298
299
300# comment 
301
302
303# comment
304
305
306c = 7
307
308
309[d]
310e = 10
311
312
313f = [
314  1,
315  2,
316]
317
318
319g = { a = 1, b = 2 }
320"#]],
321        );
322    }
323
324    #[test]
325    fn expand_blank_lines() {
326        valid(
327            "
328a = 5
329
330b = 6
331
332# comment 
333
334# comment
335
336c = 7
337
338[d]
339
340e = 10
341
342f = [
343
344  1,
345
346  2,
347
348]
349
350g = { a = 1, b = 2 }
351",
352            3,
353            3,
354            str![[r#"
355a = 5
356
357
358
359b = 6
360
361
362
363# comment 
364
365
366
367# comment
368
369
370
371c = 7
372
373
374
375[d]
376e = 10
377
378
379
380f = [
381  1,
382  2,
383]
384
385
386
387g = { a = 1, b = 2 }
388
389"#]],
390        );
391    }
392
393    #[test]
394    fn blank_line_between_array_close_and_table_open() {
395        valid(
396            r#"
397key = [
398]
399
400[b]
401"#,
402            0,
403            1,
404            str![[r#"
405key = [
406]
407
408[b]
409
410"#]],
411        );
412    }
413}