cargo_cargofmt/formatting/
unused_parents.rs

1use std::collections::HashSet;
2
3use crate::toml::TokenIndices;
4use crate::toml::TokenKind;
5use crate::toml::TomlToken;
6use crate::toml::TomlTokens;
7
8struct Table {
9    name: Vec<String>,
10    /// First token of the table's range, including any leading comments.
11    start: usize,
12    /// Equal to next table's header index or token count.
13    end: usize,
14    is_array_table: bool,
15}
16
17#[tracing::instrument]
18pub fn remove_unused_parent_tables(tokens: &mut TomlTokens<'_>) {
19    let tables = collect_tables(tokens);
20
21    if tables.is_empty() {
22        return;
23    }
24
25    let parent_names = find_parent_names(&tables);
26
27    for table in tables.iter().rev() {
28        if should_remove(table, tokens, &parent_names) {
29            for i in table.start..table.end {
30                tokens.tokens[i] = TomlToken::EMPTY;
31            }
32        }
33    }
34
35    tokens.trim_empty_whitespace();
36}
37
38fn collect_tables(tokens: &TomlTokens<'_>) -> Vec<Table> {
39    // First pass: find all headers and their starts (including leading comments)
40    let mut header_info: Vec<(usize, usize, bool)> = Vec::new(); // (header_idx, start, is_array)
41    let mut indices = TokenIndices::new();
42
43    while let Some(i) = indices.next_index(tokens) {
44        let kind = tokens.tokens[i].kind;
45        if matches!(kind, TokenKind::StdTableOpen | TokenKind::ArrayTableOpen) {
46            let start = find_start(tokens, i);
47            header_info.push((i, start, kind == TokenKind::ArrayTableOpen));
48        }
49    }
50
51    // Second pass: construct tables with end boundaries
52    let mut tables = Vec::new();
53    for (idx, &(header_idx, start, is_array_table)) in header_info.iter().enumerate() {
54        let end = match header_info.get(idx + 1) {
55            Some(&(next_header_idx, _, _)) => next_header_idx,
56            None => tokens.len(),
57        };
58        let (name, _) = parse_table_name(tokens, header_idx + 1);
59        tables.push(Table {
60            name,
61            start,
62            end,
63            is_array_table,
64        });
65    }
66
67    tables
68}
69
70fn find_start(tokens: &TomlTokens<'_>, header_idx: usize) -> usize {
71    if header_idx == 0 {
72        return 0;
73    }
74
75    let mut newline_count = 0;
76    let mut indices = TokenIndices::from_index(header_idx);
77
78    while let Some(i) = indices.prev_index(tokens) {
79        match tokens.tokens[i].kind {
80            TokenKind::Comment => {
81                // Adjacent comment is a leading comment
82                if newline_count == 1 {
83                    return i;
84                }
85                return header_idx;
86            }
87            TokenKind::Newline => {
88                newline_count += 1;
89                if newline_count > 1 {
90                    return header_idx;
91                }
92            }
93            TokenKind::Whitespace => {}
94            _ => return header_idx,
95        }
96    }
97
98    header_idx
99}
100
101fn parse_table_name(tokens: &TomlTokens<'_>, start: usize) -> (Vec<String>, usize) {
102    let mut name = Vec::new();
103    let mut indices = TokenIndices::from_index(start);
104
105    while let Some(i) = indices.next_index(tokens) {
106        match tokens.tokens[i].kind {
107            TokenKind::SimpleKey => {
108                let token = &tokens.tokens[i];
109                name.push(token.decoded.as_ref().unwrap_or(&token.raw).to_string());
110            }
111            TokenKind::KeySep | TokenKind::Whitespace => {}
112            _ => {
113                return (name, i);
114            }
115        }
116    }
117
118    (name, tokens.len().saturating_sub(1))
119}
120
121fn find_parent_names(tables: &[Table]) -> HashSet<Vec<String>> {
122    tables
123        .iter()
124        .flat_map(|t| (1..t.name.len()).map(|len| t.name[..len].to_vec()))
125        .collect()
126}
127
128fn should_remove(
129    table: &Table,
130    tokens: &TomlTokens<'_>,
131    parent_names: &HashSet<Vec<String>>,
132) -> bool {
133    if table.is_array_table {
134        return false;
135    }
136
137    if !parent_names.contains(&table.name) {
138        return false;
139    }
140
141    !has_body(tokens, table.start, table.end)
142}
143
144fn has_body(tokens: &TomlTokens<'_>, start: usize, end: usize) -> bool {
145    let mut in_header = false;
146
147    for i in start..end {
148        match tokens.tokens[i].kind {
149            TokenKind::StdTableOpen | TokenKind::ArrayTableOpen => {
150                in_header = true;
151            }
152            TokenKind::StdTableClose | TokenKind::ArrayTableClose => {
153                in_header = false;
154            }
155            TokenKind::Whitespace | TokenKind::Newline => {}
156            _ if !in_header => {
157                return true;
158            }
159            _ => {}
160        }
161    }
162
163    false
164}
165
166#[cfg(test)]
167mod test {
168    use snapbox::assert_data_eq;
169    use snapbox::str;
170    use snapbox::IntoData;
171
172    #[track_caller]
173    fn valid(input: &str, expected: impl IntoData) {
174        let mut tokens = crate::toml::TomlTokens::parse(input);
175        super::remove_unused_parent_tables(&mut tokens);
176        let actual = tokens.to_string();
177
178        assert_data_eq!(&actual, expected);
179
180        let (_, errors) = toml::de::DeTable::parse_recoverable(&actual);
181        if !errors.is_empty() {
182            use std::fmt::Write as _;
183            let mut result = String::new();
184            writeln!(&mut result, "---").unwrap();
185            for error in errors {
186                writeln!(&mut result, "{error}").unwrap();
187                writeln!(&mut result, "---").unwrap();
188            }
189            panic!("failed to parse\n---\n{actual}\n{result}");
190        }
191    }
192
193    #[test]
194    fn empty_input() {
195        valid("", str![]);
196    }
197
198    #[test]
199    fn remove_empty_parent_without_comment() {
200        valid(
201            "[parent]
202[parent.child]
203key = 1
204",
205            str![[r#"
206[parent.child]
207key = 1
208
209"#]],
210        );
211    }
212
213    #[test]
214    fn remove_multiple_empty_parents() {
215        valid(
216            "[a]
217[a.b]
218[x]
219[x.y]
220",
221            str![[r#"
222[a.b]
223[x.y]
224
225"#]],
226        );
227    }
228
229    #[test]
230    fn remove_deeply_nested_empty_parents() {
231        valid(
232            "[a]
233[a.b]
234[a.b.c]
235key = 1
236",
237            str![[r#"
238[a.b.c]
239key = 1
240
241"#]],
242        );
243    }
244
245    #[test]
246    fn preserve_parent_with_trailing_comment() {
247        valid(
248            "[parent] # important section
249[parent.child]
250key = 1
251",
252            str![[r#"
253[parent] # important section
254[parent.child]
255key = 1
256
257"#]],
258        );
259    }
260
261    #[test]
262    fn preserve_parent_with_comment_no_space() {
263        valid(
264            "[parent]# comment
265[parent.child]
266",
267            str![[r#"
268[parent]# comment
269[parent.child]
270
271"#]],
272        );
273    }
274
275    #[test]
276    fn preserve_parent_with_content() {
277        valid(
278            r#"[parent]
279key = "value"
280[parent.child]
281other = 1
282"#,
283            str![[r#"
284[parent]
285key = "value"
286[parent.child]
287other = 1
288
289"#]],
290        );
291    }
292
293    #[test]
294    fn preserve_standalone_table_no_children() {
295        valid(
296            "[standalone]
297",
298            str![[r#"
299[standalone]
300
301"#]],
302        );
303    }
304
305    #[test]
306    fn remove_empty_parent_of_array_table() {
307        valid(
308            r#"[servers]
309[[servers.production]]
310ip = "10.0.0.1"
311"#,
312            str![[r#"
313[[servers.production]]
314ip = "10.0.0.1"
315
316"#]],
317        );
318    }
319
320    #[test]
321    fn preserve_array_table_parent_with_comment() {
322        valid(
323            r#"[servers] # Server configurations
324[[servers.production]]
325ip = "10.0.0.1"
326"#,
327            str![[r#"
328[servers] # Server configurations
329[[servers.production]]
330ip = "10.0.0.1"
331
332"#]],
333        );
334    }
335
336    #[test]
337    fn preserve_array_table_even_if_empty() {
338        valid(
339            "[[a]]
340[[a.b]]
341key = 1
342",
343            str![[r#"
344[[a]]
345[[a.b]]
346key = 1
347
348"#]],
349        );
350    }
351
352    #[test]
353    fn mixed_parent_child_relationships() {
354        valid(
355            "[a]
356[a.b]
357key = 1
358
359[c]
360value = 2
361
362[d]
363[d.e]
364[d.e.f]
365deep = 3
366",
367            str![[r#"
368[a.b]
369key = 1
370
371[c]
372value = 2
373
374[d.e.f]
375deep = 3
376
377"#]],
378        );
379    }
380
381    #[test]
382    fn sibling_tables_not_affected() {
383        valid(
384            "[a.b]
385x = 1
386[a.c]
387y = 2
388",
389            str![[r#"
390[a.b]
391x = 1
392[a.c]
393y = 2
394
395"#]],
396        );
397    }
398
399    #[test]
400    fn only_key_values_no_tables() {
401        valid(
402            r#"key = "value"
403other = 123
404"#,
405            str![[r#"
406key = "value"
407other = 123
408
409"#]],
410        );
411    }
412
413    #[test]
414    fn quoted_table_names() {
415        valid(
416            r#"["quoted"]
417["quoted".child]
418"#,
419            str![[r#"
420["quoted".child]
421
422"#]],
423        );
424    }
425
426    #[test]
427    fn preserve_blank_lines_between_remaining_tables() {
428        valid(
429            "[a]
430[a.b]
431
432[c]
433[c.d]
434",
435            str![[r#"
436[a.b]
437
438[c.d]
439
440"#]],
441        );
442    }
443
444    #[test]
445    fn parent_between_children() {
446        valid(
447            "[a.first]
448x = 1
449[a]
450[a.second]
451y = 2
452",
453            str![[r#"
454[a.first]
455x = 1
456[a.second]
457y = 2
458
459"#]],
460        );
461    }
462
463    #[test]
464    fn parent_after_child() {
465        valid(
466            "[parent.child]
467
468[parent]
469",
470            str![[r#"
471[parent.child]
472
473
474"#]],
475        );
476    }
477
478    #[test]
479    fn child_precedes_parent_adjacent_comment() {
480        valid(
481            "[parent.child]
482key = 1
483# comment
484[parent]
485",
486            str![[r#"
487[parent.child]
488key = 1
489# comment
490[parent]
491
492"#]],
493        );
494    }
495
496    #[test]
497    fn child_precedes_parent_body_comment() {
498        valid(
499            "[parent.child]
500key = 1
501# comment
502
503[parent]
504",
505            str![[r#"
506[parent.child]
507key = 1
508# comment
509
510
511"#]],
512        );
513    }
514
515    #[test]
516    fn leading_comment_before_parent() {
517        valid(
518            "# leading comment
519[parent]
520[parent.child]
521",
522            str![[r#"
523# leading comment
524[parent]
525[parent.child]
526
527"#]],
528        );
529    }
530
531    #[test]
532    fn body_comment_blank_line_before_parent() {
533        valid(
534            "# body comment
535
536[parent]
537[parent.child]
538",
539            str![[r#"
540# body comment
541
542[parent.child]
543
544"#]],
545        );
546    }
547
548    #[test]
549    fn leading_comment_after_other_table() {
550        valid(
551            "[other]
552
553# leading comment
554[parent]
555[parent.child]
556",
557            str![[r#"
558[other]
559
560# leading comment
561[parent]
562[parent.child]
563
564"#]],
565        );
566    }
567
568    #[test]
569    fn ambiguous_comment_after_other_table() {
570        valid(
571            "[other]
572# ambiguous comment
573[parent]
574[parent.child]
575",
576            str![[r#"
577[other]
578# ambiguous comment
579[parent]
580[parent.child]
581
582"#]],
583        );
584    }
585
586    #[test]
587    fn body_comment_blank_lines_after_other_table() {
588        valid(
589            "[other]
590
591# body comment
592
593[parent]
594[parent.child]
595",
596            str![[r#"
597[other]
598
599# body comment
600
601[parent.child]
602
603"#]],
604        );
605    }
606
607    #[test]
608    fn detached_body_comment_before_other_table() {
609        valid(
610            "[parent]
611
612# comment about parent fields
613
614[other]
615[parent.child]
616",
617            str![[r#"
618[parent]
619
620# comment about parent fields
621
622[other]
623[parent.child]
624
625"#]],
626        );
627    }
628
629    #[test]
630    fn body_comment_after_parent_header() {
631        valid(
632            "[parent]
633# body comment
634
635[parent.child]
636",
637            str![[r#"
638[parent]
639# body comment
640
641[parent.child]
642
643"#]],
644        );
645    }
646
647    #[test]
648    fn body_comment_blank_line_after_parent_header() {
649        valid(
650            "[parent]
651
652# body comment
653
654[parent.child]
655",
656            str![[r#"
657[parent]
658
659# body comment
660
661[parent.child]
662
663"#]],
664        );
665    }
666
667    #[test]
668    fn ambiguous_comment_after_parent_header() {
669        valid(
670            "[parent]
671# ambiguous comment
672[parent.child]
673",
674            str![[r#"
675[parent]
676# ambiguous comment
677[parent.child]
678
679"#]],
680        );
681    }
682
683    #[test]
684    fn whitespace_between_parent_and_child() {
685        valid(
686            "[parent]
687
688[parent.child]
689",
690            str![[r#"
691[parent.child]
692
693"#]],
694        );
695    }
696
697    #[test]
698    fn whitespace_on_blank_line_between_parent_and_child() {
699        valid(
700            "[parent]\n    \n[parent.child]\n",
701            str![[r#"
702[parent.child]
703
704"#]],
705        );
706    }
707
708    #[test]
709    fn leading_comment_for_child_after_blank_line() {
710        // Comment is adjacent to child, so included in child's start.
711        // But also within parent's end range, so parent is preserved.
712        valid(
713            "[parent]
714
715# leading comment for child
716[parent.child]
717key = 1
718",
719            str![[r#"
720[parent]
721
722# leading comment for child
723[parent.child]
724key = 1
725
726"#]],
727        );
728    }
729
730    #[test]
731    fn eof_without_trailing_newline() {
732        valid(
733            "[parent]
734[parent.child]
735key = 1",
736            str![[r#"
737[parent.child]
738key = 1
739"#]],
740        );
741    }
742}