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