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