makefile_lossless/ast/conditional.rs
1use super::makefile::MakefileItem;
2use crate::lossless::{remove_with_preceding_comments, Conditional, Error, ErrorInfo, ParseError};
3use crate::SyntaxKind::*;
4use rowan::ast::AstNode;
5use rowan::{GreenNodeBuilder, SyntaxNode};
6
7impl Conditional {
8 /// Get the parent item of this conditional, if any
9 ///
10 /// Returns `Some(MakefileItem)` if this conditional has a parent that is a MakefileItem
11 /// (e.g., another Conditional for nested conditionals), or `None` if the parent is the root Makefile node.
12 ///
13 /// # Example
14 /// ```
15 /// use makefile_lossless::Makefile;
16 ///
17 /// let makefile: Makefile = r#"ifdef OUTER
18 /// ifdef INNER
19 /// VAR = value
20 /// endif
21 /// endif
22 /// "#.parse().unwrap();
23 ///
24 /// let outer = makefile.conditionals().next().unwrap();
25 /// let inner = outer.if_items().find_map(|item| {
26 /// if let makefile_lossless::MakefileItem::Conditional(c) = item {
27 /// Some(c)
28 /// } else {
29 /// None
30 /// }
31 /// }).unwrap();
32 /// // Inner conditional's parent is the outer conditional
33 /// assert!(inner.parent().is_some());
34 /// ```
35 pub fn parent(&self) -> Option<MakefileItem> {
36 self.syntax().parent().and_then(MakefileItem::cast)
37 }
38
39 /// Get the type of conditional (ifdef, ifndef, ifeq, ifneq)
40 pub fn conditional_type(&self) -> Option<String> {
41 self.syntax()
42 .children()
43 .find(|it| it.kind() == CONDITIONAL_IF)?
44 .children_with_tokens()
45 .find(|it| it.kind() == IDENTIFIER)
46 .map(|it| it.as_token().unwrap().text().to_string())
47 }
48
49 /// Get the condition expression
50 pub fn condition(&self) -> Option<String> {
51 let if_node = self
52 .syntax()
53 .children()
54 .find(|it| it.kind() == CONDITIONAL_IF)?;
55
56 // Find the EXPR node which contains the condition
57 let expr_node = if_node.children().find(|it| it.kind() == EXPR)?;
58
59 Some(expr_node.text().to_string().trim().to_string())
60 }
61
62 /// Check if this conditional has an else clause
63 pub fn has_else(&self) -> bool {
64 self.syntax()
65 .children()
66 .any(|it| it.kind() == CONDITIONAL_ELSE)
67 }
68
69 /// Get the body content of the if branch
70 pub fn if_body(&self) -> Option<String> {
71 let mut body = String::new();
72 let mut in_if_body = false;
73
74 for child in self.syntax().children_with_tokens() {
75 if child.kind() == CONDITIONAL_IF {
76 in_if_body = true;
77 continue;
78 }
79 if child.kind() == CONDITIONAL_ELSE || child.kind() == CONDITIONAL_ENDIF {
80 break;
81 }
82 if in_if_body {
83 body.push_str(child.to_string().as_str());
84 }
85 }
86
87 if body.is_empty() {
88 None
89 } else {
90 Some(body)
91 }
92 }
93
94 /// Get the body content of the else branch (if it exists)
95 pub fn else_body(&self) -> Option<String> {
96 if !self.has_else() {
97 return None;
98 }
99
100 let mut body = String::new();
101 let mut in_else_body = false;
102
103 for child in self.syntax().children_with_tokens() {
104 if child.kind() == CONDITIONAL_ELSE {
105 in_else_body = true;
106 continue;
107 }
108 if child.kind() == CONDITIONAL_ENDIF {
109 break;
110 }
111 if in_else_body {
112 body.push_str(child.to_string().as_str());
113 }
114 }
115
116 if body.is_empty() {
117 None
118 } else {
119 Some(body)
120 }
121 }
122
123 /// Remove this conditional from the makefile
124 pub fn remove(&mut self) -> Result<(), Error> {
125 let Some(parent) = self.syntax().parent() else {
126 return Err(Error::Parse(ParseError {
127 errors: vec![ErrorInfo {
128 message: "Cannot remove conditional: no parent node".to_string(),
129 line: 1,
130 context: "conditional_remove".to_string(),
131 }],
132 }));
133 };
134
135 remove_with_preceding_comments(self.syntax(), &parent);
136
137 Ok(())
138 }
139
140 /// Remove the conditional directives (ifdef/endif) but keep the body content
141 ///
142 /// This "unwraps" the conditional, keeping only the if branch content.
143 /// Returns an error if the conditional has an else clause.
144 ///
145 /// # Example
146 /// ```
147 /// use makefile_lossless::Makefile;
148 /// let mut makefile: Makefile = r#"ifdef DEBUG
149 /// VAR = debug
150 /// endif
151 /// "#.parse().unwrap();
152 /// let mut cond = makefile.conditionals().next().unwrap();
153 /// cond.unwrap().unwrap();
154 /// // Now makefile contains just "VAR = debug\n"
155 /// assert!(makefile.to_string().contains("VAR = debug"));
156 /// assert!(!makefile.to_string().contains("ifdef"));
157 /// ```
158 pub fn unwrap(&mut self) -> Result<(), Error> {
159 // Check if there's an else clause
160 if self.has_else() {
161 return Err(Error::Parse(ParseError {
162 errors: vec![ErrorInfo {
163 message: "Cannot unwrap conditional with else clause".to_string(),
164 line: 1,
165 context: "conditional_unwrap".to_string(),
166 }],
167 }));
168 }
169
170 let Some(parent) = self.syntax().parent() else {
171 return Err(Error::Parse(ParseError {
172 errors: vec![ErrorInfo {
173 message: "Cannot unwrap conditional: no parent node".to_string(),
174 line: 1,
175 context: "conditional_unwrap".to_string(),
176 }],
177 }));
178 };
179
180 // Collect the body items (everything between CONDITIONAL_IF and CONDITIONAL_ENDIF)
181 let body_nodes: Vec<_> = self
182 .syntax()
183 .children_with_tokens()
184 .skip_while(|n| n.kind() != CONDITIONAL_IF)
185 .skip(1) // Skip CONDITIONAL_IF itself
186 .take_while(|n| n.kind() != CONDITIONAL_ENDIF)
187 .collect();
188
189 // Find the position of this conditional in parent
190 let conditional_index = self.syntax().index();
191
192 // Replace the entire conditional with just its body items
193 parent.splice_children(conditional_index..conditional_index + 1, body_nodes);
194
195 Ok(())
196 }
197
198 /// Get all items (rules, variables, includes, nested conditionals) in the if branch
199 ///
200 /// # Example
201 /// ```
202 /// use makefile_lossless::Makefile;
203 /// let makefile: Makefile = r#"ifdef DEBUG
204 /// VAR = debug
205 /// rule:
206 /// command
207 /// endif
208 /// "#.parse().unwrap();
209 /// let cond = makefile.conditionals().next().unwrap();
210 /// let items: Vec<_> = cond.if_items().collect();
211 /// assert_eq!(items.len(), 2); // One variable, one rule
212 /// ```
213 pub fn if_items(&self) -> impl Iterator<Item = MakefileItem> + '_ {
214 self.syntax()
215 .children()
216 .skip_while(|n| n.kind() != CONDITIONAL_IF)
217 .skip(1) // Skip the CONDITIONAL_IF itself
218 .take_while(|n| n.kind() != CONDITIONAL_ELSE && n.kind() != CONDITIONAL_ENDIF)
219 .filter_map(MakefileItem::cast)
220 }
221
222 /// Get all items (rules, variables, includes, nested conditionals) in the else branch
223 ///
224 /// # Example
225 /// ```
226 /// use makefile_lossless::Makefile;
227 /// let makefile: Makefile = r#"ifdef DEBUG
228 /// VAR = debug
229 /// else
230 /// VAR = release
231 /// endif
232 /// "#.parse().unwrap();
233 /// let cond = makefile.conditionals().next().unwrap();
234 /// let items: Vec<_> = cond.else_items().collect();
235 /// assert_eq!(items.len(), 1); // One variable in else branch
236 /// ```
237 pub fn else_items(&self) -> impl Iterator<Item = MakefileItem> + '_ {
238 self.syntax()
239 .children()
240 .skip_while(|n| n.kind() != CONDITIONAL_ELSE)
241 .skip(1) // Skip the CONDITIONAL_ELSE itself
242 .take_while(|n| n.kind() != CONDITIONAL_ENDIF)
243 .filter_map(MakefileItem::cast)
244 }
245
246 /// Add an item to the if branch of the conditional
247 ///
248 /// # Example
249 /// ```
250 /// use makefile_lossless::{Makefile, MakefileItem};
251 /// let mut makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
252 /// let mut cond = makefile.conditionals().next().unwrap();
253 /// let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
254 /// let var = temp.variable_definitions().next().unwrap();
255 /// cond.add_if_item(MakefileItem::Variable(var));
256 /// assert!(makefile.to_string().contains("CFLAGS = -g"));
257 /// ```
258 pub fn add_if_item(&mut self, item: MakefileItem) {
259 let item_node = item.syntax().clone();
260
261 // Find position after CONDITIONAL_IF
262 let insert_pos = self
263 .syntax()
264 .children_with_tokens()
265 .position(|n| n.kind() == CONDITIONAL_IF)
266 .map(|p| p + 1)
267 .unwrap_or(0);
268
269 self.syntax()
270 .splice_children(insert_pos..insert_pos, vec![item_node.into()]);
271 }
272
273 /// Add an item to the else branch of the conditional
274 ///
275 /// If the conditional doesn't have an else branch, this will create one.
276 ///
277 /// # Example
278 /// ```
279 /// use makefile_lossless::{Makefile, MakefileItem};
280 /// let mut makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
281 /// let mut cond = makefile.conditionals().next().unwrap();
282 /// let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
283 /// let var = temp.variable_definitions().next().unwrap();
284 /// cond.add_else_item(MakefileItem::Variable(var));
285 /// assert!(makefile.to_string().contains("else"));
286 /// assert!(makefile.to_string().contains("CFLAGS = -O2"));
287 /// ```
288 pub fn add_else_item(&mut self, item: MakefileItem) {
289 // Ensure there's an else clause
290 if !self.has_else() {
291 self.add_else_clause();
292 }
293
294 let item_node = item.syntax().clone();
295
296 // Find position after CONDITIONAL_ELSE
297 let insert_pos = self
298 .syntax()
299 .children_with_tokens()
300 .position(|n| n.kind() == CONDITIONAL_ELSE)
301 .map(|p| p + 1)
302 .unwrap_or(0);
303
304 self.syntax()
305 .splice_children(insert_pos..insert_pos, vec![item_node.into()]);
306 }
307
308 /// Add an else clause to the conditional if it doesn't already have one
309 fn add_else_clause(&mut self) {
310 if self.has_else() {
311 return;
312 }
313
314 let mut builder = GreenNodeBuilder::new();
315 builder.start_node(CONDITIONAL_ELSE.into());
316 builder.token(IDENTIFIER.into(), "else");
317 builder.token(NEWLINE.into(), "\n");
318 builder.finish_node();
319
320 let syntax = SyntaxNode::new_root_mut(builder.finish());
321
322 // Find position before CONDITIONAL_ENDIF
323 let insert_pos = self
324 .syntax()
325 .children_with_tokens()
326 .position(|n| n.kind() == CONDITIONAL_ENDIF)
327 .unwrap_or(self.syntax().children_with_tokens().count());
328
329 self.syntax()
330 .splice_children(insert_pos..insert_pos, vec![syntax.into()]);
331 }
332}
333
334#[cfg(test)]
335mod tests {
336
337 use crate::lossless::Makefile;
338
339 #[test]
340 fn test_conditional_parent() {
341 let makefile: Makefile = r#"ifdef DEBUG
342VAR = debug
343endif
344"#
345 .parse()
346 .unwrap();
347
348 let cond = makefile.conditionals().next().unwrap();
349 let parent = cond.parent();
350 // Parent is ROOT node which doesn't cast to MakefileItem
351 assert!(parent.is_none());
352 }
353}