1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! Node-kind dispatch for the bash reformatter: [`Formatter::format_node`]
//! plus the leaf-construct helpers (`format_command`,
//! `format_command_words`, `format_cond_node`) that don't warrant a
//! dedicated compound-construct file.
use crate::ast::{Node, NodeKind};
use super::formatter::Formatter;
use super::words::process_word_value;
impl Formatter {
/// Format a single AST node into canonical bash source. Top-level
/// dispatch for the reformatter — delegates each node kind to the
/// dedicated helper method (most live in `compound.rs`).
///
/// Simple leaf kinds are handled inline; everything else is routed
/// through [`Self::format_compound_node`] so each match body stays
/// under the clippy `too_many_lines` threshold.
pub(super) fn format_node(&mut self, node: &Node) {
match &node.kind {
NodeKind::Word { value, .. } => self.write_str(value),
NodeKind::Pipeline { commands, .. } => self.format_pipeline(commands),
NodeKind::List { items } => self.format_list(items),
NodeKind::Empty => {}
_ => self.format_compound_node(node),
}
}
/// Dispatch for compound and leaf-with-fields node kinds. Each arm
/// delegates to a helper taking `&Node`; the helper destructures
/// via `let-else` internally. This keeps the dispatcher flat so
/// the function stays under 60 lines.
fn format_compound_node(&mut self, node: &Node) {
match &node.kind {
NodeKind::Command { .. } => self.format_command(node),
NodeKind::If { .. } => self.format_if(node),
NodeKind::While { .. } => self.format_while_until(node, "while"),
NodeKind::Until { .. } => self.format_while_until(node, "until"),
NodeKind::For { .. } => self.format_for(node),
NodeKind::Select { .. } => self.format_select(node),
NodeKind::ForArith { .. } => self.format_for_arith(node),
NodeKind::Case { .. } => self.format_case(node),
NodeKind::Function { .. } => self.format_function(node),
NodeKind::Subshell { .. } => self.format_subshell(node),
NodeKind::BraceGroup { .. } => self.format_brace_group(node),
NodeKind::Negation { .. } => self.format_negation(node),
NodeKind::Time { .. } => self.format_time(node),
NodeKind::Coproc { .. } => self.format_coproc(node),
NodeKind::ConditionalExpr { .. } => self.format_conditional_expr(node),
_ => self.write_str(&node.to_string()),
}
}
pub(super) fn format_command(&mut self, node: &Node) {
let NodeKind::Command {
assignments,
words,
redirects,
} = &node.kind
else {
return;
};
self.format_command_words(assignments, words);
// Phase 1: all redirect heads inline on the command line.
// For heredocs this is just the `<<DELIM` part — the bodies are
// deferred so bash's canonical form can put every op on the same
// line regardless of heredoc ordering (see #39).
for (i, r) in redirects.iter().enumerate() {
if !assignments.is_empty() || !words.is_empty() || i > 0 {
self.write_char(' ');
}
self.format_redirect_inline(r);
}
// Phase 2: concatenate heredoc bodies in source order after the
// command line. Exactly one `\n` separates the command line from
// the first body; consecutive bodies have none between them
// (each body already ends with its delimiter's trailing `\n`).
let mut first_heredoc = true;
for r in redirects {
if let NodeKind::HereDoc {
delimiter, content, ..
} = &r.kind
{
if first_heredoc {
self.write_char('\n');
first_heredoc = false;
}
self.write_heredoc_body(content, delimiter);
}
}
}
/// Writes assignments and command words as space-separated bash tokens.
pub(super) fn format_command_words(&mut self, assignments: &[Node], words: &[Node]) {
for (i, w) in assignments.iter().chain(words.iter()).enumerate() {
if i > 0 {
self.write_char(' ');
}
if let NodeKind::Word { value, spans, .. } = &w.kind {
self.write_str(&process_word_value(value, spans));
} else {
self.write_str(&w.to_string());
}
}
}
/// Formats a conditional expression node as canonical bash source.
/// Callers place the `[[ ` / ` ]]` delimiters; this helper emits
/// only the inner expression.
pub(super) fn format_cond_node(&mut self, node: &Node) {
match &node.kind {
NodeKind::UnaryTest { op, operand } => {
self.write_str(op);
self.write_char(' ');
self.format_cond_node(operand);
}
NodeKind::BinaryTest { op, left, right } => {
self.format_cond_node(left);
self.write_char(' ');
self.write_str(op);
self.write_char(' ');
self.format_cond_node(right);
}
NodeKind::CondAnd { left, right } => {
self.format_cond_node(left);
self.write_str(" && ");
self.format_cond_node(right);
}
NodeKind::CondOr { left, right } => {
self.format_cond_node(left);
self.write_str(" || ");
self.format_cond_node(right);
}
NodeKind::CondNot { operand } => {
self.write_str("! ");
self.format_cond_node(operand);
}
NodeKind::CondTerm { value, .. } => {
self.write_str(value);
}
NodeKind::CondParen { inner } => {
self.write_str("( ");
self.format_cond_node(inner);
self.write_str(" )");
}
_ => {
self.write_str(&node.to_string());
}
}
}
}