Skip to main content

makefile_lossless/ast/
makefile.rs

1use crate::lossless::{
2    parse, Conditional, Error, ErrorInfo, Include, Makefile, ParseError, Rule, SyntaxNode,
3    VariableDefinition,
4};
5use crate::pattern::matches_pattern;
6use crate::SyntaxKind::*;
7use rowan::ast::AstNode;
8use rowan::GreenNodeBuilder;
9use std::collections::VecDeque;
10
11/// Represents different types of items that can appear in a Makefile
12#[derive(Clone)]
13pub enum MakefileItem {
14    /// A rule definition (e.g., "target: prerequisites")
15    Rule(Rule),
16    /// A variable definition (e.g., "VAR = value")
17    Variable(VariableDefinition),
18    /// An include directive (e.g., "include foo.mk")
19    Include(Include),
20    /// A conditional block (e.g., "ifdef DEBUG ... endif")
21    Conditional(Conditional),
22}
23
24impl MakefileItem {
25    /// Try to cast a syntax node to a MakefileItem
26    pub(crate) fn cast(node: SyntaxNode) -> Option<Self> {
27        if let Some(rule) = Rule::cast(node.clone()) {
28            Some(MakefileItem::Rule(rule))
29        } else if let Some(var) = VariableDefinition::cast(node.clone()) {
30            Some(MakefileItem::Variable(var))
31        } else if let Some(inc) = Include::cast(node.clone()) {
32            Some(MakefileItem::Include(inc))
33        } else {
34            Conditional::cast(node).map(MakefileItem::Conditional)
35        }
36    }
37
38    /// Get the underlying syntax node
39    pub(crate) fn syntax(&self) -> &SyntaxNode {
40        match self {
41            MakefileItem::Rule(r) => r.syntax(),
42            MakefileItem::Variable(v) => v.syntax(),
43            MakefileItem::Include(i) => i.syntax(),
44            MakefileItem::Conditional(c) => c.syntax(),
45        }
46    }
47
48    /// Helper to get parent node or return an appropriate error
49    fn get_parent_or_error(&self, action: &str, method: &str) -> Result<SyntaxNode, Error> {
50        self.syntax().parent().ok_or_else(|| {
51            Error::Parse(ParseError {
52                errors: vec![ErrorInfo {
53                    message: format!("Cannot {} item without parent", action),
54                    line: 1,
55                    context: format!("MakefileItem::{}", method),
56                }],
57            })
58        })
59    }
60
61    /// Check if a token is a regular comment (not a shebang)
62    fn is_regular_comment(token: &rowan::SyntaxToken<crate::lossless::Lang>) -> bool {
63        token.kind() == COMMENT && !token.text().starts_with("#!")
64    }
65
66    /// Extract comment text from a comment token, removing '#' prefix
67    fn extract_comment_text(token: &rowan::SyntaxToken<crate::lossless::Lang>) -> String {
68        let text = token.text();
69        text.strip_prefix("# ")
70            .or_else(|| text.strip_prefix('#'))
71            .unwrap_or(text)
72            .to_string()
73    }
74
75    /// Helper to find all preceding comment-related elements up to the first non-comment element
76    ///
77    /// Returns elements in reverse order (from closest to furthest from the item)
78    fn collect_preceding_comment_elements(
79        &self,
80    ) -> Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<crate::lossless::Lang>>> {
81        let mut elements = Vec::new();
82        let mut current = self.syntax().prev_sibling_or_token();
83
84        while let Some(element) = current {
85            match &element {
86                rowan::NodeOrToken::Token(token) if Self::is_regular_comment(token) => {
87                    elements.push(element.clone());
88                }
89                rowan::NodeOrToken::Token(token)
90                    if token.kind() == NEWLINE || token.kind() == WHITESPACE =>
91                {
92                    elements.push(element.clone());
93                }
94                rowan::NodeOrToken::Node(n) if n.kind() == BLANK_LINE => {
95                    elements.push(element.clone());
96                }
97                rowan::NodeOrToken::Token(token) if token.kind() == COMMENT => {
98                    // Hit a shebang, stop here
99                    break;
100                }
101                _ => break,
102            }
103            current = element.prev_sibling_or_token();
104        }
105
106        elements
107    }
108
109    /// Helper to parse comment text and extract properly formatted comment tokens
110    fn parse_comment_tokens(
111        comment_text: &str,
112    ) -> (
113        rowan::SyntaxToken<crate::lossless::Lang>,
114        Option<rowan::SyntaxToken<crate::lossless::Lang>>,
115    ) {
116        let comment_line = format!("# {}\n", comment_text);
117        let temp_makefile = crate::lossless::parse(&comment_line, None);
118        let root = temp_makefile.root();
119
120        let mut comment_token = None;
121        let mut newline_token = None;
122        let mut found_comment = false;
123
124        for element in root.syntax().children_with_tokens() {
125            if let rowan::NodeOrToken::Token(token) = element {
126                if token.kind() == COMMENT {
127                    comment_token = Some(token);
128                    found_comment = true;
129                } else if token.kind() == NEWLINE && found_comment && newline_token.is_none() {
130                    newline_token = Some(token);
131                    break;
132                }
133            }
134        }
135
136        (
137            comment_token.expect("Failed to extract comment token"),
138            newline_token,
139        )
140    }
141
142    /// Replace this MakefileItem with another MakefileItem
143    ///
144    /// This preserves the position of the original item but replaces its content
145    /// with the new item. Preceding comments are preserved.
146    ///
147    /// # Example
148    /// ```
149    /// use makefile_lossless::{Makefile, MakefileItem};
150    /// let mut makefile: Makefile = "VAR1 = old\nrule:\n\tcommand\n".parse().unwrap();
151    /// let temp: Makefile = "VAR2 = new\n".parse().unwrap();
152    /// let new_var = temp.variable_definitions().next().unwrap();
153    /// let mut first_item = makefile.items().next().unwrap();
154    /// first_item.replace(MakefileItem::Variable(new_var)).unwrap();
155    /// assert!(makefile.to_string().contains("VAR2 = new"));
156    /// assert!(!makefile.to_string().contains("VAR1"));
157    /// ```
158    pub fn replace(&mut self, new_item: MakefileItem) -> Result<(), Error> {
159        let parent = self.get_parent_or_error("replace", "replace")?;
160        let current_index = self.syntax().index();
161
162        // Replace the current node with the new item's syntax
163        parent.splice_children(
164            current_index..current_index + 1,
165            vec![new_item.syntax().clone().into()],
166        );
167
168        // Update self to point to the new item
169        *self = new_item;
170
171        Ok(())
172    }
173
174    /// Add a comment before this MakefileItem
175    ///
176    /// The comment text should not include the leading '#' character.
177    /// Multiple comment lines can be added by calling this method multiple times.
178    ///
179    /// # Example
180    /// ```
181    /// use makefile_lossless::Makefile;
182    /// let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
183    /// let mut item = makefile.items().next().unwrap();
184    /// item.add_comment("This is a variable").unwrap();
185    /// assert!(makefile.to_string().contains("# This is a variable"));
186    /// ```
187    pub fn add_comment(&mut self, comment_text: &str) -> Result<(), Error> {
188        let parent = self.get_parent_or_error("add comment to", "add_comment")?;
189        let current_index = self.syntax().index();
190
191        // Get properly formatted comment tokens
192        let (comment_token, newline_token) = Self::parse_comment_tokens(comment_text);
193
194        let mut elements = vec![rowan::NodeOrToken::Token(comment_token)];
195        if let Some(newline) = newline_token {
196            elements.push(rowan::NodeOrToken::Token(newline));
197        }
198
199        // Insert comment and newline before the current item
200        parent.splice_children(current_index..current_index, elements);
201
202        Ok(())
203    }
204
205    /// Get all preceding comments for this MakefileItem
206    ///
207    /// Returns an iterator of comment strings (without the leading '#' and whitespace).
208    ///
209    /// # Example
210    /// ```
211    /// use makefile_lossless::Makefile;
212    /// let makefile: Makefile = "# Comment 1\n# Comment 2\nVAR = value\n".parse().unwrap();
213    /// let item = makefile.items().next().unwrap();
214    /// let comments: Vec<_> = item.preceding_comments().collect();
215    /// assert_eq!(comments.len(), 2);
216    /// assert_eq!(comments[0], "Comment 1");
217    /// assert_eq!(comments[1], "Comment 2");
218    /// ```
219    pub fn preceding_comments(&self) -> impl Iterator<Item = String> {
220        let elements = self.collect_preceding_comment_elements();
221        let mut comments = Vec::new();
222
223        // Process elements in reverse order (furthest to closest)
224        for element in elements.iter().rev() {
225            if let rowan::NodeOrToken::Token(token) = element {
226                if token.kind() == COMMENT {
227                    comments.push(Self::extract_comment_text(token));
228                }
229            }
230        }
231
232        comments.into_iter()
233    }
234
235    /// Remove all preceding comments for this MakefileItem
236    ///
237    /// Returns the number of comments removed.
238    ///
239    /// # Example
240    /// ```
241    /// use makefile_lossless::Makefile;
242    /// let mut makefile: Makefile = "# Comment 1\n# Comment 2\nVAR = value\n".parse().unwrap();
243    /// let mut item = makefile.items().next().unwrap();
244    /// let count = item.remove_comments().unwrap();
245    /// assert_eq!(count, 2);
246    /// assert!(!makefile.to_string().contains("# Comment"));
247    /// ```
248    pub fn remove_comments(&mut self) -> Result<usize, Error> {
249        let parent = self.get_parent_or_error("remove comments from", "remove_comments")?;
250        let collected_elements = self.collect_preceding_comment_elements();
251
252        // Count the comments
253        let mut comment_count = 0;
254        for element in collected_elements.iter() {
255            if let rowan::NodeOrToken::Token(token) = element {
256                if token.kind() == COMMENT {
257                    comment_count += 1;
258                }
259            }
260        }
261
262        // Determine which elements to remove - similar to remove_with_preceding_comments
263        // We remove comments and up to 1 blank line worth of newlines
264        let mut elements_to_remove = Vec::new();
265        let mut consecutive_newlines = 0;
266        for element in collected_elements.iter().rev() {
267            let should_remove = match element {
268                rowan::NodeOrToken::Token(token) if token.kind() == COMMENT => {
269                    consecutive_newlines = 0;
270                    true // Remove comments
271                }
272                rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
273                    consecutive_newlines += 1;
274                    comment_count > 0 && consecutive_newlines <= 1
275                }
276                rowan::NodeOrToken::Token(token) if token.kind() == WHITESPACE => comment_count > 0,
277                rowan::NodeOrToken::Node(n) if n.kind() == BLANK_LINE => {
278                    consecutive_newlines += 1;
279                    comment_count > 0 && consecutive_newlines <= 1
280                }
281                _ => false,
282            };
283
284            if should_remove {
285                elements_to_remove.push(element.clone());
286            }
287        }
288
289        // Remove elements in reverse order (from highest index to lowest)
290        elements_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
291        for element in elements_to_remove {
292            let idx = element.index();
293            parent.splice_children(idx..idx + 1, vec![]);
294        }
295
296        Ok(comment_count)
297    }
298
299    /// Modify the first preceding comment for this MakefileItem
300    ///
301    /// Returns `true` if a comment was found and modified, `false` if no comment exists.
302    /// The comment text should not include the leading '#' character.
303    ///
304    /// # Example
305    /// ```
306    /// use makefile_lossless::Makefile;
307    /// let mut makefile: Makefile = "# Old comment\nVAR = value\n".parse().unwrap();
308    /// let mut item = makefile.items().next().unwrap();
309    /// let modified = item.modify_comment("New comment").unwrap();
310    /// assert!(modified);
311    /// assert!(makefile.to_string().contains("# New comment"));
312    /// assert!(!makefile.to_string().contains("# Old comment"));
313    /// ```
314    pub fn modify_comment(&mut self, new_comment_text: &str) -> Result<bool, Error> {
315        let parent = self.get_parent_or_error("modify comment for", "modify_comment")?;
316
317        // Find the first preceding comment (closest to the item)
318        let collected_elements = self.collect_preceding_comment_elements();
319        let comment_element = collected_elements.iter().find(|element| {
320            if let rowan::NodeOrToken::Token(token) = element {
321                token.kind() == COMMENT
322            } else {
323                false
324            }
325        });
326
327        if let Some(element) = comment_element {
328            let idx = element.index();
329            let (new_comment_token, _) = Self::parse_comment_tokens(new_comment_text);
330            parent.splice_children(
331                idx..idx + 1,
332                vec![rowan::NodeOrToken::Token(new_comment_token)],
333            );
334            Ok(true)
335        } else {
336            Ok(false)
337        }
338    }
339
340    /// Insert a new MakefileItem before this item
341    ///
342    /// This inserts the new item immediately before the current item in the makefile.
343    /// The new item is inserted at the same level as the current item.
344    ///
345    /// # Example
346    /// ```
347    /// use makefile_lossless::{Makefile, MakefileItem};
348    /// let mut makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
349    /// let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
350    /// let new_var = temp.variable_definitions().next().unwrap();
351    /// let mut second_item = makefile.items().nth(1).unwrap();
352    /// second_item.insert_before(MakefileItem::Variable(new_var)).unwrap();
353    /// let result = makefile.to_string();
354    /// assert!(result.contains("VAR1 = first\nVAR_NEW = inserted\nVAR2 = second"));
355    /// ```
356    pub fn insert_before(&mut self, new_item: MakefileItem) -> Result<(), Error> {
357        let parent = self.get_parent_or_error("insert before", "insert_before")?;
358        let current_index = self.syntax().index();
359
360        // Insert the new item before the current item
361        parent.splice_children(
362            current_index..current_index,
363            vec![new_item.syntax().clone().into()],
364        );
365
366        Ok(())
367    }
368
369    /// Insert a new MakefileItem after this item
370    ///
371    /// This inserts the new item immediately after the current item in the makefile.
372    /// The new item is inserted at the same level as the current item.
373    ///
374    /// # Example
375    /// ```
376    /// use makefile_lossless::{Makefile, MakefileItem};
377    /// let mut makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
378    /// let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
379    /// let new_var = temp.variable_definitions().next().unwrap();
380    /// let mut first_item = makefile.items().next().unwrap();
381    /// first_item.insert_after(MakefileItem::Variable(new_var)).unwrap();
382    /// let result = makefile.to_string();
383    /// assert!(result.contains("VAR1 = first\nVAR_NEW = inserted\nVAR2 = second"));
384    /// ```
385    pub fn insert_after(&mut self, new_item: MakefileItem) -> Result<(), Error> {
386        let parent = self.get_parent_or_error("insert after", "insert_after")?;
387        let current_index = self.syntax().index();
388
389        // Insert the new item after the current item
390        parent.splice_children(
391            current_index + 1..current_index + 1,
392            vec![new_item.syntax().clone().into()],
393        );
394
395        Ok(())
396    }
397}
398
399// Internal trait for extracting specific item types from MakefileItem
400trait ExtractFromItem: Sized {
401    fn extract(item: MakefileItem) -> Option<Self>;
402}
403
404impl ExtractFromItem for Rule {
405    fn extract(item: MakefileItem) -> Option<Self> {
406        match item {
407            MakefileItem::Rule(r) => Some(r),
408            _ => None,
409        }
410    }
411}
412
413impl ExtractFromItem for VariableDefinition {
414    fn extract(item: MakefileItem) -> Option<Self> {
415        match item {
416            MakefileItem::Variable(v) => Some(v),
417            _ => None,
418        }
419    }
420}
421
422// Internal stack-based iterator for recursively collecting items from conditionals
423struct RecursiveItemsIter<T> {
424    stack: VecDeque<MakefileItem>,
425    _phantom: std::marker::PhantomData<T>,
426}
427
428impl<T> RecursiveItemsIter<T> {
429    fn new(items: impl Iterator<Item = MakefileItem>) -> Self {
430        Self {
431            stack: items.collect(),
432            _phantom: std::marker::PhantomData,
433        }
434    }
435}
436
437impl<T: ExtractFromItem> Iterator for RecursiveItemsIter<T> {
438    type Item = T;
439
440    fn next(&mut self) -> Option<Self::Item> {
441        while let Some(item) = self.stack.pop_front() {
442            if let MakefileItem::Conditional(ref cond) = item {
443                // Push in natural order since we pop from front
444                self.stack.extend(cond.if_items());
445                self.stack.extend(cond.else_items());
446            }
447            if let Some(extracted) = T::extract(item) {
448                return Some(extracted);
449            }
450        }
451        None
452    }
453}
454
455impl Makefile {
456    /// Create a new empty makefile
457    pub fn new() -> Makefile {
458        let mut builder = GreenNodeBuilder::new();
459
460        builder.start_node(ROOT.into());
461        builder.finish_node();
462
463        let syntax = SyntaxNode::new_root_mut(builder.finish());
464        Makefile::cast(syntax).unwrap()
465    }
466
467    /// Parse makefile text, returning a Parse result
468    pub fn parse(text: &str) -> crate::Parse<Makefile> {
469        crate::Parse::<Makefile>::parse_makefile(text)
470    }
471
472    /// Get the text content of the makefile
473    pub fn code(&self) -> String {
474        self.syntax().text().to_string()
475    }
476
477    /// Check if this node is the root of a makefile
478    pub fn is_root(&self) -> bool {
479        self.syntax().kind() == ROOT
480    }
481
482    /// Read a makefile from a reader
483    pub fn read<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
484        let mut buf = String::new();
485        r.read_to_string(&mut buf)?;
486        buf.parse()
487    }
488
489    /// Read makefile from a reader, but allow syntax errors
490    pub fn read_relaxed<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
491        let mut buf = String::new();
492        r.read_to_string(&mut buf)?;
493
494        let parsed = parse(&buf, None);
495        Ok(parsed.root())
496    }
497
498    /// Retrieve the rules in the makefile
499    ///
500    /// # Example
501    /// ```
502    /// use makefile_lossless::Makefile;
503    /// let makefile: Makefile = "rule: dependency\n\tcommand\n".parse().unwrap();
504    /// assert_eq!(makefile.rules().count(), 1);
505    /// ```
506    pub fn rules(&self) -> impl Iterator<Item = Rule> + '_ {
507        RecursiveItemsIter::new(self.items())
508    }
509
510    /// Get all rules that have a specific target
511    pub fn rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator<Item = Rule> + 'a {
512        self.rules()
513            .filter(move |rule| rule.targets().any(|t| t == target))
514    }
515
516    /// Get all variable definitions in the makefile
517    pub fn variable_definitions(&self) -> impl Iterator<Item = VariableDefinition> + '_ {
518        RecursiveItemsIter::new(self.items())
519    }
520
521    /// Get all conditionals in the makefile (top-level only)
522    pub fn conditionals(&self) -> impl Iterator<Item = Conditional> + '_ {
523        self.items().filter_map(|item| match item {
524            MakefileItem::Conditional(c) => Some(c),
525            _ => None,
526        })
527    }
528
529    /// Get all top-level items (rules, variables, includes, conditionals) in the makefile
530    ///
531    /// # Example
532    /// ```
533    /// use makefile_lossless::{Makefile, MakefileItem};
534    /// let makefile: Makefile = r#"VAR = value
535    /// ifdef DEBUG
536    /// CFLAGS = -g
537    /// endif
538    /// rule:
539    /// 	command
540    /// "#.parse().unwrap();
541    /// let items: Vec<_> = makefile.items().collect();
542    /// assert_eq!(items.len(), 3); // VAR, conditional, rule
543    /// ```
544    pub fn items(&self) -> impl Iterator<Item = MakefileItem> + '_ {
545        self.syntax().children().filter_map(MakefileItem::cast)
546    }
547
548    /// Find all variables by name
549    ///
550    /// Returns an iterator over all variable definitions with the given name.
551    /// Makefiles can have multiple definitions of the same variable.
552    ///
553    /// # Example
554    /// ```
555    /// use makefile_lossless::Makefile;
556    /// let makefile: Makefile = "VAR1 = value1\nVAR2 = value2\nVAR1 = value3\n".parse().unwrap();
557    /// let vars: Vec<_> = makefile.find_variable("VAR1").collect();
558    /// assert_eq!(vars.len(), 2);
559    /// assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
560    /// assert_eq!(vars[1].raw_value(), Some("value3".to_string()));
561    /// ```
562    pub fn find_variable<'a>(
563        &'a self,
564        name: &'a str,
565    ) -> impl Iterator<Item = VariableDefinition> + 'a {
566        self.variable_definitions()
567            .filter(move |var| var.name().as_deref() == Some(name))
568    }
569
570    /// Add a new rule to the makefile
571    ///
572    /// # Example
573    /// ```
574    /// use makefile_lossless::Makefile;
575    /// let mut makefile = Makefile::new();
576    /// makefile.add_rule("rule");
577    /// assert_eq!(makefile.to_string(), "rule:\n");
578    /// ```
579    pub fn add_rule(&mut self, target: &str) -> Rule {
580        let mut builder = GreenNodeBuilder::new();
581        builder.start_node(RULE.into());
582        builder.token(IDENTIFIER.into(), target);
583        builder.token(OPERATOR.into(), ":");
584        builder.token(NEWLINE.into(), "\n");
585        builder.finish_node();
586
587        let syntax = SyntaxNode::new_root_mut(builder.finish());
588        let pos = self.syntax().children_with_tokens().count();
589
590        // Add a blank line before the new rule if there are existing rules
591        // This maintains standard makefile formatting
592        let needs_blank_line = self.syntax().children().any(|c| c.kind() == RULE);
593
594        if needs_blank_line {
595            // Create a BLANK_LINE node
596            let mut bl_builder = GreenNodeBuilder::new();
597            bl_builder.start_node(BLANK_LINE.into());
598            bl_builder.token(NEWLINE.into(), "\n");
599            bl_builder.finish_node();
600            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
601
602            self.syntax()
603                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
604        } else {
605            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
606        }
607
608        // Use children().count() - 1 to get the last added child node
609        // (not children_with_tokens().count() which includes tokens)
610        Rule::cast(self.syntax().children().last().unwrap()).unwrap()
611    }
612
613    /// Add a new conditional to the makefile
614    ///
615    /// # Arguments
616    /// * `conditional_type` - The type of conditional: "ifdef", "ifndef", "ifeq", or "ifneq"
617    /// * `condition` - The condition expression (e.g., "DEBUG" for ifdef/ifndef, or "(a,b)" for ifeq/ifneq)
618    /// * `if_body` - The content of the if branch
619    /// * `else_body` - Optional content for the else branch
620    ///
621    /// # Example
622    /// ```
623    /// use makefile_lossless::Makefile;
624    /// let mut makefile = Makefile::new();
625    /// makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
626    /// assert!(makefile.to_string().contains("ifdef DEBUG"));
627    /// ```
628    pub fn add_conditional(
629        &mut self,
630        conditional_type: &str,
631        condition: &str,
632        if_body: &str,
633        else_body: Option<&str>,
634    ) -> Result<Conditional, Error> {
635        // Validate conditional type
636        if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&conditional_type) {
637            return Err(Error::Parse(ParseError {
638                errors: vec![ErrorInfo {
639                    message: format!(
640                        "Invalid conditional type: {}. Must be one of: ifdef, ifndef, ifeq, ifneq",
641                        conditional_type
642                    ),
643                    line: 1,
644                    context: "add_conditional".to_string(),
645                }],
646            }));
647        }
648
649        let mut builder = GreenNodeBuilder::new();
650        builder.start_node(CONDITIONAL.into());
651
652        // Build CONDITIONAL_IF
653        builder.start_node(CONDITIONAL_IF.into());
654        builder.token(IDENTIFIER.into(), conditional_type);
655        builder.token(WHITESPACE.into(), " ");
656
657        // Wrap condition in EXPR node
658        builder.start_node(EXPR.into());
659        builder.token(IDENTIFIER.into(), condition);
660        builder.finish_node();
661
662        builder.token(NEWLINE.into(), "\n");
663        builder.finish_node();
664
665        // Add if body content
666        if !if_body.is_empty() {
667            for line in if_body.lines() {
668                if !line.is_empty() {
669                    builder.token(IDENTIFIER.into(), line);
670                }
671                builder.token(NEWLINE.into(), "\n");
672            }
673            // Add final newline if if_body doesn't end with one
674            if !if_body.ends_with('\n') && !if_body.is_empty() {
675                builder.token(NEWLINE.into(), "\n");
676            }
677        }
678
679        // Add else clause if provided
680        if let Some(else_content) = else_body {
681            builder.start_node(CONDITIONAL_ELSE.into());
682            builder.token(IDENTIFIER.into(), "else");
683            builder.token(NEWLINE.into(), "\n");
684            builder.finish_node();
685
686            // Add else body content
687            if !else_content.is_empty() {
688                for line in else_content.lines() {
689                    if !line.is_empty() {
690                        builder.token(IDENTIFIER.into(), line);
691                    }
692                    builder.token(NEWLINE.into(), "\n");
693                }
694                // Add final newline if else_content doesn't end with one
695                if !else_content.ends_with('\n') && !else_content.is_empty() {
696                    builder.token(NEWLINE.into(), "\n");
697                }
698            }
699        }
700
701        // Build CONDITIONAL_ENDIF
702        builder.start_node(CONDITIONAL_ENDIF.into());
703        builder.token(IDENTIFIER.into(), "endif");
704        builder.token(NEWLINE.into(), "\n");
705        builder.finish_node();
706
707        builder.finish_node();
708
709        let syntax = SyntaxNode::new_root_mut(builder.finish());
710        let pos = self.syntax().children_with_tokens().count();
711
712        // Add a blank line before the new conditional if there are existing elements
713        let needs_blank_line = self
714            .syntax()
715            .children()
716            .any(|c| c.kind() == RULE || c.kind() == VARIABLE || c.kind() == CONDITIONAL);
717
718        if needs_blank_line {
719            // Create a BLANK_LINE node
720            let mut bl_builder = GreenNodeBuilder::new();
721            bl_builder.start_node(BLANK_LINE.into());
722            bl_builder.token(NEWLINE.into(), "\n");
723            bl_builder.finish_node();
724            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
725
726            self.syntax()
727                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
728        } else {
729            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
730        }
731
732        // Return the newly added conditional
733        Ok(Conditional::cast(self.syntax().children().last().unwrap()).unwrap())
734    }
735
736    /// Add a new conditional to the makefile with typed items
737    ///
738    /// This is a more type-safe alternative to `add_conditional` that accepts iterators of
739    /// `MakefileItem` instead of raw strings.
740    ///
741    /// # Arguments
742    /// * `conditional_type` - The type of conditional: "ifdef", "ifndef", "ifeq", or "ifneq"
743    /// * `condition` - The condition expression (e.g., "DEBUG" for ifdef/ifndef, or "(a,b)" for ifeq/ifneq)
744    /// * `if_items` - Items for the if branch
745    /// * `else_items` - Optional items for the else branch
746    ///
747    /// # Example
748    /// ```
749    /// use makefile_lossless::{Makefile, MakefileItem};
750    /// let mut makefile = Makefile::new();
751    /// let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
752    /// let var1 = temp1.variable_definitions().next().unwrap();
753    /// let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
754    /// let var2 = temp2.variable_definitions().next().unwrap();
755    /// makefile.add_conditional_with_items(
756    ///     "ifdef",
757    ///     "DEBUG",
758    ///     vec![MakefileItem::Variable(var1)],
759    ///     Some(vec![MakefileItem::Variable(var2)])
760    /// ).unwrap();
761    /// assert!(makefile.to_string().contains("ifdef DEBUG"));
762    /// assert!(makefile.to_string().contains("CFLAGS = -g"));
763    /// assert!(makefile.to_string().contains("CFLAGS = -O2"));
764    /// ```
765    pub fn add_conditional_with_items<I1, I2>(
766        &mut self,
767        conditional_type: &str,
768        condition: &str,
769        if_items: I1,
770        else_items: Option<I2>,
771    ) -> Result<Conditional, Error>
772    where
773        I1: IntoIterator<Item = MakefileItem>,
774        I2: IntoIterator<Item = MakefileItem>,
775    {
776        // Validate conditional type
777        if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&conditional_type) {
778            return Err(Error::Parse(ParseError {
779                errors: vec![ErrorInfo {
780                    message: format!(
781                        "Invalid conditional type: {}. Must be one of: ifdef, ifndef, ifeq, ifneq",
782                        conditional_type
783                    ),
784                    line: 1,
785                    context: "add_conditional_with_items".to_string(),
786                }],
787            }));
788        }
789
790        let mut builder = GreenNodeBuilder::new();
791        builder.start_node(CONDITIONAL.into());
792
793        // Build CONDITIONAL_IF
794        builder.start_node(CONDITIONAL_IF.into());
795        builder.token(IDENTIFIER.into(), conditional_type);
796        builder.token(WHITESPACE.into(), " ");
797
798        // Wrap condition in EXPR node
799        builder.start_node(EXPR.into());
800        builder.token(IDENTIFIER.into(), condition);
801        builder.finish_node();
802
803        builder.token(NEWLINE.into(), "\n");
804        builder.finish_node();
805
806        // Add if branch items
807        for item in if_items {
808            // Clone the item's syntax tree into our builder
809            let item_text = item.syntax().to_string();
810            // Parse it again to get green nodes
811            builder.token(IDENTIFIER.into(), item_text.trim());
812            builder.token(NEWLINE.into(), "\n");
813        }
814
815        // Add else clause if provided
816        if let Some(else_iter) = else_items {
817            builder.start_node(CONDITIONAL_ELSE.into());
818            builder.token(IDENTIFIER.into(), "else");
819            builder.token(NEWLINE.into(), "\n");
820            builder.finish_node();
821
822            // Add else branch items
823            for item in else_iter {
824                let item_text = item.syntax().to_string();
825                builder.token(IDENTIFIER.into(), item_text.trim());
826                builder.token(NEWLINE.into(), "\n");
827            }
828        }
829
830        // Build CONDITIONAL_ENDIF
831        builder.start_node(CONDITIONAL_ENDIF.into());
832        builder.token(IDENTIFIER.into(), "endif");
833        builder.token(NEWLINE.into(), "\n");
834        builder.finish_node();
835
836        builder.finish_node();
837
838        let syntax = SyntaxNode::new_root_mut(builder.finish());
839        let pos = self.syntax().children_with_tokens().count();
840
841        // Add a blank line before the new conditional if there are existing elements
842        let needs_blank_line = self
843            .syntax()
844            .children()
845            .any(|c| c.kind() == RULE || c.kind() == VARIABLE || c.kind() == CONDITIONAL);
846
847        if needs_blank_line {
848            // Create a BLANK_LINE node
849            let mut bl_builder = GreenNodeBuilder::new();
850            bl_builder.start_node(BLANK_LINE.into());
851            bl_builder.token(NEWLINE.into(), "\n");
852            bl_builder.finish_node();
853            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
854
855            self.syntax()
856                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
857        } else {
858            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
859        }
860
861        // Return the newly added conditional
862        Ok(Conditional::cast(self.syntax().children().last().unwrap()).unwrap())
863    }
864
865    /// Read the makefile
866    pub fn from_reader<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
867        let mut buf = String::new();
868        r.read_to_string(&mut buf)?;
869
870        let parsed = parse(&buf, None);
871        if !parsed.errors.is_empty() {
872            Err(Error::Parse(ParseError {
873                errors: parsed.errors,
874            }))
875        } else {
876            Ok(parsed.root())
877        }
878    }
879
880    /// Replace rule at given index with a new rule
881    ///
882    /// # Example
883    /// ```
884    /// use makefile_lossless::Makefile;
885    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
886    /// let new_rule: makefile_lossless::Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
887    /// makefile.replace_rule(0, new_rule).unwrap();
888    /// assert!(makefile.rules().any(|r| r.targets().any(|t| t == "new_rule")));
889    /// ```
890    pub fn replace_rule(&mut self, index: usize, new_rule: Rule) -> Result<(), Error> {
891        let rules: Vec<_> = self
892            .syntax()
893            .children()
894            .filter(|n| n.kind() == RULE)
895            .collect();
896
897        if rules.is_empty() {
898            return Err(Error::Parse(ParseError {
899                errors: vec![ErrorInfo {
900                    message: "Cannot replace rule in empty makefile".to_string(),
901                    line: 1,
902                    context: "replace_rule".to_string(),
903                }],
904            }));
905        }
906
907        if index >= rules.len() {
908            return Err(Error::Parse(ParseError {
909                errors: vec![ErrorInfo {
910                    message: format!(
911                        "Rule index {} out of bounds (max {})",
912                        index,
913                        rules.len() - 1
914                    ),
915                    line: 1,
916                    context: "replace_rule".to_string(),
917                }],
918            }));
919        }
920
921        let target_node = &rules[index];
922        let target_index = target_node.index();
923
924        // Replace the rule at the target index
925        self.syntax().splice_children(
926            target_index..target_index + 1,
927            vec![new_rule.syntax().clone().into()],
928        );
929        Ok(())
930    }
931
932    /// Remove rule at given index
933    ///
934    /// # Example
935    /// ```
936    /// use makefile_lossless::Makefile;
937    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
938    /// let removed = makefile.remove_rule(0).unwrap();
939    /// assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule1"]);
940    /// assert_eq!(makefile.rules().count(), 1);
941    /// ```
942    pub fn remove_rule(&mut self, index: usize) -> Result<Rule, Error> {
943        let rules: Vec<_> = self
944            .syntax()
945            .children()
946            .filter(|n| n.kind() == RULE)
947            .collect();
948
949        if rules.is_empty() {
950            return Err(Error::Parse(ParseError {
951                errors: vec![ErrorInfo {
952                    message: "Cannot remove rule from empty makefile".to_string(),
953                    line: 1,
954                    context: "remove_rule".to_string(),
955                }],
956            }));
957        }
958
959        if index >= rules.len() {
960            return Err(Error::Parse(ParseError {
961                errors: vec![ErrorInfo {
962                    message: format!(
963                        "Rule index {} out of bounds (max {})",
964                        index,
965                        rules.len() - 1
966                    ),
967                    line: 1,
968                    context: "remove_rule".to_string(),
969                }],
970            }));
971        }
972
973        let target_node = rules[index].clone();
974        let target_index = target_node.index();
975
976        // Remove the rule at the target index
977        self.syntax()
978            .splice_children(target_index..target_index + 1, vec![]);
979        Ok(Rule::cast(target_node).unwrap())
980    }
981
982    /// Insert rule at given position
983    ///
984    /// # Example
985    /// ```
986    /// use makefile_lossless::Makefile;
987    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
988    /// let new_rule: makefile_lossless::Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
989    /// makefile.insert_rule(1, new_rule).unwrap();
990    /// let targets: Vec<_> = makefile.rules().flat_map(|r| r.targets().collect::<Vec<_>>()).collect();
991    /// assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
992    /// ```
993    pub fn insert_rule(&mut self, index: usize, new_rule: Rule) -> Result<(), Error> {
994        let rules: Vec<_> = self
995            .syntax()
996            .children()
997            .filter(|n| n.kind() == RULE)
998            .collect();
999
1000        if index > rules.len() {
1001            return Err(Error::Parse(ParseError {
1002                errors: vec![ErrorInfo {
1003                    message: format!("Rule index {} out of bounds (max {})", index, rules.len()),
1004                    line: 1,
1005                    context: "insert_rule".to_string(),
1006                }],
1007            }));
1008        }
1009
1010        let target_index = if index == rules.len() {
1011            // Insert at the end
1012            self.syntax().children_with_tokens().count()
1013        } else {
1014            // Insert before the rule at the given index
1015            rules[index].index()
1016        };
1017
1018        // Build the nodes to insert
1019        let mut nodes_to_insert = Vec::new();
1020
1021        // Determine if we need to add blank lines to maintain formatting consistency
1022        if index == 0 && !rules.is_empty() {
1023            // Inserting before the first rule - check if first rule has a blank line before it
1024            // If so, we should add one after our new rule instead
1025            // For now, just add the rule without a blank line before it
1026            nodes_to_insert.push(new_rule.syntax().clone().into());
1027
1028            // Add a blank line after the new rule
1029            let mut bl_builder = GreenNodeBuilder::new();
1030            bl_builder.start_node(BLANK_LINE.into());
1031            bl_builder.token(NEWLINE.into(), "\n");
1032            bl_builder.finish_node();
1033            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1034            nodes_to_insert.push(blank_line.into());
1035        } else if index < rules.len() {
1036            // Inserting in the middle (before an existing rule)
1037            // The syntax tree structure is: ... [maybe BLANK_LINE] RULE(target) ...
1038            // We're inserting right before RULE(target)
1039
1040            // If there's a BLANK_LINE immediately before the target rule,
1041            // it will stay there and separate the previous rule from our new rule.
1042            // We don't need to add a BLANK_LINE before our new rule in that case.
1043
1044            // But we DO need to add a BLANK_LINE after our new rule to separate it
1045            // from the target rule (which we're inserting before).
1046
1047            // Check if there's a blank line immediately before target_index
1048            let has_blank_before = if target_index > 0 {
1049                self.syntax()
1050                    .children_with_tokens()
1051                    .nth(target_index - 1)
1052                    .and_then(|n| n.as_node().map(|node| node.kind() == BLANK_LINE))
1053                    .unwrap_or(false)
1054            } else {
1055                false
1056            };
1057
1058            // Only add a blank before if there isn't one already and we're not at the start
1059            if !has_blank_before && index > 0 {
1060                let mut bl_builder = GreenNodeBuilder::new();
1061                bl_builder.start_node(BLANK_LINE.into());
1062                bl_builder.token(NEWLINE.into(), "\n");
1063                bl_builder.finish_node();
1064                let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1065                nodes_to_insert.push(blank_line.into());
1066            }
1067
1068            // Add the new rule
1069            nodes_to_insert.push(new_rule.syntax().clone().into());
1070
1071            // Always add a blank line after the new rule to separate it from the next rule
1072            let mut bl_builder = GreenNodeBuilder::new();
1073            bl_builder.start_node(BLANK_LINE.into());
1074            bl_builder.token(NEWLINE.into(), "\n");
1075            bl_builder.finish_node();
1076            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1077            nodes_to_insert.push(blank_line.into());
1078        } else {
1079            // Inserting at the end when there are existing rules
1080            // Add a blank line before the new rule
1081            let mut bl_builder = GreenNodeBuilder::new();
1082            bl_builder.start_node(BLANK_LINE.into());
1083            bl_builder.token(NEWLINE.into(), "\n");
1084            bl_builder.finish_node();
1085            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1086            nodes_to_insert.push(blank_line.into());
1087
1088            // Add the new rule
1089            nodes_to_insert.push(new_rule.syntax().clone().into());
1090        }
1091
1092        // Insert all nodes at the target index
1093        self.syntax()
1094            .splice_children(target_index..target_index, nodes_to_insert);
1095        Ok(())
1096    }
1097
1098    /// Get all include directives in the makefile
1099    ///
1100    /// # Example
1101    /// ```
1102    /// use makefile_lossless::Makefile;
1103    /// let makefile: Makefile = "include config.mk\n-include .env\n".parse().unwrap();
1104    /// let includes = makefile.includes().collect::<Vec<_>>();
1105    /// assert_eq!(includes.len(), 2);
1106    /// ```
1107    pub fn includes(&self) -> impl Iterator<Item = Include> {
1108        self.syntax().children().filter_map(Include::cast)
1109    }
1110
1111    /// Get all included file paths
1112    ///
1113    /// # Example
1114    /// ```
1115    /// use makefile_lossless::Makefile;
1116    /// let makefile: Makefile = "include config.mk\n-include .env\n".parse().unwrap();
1117    /// let paths = makefile.included_files().collect::<Vec<_>>();
1118    /// assert_eq!(paths, vec!["config.mk", ".env"]);
1119    /// ```
1120    pub fn included_files(&self) -> impl Iterator<Item = String> + '_ {
1121        // We need to collect all Include nodes from anywhere in the syntax tree,
1122        // not just direct children of the root, to handle includes in conditionals
1123        fn collect_includes(node: &SyntaxNode) -> Vec<Include> {
1124            let mut includes = Vec::new();
1125
1126            // First check if this node itself is an Include
1127            if let Some(include) = Include::cast(node.clone()) {
1128                includes.push(include);
1129            }
1130
1131            // Then recurse into all children
1132            for child in node.children() {
1133                includes.extend(collect_includes(&child));
1134            }
1135
1136            includes
1137        }
1138
1139        // Start collection from the root node
1140        let includes = collect_includes(self.syntax());
1141
1142        // Convert to an iterator of paths
1143        includes.into_iter().map(|include| {
1144            include
1145                .syntax()
1146                .children()
1147                .find(|node| node.kind() == EXPR)
1148                .map(|expr| expr.text().to_string().trim().to_string())
1149                .unwrap_or_default()
1150        })
1151    }
1152
1153    /// Find the first rule with a specific target name
1154    ///
1155    /// # Example
1156    /// ```
1157    /// use makefile_lossless::Makefile;
1158    /// let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1159    /// let rule = makefile.find_rule_by_target("rule2");
1160    /// assert!(rule.is_some());
1161    /// assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
1162    /// ```
1163    pub fn find_rule_by_target(&self, target: &str) -> Option<Rule> {
1164        self.rules()
1165            .find(|rule| rule.targets().any(|t| t == target))
1166    }
1167
1168    /// Find all rules with a specific target name
1169    ///
1170    /// # Example
1171    /// ```
1172    /// use makefile_lossless::Makefile;
1173    /// let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n".parse().unwrap();
1174    /// let rules: Vec<_> = makefile.find_rules_by_target("rule1").collect();
1175    /// assert_eq!(rules.len(), 2);
1176    /// ```
1177    pub fn find_rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator<Item = Rule> + 'a {
1178        self.rules_by_target(target)
1179    }
1180
1181    /// Find the first rule whose target matches the given pattern
1182    ///
1183    /// Supports make-style pattern matching where `%` in a rule's target acts as a wildcard.
1184    /// For example, a rule with target `%.o` will match `foo.o`, `bar.o`, etc.
1185    ///
1186    /// # Example
1187    /// ```
1188    /// use makefile_lossless::Makefile;
1189    /// let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
1190    /// let rule = makefile.find_rule_by_target_pattern("foo.o");
1191    /// assert!(rule.is_some());
1192    /// ```
1193    pub fn find_rule_by_target_pattern(&self, target: &str) -> Option<Rule> {
1194        self.rules()
1195            .find(|rule| rule.targets().any(|t| matches_pattern(&t, target)))
1196    }
1197
1198    /// Find all rules whose targets match the given pattern
1199    ///
1200    /// Supports make-style pattern matching where `%` in a rule's target acts as a wildcard.
1201    /// For example, a rule with target `%.o` will match `foo.o`, `bar.o`, etc.
1202    ///
1203    /// # Example
1204    /// ```
1205    /// use makefile_lossless::Makefile;
1206    /// let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n".parse().unwrap();
1207    /// let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
1208    /// assert_eq!(rules.len(), 2);
1209    /// ```
1210    pub fn find_rules_by_target_pattern<'a>(
1211        &'a self,
1212        target: &'a str,
1213    ) -> impl Iterator<Item = Rule> + 'a {
1214        self.rules()
1215            .filter(move |rule| rule.targets().any(|t| matches_pattern(&t, target)))
1216    }
1217
1218    /// Add a target to .PHONY (creates .PHONY rule if it doesn't exist)
1219    ///
1220    /// # Example
1221    /// ```
1222    /// use makefile_lossless::Makefile;
1223    /// let mut makefile = Makefile::new();
1224    /// makefile.add_phony_target("clean").unwrap();
1225    /// assert!(makefile.is_phony("clean"));
1226    /// ```
1227    pub fn add_phony_target(&mut self, target: &str) -> Result<(), Error> {
1228        // Find existing .PHONY rule
1229        if let Some(mut phony_rule) = self.find_rule_by_target(".PHONY") {
1230            // Check if target is already in prerequisites
1231            if !phony_rule.prerequisites().any(|p| p == target) {
1232                phony_rule.add_prerequisite(target)?;
1233            }
1234        } else {
1235            // Create new .PHONY rule
1236            let mut phony_rule = self.add_rule(".PHONY");
1237            phony_rule.add_prerequisite(target)?;
1238        }
1239        Ok(())
1240    }
1241
1242    /// Remove a target from .PHONY (removes .PHONY rule if it becomes empty)
1243    ///
1244    /// Returns `true` if the target was found and removed, `false` if it wasn't in .PHONY.
1245    /// If there are multiple .PHONY rules, it removes the target from the first rule that contains it.
1246    ///
1247    /// # Example
1248    /// ```
1249    /// use makefile_lossless::Makefile;
1250    /// let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
1251    /// assert!(makefile.remove_phony_target("clean").unwrap());
1252    /// assert!(!makefile.is_phony("clean"));
1253    /// assert!(makefile.is_phony("test"));
1254    /// ```
1255    pub fn remove_phony_target(&mut self, target: &str) -> Result<bool, Error> {
1256        // Find the first .PHONY rule that contains the target
1257        let mut phony_rule = None;
1258        for rule in self.rules_by_target(".PHONY") {
1259            if rule.prerequisites().any(|p| p == target) {
1260                phony_rule = Some(rule);
1261                break;
1262            }
1263        }
1264
1265        let mut phony_rule = match phony_rule {
1266            Some(rule) => rule,
1267            None => return Ok(false),
1268        };
1269
1270        // Count prerequisites before removal
1271        let prereq_count = phony_rule.prerequisites().count();
1272
1273        // Remove the prerequisite
1274        phony_rule.remove_prerequisite(target)?;
1275
1276        // Check if .PHONY has no more prerequisites, if so remove the rule
1277        if prereq_count == 1 {
1278            // We just removed the last prerequisite, so remove the entire rule
1279            phony_rule.remove()?;
1280        }
1281
1282        Ok(true)
1283    }
1284
1285    /// Check if a target is marked as phony
1286    ///
1287    /// # Example
1288    /// ```
1289    /// use makefile_lossless::Makefile;
1290    /// let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
1291    /// assert!(makefile.is_phony("clean"));
1292    /// assert!(makefile.is_phony("test"));
1293    /// assert!(!makefile.is_phony("build"));
1294    /// ```
1295    pub fn is_phony(&self, target: &str) -> bool {
1296        // Check all .PHONY rules since there can be multiple
1297        self.rules_by_target(".PHONY")
1298            .any(|rule| rule.prerequisites().any(|p| p == target))
1299    }
1300
1301    /// Get all phony targets
1302    ///
1303    /// # Example
1304    /// ```
1305    /// use makefile_lossless::Makefile;
1306    /// let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
1307    /// let phony_targets: Vec<_> = makefile.phony_targets().collect();
1308    /// assert_eq!(phony_targets, vec!["clean", "test", "build"]);
1309    /// ```
1310    pub fn phony_targets(&self) -> impl Iterator<Item = String> + '_ {
1311        // Collect from all .PHONY rules since there can be multiple
1312        self.rules_by_target(".PHONY")
1313            .flat_map(|rule| rule.prerequisites().collect::<Vec<_>>())
1314    }
1315
1316    /// Add a new include directive at the beginning of the makefile
1317    ///
1318    /// # Arguments
1319    /// * `path` - The file path to include (e.g., "config.mk")
1320    ///
1321    /// # Example
1322    /// ```
1323    /// use makefile_lossless::Makefile;
1324    /// let mut makefile = Makefile::new();
1325    /// makefile.add_include("config.mk");
1326    /// assert_eq!(makefile.included_files().collect::<Vec<_>>(), vec!["config.mk"]);
1327    /// ```
1328    pub fn add_include(&mut self, path: &str) -> Include {
1329        let mut builder = GreenNodeBuilder::new();
1330        builder.start_node(INCLUDE.into());
1331        builder.token(IDENTIFIER.into(), "include");
1332        builder.token(WHITESPACE.into(), " ");
1333
1334        // Wrap path in EXPR node
1335        builder.start_node(EXPR.into());
1336        builder.token(IDENTIFIER.into(), path);
1337        builder.finish_node();
1338
1339        builder.token(NEWLINE.into(), "\n");
1340        builder.finish_node();
1341
1342        let syntax = SyntaxNode::new_root_mut(builder.finish());
1343
1344        // Insert at the beginning (position 0)
1345        self.syntax().splice_children(0..0, vec![syntax.into()]);
1346
1347        // Return the newly added include (first child)
1348        Include::cast(self.syntax().children().next().unwrap()).unwrap()
1349    }
1350
1351    /// Insert an include directive at a specific position
1352    ///
1353    /// The position is relative to other top-level items (rules, variables, includes, conditionals).
1354    ///
1355    /// # Arguments
1356    /// * `index` - The position to insert at (0 = beginning, items().count() = end)
1357    /// * `path` - The file path to include (e.g., "config.mk")
1358    ///
1359    /// # Example
1360    /// ```
1361    /// use makefile_lossless::Makefile;
1362    /// let mut makefile: Makefile = "VAR = value\nrule:\n\tcommand\n".parse().unwrap();
1363    /// makefile.insert_include(1, "config.mk").unwrap();
1364    /// let items: Vec<_> = makefile.items().collect();
1365    /// assert_eq!(items.len(), 3); // VAR, include, rule
1366    /// ```
1367    pub fn insert_include(&mut self, index: usize, path: &str) -> Result<Include, Error> {
1368        let items: Vec<_> = self.syntax().children().collect();
1369
1370        if index > items.len() {
1371            return Err(Error::Parse(ParseError {
1372                errors: vec![ErrorInfo {
1373                    message: format!("Index {} out of bounds (max {})", index, items.len()),
1374                    line: 1,
1375                    context: "insert_include".to_string(),
1376                }],
1377            }));
1378        }
1379
1380        let mut builder = GreenNodeBuilder::new();
1381        builder.start_node(INCLUDE.into());
1382        builder.token(IDENTIFIER.into(), "include");
1383        builder.token(WHITESPACE.into(), " ");
1384
1385        // Wrap path in EXPR node
1386        builder.start_node(EXPR.into());
1387        builder.token(IDENTIFIER.into(), path);
1388        builder.finish_node();
1389
1390        builder.token(NEWLINE.into(), "\n");
1391        builder.finish_node();
1392
1393        let syntax = SyntaxNode::new_root_mut(builder.finish());
1394
1395        let target_index = if index == items.len() {
1396            // Insert at the end
1397            self.syntax().children_with_tokens().count()
1398        } else {
1399            // Insert before the item at the given index
1400            items[index].index()
1401        };
1402
1403        // Insert the include node
1404        self.syntax()
1405            .splice_children(target_index..target_index, vec![syntax.into()]);
1406
1407        // Find and return the newly added include
1408        // It should be at the child index we inserted at
1409        Ok(Include::cast(self.syntax().children().nth(index).unwrap()).unwrap())
1410    }
1411
1412    /// Insert an include directive after a specific MakefileItem
1413    ///
1414    /// This is useful when you want to insert an include relative to another item in the makefile.
1415    ///
1416    /// # Arguments
1417    /// * `after` - The MakefileItem to insert after
1418    /// * `path` - The file path to include (e.g., "config.mk")
1419    ///
1420    /// # Example
1421    /// ```
1422    /// use makefile_lossless::Makefile;
1423    /// let mut makefile: Makefile = "VAR1 = value1\nVAR2 = value2\n".parse().unwrap();
1424    /// let first_var = makefile.items().next().unwrap();
1425    /// makefile.insert_include_after(&first_var, "config.mk").unwrap();
1426    /// let paths: Vec<_> = makefile.included_files().collect();
1427    /// assert_eq!(paths, vec!["config.mk"]);
1428    /// ```
1429    pub fn insert_include_after(
1430        &mut self,
1431        after: &MakefileItem,
1432        path: &str,
1433    ) -> Result<Include, Error> {
1434        let mut builder = GreenNodeBuilder::new();
1435        builder.start_node(INCLUDE.into());
1436        builder.token(IDENTIFIER.into(), "include");
1437        builder.token(WHITESPACE.into(), " ");
1438
1439        // Wrap path in EXPR node
1440        builder.start_node(EXPR.into());
1441        builder.token(IDENTIFIER.into(), path);
1442        builder.finish_node();
1443
1444        builder.token(NEWLINE.into(), "\n");
1445        builder.finish_node();
1446
1447        let syntax = SyntaxNode::new_root_mut(builder.finish());
1448
1449        // Find the position of the item to insert after
1450        let after_syntax = after.syntax();
1451        let target_index = after_syntax.index() + 1;
1452
1453        // Insert the include node after the target item
1454        self.syntax()
1455            .splice_children(target_index..target_index, vec![syntax.into()]);
1456
1457        // Find and return the newly added include
1458        // It should be the child immediately after the 'after' item
1459        let after_child_index = self
1460            .syntax()
1461            .children()
1462            .position(|child| child.text_range() == after_syntax.text_range())
1463            .ok_or_else(|| {
1464                Error::Parse(ParseError {
1465                    errors: vec![ErrorInfo {
1466                        message: "Could not find the reference item".to_string(),
1467                        line: 1,
1468                        context: "insert_include_after".to_string(),
1469                    }],
1470                })
1471            })?;
1472
1473        Ok(Include::cast(self.syntax().children().nth(after_child_index + 1).unwrap()).unwrap())
1474    }
1475}
1476
1477#[cfg(test)]
1478mod tests {
1479    use super::*;
1480
1481    #[test]
1482    fn test_makefile_item_replace_variable_with_variable() {
1483        let makefile: Makefile = "VAR1 = old\nrule:\n\tcommand\n".parse().unwrap();
1484        let temp: Makefile = "VAR2 = new\n".parse().unwrap();
1485        let new_var = temp.variable_definitions().next().unwrap();
1486        let mut first_item = makefile.items().next().unwrap();
1487        first_item.replace(MakefileItem::Variable(new_var)).unwrap();
1488
1489        let result = makefile.to_string();
1490        assert_eq!(result, "VAR2 = new\nrule:\n\tcommand\n");
1491    }
1492
1493    #[test]
1494    fn test_makefile_item_replace_variable_with_rule() {
1495        let makefile: Makefile = "VAR1 = value\nrule1:\n\tcommand1\n".parse().unwrap();
1496        let temp: Makefile = "new_rule:\n\tnew_command\n".parse().unwrap();
1497        let new_rule = temp.rules().next().unwrap();
1498        let mut first_item = makefile.items().next().unwrap();
1499        first_item.replace(MakefileItem::Rule(new_rule)).unwrap();
1500
1501        let result = makefile.to_string();
1502        assert_eq!(result, "new_rule:\n\tnew_command\nrule1:\n\tcommand1\n");
1503    }
1504
1505    #[test]
1506    fn test_makefile_item_replace_preserves_position() {
1507        let makefile: Makefile = "VAR1 = first\nVAR2 = second\nVAR3 = third\n"
1508            .parse()
1509            .unwrap();
1510        let temp: Makefile = "NEW = replacement\n".parse().unwrap();
1511        let new_var = temp.variable_definitions().next().unwrap();
1512
1513        // Replace the second item
1514        let mut second_item = makefile.items().nth(1).unwrap();
1515        second_item
1516            .replace(MakefileItem::Variable(new_var))
1517            .unwrap();
1518
1519        let items: Vec<_> = makefile.variable_definitions().collect();
1520        assert_eq!(items.len(), 3);
1521        assert_eq!(items[0].name(), Some("VAR1".to_string()));
1522        assert_eq!(items[1].name(), Some("NEW".to_string()));
1523        assert_eq!(items[2].name(), Some("VAR3".to_string()));
1524    }
1525
1526    #[test]
1527    fn test_makefile_item_add_comment() {
1528        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1529        let mut item = makefile.items().next().unwrap();
1530        item.add_comment("This is a variable").unwrap();
1531
1532        let result = makefile.to_string();
1533        assert_eq!(result, "# This is a variable\nVAR = value\n");
1534    }
1535
1536    #[test]
1537    fn test_makefile_item_add_multiple_comments() {
1538        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1539        let mut item = makefile.items().next().unwrap();
1540        item.add_comment("Comment 1").unwrap();
1541        // Note: After modifying the tree, we need to get a fresh reference
1542        let mut item = makefile.items().next().unwrap();
1543        item.add_comment("Comment 2").unwrap();
1544
1545        let result = makefile.to_string();
1546        // Comments are added before the item, so adding Comment 2 after Comment 1
1547        // results in Comment 1 appearing first (furthest from item), then Comment 2
1548        assert_eq!(result, "# Comment 1\n# Comment 2\nVAR = value\n");
1549    }
1550
1551    #[test]
1552    fn test_makefile_item_preceding_comments() {
1553        let makefile: Makefile = "# Comment 1\n# Comment 2\nVAR = value\n".parse().unwrap();
1554        let item = makefile.items().next().unwrap();
1555        let comments: Vec<_> = item.preceding_comments().collect();
1556        assert_eq!(comments.len(), 2);
1557        assert_eq!(comments[0], "Comment 1");
1558        assert_eq!(comments[1], "Comment 2");
1559    }
1560
1561    #[test]
1562    fn test_makefile_item_preceding_comments_no_comments() {
1563        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1564        let item = makefile.items().next().unwrap();
1565        let comments: Vec<_> = item.preceding_comments().collect();
1566        assert_eq!(comments.len(), 0);
1567    }
1568
1569    #[test]
1570    fn test_makefile_item_preceding_comments_ignores_shebang() {
1571        let makefile: Makefile = "#!/usr/bin/make\n# Real comment\nVAR = value\n"
1572            .parse()
1573            .unwrap();
1574        let item = makefile.items().next().unwrap();
1575        let comments: Vec<_> = item.preceding_comments().collect();
1576        assert_eq!(comments.len(), 1);
1577        assert_eq!(comments[0], "Real comment");
1578    }
1579
1580    #[test]
1581    fn test_makefile_item_remove_comments() {
1582        let makefile: Makefile = "# Comment 1\n# Comment 2\nVAR = value\n".parse().unwrap();
1583        // Get a fresh reference to the item to ensure we have the current tree state
1584        let mut item = makefile.items().next().unwrap();
1585        let count = item.remove_comments().unwrap();
1586
1587        assert_eq!(count, 2);
1588        let result = makefile.to_string();
1589        assert_eq!(result, "VAR = value\n");
1590    }
1591
1592    #[test]
1593    fn test_makefile_item_remove_comments_no_comments() {
1594        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1595        let mut item = makefile.items().next().unwrap();
1596        let count = item.remove_comments().unwrap();
1597
1598        assert_eq!(count, 0);
1599        assert_eq!(makefile.to_string(), "VAR = value\n");
1600    }
1601
1602    #[test]
1603    fn test_makefile_item_modify_comment() {
1604        let makefile: Makefile = "# Old comment\nVAR = value\n".parse().unwrap();
1605        let mut item = makefile.items().next().unwrap();
1606        let modified = item.modify_comment("New comment").unwrap();
1607
1608        assert!(modified);
1609        let result = makefile.to_string();
1610        assert_eq!(result, "# New comment\nVAR = value\n");
1611    }
1612
1613    #[test]
1614    fn test_makefile_item_modify_comment_no_comment() {
1615        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1616        let mut item = makefile.items().next().unwrap();
1617        let modified = item.modify_comment("New comment").unwrap();
1618
1619        assert!(!modified);
1620        assert_eq!(makefile.to_string(), "VAR = value\n");
1621    }
1622
1623    #[test]
1624    fn test_makefile_item_modify_comment_modifies_closest() {
1625        let makefile: Makefile = "# Comment 1\n# Comment 2\n# Comment 3\nVAR = value\n"
1626            .parse()
1627            .unwrap();
1628        let mut item = makefile.items().next().unwrap();
1629        let modified = item.modify_comment("Modified").unwrap();
1630
1631        assert!(modified);
1632        let result = makefile.to_string();
1633        assert_eq!(
1634            result,
1635            "# Comment 1\n# Comment 2\n# Modified\nVAR = value\n"
1636        );
1637    }
1638
1639    #[test]
1640    fn test_makefile_item_comment_workflow() {
1641        // Test adding, modifying, and removing comments in sequence
1642        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1643        let mut item = makefile.items().next().unwrap();
1644
1645        // Add a comment
1646        item.add_comment("Initial comment").unwrap();
1647        assert_eq!(makefile.to_string(), "# Initial comment\nVAR = value\n");
1648
1649        // Get a fresh reference after modification
1650        let mut item = makefile.items().next().unwrap();
1651        // Modify it
1652        item.modify_comment("Updated comment").unwrap();
1653        assert_eq!(makefile.to_string(), "# Updated comment\nVAR = value\n");
1654
1655        // Get a fresh reference after modification
1656        let mut item = makefile.items().next().unwrap();
1657        // Remove it
1658        let count = item.remove_comments().unwrap();
1659        assert_eq!(count, 1);
1660        assert_eq!(makefile.to_string(), "VAR = value\n");
1661    }
1662
1663    #[test]
1664    fn test_makefile_item_replace_with_comments() {
1665        let makefile: Makefile = "# Comment for VAR1\nVAR1 = old\nrule:\n\tcommand\n"
1666            .parse()
1667            .unwrap();
1668        let temp: Makefile = "VAR2 = new\n".parse().unwrap();
1669        let new_var = temp.variable_definitions().next().unwrap();
1670        let mut first_item = makefile.items().next().unwrap();
1671
1672        // Verify comment exists before replace
1673        let comments: Vec<_> = first_item.preceding_comments().collect();
1674        assert_eq!(comments.len(), 1);
1675        assert_eq!(comments[0], "Comment for VAR1");
1676
1677        // Replace the item
1678        first_item.replace(MakefileItem::Variable(new_var)).unwrap();
1679
1680        let result = makefile.to_string();
1681        // The comment should still be there (replace preserves preceding comments)
1682        assert_eq!(result, "# Comment for VAR1\nVAR2 = new\nrule:\n\tcommand\n");
1683    }
1684
1685    #[test]
1686    fn test_makefile_item_insert_before_variable() {
1687        let makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
1688        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1689        let new_var = temp.variable_definitions().next().unwrap();
1690        let mut second_item = makefile.items().nth(1).unwrap();
1691        second_item
1692            .insert_before(MakefileItem::Variable(new_var))
1693            .unwrap();
1694
1695        let result = makefile.to_string();
1696        assert_eq!(result, "VAR1 = first\nVAR_NEW = inserted\nVAR2 = second\n");
1697    }
1698
1699    #[test]
1700    fn test_makefile_item_insert_after_variable() {
1701        let makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
1702        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1703        let new_var = temp.variable_definitions().next().unwrap();
1704        let mut first_item = makefile.items().next().unwrap();
1705        first_item
1706            .insert_after(MakefileItem::Variable(new_var))
1707            .unwrap();
1708
1709        let result = makefile.to_string();
1710        assert_eq!(result, "VAR1 = first\nVAR_NEW = inserted\nVAR2 = second\n");
1711    }
1712
1713    #[test]
1714    fn test_makefile_item_insert_before_first_item() {
1715        let makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
1716        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1717        let new_var = temp.variable_definitions().next().unwrap();
1718        let mut first_item = makefile.items().next().unwrap();
1719        first_item
1720            .insert_before(MakefileItem::Variable(new_var))
1721            .unwrap();
1722
1723        let result = makefile.to_string();
1724        assert_eq!(result, "VAR_NEW = inserted\nVAR1 = first\nVAR2 = second\n");
1725    }
1726
1727    #[test]
1728    fn test_makefile_item_insert_after_last_item() {
1729        let makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
1730        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1731        let new_var = temp.variable_definitions().next().unwrap();
1732        let mut last_item = makefile.items().nth(1).unwrap();
1733        last_item
1734            .insert_after(MakefileItem::Variable(new_var))
1735            .unwrap();
1736
1737        let result = makefile.to_string();
1738        assert_eq!(result, "VAR1 = first\nVAR2 = second\nVAR_NEW = inserted\n");
1739    }
1740
1741    #[test]
1742    fn test_makefile_item_insert_before_include() {
1743        let makefile: Makefile = "VAR1 = value\nrule:\n\tcommand\n".parse().unwrap();
1744        let temp: Makefile = "include test.mk\n".parse().unwrap();
1745        let new_include = temp.includes().next().unwrap();
1746        let mut first_item = makefile.items().next().unwrap();
1747        first_item
1748            .insert_before(MakefileItem::Include(new_include))
1749            .unwrap();
1750
1751        let result = makefile.to_string();
1752        assert_eq!(result, "include test.mk\nVAR1 = value\nrule:\n\tcommand\n");
1753    }
1754
1755    #[test]
1756    fn test_makefile_item_insert_after_include() {
1757        let makefile: Makefile = "VAR1 = value\nrule:\n\tcommand\n".parse().unwrap();
1758        let temp: Makefile = "include test.mk\n".parse().unwrap();
1759        let new_include = temp.includes().next().unwrap();
1760        let mut first_item = makefile.items().next().unwrap();
1761        first_item
1762            .insert_after(MakefileItem::Include(new_include))
1763            .unwrap();
1764
1765        let result = makefile.to_string();
1766        assert_eq!(result, "VAR1 = value\ninclude test.mk\nrule:\n\tcommand\n");
1767    }
1768
1769    #[test]
1770    fn test_makefile_item_insert_before_rule() {
1771        let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1772        let temp: Makefile = "new_rule:\n\tnew_command\n".parse().unwrap();
1773        let new_rule = temp.rules().next().unwrap();
1774        let mut second_item = makefile.items().nth(1).unwrap();
1775        second_item
1776            .insert_before(MakefileItem::Rule(new_rule))
1777            .unwrap();
1778
1779        let result = makefile.to_string();
1780        assert_eq!(
1781            result,
1782            "rule1:\n\tcommand1\nnew_rule:\n\tnew_command\nrule2:\n\tcommand2\n"
1783        );
1784    }
1785
1786    #[test]
1787    fn test_makefile_item_insert_after_rule() {
1788        let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1789        let temp: Makefile = "new_rule:\n\tnew_command\n".parse().unwrap();
1790        let new_rule = temp.rules().next().unwrap();
1791        let mut first_item = makefile.items().next().unwrap();
1792        first_item
1793            .insert_after(MakefileItem::Rule(new_rule))
1794            .unwrap();
1795
1796        let result = makefile.to_string();
1797        assert_eq!(
1798            result,
1799            "rule1:\n\tcommand1\nnew_rule:\n\tnew_command\nrule2:\n\tcommand2\n"
1800        );
1801    }
1802
1803    #[test]
1804    fn test_makefile_item_insert_before_with_comments() {
1805        let makefile: Makefile = "# Comment 1\nVAR1 = first\n# Comment 2\nVAR2 = second\n"
1806            .parse()
1807            .unwrap();
1808        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1809        let new_var = temp.variable_definitions().next().unwrap();
1810        let mut second_item = makefile.items().nth(1).unwrap();
1811        second_item
1812            .insert_before(MakefileItem::Variable(new_var))
1813            .unwrap();
1814
1815        let result = makefile.to_string();
1816        // The new variable should be inserted before Comment 2 (which precedes VAR2)
1817        // This is correct because insert_before inserts before the item and its preceding comments
1818        assert_eq!(
1819            result,
1820            "# Comment 1\nVAR1 = first\n# Comment 2\nVAR_NEW = inserted\nVAR2 = second\n"
1821        );
1822    }
1823
1824    #[test]
1825    fn test_makefile_item_insert_after_with_comments() {
1826        let makefile: Makefile = "# Comment 1\nVAR1 = first\n# Comment 2\nVAR2 = second\n"
1827            .parse()
1828            .unwrap();
1829        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1830        let new_var = temp.variable_definitions().next().unwrap();
1831        let mut first_item = makefile.items().next().unwrap();
1832        first_item
1833            .insert_after(MakefileItem::Variable(new_var))
1834            .unwrap();
1835
1836        let result = makefile.to_string();
1837        // The new variable should be inserted between VAR1 and Comment 2/VAR2
1838        assert_eq!(
1839            result,
1840            "# Comment 1\nVAR1 = first\nVAR_NEW = inserted\n# Comment 2\nVAR2 = second\n"
1841        );
1842    }
1843
1844    #[test]
1845    fn test_makefile_item_insert_before_preserves_formatting() {
1846        let makefile: Makefile = "VAR1  =  first\nVAR2  =  second\n".parse().unwrap();
1847        let temp: Makefile = "VAR_NEW  =  inserted\n".parse().unwrap();
1848        let new_var = temp.variable_definitions().next().unwrap();
1849        let mut second_item = makefile.items().nth(1).unwrap();
1850        second_item
1851            .insert_before(MakefileItem::Variable(new_var))
1852            .unwrap();
1853
1854        let result = makefile.to_string();
1855        // Formatting of the new item is preserved from its source
1856        assert_eq!(
1857            result,
1858            "VAR1  =  first\nVAR_NEW  =  inserted\nVAR2  =  second\n"
1859        );
1860    }
1861
1862    #[test]
1863    fn test_makefile_item_insert_multiple_items() {
1864        let makefile: Makefile = "VAR1 = first\nVAR2 = last\n".parse().unwrap();
1865        let temp: Makefile = "VAR_A = a\nVAR_B = b\n".parse().unwrap();
1866        let mut new_vars: Vec<_> = temp.variable_definitions().collect();
1867
1868        let mut target_item = makefile.items().nth(1).unwrap();
1869        target_item
1870            .insert_before(MakefileItem::Variable(new_vars.pop().unwrap()))
1871            .unwrap();
1872
1873        // Get fresh reference after first insertion
1874        let mut target_item = makefile.items().nth(1).unwrap();
1875        target_item
1876            .insert_before(MakefileItem::Variable(new_vars.pop().unwrap()))
1877            .unwrap();
1878
1879        let result = makefile.to_string();
1880        assert_eq!(result, "VAR1 = first\nVAR_A = a\nVAR_B = b\nVAR2 = last\n");
1881    }
1882
1883    #[test]
1884    fn test_rules_after_nested_conditionals() {
1885        // Test for bug where .rules() returns no rules after nested conditionals
1886        // This was reported in https://bugs.debian.org/1126511
1887        let makefile_content = r#"#!/usr/bin/make -f
1888
1889ifeq ($(filter nodoc, $(DEB_BUILD_OPTIONS)),)
1890ifneq ($(shell which valadoc),)
1891  BUILD_DOC:=-Ddocs=true
1892endif
1893endif
1894
1895%:
1896	dh $@
1897
1898override_dh_auto_configure:
1899	echo test
1900"#;
1901        let makefile: Makefile = makefile_content.parse().unwrap();
1902
1903        let rules: Vec<_> = makefile.rules().collect();
1904        let targets: Vec<Vec<_>> = rules.iter().map(|r| r.targets().collect()).collect();
1905        assert_eq!(
1906            rules.len(),
1907            2,
1908            "Expected 2 rules (% and override_dh_auto_configure), got {} rules with targets: {:?}",
1909            rules.len(),
1910            targets
1911        );
1912
1913        assert_eq!(targets[0], vec!["%"]);
1914        assert_eq!(targets[1], vec!["override_dh_auto_configure"]);
1915
1916        // Also test that pattern matching works
1917        assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
1918        assert!(makefile
1919            .find_rule_by_target_pattern("build-indep")
1920            .is_some());
1921    }
1922}