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