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.recipe_nodes().map(|r| r.text())
458 }
459
460 /// Get recipe nodes with line/column information
461 ///
462 /// Returns an iterator over `Recipe` AST nodes, which support the `line()`, `column()`,
463 /// and `line_col()` methods to get position information.
464 ///
465 /// # Example
466 /// ```
467 /// use makefile_lossless::Rule;
468 ///
469 /// let rule_text = "test:\n\techo line1\n\techo line2\n";
470 /// let rule: Rule = rule_text.parse().unwrap();
471 ///
472 /// let recipe_nodes: Vec<_> = rule.recipe_nodes().collect();
473 /// assert_eq!(recipe_nodes.len(), 2);
474 /// assert_eq!(recipe_nodes[0].text(), "echo line1");
475 /// assert_eq!(recipe_nodes[0].line(), 1); // 0-indexed
476 /// assert_eq!(recipe_nodes[1].text(), "echo line2");
477 /// assert_eq!(recipe_nodes[1].line(), 2);
478 /// ```
479 pub fn recipe_nodes(&self) -> impl Iterator<Item = Recipe> {
480 self.syntax()
481 .children()
482 .filter(|it| it.kind() == RECIPE)
483 .filter_map(Recipe::cast)
484 }
485
486 /// Get all items (recipe lines and conditionals) in the rule's body
487 ///
488 /// This method iterates through the rule's body and yields both recipe lines
489 /// and any conditionals that appear within the rule.
490 ///
491 /// # Example
492 /// ```
493 /// use makefile_lossless::{Rule, RuleItem};
494 ///
495 /// let rule_text = r#"test:
496 /// echo "before"
497 /// ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
498 /// ./run-tests
499 /// endif
500 /// echo "after"
501 /// "#;
502 /// let rule: Rule = rule_text.parse().unwrap();
503 ///
504 /// let items: Vec<_> = rule.items().collect();
505 /// assert_eq!(items.len(), 3); // recipe, conditional, recipe
506 ///
507 /// match &items[0] {
508 /// RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
509 /// _ => panic!("Expected recipe"),
510 /// }
511 ///
512 /// match &items[1] {
513 /// RuleItem::Conditional(_) => {},
514 /// _ => panic!("Expected conditional"),
515 /// }
516 ///
517 /// match &items[2] {
518 /// RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
519 /// _ => panic!("Expected recipe"),
520 /// }
521 /// ```
522 pub fn items(&self) -> impl Iterator<Item = RuleItem> + '_ {
523 self.syntax()
524 .children()
525 .filter(|n| n.kind() == RECIPE || n.kind() == CONDITIONAL)
526 .filter_map(RuleItem::cast)
527 }
528
529 /// Replace the command at index i with a new line
530 ///
531 /// # Example
532 /// ```
533 /// use makefile_lossless::Rule;
534 /// let mut rule: Rule = "rule: dependency\n\tcommand".parse().unwrap();
535 /// rule.replace_command(0, "new command");
536 /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["new command"]);
537 /// ```
538 pub fn replace_command(&mut self, i: usize, line: &str) -> bool {
539 // Collect all RECIPE nodes (matching the indexing used by recipe_nodes())
540 let recipes: Vec<_> = self
541 .syntax()
542 .children()
543 .filter(|n| n.kind() == RECIPE)
544 .collect();
545
546 if i >= recipes.len() {
547 return false;
548 }
549
550 // Get the target RECIPE node and its index among all siblings
551 let target_node = &recipes[i];
552 let target_index = target_node.index();
553
554 let mut builder = GreenNodeBuilder::new();
555 builder.start_node(RECIPE.into());
556 builder.token(INDENT.into(), "\t");
557 builder.token(TEXT.into(), line);
558 builder.token(NEWLINE.into(), "\n");
559 builder.finish_node();
560
561 let syntax = SyntaxNode::new_root_mut(builder.finish());
562
563 self.syntax()
564 .splice_children(target_index..target_index + 1, vec![syntax.into()]);
565
566 true
567 }
568
569 /// Add a new command to the rule
570 ///
571 /// # Example
572 /// ```
573 /// use makefile_lossless::Rule;
574 /// let mut rule: Rule = "rule: dependency\n\tcommand".parse().unwrap();
575 /// rule.push_command("command2");
576 /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command", "command2"]);
577 /// ```
578 pub fn push_command(&mut self, line: &str) {
579 // Find the latest RECIPE entry, then append the new line after it.
580 let index = self
581 .syntax()
582 .children_with_tokens()
583 .filter(|it| it.kind() == RECIPE)
584 .last();
585
586 let index = index.map_or_else(
587 || self.syntax().children_with_tokens().count(),
588 |it| it.index() + 1,
589 );
590
591 let mut builder = GreenNodeBuilder::new();
592 builder.start_node(RECIPE.into());
593 builder.token(INDENT.into(), "\t");
594 builder.token(TEXT.into(), line);
595 builder.token(NEWLINE.into(), "\n");
596 builder.finish_node();
597 let syntax = SyntaxNode::new_root_mut(builder.finish());
598
599 self.syntax()
600 .splice_children(index..index, vec![syntax.into()]);
601 }
602
603 /// Remove command at given index
604 ///
605 /// # Example
606 /// ```
607 /// use makefile_lossless::Rule;
608 /// let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
609 /// rule.remove_command(0);
610 /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command2"]);
611 /// ```
612 pub fn remove_command(&mut self, index: usize) -> bool {
613 let recipes: Vec<_> = self
614 .syntax()
615 .children()
616 .filter(|n| n.kind() == RECIPE)
617 .collect();
618
619 if index >= recipes.len() {
620 return false;
621 }
622
623 let target_node = &recipes[index];
624 let target_index = target_node.index();
625
626 self.syntax()
627 .splice_children(target_index..target_index + 1, vec![]);
628 true
629 }
630
631 /// Insert command at given index
632 ///
633 /// # Example
634 /// ```
635 /// use makefile_lossless::Rule;
636 /// let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
637 /// rule.insert_command(1, "inserted_command");
638 /// let recipes: Vec<_> = rule.recipes().collect();
639 /// assert_eq!(recipes, vec!["command1", "inserted_command", "command2"]);
640 /// ```
641 pub fn insert_command(&mut self, index: usize, line: &str) -> bool {
642 let recipes: Vec<_> = self
643 .syntax()
644 .children()
645 .filter(|n| n.kind() == RECIPE)
646 .collect();
647
648 if index > recipes.len() {
649 return false;
650 }
651
652 let target_index = if index == recipes.len() {
653 // Insert at the end - find position after last recipe
654 recipes.last().map(|n| n.index() + 1).unwrap_or_else(|| {
655 // No recipes exist, insert after the rule header
656 self.syntax().children_with_tokens().count()
657 })
658 } else {
659 // Insert before the recipe at the given index
660 recipes[index].index()
661 };
662
663 let mut builder = GreenNodeBuilder::new();
664 builder.start_node(RECIPE.into());
665 builder.token(INDENT.into(), "\t");
666 builder.token(TEXT.into(), line);
667 builder.token(NEWLINE.into(), "\n");
668 builder.finish_node();
669 let syntax = SyntaxNode::new_root_mut(builder.finish());
670
671 self.syntax()
672 .splice_children(target_index..target_index, vec![syntax.into()]);
673 true
674 }
675
676 /// Get the number of commands/recipes in this rule
677 ///
678 /// # Example
679 /// ```
680 /// use makefile_lossless::Rule;
681 /// let rule: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
682 /// assert_eq!(rule.recipe_count(), 2);
683 /// ```
684 pub fn recipe_count(&self) -> usize {
685 self.syntax()
686 .children()
687 .filter(|n| n.kind() == RECIPE)
688 .count()
689 }
690
691 /// Clear all commands from this rule
692 ///
693 /// # Example
694 /// ```
695 /// use makefile_lossless::Rule;
696 /// let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
697 /// rule.clear_commands();
698 /// assert_eq!(rule.recipe_count(), 0);
699 /// ```
700 pub fn clear_commands(&mut self) {
701 let recipes: Vec<_> = self
702 .syntax()
703 .children()
704 .filter(|n| n.kind() == RECIPE)
705 .collect();
706
707 if recipes.is_empty() {
708 return;
709 }
710
711 // Remove all recipes in reverse order to maintain correct indices
712 for recipe in recipes.iter().rev() {
713 let index = recipe.index();
714 self.syntax().splice_children(index..index + 1, vec![]);
715 }
716 }
717
718 /// Remove a prerequisite from this rule
719 ///
720 /// Returns `true` if the prerequisite was found and removed, `false` if it wasn't found.
721 ///
722 /// # Example
723 /// ```
724 /// use makefile_lossless::Rule;
725 /// let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
726 /// assert!(rule.remove_prerequisite("dep2").unwrap());
727 /// assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1", "dep3"]);
728 /// assert!(!rule.remove_prerequisite("nonexistent").unwrap());
729 /// ```
730 pub fn remove_prerequisite(&mut self, target: &str) -> Result<bool, Error> {
731 // Find the PREREQUISITES node after the OPERATOR
732 let mut found_operator = false;
733 let mut prereqs_node = None;
734
735 for child in self.syntax().children_with_tokens() {
736 if let Some(token) = child.as_token() {
737 if token.kind() == OPERATOR {
738 found_operator = true;
739 }
740 } else if let Some(node) = child.as_node() {
741 if found_operator && node.kind() == PREREQUISITES {
742 prereqs_node = Some(node.clone());
743 break;
744 }
745 }
746 }
747
748 let prereqs_node = match prereqs_node {
749 Some(node) => node,
750 None => return Ok(false), // No prerequisites
751 };
752
753 // Collect current prerequisites
754 let current_prereqs: Vec<String> = self.prerequisites().collect();
755
756 // Check if target exists
757 if !current_prereqs.iter().any(|p| p == target) {
758 return Ok(false);
759 }
760
761 // Filter out the target
762 let new_prereqs: Vec<String> = current_prereqs
763 .into_iter()
764 .filter(|p| p != target)
765 .collect();
766
767 // Check if the existing PREREQUISITES node starts with whitespace
768 let has_leading_whitespace = prereqs_node
769 .children_with_tokens()
770 .next()
771 .map(|e| matches!(e.as_token().map(|t| t.kind()), Some(WHITESPACE)))
772 .unwrap_or(false);
773
774 // Rebuild the PREREQUISITES node with the new prerequisites
775 let prereqs_index = prereqs_node.index();
776 let new_prereqs_node = build_prerequisites_node(&new_prereqs, has_leading_whitespace);
777
778 self.syntax().splice_children(
779 prereqs_index..prereqs_index + 1,
780 vec![new_prereqs_node.into()],
781 );
782
783 Ok(true)
784 }
785
786 /// Add a prerequisite to this rule
787 ///
788 /// # Example
789 /// ```
790 /// use makefile_lossless::Rule;
791 /// let mut rule: Rule = "target: dep1\n".parse().unwrap();
792 /// rule.add_prerequisite("dep2").unwrap();
793 /// assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1", "dep2"]);
794 /// ```
795 pub fn add_prerequisite(&mut self, target: &str) -> Result<(), Error> {
796 let mut current_prereqs: Vec<String> = self.prerequisites().collect();
797 current_prereqs.push(target.to_string());
798 self.set_prerequisites(current_prereqs.iter().map(|s| s.as_str()).collect())
799 }
800
801 /// Set the prerequisites for this rule, replacing any existing ones
802 ///
803 /// # Example
804 /// ```
805 /// use makefile_lossless::Rule;
806 /// let mut rule: Rule = "target: old_dep\n".parse().unwrap();
807 /// rule.set_prerequisites(vec!["new_dep1", "new_dep2"]).unwrap();
808 /// assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["new_dep1", "new_dep2"]);
809 /// ```
810 pub fn set_prerequisites(&mut self, prereqs: Vec<&str>) -> Result<(), Error> {
811 // Find the PREREQUISITES node after the OPERATOR, or the position to insert it
812 let mut prereqs_index = None;
813 let mut operator_found = false;
814
815 for child in self.syntax().children_with_tokens() {
816 if let Some(token) = child.as_token() {
817 if token.kind() == OPERATOR {
818 operator_found = true;
819 }
820 } else if let Some(node) = child.as_node() {
821 if operator_found && node.kind() == PREREQUISITES {
822 prereqs_index = Some((node.index(), true)); // (index, exists)
823 break;
824 }
825 }
826 }
827
828 match prereqs_index {
829 Some((idx, true)) => {
830 // Check if there's whitespace between OPERATOR and PREREQUISITES
831 let has_external_whitespace = self
832 .syntax()
833 .children_with_tokens()
834 .skip_while(|e| !matches!(e.as_token().map(|t| t.kind()), Some(OPERATOR)))
835 .nth(1) // Skip the OPERATOR itself and get next
836 .map(|e| matches!(e.as_token().map(|t| t.kind()), Some(WHITESPACE)))
837 .unwrap_or(false);
838
839 let new_prereqs = build_prerequisites_node(
840 &prereqs.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
841 !has_external_whitespace, // Include leading space only if no external whitespace
842 );
843 self.syntax()
844 .splice_children(idx..idx + 1, vec![new_prereqs.into()]);
845 }
846 _ => {
847 // Insert new PREREQUISITES (need leading space inside node)
848 let new_prereqs = build_prerequisites_node(
849 &prereqs.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
850 true, // Include leading space
851 );
852
853 let insert_pos = self
854 .syntax()
855 .children_with_tokens()
856 .position(|t| t.as_token().map(|t| t.kind() == OPERATOR).unwrap_or(false))
857 .map(|p| p + 1)
858 .ok_or_else(|| {
859 Error::Parse(ParseError {
860 errors: vec![ErrorInfo {
861 message: "No operator found in rule".to_string(),
862 line: 1,
863 context: "set_prerequisites".to_string(),
864 }],
865 })
866 })?;
867
868 self.syntax()
869 .splice_children(insert_pos..insert_pos, vec![new_prereqs.into()]);
870 }
871 }
872
873 Ok(())
874 }
875
876 /// Rename a target in this rule
877 ///
878 /// Returns `Ok(true)` if the target was found and renamed, `Ok(false)` if the target was not found.
879 ///
880 /// # Example
881 /// ```
882 /// use makefile_lossless::Rule;
883 /// let mut rule: Rule = "old_target: dependency\n\tcommand".parse().unwrap();
884 /// rule.rename_target("old_target", "new_target").unwrap();
885 /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
886 /// ```
887 pub fn rename_target(&mut self, old_name: &str, new_name: &str) -> Result<bool, Error> {
888 // Collect current targets
889 let current_targets: Vec<String> = self.targets().collect();
890
891 // Check if the target to rename exists
892 if !current_targets.iter().any(|t| t == old_name) {
893 return Ok(false);
894 }
895
896 // Create new target list with the renamed target
897 let new_targets: Vec<String> = current_targets
898 .into_iter()
899 .map(|t| {
900 if t == old_name {
901 new_name.to_string()
902 } else {
903 t
904 }
905 })
906 .collect();
907
908 // Find the TARGETS node
909 let mut targets_index = None;
910 for (idx, child) in self.syntax().children_with_tokens().enumerate() {
911 if let Some(node) = child.as_node() {
912 if node.kind() == TARGETS {
913 targets_index = Some(idx);
914 break;
915 }
916 }
917 }
918
919 let targets_index = targets_index.ok_or_else(|| {
920 Error::Parse(ParseError {
921 errors: vec![ErrorInfo {
922 message: "No TARGETS node found in rule".to_string(),
923 line: 1,
924 context: "rename_target".to_string(),
925 }],
926 })
927 })?;
928
929 // Build new targets node
930 let new_targets_node = build_targets_node(&new_targets);
931
932 // Replace the TARGETS node
933 self.syntax().splice_children(
934 targets_index..targets_index + 1,
935 vec![new_targets_node.into()],
936 );
937
938 Ok(true)
939 }
940
941 /// Add a target to this rule
942 ///
943 /// # Example
944 /// ```
945 /// use makefile_lossless::Rule;
946 /// let mut rule: Rule = "target1: dependency\n\tcommand".parse().unwrap();
947 /// rule.add_target("target2").unwrap();
948 /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target1", "target2"]);
949 /// ```
950 pub fn add_target(&mut self, target: &str) -> Result<(), Error> {
951 let mut current_targets: Vec<String> = self.targets().collect();
952 current_targets.push(target.to_string());
953 self.set_targets(current_targets.iter().map(|s| s.as_str()).collect())
954 }
955
956 /// Set the targets for this rule, replacing any existing ones
957 ///
958 /// Returns an error if the targets list is empty (rules must have at least one target).
959 ///
960 /// # Example
961 /// ```
962 /// use makefile_lossless::Rule;
963 /// let mut rule: Rule = "old_target: dependency\n\tcommand".parse().unwrap();
964 /// rule.set_targets(vec!["new_target1", "new_target2"]).unwrap();
965 /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target1", "new_target2"]);
966 /// ```
967 pub fn set_targets(&mut self, targets: Vec<&str>) -> Result<(), Error> {
968 // Ensure targets list is not empty
969 if targets.is_empty() {
970 return Err(Error::Parse(ParseError {
971 errors: vec![ErrorInfo {
972 message: "Cannot set empty targets list for a rule".to_string(),
973 line: 1,
974 context: "set_targets".to_string(),
975 }],
976 }));
977 }
978
979 // Find the TARGETS node
980 let mut targets_index = None;
981 for (idx, child) in self.syntax().children_with_tokens().enumerate() {
982 if let Some(node) = child.as_node() {
983 if node.kind() == TARGETS {
984 targets_index = Some(idx);
985 break;
986 }
987 }
988 }
989
990 let targets_index = targets_index.ok_or_else(|| {
991 Error::Parse(ParseError {
992 errors: vec![ErrorInfo {
993 message: "No TARGETS node found in rule".to_string(),
994 line: 1,
995 context: "set_targets".to_string(),
996 }],
997 })
998 })?;
999
1000 // Build new targets node
1001 let new_targets_node =
1002 build_targets_node(&targets.iter().map(|s| s.to_string()).collect::<Vec<_>>());
1003
1004 // Replace the TARGETS node
1005 self.syntax().splice_children(
1006 targets_index..targets_index + 1,
1007 vec![new_targets_node.into()],
1008 );
1009
1010 Ok(())
1011 }
1012
1013 /// Check if this rule has a specific target
1014 ///
1015 /// # Example
1016 /// ```
1017 /// use makefile_lossless::Rule;
1018 /// let rule: Rule = "target1 target2: dependency\n\tcommand".parse().unwrap();
1019 /// assert!(rule.has_target("target1"));
1020 /// assert!(rule.has_target("target2"));
1021 /// assert!(!rule.has_target("target3"));
1022 /// ```
1023 pub fn has_target(&self, target: &str) -> bool {
1024 self.targets().any(|t| t == target)
1025 }
1026
1027 /// Remove a target from this rule
1028 ///
1029 /// Returns `Ok(true)` if the target was found and removed, `Ok(false)` if the target was not found.
1030 /// Returns an error if attempting to remove the last target (rules must have at least one target).
1031 ///
1032 /// # Example
1033 /// ```
1034 /// use makefile_lossless::Rule;
1035 /// let mut rule: Rule = "target1 target2: dependency\n\tcommand".parse().unwrap();
1036 /// rule.remove_target("target1").unwrap();
1037 /// assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
1038 /// ```
1039 pub fn remove_target(&mut self, target_name: &str) -> Result<bool, Error> {
1040 // Collect current targets
1041 let current_targets: Vec<String> = self.targets().collect();
1042
1043 // Check if the target exists
1044 if !current_targets.iter().any(|t| t == target_name) {
1045 return Ok(false);
1046 }
1047
1048 // Filter out the target to remove
1049 let new_targets: Vec<String> = current_targets
1050 .into_iter()
1051 .filter(|t| t != target_name)
1052 .collect();
1053
1054 // If no targets remain, return an error
1055 if new_targets.is_empty() {
1056 return Err(Error::Parse(ParseError {
1057 errors: vec![ErrorInfo {
1058 message: "Cannot remove all targets from a rule".to_string(),
1059 line: 1,
1060 context: "remove_target".to_string(),
1061 }],
1062 }));
1063 }
1064
1065 // Find the TARGETS node
1066 let mut targets_index = None;
1067 for (idx, child) in self.syntax().children_with_tokens().enumerate() {
1068 if let Some(node) = child.as_node() {
1069 if node.kind() == TARGETS {
1070 targets_index = Some(idx);
1071 break;
1072 }
1073 }
1074 }
1075
1076 let targets_index = targets_index.ok_or_else(|| {
1077 Error::Parse(ParseError {
1078 errors: vec![ErrorInfo {
1079 message: "No TARGETS node found in rule".to_string(),
1080 line: 1,
1081 context: "remove_target".to_string(),
1082 }],
1083 })
1084 })?;
1085
1086 // Build new targets node
1087 let new_targets_node = build_targets_node(&new_targets);
1088
1089 // Replace the TARGETS node
1090 self.syntax().splice_children(
1091 targets_index..targets_index + 1,
1092 vec![new_targets_node.into()],
1093 );
1094
1095 Ok(true)
1096 }
1097
1098 /// Remove this rule from its parent Makefile
1099 ///
1100 /// # Example
1101 /// ```
1102 /// use makefile_lossless::Makefile;
1103 /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1104 /// let rule = makefile.rules().next().unwrap();
1105 /// rule.remove().unwrap();
1106 /// assert_eq!(makefile.rules().count(), 1);
1107 /// ```
1108 ///
1109 /// This will also remove any preceding comments and up to 1 empty line before the rule.
1110 /// When removing the last rule in a makefile, this will also trim any trailing blank lines
1111 /// from the previous rule to avoid leaving extra whitespace at the end of the file.
1112 pub fn remove(self) -> Result<(), Error> {
1113 let parent = self.syntax().parent().ok_or_else(|| {
1114 Error::Parse(ParseError {
1115 errors: vec![ErrorInfo {
1116 message: "Rule has no parent".to_string(),
1117 line: 1,
1118 context: "remove".to_string(),
1119 }],
1120 })
1121 })?;
1122
1123 // Check if this is the last rule by seeing if there's any next sibling that's a RULE
1124 let is_last_rule = self
1125 .syntax()
1126 .siblings(rowan::Direction::Next)
1127 .skip(1) // Skip self
1128 .all(|sibling| sibling.kind() != RULE);
1129
1130 remove_with_preceding_comments(self.syntax(), &parent);
1131
1132 // If we removed the last rule, trim trailing newlines from the last remaining RULE
1133 if is_last_rule {
1134 // Find the last RULE node in the parent
1135 if let Some(last_rule_node) = parent
1136 .children()
1137 .filter(|child| child.kind() == RULE)
1138 .last()
1139 {
1140 trim_trailing_newlines(&last_rule_node);
1141 }
1142 }
1143
1144 Ok(())
1145 }
1146}
1147
1148impl Default for Makefile {
1149 fn default() -> Self {
1150 Self::new()
1151 }
1152}