cargo_cargofmt/formatting/
unused_parents.rs1use 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 start: usize,
12 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 let mut header_info: Vec<(usize, usize, bool)> = Vec::new(); 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 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 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 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}