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}