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}