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
48impl Makefile {
49    /// Create a new empty makefile
50    pub fn new() -> Makefile {
51        let mut builder = GreenNodeBuilder::new();
52
53        builder.start_node(ROOT.into());
54        builder.finish_node();
55
56        let syntax = SyntaxNode::new_root_mut(builder.finish());
57        Makefile::cast(syntax).unwrap()
58    }
59
60    /// Parse makefile text, returning a Parse result
61    pub fn parse(text: &str) -> crate::Parse<Makefile> {
62        crate::Parse::<Makefile>::parse_makefile(text)
63    }
64
65    /// Get the text content of the makefile
66    pub fn code(&self) -> String {
67        self.syntax().text().to_string()
68    }
69
70    /// Check if this node is the root of a makefile
71    pub fn is_root(&self) -> bool {
72        self.syntax().kind() == ROOT
73    }
74
75    /// Read a makefile from a reader
76    pub fn read<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
77        let mut buf = String::new();
78        r.read_to_string(&mut buf)?;
79        buf.parse()
80    }
81
82    /// Read makefile from a reader, but allow syntax errors
83    pub fn read_relaxed<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
84        let mut buf = String::new();
85        r.read_to_string(&mut buf)?;
86
87        let parsed = parse(&buf, None);
88        Ok(parsed.root())
89    }
90
91    /// Retrieve the rules in the makefile
92    ///
93    /// # Example
94    /// ```
95    /// use makefile_lossless::Makefile;
96    /// let makefile: Makefile = "rule: dependency\n\tcommand\n".parse().unwrap();
97    /// assert_eq!(makefile.rules().count(), 1);
98    /// ```
99    pub fn rules(&self) -> impl Iterator<Item = Rule> + '_ {
100        self.syntax().children().filter_map(Rule::cast)
101    }
102
103    /// Get all rules that have a specific target
104    pub fn rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator<Item = Rule> + 'a {
105        self.rules()
106            .filter(move |rule| rule.targets().any(|t| t == target))
107    }
108
109    /// Get all variable definitions in the makefile
110    pub fn variable_definitions(&self) -> impl Iterator<Item = VariableDefinition> {
111        self.syntax()
112            .children()
113            .filter_map(VariableDefinition::cast)
114    }
115
116    /// Get all conditionals in the makefile
117    pub fn conditionals(&self) -> impl Iterator<Item = Conditional> + '_ {
118        self.syntax().children().filter_map(Conditional::cast)
119    }
120
121    /// Get all top-level items (rules, variables, includes, conditionals) in the makefile
122    ///
123    /// # Example
124    /// ```
125    /// use makefile_lossless::{Makefile, MakefileItem};
126    /// let makefile: Makefile = r#"VAR = value
127    /// ifdef DEBUG
128    /// CFLAGS = -g
129    /// endif
130    /// rule:
131    /// 	command
132    /// "#.parse().unwrap();
133    /// let items: Vec<_> = makefile.items().collect();
134    /// assert_eq!(items.len(), 3); // VAR, conditional, rule
135    /// ```
136    pub fn items(&self) -> impl Iterator<Item = MakefileItem> + '_ {
137        self.syntax().children().filter_map(MakefileItem::cast)
138    }
139
140    /// Find all variables by name
141    ///
142    /// Returns an iterator over all variable definitions with the given name.
143    /// Makefiles can have multiple definitions of the same variable.
144    ///
145    /// # Example
146    /// ```
147    /// use makefile_lossless::Makefile;
148    /// let makefile: Makefile = "VAR1 = value1\nVAR2 = value2\nVAR1 = value3\n".parse().unwrap();
149    /// let vars: Vec<_> = makefile.find_variable("VAR1").collect();
150    /// assert_eq!(vars.len(), 2);
151    /// assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
152    /// assert_eq!(vars[1].raw_value(), Some("value3".to_string()));
153    /// ```
154    pub fn find_variable<'a>(
155        &'a self,
156        name: &'a str,
157    ) -> impl Iterator<Item = VariableDefinition> + 'a {
158        self.variable_definitions()
159            .filter(move |var| var.name().as_deref() == Some(name))
160    }
161
162    /// Add a new rule to the makefile
163    ///
164    /// # Example
165    /// ```
166    /// use makefile_lossless::Makefile;
167    /// let mut makefile = Makefile::new();
168    /// makefile.add_rule("rule");
169    /// assert_eq!(makefile.to_string(), "rule:\n");
170    /// ```
171    pub fn add_rule(&mut self, target: &str) -> Rule {
172        let mut builder = GreenNodeBuilder::new();
173        builder.start_node(RULE.into());
174        builder.token(IDENTIFIER.into(), target);
175        builder.token(OPERATOR.into(), ":");
176        builder.token(NEWLINE.into(), "\n");
177        builder.finish_node();
178
179        let syntax = SyntaxNode::new_root_mut(builder.finish());
180        let pos = self.syntax().children_with_tokens().count();
181
182        // Add a blank line before the new rule if there are existing rules
183        // This maintains standard makefile formatting
184        let needs_blank_line = self.syntax().children().any(|c| c.kind() == RULE);
185
186        if needs_blank_line {
187            // Create a BLANK_LINE node
188            let mut bl_builder = GreenNodeBuilder::new();
189            bl_builder.start_node(BLANK_LINE.into());
190            bl_builder.token(NEWLINE.into(), "\n");
191            bl_builder.finish_node();
192            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
193
194            self.syntax()
195                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
196        } else {
197            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
198        }
199
200        // Use children().count() - 1 to get the last added child node
201        // (not children_with_tokens().count() which includes tokens)
202        Rule::cast(self.syntax().children().last().unwrap()).unwrap()
203    }
204
205    /// Add a new conditional to the makefile
206    ///
207    /// # Arguments
208    /// * `conditional_type` - The type of conditional: "ifdef", "ifndef", "ifeq", or "ifneq"
209    /// * `condition` - The condition expression (e.g., "DEBUG" for ifdef/ifndef, or "(a,b)" for ifeq/ifneq)
210    /// * `if_body` - The content of the if branch
211    /// * `else_body` - Optional content for the else branch
212    ///
213    /// # Example
214    /// ```
215    /// use makefile_lossless::Makefile;
216    /// let mut makefile = Makefile::new();
217    /// makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
218    /// assert!(makefile.to_string().contains("ifdef DEBUG"));
219    /// ```
220    pub fn add_conditional(
221        &mut self,
222        conditional_type: &str,
223        condition: &str,
224        if_body: &str,
225        else_body: Option<&str>,
226    ) -> Result<Conditional, Error> {
227        // Validate conditional type
228        if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&conditional_type) {
229            return Err(Error::Parse(ParseError {
230                errors: vec![ErrorInfo {
231                    message: format!(
232                        "Invalid conditional type: {}. Must be one of: ifdef, ifndef, ifeq, ifneq",
233                        conditional_type
234                    ),
235                    line: 1,
236                    context: "add_conditional".to_string(),
237                }],
238            }));
239        }
240
241        let mut builder = GreenNodeBuilder::new();
242        builder.start_node(CONDITIONAL.into());
243
244        // Build CONDITIONAL_IF
245        builder.start_node(CONDITIONAL_IF.into());
246        builder.token(IDENTIFIER.into(), conditional_type);
247        builder.token(WHITESPACE.into(), " ");
248
249        // Wrap condition in EXPR node
250        builder.start_node(EXPR.into());
251        builder.token(IDENTIFIER.into(), condition);
252        builder.finish_node();
253
254        builder.token(NEWLINE.into(), "\n");
255        builder.finish_node();
256
257        // Add if body content
258        if !if_body.is_empty() {
259            for line in if_body.lines() {
260                if !line.is_empty() {
261                    builder.token(IDENTIFIER.into(), line);
262                }
263                builder.token(NEWLINE.into(), "\n");
264            }
265            // Add final newline if if_body doesn't end with one
266            if !if_body.ends_with('\n') && !if_body.is_empty() {
267                builder.token(NEWLINE.into(), "\n");
268            }
269        }
270
271        // Add else clause if provided
272        if let Some(else_content) = else_body {
273            builder.start_node(CONDITIONAL_ELSE.into());
274            builder.token(IDENTIFIER.into(), "else");
275            builder.token(NEWLINE.into(), "\n");
276            builder.finish_node();
277
278            // Add else body content
279            if !else_content.is_empty() {
280                for line in else_content.lines() {
281                    if !line.is_empty() {
282                        builder.token(IDENTIFIER.into(), line);
283                    }
284                    builder.token(NEWLINE.into(), "\n");
285                }
286                // Add final newline if else_content doesn't end with one
287                if !else_content.ends_with('\n') && !else_content.is_empty() {
288                    builder.token(NEWLINE.into(), "\n");
289                }
290            }
291        }
292
293        // Build CONDITIONAL_ENDIF
294        builder.start_node(CONDITIONAL_ENDIF.into());
295        builder.token(IDENTIFIER.into(), "endif");
296        builder.token(NEWLINE.into(), "\n");
297        builder.finish_node();
298
299        builder.finish_node();
300
301        let syntax = SyntaxNode::new_root_mut(builder.finish());
302        let pos = self.syntax().children_with_tokens().count();
303
304        // Add a blank line before the new conditional if there are existing elements
305        let needs_blank_line = self
306            .syntax()
307            .children()
308            .any(|c| c.kind() == RULE || c.kind() == VARIABLE || c.kind() == CONDITIONAL);
309
310        if needs_blank_line {
311            // Create a BLANK_LINE node
312            let mut bl_builder = GreenNodeBuilder::new();
313            bl_builder.start_node(BLANK_LINE.into());
314            bl_builder.token(NEWLINE.into(), "\n");
315            bl_builder.finish_node();
316            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
317
318            self.syntax()
319                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
320        } else {
321            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
322        }
323
324        // Return the newly added conditional
325        Ok(Conditional::cast(self.syntax().children().last().unwrap()).unwrap())
326    }
327
328    /// Add a new conditional to the makefile with typed items
329    ///
330    /// This is a more type-safe alternative to `add_conditional` that accepts iterators of
331    /// `MakefileItem` instead of raw strings.
332    ///
333    /// # Arguments
334    /// * `conditional_type` - The type of conditional: "ifdef", "ifndef", "ifeq", or "ifneq"
335    /// * `condition` - The condition expression (e.g., "DEBUG" for ifdef/ifndef, or "(a,b)" for ifeq/ifneq)
336    /// * `if_items` - Items for the if branch
337    /// * `else_items` - Optional items for the else branch
338    ///
339    /// # Example
340    /// ```
341    /// use makefile_lossless::{Makefile, MakefileItem};
342    /// let mut makefile = Makefile::new();
343    /// let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
344    /// let var1 = temp1.variable_definitions().next().unwrap();
345    /// let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
346    /// let var2 = temp2.variable_definitions().next().unwrap();
347    /// makefile.add_conditional_with_items(
348    ///     "ifdef",
349    ///     "DEBUG",
350    ///     vec![MakefileItem::Variable(var1)],
351    ///     Some(vec![MakefileItem::Variable(var2)])
352    /// ).unwrap();
353    /// assert!(makefile.to_string().contains("ifdef DEBUG"));
354    /// assert!(makefile.to_string().contains("CFLAGS = -g"));
355    /// assert!(makefile.to_string().contains("CFLAGS = -O2"));
356    /// ```
357    pub fn add_conditional_with_items<I1, I2>(
358        &mut self,
359        conditional_type: &str,
360        condition: &str,
361        if_items: I1,
362        else_items: Option<I2>,
363    ) -> Result<Conditional, Error>
364    where
365        I1: IntoIterator<Item = MakefileItem>,
366        I2: IntoIterator<Item = MakefileItem>,
367    {
368        // Validate conditional type
369        if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&conditional_type) {
370            return Err(Error::Parse(ParseError {
371                errors: vec![ErrorInfo {
372                    message: format!(
373                        "Invalid conditional type: {}. Must be one of: ifdef, ifndef, ifeq, ifneq",
374                        conditional_type
375                    ),
376                    line: 1,
377                    context: "add_conditional_with_items".to_string(),
378                }],
379            }));
380        }
381
382        let mut builder = GreenNodeBuilder::new();
383        builder.start_node(CONDITIONAL.into());
384
385        // Build CONDITIONAL_IF
386        builder.start_node(CONDITIONAL_IF.into());
387        builder.token(IDENTIFIER.into(), conditional_type);
388        builder.token(WHITESPACE.into(), " ");
389
390        // Wrap condition in EXPR node
391        builder.start_node(EXPR.into());
392        builder.token(IDENTIFIER.into(), condition);
393        builder.finish_node();
394
395        builder.token(NEWLINE.into(), "\n");
396        builder.finish_node();
397
398        // Add if branch items
399        for item in if_items {
400            // Clone the item's syntax tree into our builder
401            let item_text = item.syntax().to_string();
402            // Parse it again to get green nodes
403            builder.token(IDENTIFIER.into(), item_text.trim());
404            builder.token(NEWLINE.into(), "\n");
405        }
406
407        // Add else clause if provided
408        if let Some(else_iter) = else_items {
409            builder.start_node(CONDITIONAL_ELSE.into());
410            builder.token(IDENTIFIER.into(), "else");
411            builder.token(NEWLINE.into(), "\n");
412            builder.finish_node();
413
414            // Add else branch items
415            for item in else_iter {
416                let item_text = item.syntax().to_string();
417                builder.token(IDENTIFIER.into(), item_text.trim());
418                builder.token(NEWLINE.into(), "\n");
419            }
420        }
421
422        // Build CONDITIONAL_ENDIF
423        builder.start_node(CONDITIONAL_ENDIF.into());
424        builder.token(IDENTIFIER.into(), "endif");
425        builder.token(NEWLINE.into(), "\n");
426        builder.finish_node();
427
428        builder.finish_node();
429
430        let syntax = SyntaxNode::new_root_mut(builder.finish());
431        let pos = self.syntax().children_with_tokens().count();
432
433        // Add a blank line before the new conditional if there are existing elements
434        let needs_blank_line = self
435            .syntax()
436            .children()
437            .any(|c| c.kind() == RULE || c.kind() == VARIABLE || c.kind() == CONDITIONAL);
438
439        if needs_blank_line {
440            // Create a BLANK_LINE node
441            let mut bl_builder = GreenNodeBuilder::new();
442            bl_builder.start_node(BLANK_LINE.into());
443            bl_builder.token(NEWLINE.into(), "\n");
444            bl_builder.finish_node();
445            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
446
447            self.syntax()
448                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
449        } else {
450            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
451        }
452
453        // Return the newly added conditional
454        Ok(Conditional::cast(self.syntax().children().last().unwrap()).unwrap())
455    }
456
457    /// Read the makefile
458    pub fn from_reader<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
459        let mut buf = String::new();
460        r.read_to_string(&mut buf)?;
461
462        let parsed = parse(&buf, None);
463        if !parsed.errors.is_empty() {
464            Err(Error::Parse(ParseError {
465                errors: parsed.errors,
466            }))
467        } else {
468            Ok(parsed.root())
469        }
470    }
471
472    /// Replace rule at given index with a new rule
473    ///
474    /// # Example
475    /// ```
476    /// use makefile_lossless::Makefile;
477    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
478    /// let new_rule: makefile_lossless::Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
479    /// makefile.replace_rule(0, new_rule).unwrap();
480    /// assert!(makefile.rules().any(|r| r.targets().any(|t| t == "new_rule")));
481    /// ```
482    pub fn replace_rule(&mut self, index: usize, new_rule: Rule) -> Result<(), Error> {
483        let rules: Vec<_> = self
484            .syntax()
485            .children()
486            .filter(|n| n.kind() == RULE)
487            .collect();
488
489        if rules.is_empty() {
490            return Err(Error::Parse(ParseError {
491                errors: vec![ErrorInfo {
492                    message: "Cannot replace rule in empty makefile".to_string(),
493                    line: 1,
494                    context: "replace_rule".to_string(),
495                }],
496            }));
497        }
498
499        if index >= rules.len() {
500            return Err(Error::Parse(ParseError {
501                errors: vec![ErrorInfo {
502                    message: format!(
503                        "Rule index {} out of bounds (max {})",
504                        index,
505                        rules.len() - 1
506                    ),
507                    line: 1,
508                    context: "replace_rule".to_string(),
509                }],
510            }));
511        }
512
513        let target_node = &rules[index];
514        let target_index = target_node.index();
515
516        // Replace the rule at the target index
517        self.syntax().splice_children(
518            target_index..target_index + 1,
519            vec![new_rule.syntax().clone().into()],
520        );
521        Ok(())
522    }
523
524    /// Remove rule at given index
525    ///
526    /// # Example
527    /// ```
528    /// use makefile_lossless::Makefile;
529    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
530    /// let removed = makefile.remove_rule(0).unwrap();
531    /// assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule1"]);
532    /// assert_eq!(makefile.rules().count(), 1);
533    /// ```
534    pub fn remove_rule(&mut self, index: usize) -> Result<Rule, Error> {
535        let rules: Vec<_> = self
536            .syntax()
537            .children()
538            .filter(|n| n.kind() == RULE)
539            .collect();
540
541        if rules.is_empty() {
542            return Err(Error::Parse(ParseError {
543                errors: vec![ErrorInfo {
544                    message: "Cannot remove rule from empty makefile".to_string(),
545                    line: 1,
546                    context: "remove_rule".to_string(),
547                }],
548            }));
549        }
550
551        if index >= rules.len() {
552            return Err(Error::Parse(ParseError {
553                errors: vec![ErrorInfo {
554                    message: format!(
555                        "Rule index {} out of bounds (max {})",
556                        index,
557                        rules.len() - 1
558                    ),
559                    line: 1,
560                    context: "remove_rule".to_string(),
561                }],
562            }));
563        }
564
565        let target_node = rules[index].clone();
566        let target_index = target_node.index();
567
568        // Remove the rule at the target index
569        self.syntax()
570            .splice_children(target_index..target_index + 1, vec![]);
571        Ok(Rule::cast(target_node).unwrap())
572    }
573
574    /// Insert rule at given position
575    ///
576    /// # Example
577    /// ```
578    /// use makefile_lossless::Makefile;
579    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
580    /// let new_rule: makefile_lossless::Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
581    /// makefile.insert_rule(1, new_rule).unwrap();
582    /// let targets: Vec<_> = makefile.rules().flat_map(|r| r.targets().collect::<Vec<_>>()).collect();
583    /// assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
584    /// ```
585    pub fn insert_rule(&mut self, index: usize, new_rule: Rule) -> Result<(), Error> {
586        let rules: Vec<_> = self
587            .syntax()
588            .children()
589            .filter(|n| n.kind() == RULE)
590            .collect();
591
592        if index > rules.len() {
593            return Err(Error::Parse(ParseError {
594                errors: vec![ErrorInfo {
595                    message: format!("Rule index {} out of bounds (max {})", index, rules.len()),
596                    line: 1,
597                    context: "insert_rule".to_string(),
598                }],
599            }));
600        }
601
602        let target_index = if index == rules.len() {
603            // Insert at the end
604            self.syntax().children_with_tokens().count()
605        } else {
606            // Insert before the rule at the given index
607            rules[index].index()
608        };
609
610        // Build the nodes to insert
611        let mut nodes_to_insert = Vec::new();
612
613        // Determine if we need to add blank lines to maintain formatting consistency
614        if index == 0 && !rules.is_empty() {
615            // Inserting before the first rule - check if first rule has a blank line before it
616            // If so, we should add one after our new rule instead
617            // For now, just add the rule without a blank line before it
618            nodes_to_insert.push(new_rule.syntax().clone().into());
619
620            // Add a blank line after the new rule
621            let mut bl_builder = GreenNodeBuilder::new();
622            bl_builder.start_node(BLANK_LINE.into());
623            bl_builder.token(NEWLINE.into(), "\n");
624            bl_builder.finish_node();
625            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
626            nodes_to_insert.push(blank_line.into());
627        } else if index < rules.len() {
628            // Inserting in the middle (before an existing rule)
629            // The syntax tree structure is: ... [maybe BLANK_LINE] RULE(target) ...
630            // We're inserting right before RULE(target)
631
632            // If there's a BLANK_LINE immediately before the target rule,
633            // it will stay there and separate the previous rule from our new rule.
634            // We don't need to add a BLANK_LINE before our new rule in that case.
635
636            // But we DO need to add a BLANK_LINE after our new rule to separate it
637            // from the target rule (which we're inserting before).
638
639            // Check if there's a blank line immediately before target_index
640            let has_blank_before = if target_index > 0 {
641                self.syntax()
642                    .children_with_tokens()
643                    .nth(target_index - 1)
644                    .and_then(|n| n.as_node().map(|node| node.kind() == BLANK_LINE))
645                    .unwrap_or(false)
646            } else {
647                false
648            };
649
650            // Only add a blank before if there isn't one already and we're not at the start
651            if !has_blank_before && index > 0 {
652                let mut bl_builder = GreenNodeBuilder::new();
653                bl_builder.start_node(BLANK_LINE.into());
654                bl_builder.token(NEWLINE.into(), "\n");
655                bl_builder.finish_node();
656                let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
657                nodes_to_insert.push(blank_line.into());
658            }
659
660            // Add the new rule
661            nodes_to_insert.push(new_rule.syntax().clone().into());
662
663            // Always add a blank line after the new rule to separate it from the next rule
664            let mut bl_builder = GreenNodeBuilder::new();
665            bl_builder.start_node(BLANK_LINE.into());
666            bl_builder.token(NEWLINE.into(), "\n");
667            bl_builder.finish_node();
668            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
669            nodes_to_insert.push(blank_line.into());
670        } else {
671            // Inserting at the end when there are existing rules
672            // Add a blank line before the new rule
673            let mut bl_builder = GreenNodeBuilder::new();
674            bl_builder.start_node(BLANK_LINE.into());
675            bl_builder.token(NEWLINE.into(), "\n");
676            bl_builder.finish_node();
677            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
678            nodes_to_insert.push(blank_line.into());
679
680            // Add the new rule
681            nodes_to_insert.push(new_rule.syntax().clone().into());
682        }
683
684        // Insert all nodes at the target index
685        self.syntax()
686            .splice_children(target_index..target_index, nodes_to_insert);
687        Ok(())
688    }
689
690    /// Get all include directives in the makefile
691    ///
692    /// # Example
693    /// ```
694    /// use makefile_lossless::Makefile;
695    /// let makefile: Makefile = "include config.mk\n-include .env\n".parse().unwrap();
696    /// let includes = makefile.includes().collect::<Vec<_>>();
697    /// assert_eq!(includes.len(), 2);
698    /// ```
699    pub fn includes(&self) -> impl Iterator<Item = Include> {
700        self.syntax().children().filter_map(Include::cast)
701    }
702
703    /// Get all included file paths
704    ///
705    /// # Example
706    /// ```
707    /// use makefile_lossless::Makefile;
708    /// let makefile: Makefile = "include config.mk\n-include .env\n".parse().unwrap();
709    /// let paths = makefile.included_files().collect::<Vec<_>>();
710    /// assert_eq!(paths, vec!["config.mk", ".env"]);
711    /// ```
712    pub fn included_files(&self) -> impl Iterator<Item = String> + '_ {
713        // We need to collect all Include nodes from anywhere in the syntax tree,
714        // not just direct children of the root, to handle includes in conditionals
715        fn collect_includes(node: &SyntaxNode) -> Vec<Include> {
716            let mut includes = Vec::new();
717
718            // First check if this node itself is an Include
719            if let Some(include) = Include::cast(node.clone()) {
720                includes.push(include);
721            }
722
723            // Then recurse into all children
724            for child in node.children() {
725                includes.extend(collect_includes(&child));
726            }
727
728            includes
729        }
730
731        // Start collection from the root node
732        let includes = collect_includes(self.syntax());
733
734        // Convert to an iterator of paths
735        includes.into_iter().map(|include| {
736            include
737                .syntax()
738                .children()
739                .find(|node| node.kind() == EXPR)
740                .map(|expr| expr.text().to_string().trim().to_string())
741                .unwrap_or_default()
742        })
743    }
744
745    /// Find the first rule with a specific target name
746    ///
747    /// # Example
748    /// ```
749    /// use makefile_lossless::Makefile;
750    /// let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
751    /// let rule = makefile.find_rule_by_target("rule2");
752    /// assert!(rule.is_some());
753    /// assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
754    /// ```
755    pub fn find_rule_by_target(&self, target: &str) -> Option<Rule> {
756        self.rules()
757            .find(|rule| rule.targets().any(|t| t == target))
758    }
759
760    /// Find all rules with a specific target name
761    ///
762    /// # Example
763    /// ```
764    /// use makefile_lossless::Makefile;
765    /// let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n".parse().unwrap();
766    /// let rules: Vec<_> = makefile.find_rules_by_target("rule1").collect();
767    /// assert_eq!(rules.len(), 2);
768    /// ```
769    pub fn find_rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator<Item = Rule> + 'a {
770        self.rules_by_target(target)
771    }
772
773    /// Find the first rule whose target matches the given pattern
774    ///
775    /// Supports make-style pattern matching where `%` in a rule's target acts as a wildcard.
776    /// For example, a rule with target `%.o` will match `foo.o`, `bar.o`, etc.
777    ///
778    /// # Example
779    /// ```
780    /// use makefile_lossless::Makefile;
781    /// let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
782    /// let rule = makefile.find_rule_by_target_pattern("foo.o");
783    /// assert!(rule.is_some());
784    /// ```
785    pub fn find_rule_by_target_pattern(&self, target: &str) -> Option<Rule> {
786        self.rules()
787            .find(|rule| rule.targets().any(|t| matches_pattern(&t, target)))
788    }
789
790    /// Find all rules whose targets match the given pattern
791    ///
792    /// Supports make-style pattern matching where `%` in a rule's target acts as a wildcard.
793    /// For example, a rule with target `%.o` will match `foo.o`, `bar.o`, etc.
794    ///
795    /// # Example
796    /// ```
797    /// use makefile_lossless::Makefile;
798    /// let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n".parse().unwrap();
799    /// let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
800    /// assert_eq!(rules.len(), 2);
801    /// ```
802    pub fn find_rules_by_target_pattern<'a>(
803        &'a self,
804        target: &'a str,
805    ) -> impl Iterator<Item = Rule> + 'a {
806        self.rules()
807            .filter(move |rule| rule.targets().any(|t| matches_pattern(&t, target)))
808    }
809
810    /// Add a target to .PHONY (creates .PHONY rule if it doesn't exist)
811    ///
812    /// # Example
813    /// ```
814    /// use makefile_lossless::Makefile;
815    /// let mut makefile = Makefile::new();
816    /// makefile.add_phony_target("clean").unwrap();
817    /// assert!(makefile.is_phony("clean"));
818    /// ```
819    pub fn add_phony_target(&mut self, target: &str) -> Result<(), Error> {
820        // Find existing .PHONY rule
821        if let Some(mut phony_rule) = self.find_rule_by_target(".PHONY") {
822            // Check if target is already in prerequisites
823            if !phony_rule.prerequisites().any(|p| p == target) {
824                phony_rule.add_prerequisite(target)?;
825            }
826        } else {
827            // Create new .PHONY rule
828            let mut phony_rule = self.add_rule(".PHONY");
829            phony_rule.add_prerequisite(target)?;
830        }
831        Ok(())
832    }
833
834    /// Remove a target from .PHONY (removes .PHONY rule if it becomes empty)
835    ///
836    /// Returns `true` if the target was found and removed, `false` if it wasn't in .PHONY.
837    /// If there are multiple .PHONY rules, it removes the target from the first rule that contains it.
838    ///
839    /// # Example
840    /// ```
841    /// use makefile_lossless::Makefile;
842    /// let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
843    /// assert!(makefile.remove_phony_target("clean").unwrap());
844    /// assert!(!makefile.is_phony("clean"));
845    /// assert!(makefile.is_phony("test"));
846    /// ```
847    pub fn remove_phony_target(&mut self, target: &str) -> Result<bool, Error> {
848        // Find the first .PHONY rule that contains the target
849        let mut phony_rule = None;
850        for rule in self.rules_by_target(".PHONY") {
851            if rule.prerequisites().any(|p| p == target) {
852                phony_rule = Some(rule);
853                break;
854            }
855        }
856
857        let mut phony_rule = match phony_rule {
858            Some(rule) => rule,
859            None => return Ok(false),
860        };
861
862        // Count prerequisites before removal
863        let prereq_count = phony_rule.prerequisites().count();
864
865        // Remove the prerequisite
866        phony_rule.remove_prerequisite(target)?;
867
868        // Check if .PHONY has no more prerequisites, if so remove the rule
869        if prereq_count == 1 {
870            // We just removed the last prerequisite, so remove the entire rule
871            phony_rule.remove()?;
872        }
873
874        Ok(true)
875    }
876
877    /// Check if a target is marked as phony
878    ///
879    /// # Example
880    /// ```
881    /// use makefile_lossless::Makefile;
882    /// let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
883    /// assert!(makefile.is_phony("clean"));
884    /// assert!(makefile.is_phony("test"));
885    /// assert!(!makefile.is_phony("build"));
886    /// ```
887    pub fn is_phony(&self, target: &str) -> bool {
888        // Check all .PHONY rules since there can be multiple
889        self.rules_by_target(".PHONY")
890            .any(|rule| rule.prerequisites().any(|p| p == target))
891    }
892
893    /// Get all phony targets
894    ///
895    /// # Example
896    /// ```
897    /// use makefile_lossless::Makefile;
898    /// let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
899    /// let phony_targets: Vec<_> = makefile.phony_targets().collect();
900    /// assert_eq!(phony_targets, vec!["clean", "test", "build"]);
901    /// ```
902    pub fn phony_targets(&self) -> impl Iterator<Item = String> + '_ {
903        // Collect from all .PHONY rules since there can be multiple
904        self.rules_by_target(".PHONY")
905            .flat_map(|rule| rule.prerequisites().collect::<Vec<_>>())
906    }
907}