Skip to main content

makefile_lossless/ast/
rule.rs

1use super::makefile::MakefileItem;
2use crate::lossless::{
3    remove_with_preceding_comments, trim_trailing_newlines, Conditional, Error, ErrorInfo,
4    Makefile, ParseError, Recipe, Rule, SyntaxElement, SyntaxNode,
5};
6use crate::SyntaxKind::*;
7use rowan::ast::AstNode;
8use rowan::GreenNodeBuilder;
9
10// Helper function to build a PREREQUISITES node containing PREREQUISITE nodes
11fn build_prerequisites_node(prereqs: &[String], include_leading_space: bool) -> SyntaxNode {
12    let mut builder = GreenNodeBuilder::new();
13    builder.start_node(PREREQUISITES.into());
14
15    for (i, prereq) in prereqs.iter().enumerate() {
16        // Add space: before first prerequisite if requested, and between all prerequisites
17        if (i == 0 && include_leading_space) || i > 0 {
18            builder.token(WHITESPACE.into(), " ");
19        }
20
21        // Build each PREREQUISITE node
22        builder.start_node(PREREQUISITE.into());
23        builder.token(IDENTIFIER.into(), prereq);
24        builder.finish_node();
25    }
26
27    builder.finish_node();
28    SyntaxNode::new_root_mut(builder.finish())
29}
30
31// Helper function to build targets section (TARGETS node)
32fn build_targets_node(targets: &[String]) -> SyntaxNode {
33    let mut builder = GreenNodeBuilder::new();
34    builder.start_node(TARGETS.into());
35
36    for (i, target) in targets.iter().enumerate() {
37        if i > 0 {
38            builder.token(WHITESPACE.into(), " ");
39        }
40        builder.token(IDENTIFIER.into(), target);
41    }
42
43    builder.finish_node();
44    SyntaxNode::new_root_mut(builder.finish())
45}
46
47/// Represents different types of items that can appear in a Rule's body
48#[derive(Clone)]
49pub enum RuleItem {
50    /// A recipe line (command to execute)
51    Recipe(String),
52    /// A conditional block within the rule
53    Conditional(Conditional),
54}
55
56impl std::fmt::Debug for RuleItem {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            RuleItem::Recipe(text) => f.debug_tuple("Recipe").field(text).finish(),
60            RuleItem::Conditional(_) => f
61                .debug_tuple("Conditional")
62                .field(&"<Conditional>")
63                .finish(),
64        }
65    }
66}
67
68impl RuleItem {
69    /// Try to cast a syntax node to a RuleItem
70    pub(crate) fn cast(node: SyntaxNode) -> Option<Self> {
71        match node.kind() {
72            RECIPE => {
73                // Extract the recipe text from the RECIPE node
74                let text = node.children_with_tokens().find_map(|it| {
75                    if let Some(token) = it.as_token() {
76                        if token.kind() == TEXT {
77                            return Some(token.text().to_string());
78                        }
79                    }
80                    None
81                })?;
82                Some(RuleItem::Recipe(text))
83            }
84            CONDITIONAL => Conditional::cast(node).map(RuleItem::Conditional),
85            _ => None,
86        }
87    }
88}
89
90impl Rule {
91    /// Parse rule text, returning a Parse result
92    pub fn parse(text: &str) -> crate::Parse<Rule> {
93        crate::Parse::<Rule>::parse_rule(text)
94    }
95
96    /// Create a new rule with the given targets, prerequisites, and recipes
97    ///
98    /// # Arguments
99    /// * `targets` - A slice of target names
100    /// * `prerequisites` - A slice of prerequisite names (can be empty)
101    /// * `recipes` - A slice of recipe lines (can be empty)
102    ///
103    /// # Example
104    /// ```
105    /// use makefile_lossless::Rule;
106    ///
107    /// let rule = Rule::new(&["all"], &["build", "test"], &["echo Done"]);
108    /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["all"]);
109    /// assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["build", "test"]);
110    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo Done"]);
111    /// ```
112    pub fn new(targets: &[&str], prerequisites: &[&str], recipes: &[&str]) -> Rule {
113        let mut builder = GreenNodeBuilder::new();
114        builder.start_node(RULE.into());
115
116        // Build targets
117        for (i, target) in targets.iter().enumerate() {
118            if i > 0 {
119                builder.token(WHITESPACE.into(), " ");
120            }
121            builder.token(IDENTIFIER.into(), target);
122        }
123
124        // Add colon
125        builder.token(OPERATOR.into(), ":");
126
127        // Build prerequisites
128        if !prerequisites.is_empty() {
129            builder.token(WHITESPACE.into(), " ");
130            builder.start_node(PREREQUISITES.into());
131
132            for (i, prereq) in prerequisites.iter().enumerate() {
133                if i > 0 {
134                    builder.token(WHITESPACE.into(), " ");
135                }
136                builder.start_node(PREREQUISITE.into());
137                builder.token(IDENTIFIER.into(), prereq);
138                builder.finish_node();
139            }
140
141            builder.finish_node();
142        }
143
144        // Add newline after rule declaration
145        builder.token(NEWLINE.into(), "\n");
146
147        // Build recipes
148        for recipe in recipes {
149            builder.start_node(RECIPE.into());
150            builder.token(INDENT.into(), "\t");
151            builder.token(TEXT.into(), recipe);
152            builder.token(NEWLINE.into(), "\n");
153            builder.finish_node();
154        }
155
156        builder.finish_node();
157
158        let syntax = SyntaxNode::new_root_mut(builder.finish());
159        Rule::cast(syntax).unwrap()
160    }
161
162    /// Get the parent item of this rule, if any
163    ///
164    /// Returns `Some(MakefileItem)` if this rule has a parent that is a MakefileItem
165    /// (e.g., a Conditional), or `None` if the parent is the root Makefile node.
166    ///
167    /// # Example
168    /// ```
169    /// use makefile_lossless::Makefile;
170    ///
171    /// let makefile: Makefile = r#"ifdef DEBUG
172    /// all:
173    ///     echo "test"
174    /// endif
175    /// "#.parse().unwrap();
176    ///
177    /// let cond = makefile.conditionals().next().unwrap();
178    /// let rule = cond.if_items().next().unwrap();
179    /// // Rule's parent is the conditional
180    /// assert!(matches!(rule, makefile_lossless::MakefileItem::Rule(_)));
181    /// ```
182    pub fn parent(&self) -> Option<MakefileItem> {
183        self.syntax().parent().and_then(MakefileItem::cast)
184    }
185
186    /// Check if this is a double-colon rule (`target:: prereqs`).
187    ///
188    /// Double-colon rules allow multiple recipe blocks for the same target,
189    /// each executed independently when its prerequisites are newer.
190    ///
191    /// # Example
192    /// ```
193    /// use makefile_lossless::Makefile;
194    /// let makefile: Makefile = "all:: dep1\n\techo first\n".parse().unwrap();
195    /// let rule = makefile.rules().next().unwrap();
196    /// assert!(rule.is_double_colon());
197    /// ```
198    pub fn is_double_colon(&self) -> bool {
199        self.syntax()
200            .children_with_tokens()
201            .filter_map(|it| it.into_token())
202            .any(|t| t.kind() == OPERATOR && t.text() == "::")
203    }
204
205    // Helper method to collect variable references from tokens
206    fn collect_variable_reference(
207        &self,
208        tokens: &mut std::iter::Peekable<impl Iterator<Item = SyntaxElement>>,
209    ) -> Option<String> {
210        let mut var_ref = String::new();
211
212        // Check if we're at a $ token
213        if let Some(token) = tokens.next() {
214            if let Some(t) = token.as_token() {
215                if t.kind() == DOLLAR {
216                    var_ref.push_str(t.text());
217
218                    // Check if the next token is a (
219                    if let Some(next) = tokens.peek() {
220                        if let Some(nt) = next.as_token() {
221                            if nt.kind() == LPAREN {
222                                // Consume the opening parenthesis
223                                var_ref.push_str(nt.text());
224                                tokens.next();
225
226                                // Track parenthesis nesting level
227                                let mut paren_count = 1;
228
229                                // Keep consuming tokens until we find the matching closing parenthesis
230                                for next_token in tokens.by_ref() {
231                                    if let Some(nt) = next_token.as_token() {
232                                        var_ref.push_str(nt.text());
233
234                                        if nt.kind() == LPAREN {
235                                            paren_count += 1;
236                                        } else if nt.kind() == RPAREN {
237                                            paren_count -= 1;
238                                            if paren_count == 0 {
239                                                break;
240                                            }
241                                        }
242                                    }
243                                }
244
245                                return Some(var_ref);
246                            }
247                        }
248                    }
249
250                    // Handle simpler variable references (though this branch may be less common)
251                    for next_token in tokens.by_ref() {
252                        if let Some(nt) = next_token.as_token() {
253                            var_ref.push_str(nt.text());
254                            if nt.kind() == RPAREN {
255                                break;
256                            }
257                        }
258                    }
259                    return Some(var_ref);
260                }
261            }
262        }
263
264        None
265    }
266
267    // Helper method to extract targets from a TARGETS node
268    fn extract_targets_from_node(node: &SyntaxNode) -> Vec<String> {
269        let mut result = Vec::new();
270        let mut current_target = String::new();
271        let mut in_parens = 0;
272
273        for child in node.children_with_tokens() {
274            if let Some(token) = child.as_token() {
275                match token.kind() {
276                    IDENTIFIER => {
277                        current_target.push_str(token.text());
278                    }
279                    WHITESPACE => {
280                        // Only treat whitespace as a delimiter if we're not inside parentheses
281                        if in_parens == 0 && !current_target.is_empty() {
282                            result.push(current_target.clone());
283                            current_target.clear();
284                        } else if in_parens > 0 {
285                            current_target.push_str(token.text());
286                        }
287                    }
288                    LPAREN => {
289                        in_parens += 1;
290                        current_target.push_str(token.text());
291                    }
292                    RPAREN => {
293                        in_parens -= 1;
294                        current_target.push_str(token.text());
295                    }
296                    DOLLAR => {
297                        current_target.push_str(token.text());
298                    }
299                    _ => {
300                        current_target.push_str(token.text());
301                    }
302                }
303            } else if let Some(child_node) = child.as_node() {
304                // Handle nested nodes like ARCHIVE_MEMBERS
305                current_target.push_str(&child_node.text().to_string());
306            }
307        }
308
309        // Push the last target if any
310        if !current_target.is_empty() {
311            result.push(current_target);
312        }
313
314        result
315    }
316
317    /// Targets of this rule
318    ///
319    /// # Example
320    /// ```
321    /// use makefile_lossless::Rule;
322    ///
323    /// let rule: Rule = "rule: dependency\n\tcommand".parse().unwrap();
324    /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
325    /// ```
326    pub fn targets(&self) -> impl Iterator<Item = String> + '_ {
327        // First check if there's a TARGETS node
328        for child in self.syntax().children_with_tokens() {
329            if let Some(node) = child.as_node() {
330                if node.kind() == TARGETS {
331                    // Extract targets from the TARGETS node
332                    return Self::extract_targets_from_node(node).into_iter();
333                }
334            }
335            // Stop at the operator
336            if let Some(token) = child.as_token() {
337                if token.kind() == OPERATOR {
338                    break;
339                }
340            }
341        }
342
343        // Fallback to old parsing logic for backward compatibility
344        let mut result = Vec::new();
345        let mut tokens = self
346            .syntax()
347            .children_with_tokens()
348            .take_while(|it| it.as_token().map(|t| t.kind() != OPERATOR).unwrap_or(true))
349            .peekable();
350
351        while let Some(token) = tokens.peek().cloned() {
352            if let Some(node) = token.as_node() {
353                tokens.next(); // Consume the node
354                if node.kind() == EXPR {
355                    // Handle when the target is an expression node
356                    let mut var_content = String::new();
357                    for child in node.children_with_tokens() {
358                        if let Some(t) = child.as_token() {
359                            var_content.push_str(t.text());
360                        }
361                    }
362                    if !var_content.is_empty() {
363                        result.push(var_content);
364                    }
365                }
366            } else if let Some(t) = token.as_token() {
367                if t.kind() == DOLLAR {
368                    if let Some(var_ref) = self.collect_variable_reference(&mut tokens) {
369                        result.push(var_ref);
370                    }
371                } else if t.kind() == IDENTIFIER {
372                    // Check if this identifier is followed by archive members
373                    let ident_text = t.text().to_string();
374                    tokens.next(); // Consume the identifier
375
376                    // Peek ahead to see if we have archive member syntax
377                    if let Some(next) = tokens.peek() {
378                        if let Some(next_token) = next.as_token() {
379                            if next_token.kind() == LPAREN {
380                                // This is an archive member target, collect the whole thing
381                                let mut archive_target = ident_text;
382                                archive_target.push_str(next_token.text()); // Add '('
383                                tokens.next(); // Consume LPAREN
384
385                                // Collect everything until RPAREN
386                                while let Some(token) = tokens.peek() {
387                                    if let Some(node) = token.as_node() {
388                                        if node.kind() == ARCHIVE_MEMBERS {
389                                            archive_target.push_str(&node.text().to_string());
390                                            tokens.next();
391                                        } else {
392                                            tokens.next();
393                                        }
394                                    } else if let Some(t) = token.as_token() {
395                                        if t.kind() == RPAREN {
396                                            archive_target.push_str(t.text());
397                                            tokens.next();
398                                            break;
399                                        } else {
400                                            tokens.next();
401                                        }
402                                    } else {
403                                        break;
404                                    }
405                                }
406                                result.push(archive_target);
407                            } else {
408                                // Regular identifier
409                                result.push(ident_text);
410                            }
411                        } else {
412                            // Regular identifier
413                            result.push(ident_text);
414                        }
415                    } else {
416                        // Regular identifier
417                        result.push(ident_text);
418                    }
419                } else {
420                    tokens.next(); // Skip other token types
421                }
422            }
423        }
424        result.into_iter()
425    }
426
427    /// Get the prerequisites in the rule
428    ///
429    /// # Example
430    /// ```
431    /// use makefile_lossless::Rule;
432    /// let rule: Rule = "rule: dependency\n\tcommand".parse().unwrap();
433    /// assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
434    /// ```
435    pub fn prerequisites(&self) -> impl Iterator<Item = String> + '_ {
436        // Find PREREQUISITES node after OPERATOR token
437        let mut found_operator = false;
438        let mut prerequisites_node = None;
439
440        for element in self.syntax().children_with_tokens() {
441            if let Some(token) = element.as_token() {
442                if token.kind() == OPERATOR {
443                    found_operator = true;
444                }
445            } else if let Some(node) = element.as_node() {
446                if found_operator && node.kind() == PREREQUISITES {
447                    prerequisites_node = Some(node.clone());
448                    break;
449                }
450            }
451        }
452
453        let result: Vec<String> = if let Some(prereqs) = prerequisites_node {
454            // Iterate over PREREQUISITE child nodes
455            prereqs
456                .children()
457                .filter(|child| child.kind() == PREREQUISITE)
458                .map(|child| child.text().to_string().trim().to_string())
459                .collect()
460        } else {
461            Vec::new()
462        };
463
464        result.into_iter()
465    }
466
467    /// Get the commands in the rule
468    ///
469    /// # Example
470    /// ```
471    /// use makefile_lossless::Rule;
472    /// let rule: Rule = "rule: dependency\n\tcommand".parse().unwrap();
473    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
474    /// ```
475    pub fn recipes(&self) -> impl Iterator<Item = String> {
476        self.recipe_nodes().map(|r| r.text())
477    }
478
479    /// Get recipe nodes with line/column information
480    ///
481    /// Returns an iterator over `Recipe` AST nodes, which support the `line()`, `column()`,
482    /// and `line_col()` methods to get position information.
483    ///
484    /// # Example
485    /// ```
486    /// use makefile_lossless::Rule;
487    ///
488    /// let rule_text = "test:\n\techo line1\n\techo line2\n";
489    /// let rule: Rule = rule_text.parse().unwrap();
490    ///
491    /// let recipe_nodes: Vec<_> = rule.recipe_nodes().collect();
492    /// assert_eq!(recipe_nodes.len(), 2);
493    /// assert_eq!(recipe_nodes[0].text(), "echo line1");
494    /// assert_eq!(recipe_nodes[0].line(), 1); // 0-indexed
495    /// assert_eq!(recipe_nodes[1].text(), "echo line2");
496    /// assert_eq!(recipe_nodes[1].line(), 2);
497    /// ```
498    pub fn recipe_nodes(&self) -> impl Iterator<Item = Recipe> {
499        self.syntax()
500            .children()
501            .filter(|it| it.kind() == RECIPE)
502            .filter_map(Recipe::cast)
503    }
504
505    /// Get all items (recipe lines and conditionals) in the rule's body
506    ///
507    /// This method iterates through the rule's body and yields both recipe lines
508    /// and any conditionals that appear within the rule.
509    ///
510    /// # Example
511    /// ```
512    /// use makefile_lossless::{Rule, RuleItem};
513    ///
514    /// let rule_text = r#"test:
515    /// 	echo "before"
516    /// ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
517    /// 	./run-tests
518    /// endif
519    /// 	echo "after"
520    /// "#;
521    /// let rule: Rule = rule_text.parse().unwrap();
522    ///
523    /// let items: Vec<_> = rule.items().collect();
524    /// assert_eq!(items.len(), 3); // recipe, conditional, recipe
525    ///
526    /// match &items[0] {
527    ///     RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
528    ///     _ => panic!("Expected recipe"),
529    /// }
530    ///
531    /// match &items[1] {
532    ///     RuleItem::Conditional(_) => {},
533    ///     _ => panic!("Expected conditional"),
534    /// }
535    ///
536    /// match &items[2] {
537    ///     RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
538    ///     _ => panic!("Expected recipe"),
539    /// }
540    /// ```
541    pub fn items(&self) -> impl Iterator<Item = RuleItem> + '_ {
542        self.syntax()
543            .children()
544            .filter(|n| n.kind() == RECIPE || n.kind() == CONDITIONAL)
545            .filter_map(RuleItem::cast)
546    }
547
548    /// Replace the command at index i with a new line
549    ///
550    /// # Example
551    /// ```
552    /// use makefile_lossless::Rule;
553    /// let mut rule: Rule = "rule: dependency\n\tcommand".parse().unwrap();
554    /// rule.replace_command(0, "new command");
555    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["new command"]);
556    /// ```
557    pub fn replace_command(&mut self, i: usize, line: &str) -> bool {
558        // Collect all RECIPE nodes (matching the indexing used by recipe_nodes())
559        let recipes: Vec<_> = self
560            .syntax()
561            .children()
562            .filter(|n| n.kind() == RECIPE)
563            .collect();
564
565        if i >= recipes.len() {
566            return false;
567        }
568
569        // Get the target RECIPE node and its index among all siblings
570        let target_node = &recipes[i];
571        let target_index = target_node.index();
572
573        let mut builder = GreenNodeBuilder::new();
574        builder.start_node(RECIPE.into());
575        builder.token(INDENT.into(), "\t");
576        builder.token(TEXT.into(), line);
577        builder.token(NEWLINE.into(), "\n");
578        builder.finish_node();
579
580        let syntax = SyntaxNode::new_root_mut(builder.finish());
581
582        self.syntax()
583            .splice_children(target_index..target_index + 1, vec![syntax.into()]);
584
585        true
586    }
587
588    /// Add a new command to the rule
589    ///
590    /// # Example
591    /// ```
592    /// use makefile_lossless::Rule;
593    /// let mut rule: Rule = "rule: dependency\n\tcommand".parse().unwrap();
594    /// rule.push_command("command2");
595    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command", "command2"]);
596    /// ```
597    pub fn push_command(&mut self, line: &str) {
598        // Find the latest RECIPE entry, then append the new line after it.
599        let index = self
600            .syntax()
601            .children_with_tokens()
602            .filter(|it| it.kind() == RECIPE)
603            .last();
604
605        let index = index.map_or_else(
606            || self.syntax().children_with_tokens().count(),
607            |it| it.index() + 1,
608        );
609
610        let mut builder = GreenNodeBuilder::new();
611        builder.start_node(RECIPE.into());
612        builder.token(INDENT.into(), "\t");
613        builder.token(TEXT.into(), line);
614        builder.token(NEWLINE.into(), "\n");
615        builder.finish_node();
616        let syntax = SyntaxNode::new_root_mut(builder.finish());
617
618        self.syntax()
619            .splice_children(index..index, vec![syntax.into()]);
620    }
621
622    /// Remove command at given index
623    ///
624    /// # Example
625    /// ```
626    /// use makefile_lossless::Rule;
627    /// let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
628    /// rule.remove_command(0);
629    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command2"]);
630    /// ```
631    pub fn remove_command(&mut self, index: usize) -> bool {
632        let recipes: Vec<_> = self
633            .syntax()
634            .children()
635            .filter(|n| n.kind() == RECIPE)
636            .collect();
637
638        if index >= recipes.len() {
639            return false;
640        }
641
642        let target_node = &recipes[index];
643        let target_index = target_node.index();
644
645        self.syntax()
646            .splice_children(target_index..target_index + 1, vec![]);
647        true
648    }
649
650    /// Insert command at given index
651    ///
652    /// # Example
653    /// ```
654    /// use makefile_lossless::Rule;
655    /// let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
656    /// rule.insert_command(1, "inserted_command");
657    /// let recipes: Vec<_> = rule.recipes().collect();
658    /// assert_eq!(recipes, vec!["command1", "inserted_command", "command2"]);
659    /// ```
660    pub fn insert_command(&mut self, index: usize, line: &str) -> bool {
661        let recipes: Vec<_> = self
662            .syntax()
663            .children()
664            .filter(|n| n.kind() == RECIPE)
665            .collect();
666
667        if index > recipes.len() {
668            return false;
669        }
670
671        let target_index = if index == recipes.len() {
672            // Insert at the end - find position after last recipe
673            recipes.last().map(|n| n.index() + 1).unwrap_or_else(|| {
674                // No recipes exist, insert after the rule header
675                self.syntax().children_with_tokens().count()
676            })
677        } else {
678            // Insert before the recipe at the given index
679            recipes[index].index()
680        };
681
682        let mut builder = GreenNodeBuilder::new();
683        builder.start_node(RECIPE.into());
684        builder.token(INDENT.into(), "\t");
685        builder.token(TEXT.into(), line);
686        builder.token(NEWLINE.into(), "\n");
687        builder.finish_node();
688        let syntax = SyntaxNode::new_root_mut(builder.finish());
689
690        self.syntax()
691            .splice_children(target_index..target_index, vec![syntax.into()]);
692        true
693    }
694
695    /// Get the number of commands/recipes in this rule
696    ///
697    /// # Example
698    /// ```
699    /// use makefile_lossless::Rule;
700    /// let rule: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
701    /// assert_eq!(rule.recipe_count(), 2);
702    /// ```
703    pub fn recipe_count(&self) -> usize {
704        self.syntax()
705            .children()
706            .filter(|n| n.kind() == RECIPE)
707            .count()
708    }
709
710    /// Clear all commands from this rule
711    ///
712    /// # Example
713    /// ```
714    /// use makefile_lossless::Rule;
715    /// let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
716    /// rule.clear_commands();
717    /// assert_eq!(rule.recipe_count(), 0);
718    /// ```
719    pub fn clear_commands(&mut self) {
720        let recipes: Vec<_> = self
721            .syntax()
722            .children()
723            .filter(|n| n.kind() == RECIPE)
724            .collect();
725
726        if recipes.is_empty() {
727            return;
728        }
729
730        // Remove all recipes in reverse order to maintain correct indices
731        for recipe in recipes.iter().rev() {
732            let index = recipe.index();
733            self.syntax().splice_children(index..index + 1, vec![]);
734        }
735    }
736
737    /// Remove a prerequisite from this rule
738    ///
739    /// Returns `true` if the prerequisite was found and removed, `false` if it wasn't found.
740    ///
741    /// # Example
742    /// ```
743    /// use makefile_lossless::Rule;
744    /// let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
745    /// assert!(rule.remove_prerequisite("dep2").unwrap());
746    /// assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1", "dep3"]);
747    /// assert!(!rule.remove_prerequisite("nonexistent").unwrap());
748    /// ```
749    pub fn remove_prerequisite(&mut self, target: &str) -> Result<bool, Error> {
750        // Find the PREREQUISITES node after the OPERATOR
751        let mut found_operator = false;
752        let mut prereqs_node = None;
753
754        for child in self.syntax().children_with_tokens() {
755            if let Some(token) = child.as_token() {
756                if token.kind() == OPERATOR {
757                    found_operator = true;
758                }
759            } else if let Some(node) = child.as_node() {
760                if found_operator && node.kind() == PREREQUISITES {
761                    prereqs_node = Some(node.clone());
762                    break;
763                }
764            }
765        }
766
767        let prereqs_node = match prereqs_node {
768            Some(node) => node,
769            None => return Ok(false), // No prerequisites
770        };
771
772        // Collect current prerequisites
773        let current_prereqs: Vec<String> = self.prerequisites().collect();
774
775        // Check if target exists
776        if !current_prereqs.iter().any(|p| p == target) {
777            return Ok(false);
778        }
779
780        // Filter out the target
781        let new_prereqs: Vec<String> = current_prereqs
782            .into_iter()
783            .filter(|p| p != target)
784            .collect();
785
786        // Check if the existing PREREQUISITES node starts with whitespace
787        let has_leading_whitespace = prereqs_node
788            .children_with_tokens()
789            .next()
790            .map(|e| matches!(e.as_token().map(|t| t.kind()), Some(WHITESPACE)))
791            .unwrap_or(false);
792
793        // Rebuild the PREREQUISITES node with the new prerequisites
794        let prereqs_index = prereqs_node.index();
795        let new_prereqs_node = build_prerequisites_node(&new_prereqs, has_leading_whitespace);
796
797        self.syntax().splice_children(
798            prereqs_index..prereqs_index + 1,
799            vec![new_prereqs_node.into()],
800        );
801
802        Ok(true)
803    }
804
805    /// Add a prerequisite to this rule
806    ///
807    /// # Example
808    /// ```
809    /// use makefile_lossless::Rule;
810    /// let mut rule: Rule = "target: dep1\n".parse().unwrap();
811    /// rule.add_prerequisite("dep2").unwrap();
812    /// assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1", "dep2"]);
813    /// ```
814    pub fn add_prerequisite(&mut self, target: &str) -> Result<(), Error> {
815        let mut current_prereqs: Vec<String> = self.prerequisites().collect();
816        current_prereqs.push(target.to_string());
817        self.set_prerequisites(current_prereqs.iter().map(|s| s.as_str()).collect())
818    }
819
820    /// Set the prerequisites for this rule, replacing any existing ones
821    ///
822    /// # Example
823    /// ```
824    /// use makefile_lossless::Rule;
825    /// let mut rule: Rule = "target: old_dep\n".parse().unwrap();
826    /// rule.set_prerequisites(vec!["new_dep1", "new_dep2"]).unwrap();
827    /// assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["new_dep1", "new_dep2"]);
828    /// ```
829    pub fn set_prerequisites(&mut self, prereqs: Vec<&str>) -> Result<(), Error> {
830        // Find the PREREQUISITES node after the OPERATOR, or the position to insert it
831        let mut prereqs_index = None;
832        let mut operator_found = false;
833
834        for child in self.syntax().children_with_tokens() {
835            if let Some(token) = child.as_token() {
836                if token.kind() == OPERATOR {
837                    operator_found = true;
838                }
839            } else if let Some(node) = child.as_node() {
840                if operator_found && node.kind() == PREREQUISITES {
841                    prereqs_index = Some((node.index(), true)); // (index, exists)
842                    break;
843                }
844            }
845        }
846
847        match prereqs_index {
848            Some((idx, true)) => {
849                // Check if there's whitespace between OPERATOR and PREREQUISITES
850                let has_external_whitespace = self
851                    .syntax()
852                    .children_with_tokens()
853                    .skip_while(|e| !matches!(e.as_token().map(|t| t.kind()), Some(OPERATOR)))
854                    .nth(1) // Skip the OPERATOR itself and get next
855                    .map(|e| matches!(e.as_token().map(|t| t.kind()), Some(WHITESPACE)))
856                    .unwrap_or(false);
857
858                let new_prereqs = build_prerequisites_node(
859                    &prereqs.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
860                    !has_external_whitespace, // Include leading space only if no external whitespace
861                );
862                self.syntax()
863                    .splice_children(idx..idx + 1, vec![new_prereqs.into()]);
864            }
865            _ => {
866                // Insert new PREREQUISITES (need leading space inside node)
867                let new_prereqs = build_prerequisites_node(
868                    &prereqs.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
869                    true, // Include leading space
870                );
871
872                let insert_pos = self
873                    .syntax()
874                    .children_with_tokens()
875                    .position(|t| t.as_token().map(|t| t.kind() == OPERATOR).unwrap_or(false))
876                    .map(|p| p + 1)
877                    .ok_or_else(|| {
878                        Error::Parse(ParseError {
879                            errors: vec![ErrorInfo {
880                                message: "No operator found in rule".to_string(),
881                                line: 1,
882                                context: "set_prerequisites".to_string(),
883                            }],
884                        })
885                    })?;
886
887                self.syntax()
888                    .splice_children(insert_pos..insert_pos, vec![new_prereqs.into()]);
889            }
890        }
891
892        Ok(())
893    }
894
895    /// Rename a target in this rule
896    ///
897    /// Returns `Ok(true)` if the target was found and renamed, `Ok(false)` if the target was not found.
898    ///
899    /// # Example
900    /// ```
901    /// use makefile_lossless::Rule;
902    /// let mut rule: Rule = "old_target: dependency\n\tcommand".parse().unwrap();
903    /// rule.rename_target("old_target", "new_target").unwrap();
904    /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
905    /// ```
906    pub fn rename_target(&mut self, old_name: &str, new_name: &str) -> Result<bool, Error> {
907        // Collect current targets
908        let current_targets: Vec<String> = self.targets().collect();
909
910        // Check if the target to rename exists
911        if !current_targets.iter().any(|t| t == old_name) {
912            return Ok(false);
913        }
914
915        // Create new target list with the renamed target
916        let new_targets: Vec<String> = current_targets
917            .into_iter()
918            .map(|t| {
919                if t == old_name {
920                    new_name.to_string()
921                } else {
922                    t
923                }
924            })
925            .collect();
926
927        // Find the TARGETS node
928        let mut targets_index = None;
929        for (idx, child) in self.syntax().children_with_tokens().enumerate() {
930            if let Some(node) = child.as_node() {
931                if node.kind() == TARGETS {
932                    targets_index = Some(idx);
933                    break;
934                }
935            }
936        }
937
938        let targets_index = targets_index.ok_or_else(|| {
939            Error::Parse(ParseError {
940                errors: vec![ErrorInfo {
941                    message: "No TARGETS node found in rule".to_string(),
942                    line: 1,
943                    context: "rename_target".to_string(),
944                }],
945            })
946        })?;
947
948        // Build new targets node
949        let new_targets_node = build_targets_node(&new_targets);
950
951        // Replace the TARGETS node
952        self.syntax().splice_children(
953            targets_index..targets_index + 1,
954            vec![new_targets_node.into()],
955        );
956
957        Ok(true)
958    }
959
960    /// Add a target to this rule
961    ///
962    /// # Example
963    /// ```
964    /// use makefile_lossless::Rule;
965    /// let mut rule: Rule = "target1: dependency\n\tcommand".parse().unwrap();
966    /// rule.add_target("target2").unwrap();
967    /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target1", "target2"]);
968    /// ```
969    pub fn add_target(&mut self, target: &str) -> Result<(), Error> {
970        let mut current_targets: Vec<String> = self.targets().collect();
971        current_targets.push(target.to_string());
972        self.set_targets(current_targets.iter().map(|s| s.as_str()).collect())
973    }
974
975    /// Set the targets for this rule, replacing any existing ones
976    ///
977    /// Returns an error if the targets list is empty (rules must have at least one target).
978    ///
979    /// # Example
980    /// ```
981    /// use makefile_lossless::Rule;
982    /// let mut rule: Rule = "old_target: dependency\n\tcommand".parse().unwrap();
983    /// rule.set_targets(vec!["new_target1", "new_target2"]).unwrap();
984    /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target1", "new_target2"]);
985    /// ```
986    pub fn set_targets(&mut self, targets: Vec<&str>) -> Result<(), Error> {
987        // Ensure targets list is not empty
988        if targets.is_empty() {
989            return Err(Error::Parse(ParseError {
990                errors: vec![ErrorInfo {
991                    message: "Cannot set empty targets list for a rule".to_string(),
992                    line: 1,
993                    context: "set_targets".to_string(),
994                }],
995            }));
996        }
997
998        // Find the TARGETS node
999        let mut targets_index = None;
1000        for (idx, child) in self.syntax().children_with_tokens().enumerate() {
1001            if let Some(node) = child.as_node() {
1002                if node.kind() == TARGETS {
1003                    targets_index = Some(idx);
1004                    break;
1005                }
1006            }
1007        }
1008
1009        let targets_index = targets_index.ok_or_else(|| {
1010            Error::Parse(ParseError {
1011                errors: vec![ErrorInfo {
1012                    message: "No TARGETS node found in rule".to_string(),
1013                    line: 1,
1014                    context: "set_targets".to_string(),
1015                }],
1016            })
1017        })?;
1018
1019        // Build new targets node
1020        let new_targets_node =
1021            build_targets_node(&targets.iter().map(|s| s.to_string()).collect::<Vec<_>>());
1022
1023        // Replace the TARGETS node
1024        self.syntax().splice_children(
1025            targets_index..targets_index + 1,
1026            vec![new_targets_node.into()],
1027        );
1028
1029        Ok(())
1030    }
1031
1032    /// Check if this rule has a specific target
1033    ///
1034    /// # Example
1035    /// ```
1036    /// use makefile_lossless::Rule;
1037    /// let rule: Rule = "target1 target2: dependency\n\tcommand".parse().unwrap();
1038    /// assert!(rule.has_target("target1"));
1039    /// assert!(rule.has_target("target2"));
1040    /// assert!(!rule.has_target("target3"));
1041    /// ```
1042    pub fn has_target(&self, target: &str) -> bool {
1043        self.targets().any(|t| t == target)
1044    }
1045
1046    /// Remove a target from this rule
1047    ///
1048    /// Returns `Ok(true)` if the target was found and removed, `Ok(false)` if the target was not found.
1049    /// Returns an error if attempting to remove the last target (rules must have at least one target).
1050    ///
1051    /// # Example
1052    /// ```
1053    /// use makefile_lossless::Rule;
1054    /// let mut rule: Rule = "target1 target2: dependency\n\tcommand".parse().unwrap();
1055    /// rule.remove_target("target1").unwrap();
1056    /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
1057    /// ```
1058    pub fn remove_target(&mut self, target_name: &str) -> Result<bool, Error> {
1059        // Collect current targets
1060        let current_targets: Vec<String> = self.targets().collect();
1061
1062        // Check if the target exists
1063        if !current_targets.iter().any(|t| t == target_name) {
1064            return Ok(false);
1065        }
1066
1067        // Filter out the target to remove
1068        let new_targets: Vec<String> = current_targets
1069            .into_iter()
1070            .filter(|t| t != target_name)
1071            .collect();
1072
1073        // If no targets remain, return an error
1074        if new_targets.is_empty() {
1075            return Err(Error::Parse(ParseError {
1076                errors: vec![ErrorInfo {
1077                    message: "Cannot remove all targets from a rule".to_string(),
1078                    line: 1,
1079                    context: "remove_target".to_string(),
1080                }],
1081            }));
1082        }
1083
1084        // Find the TARGETS node
1085        let mut targets_index = None;
1086        for (idx, child) in self.syntax().children_with_tokens().enumerate() {
1087            if let Some(node) = child.as_node() {
1088                if node.kind() == TARGETS {
1089                    targets_index = Some(idx);
1090                    break;
1091                }
1092            }
1093        }
1094
1095        let targets_index = targets_index.ok_or_else(|| {
1096            Error::Parse(ParseError {
1097                errors: vec![ErrorInfo {
1098                    message: "No TARGETS node found in rule".to_string(),
1099                    line: 1,
1100                    context: "remove_target".to_string(),
1101                }],
1102            })
1103        })?;
1104
1105        // Build new targets node
1106        let new_targets_node = build_targets_node(&new_targets);
1107
1108        // Replace the TARGETS node
1109        self.syntax().splice_children(
1110            targets_index..targets_index + 1,
1111            vec![new_targets_node.into()],
1112        );
1113
1114        Ok(true)
1115    }
1116
1117    /// Remove this rule from its parent Makefile
1118    ///
1119    /// # Example
1120    /// ```
1121    /// use makefile_lossless::Makefile;
1122    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1123    /// let rule = makefile.rules().next().unwrap();
1124    /// rule.remove().unwrap();
1125    /// assert_eq!(makefile.rules().count(), 1);
1126    /// ```
1127    ///
1128    /// This will also remove any preceding comments and up to 1 empty line before the rule.
1129    /// When removing the last rule in a makefile, this will also trim any trailing blank lines
1130    /// from the previous rule to avoid leaving extra whitespace at the end of the file.
1131    pub fn remove(self) -> Result<(), Error> {
1132        let parent = self.syntax().parent().ok_or_else(|| {
1133            Error::Parse(ParseError {
1134                errors: vec![ErrorInfo {
1135                    message: "Rule has no parent".to_string(),
1136                    line: 1,
1137                    context: "remove".to_string(),
1138                }],
1139            })
1140        })?;
1141
1142        // Check if this is the last rule by seeing if there's any next sibling that's a RULE
1143        let is_last_rule = self
1144            .syntax()
1145            .siblings(rowan::Direction::Next)
1146            .skip(1) // Skip self
1147            .all(|sibling| sibling.kind() != RULE);
1148
1149        remove_with_preceding_comments(self.syntax(), &parent);
1150
1151        // If we removed the last rule, trim trailing newlines from the last remaining RULE
1152        if is_last_rule {
1153            // Find the last RULE node in the parent
1154            if let Some(last_rule_node) = parent
1155                .children()
1156                .filter(|child| child.kind() == RULE)
1157                .last()
1158            {
1159                trim_trailing_newlines(&last_rule_node);
1160            }
1161        }
1162
1163        Ok(())
1164    }
1165}
1166
1167impl Default for Makefile {
1168    fn default() -> Self {
1169        Self::new()
1170    }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175    use crate::Makefile;
1176
1177    #[test]
1178    fn test_rules_with_pipe_in_shell_continuation() {
1179        let input = "VAR ?= $(shell cmd | \\\n\t\tsed -e 's/foo/bar/')\n\n%:\n\tdh $@\n";
1180        let (makefile, _errors) = Makefile::from_str_relaxed(input);
1181        let rules: Vec<_> = makefile.rules().collect();
1182        assert_eq!(rules.len(), 1, "Expected 1 rule");
1183    }
1184
1185    #[test]
1186    fn test_roundtrip_with_backslash_in_variable_continuation() {
1187        let input = "DEB_UPSTREAM_VERSION ?= $(shell dpkg-parsechangelog | \\\n\
1188                     \t\t\t  sed -rne 's,^Version: ([^-]+).*,\\1,p')\n\
1189                     \n\
1190                     %:\n\
1191                     \tdh $@ --with autoreconf\n\
1192                     \n\
1193                     override_dh_strip:\n\
1194                     \tdh_strip --dbg-package=f2fs-tools-dbg\n";
1195        let (makefile, errors) = Makefile::from_str_relaxed(input);
1196        assert!(errors.is_empty(), "Unexpected parse errors: {:?}", errors);
1197
1198        let rules: Vec<_> = makefile.rules().collect();
1199        assert_eq!(rules.len(), 2, "Expected 2 rules, got {}", rules.len());
1200
1201        let output = makefile.to_string();
1202        assert_eq!(input, output, "Round-trip failed");
1203    }
1204}