makefile_lossless/ast/makefile.rs
1use crate::lossless::{
2 parse, Conditional, Error, ErrorInfo, Include, Makefile, ParseError, Rule, SyntaxNode,
3 VariableDefinition,
4};
5use crate::pattern::matches_pattern;
6use crate::SyntaxKind::*;
7use rowan::ast::AstNode;
8use rowan::GreenNodeBuilder;
9
10/// Represents different types of items that can appear in a Makefile
11#[derive(Clone)]
12pub enum MakefileItem {
13 /// A rule definition (e.g., "target: prerequisites")
14 Rule(Rule),
15 /// A variable definition (e.g., "VAR = value")
16 Variable(VariableDefinition),
17 /// An include directive (e.g., "include foo.mk")
18 Include(Include),
19 /// A conditional block (e.g., "ifdef DEBUG ... endif")
20 Conditional(Conditional),
21}
22
23impl MakefileItem {
24 /// Try to cast a syntax node to a MakefileItem
25 pub(crate) fn cast(node: SyntaxNode) -> Option<Self> {
26 if let Some(rule) = Rule::cast(node.clone()) {
27 Some(MakefileItem::Rule(rule))
28 } else if let Some(var) = VariableDefinition::cast(node.clone()) {
29 Some(MakefileItem::Variable(var))
30 } else if let Some(inc) = Include::cast(node.clone()) {
31 Some(MakefileItem::Include(inc))
32 } else {
33 Conditional::cast(node).map(MakefileItem::Conditional)
34 }
35 }
36
37 /// Get the underlying syntax node
38 pub(crate) fn syntax(&self) -> &SyntaxNode {
39 match self {
40 MakefileItem::Rule(r) => r.syntax(),
41 MakefileItem::Variable(v) => v.syntax(),
42 MakefileItem::Include(i) => i.syntax(),
43 MakefileItem::Conditional(c) => c.syntax(),
44 }
45 }
46}
47
48impl Makefile {
49 /// Create a new empty makefile
50 pub fn new() -> Makefile {
51 let mut builder = GreenNodeBuilder::new();
52
53 builder.start_node(ROOT.into());
54 builder.finish_node();
55
56 let syntax = SyntaxNode::new_root_mut(builder.finish());
57 Makefile::cast(syntax).unwrap()
58 }
59
60 /// Parse makefile text, returning a Parse result
61 pub fn parse(text: &str) -> crate::Parse<Makefile> {
62 crate::Parse::<Makefile>::parse_makefile(text)
63 }
64
65 /// Get the text content of the makefile
66 pub fn code(&self) -> String {
67 self.syntax().text().to_string()
68 }
69
70 /// Check if this node is the root of a makefile
71 pub fn is_root(&self) -> bool {
72 self.syntax().kind() == ROOT
73 }
74
75 /// Read a makefile from a reader
76 pub fn read<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
77 let mut buf = String::new();
78 r.read_to_string(&mut buf)?;
79 buf.parse()
80 }
81
82 /// Read makefile from a reader, but allow syntax errors
83 pub fn read_relaxed<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
84 let mut buf = String::new();
85 r.read_to_string(&mut buf)?;
86
87 let parsed = parse(&buf, None);
88 Ok(parsed.root())
89 }
90
91 /// Retrieve the rules in the makefile
92 ///
93 /// # Example
94 /// ```
95 /// use makefile_lossless::Makefile;
96 /// let makefile: Makefile = "rule: dependency\n\tcommand\n".parse().unwrap();
97 /// assert_eq!(makefile.rules().count(), 1);
98 /// ```
99 pub fn rules(&self) -> impl Iterator<Item = Rule> + '_ {
100 self.syntax().children().filter_map(Rule::cast)
101 }
102
103 /// Get all rules that have a specific target
104 pub fn rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator<Item = Rule> + 'a {
105 self.rules()
106 .filter(move |rule| rule.targets().any(|t| t == target))
107 }
108
109 /// Get all variable definitions in the makefile
110 pub fn variable_definitions(&self) -> impl Iterator<Item = VariableDefinition> {
111 self.syntax()
112 .children()
113 .filter_map(VariableDefinition::cast)
114 }
115
116 /// Get all conditionals in the makefile
117 pub fn conditionals(&self) -> impl Iterator<Item = Conditional> + '_ {
118 self.syntax().children().filter_map(Conditional::cast)
119 }
120
121 /// Get all top-level items (rules, variables, includes, conditionals) in the makefile
122 ///
123 /// # Example
124 /// ```
125 /// use makefile_lossless::{Makefile, MakefileItem};
126 /// let makefile: Makefile = r#"VAR = value
127 /// ifdef DEBUG
128 /// CFLAGS = -g
129 /// endif
130 /// rule:
131 /// command
132 /// "#.parse().unwrap();
133 /// let items: Vec<_> = makefile.items().collect();
134 /// assert_eq!(items.len(), 3); // VAR, conditional, rule
135 /// ```
136 pub fn items(&self) -> impl Iterator<Item = MakefileItem> + '_ {
137 self.syntax().children().filter_map(MakefileItem::cast)
138 }
139
140 /// Find all variables by name
141 ///
142 /// Returns an iterator over all variable definitions with the given name.
143 /// Makefiles can have multiple definitions of the same variable.
144 ///
145 /// # Example
146 /// ```
147 /// use makefile_lossless::Makefile;
148 /// let makefile: Makefile = "VAR1 = value1\nVAR2 = value2\nVAR1 = value3\n".parse().unwrap();
149 /// let vars: Vec<_> = makefile.find_variable("VAR1").collect();
150 /// assert_eq!(vars.len(), 2);
151 /// assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
152 /// assert_eq!(vars[1].raw_value(), Some("value3".to_string()));
153 /// ```
154 pub fn find_variable<'a>(
155 &'a self,
156 name: &'a str,
157 ) -> impl Iterator<Item = VariableDefinition> + 'a {
158 self.variable_definitions()
159 .filter(move |var| var.name().as_deref() == Some(name))
160 }
161
162 /// Add a new rule to the makefile
163 ///
164 /// # Example
165 /// ```
166 /// use makefile_lossless::Makefile;
167 /// let mut makefile = Makefile::new();
168 /// makefile.add_rule("rule");
169 /// assert_eq!(makefile.to_string(), "rule:\n");
170 /// ```
171 pub fn add_rule(&mut self, target: &str) -> Rule {
172 let mut builder = GreenNodeBuilder::new();
173 builder.start_node(RULE.into());
174 builder.token(IDENTIFIER.into(), target);
175 builder.token(OPERATOR.into(), ":");
176 builder.token(NEWLINE.into(), "\n");
177 builder.finish_node();
178
179 let syntax = SyntaxNode::new_root_mut(builder.finish());
180 let pos = self.syntax().children_with_tokens().count();
181
182 // Add a blank line before the new rule if there are existing rules
183 // This maintains standard makefile formatting
184 let needs_blank_line = self.syntax().children().any(|c| c.kind() == RULE);
185
186 if needs_blank_line {
187 // Create a BLANK_LINE node
188 let mut bl_builder = GreenNodeBuilder::new();
189 bl_builder.start_node(BLANK_LINE.into());
190 bl_builder.token(NEWLINE.into(), "\n");
191 bl_builder.finish_node();
192 let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
193
194 self.syntax()
195 .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
196 } else {
197 self.syntax().splice_children(pos..pos, vec![syntax.into()]);
198 }
199
200 // Use children().count() - 1 to get the last added child node
201 // (not children_with_tokens().count() which includes tokens)
202 Rule::cast(self.syntax().children().last().unwrap()).unwrap()
203 }
204
205 /// Add a new conditional to the makefile
206 ///
207 /// # Arguments
208 /// * `conditional_type` - The type of conditional: "ifdef", "ifndef", "ifeq", or "ifneq"
209 /// * `condition` - The condition expression (e.g., "DEBUG" for ifdef/ifndef, or "(a,b)" for ifeq/ifneq)
210 /// * `if_body` - The content of the if branch
211 /// * `else_body` - Optional content for the else branch
212 ///
213 /// # Example
214 /// ```
215 /// use makefile_lossless::Makefile;
216 /// let mut makefile = Makefile::new();
217 /// makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
218 /// assert!(makefile.to_string().contains("ifdef DEBUG"));
219 /// ```
220 pub fn add_conditional(
221 &mut self,
222 conditional_type: &str,
223 condition: &str,
224 if_body: &str,
225 else_body: Option<&str>,
226 ) -> Result<Conditional, Error> {
227 // Validate conditional type
228 if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&conditional_type) {
229 return Err(Error::Parse(ParseError {
230 errors: vec![ErrorInfo {
231 message: format!(
232 "Invalid conditional type: {}. Must be one of: ifdef, ifndef, ifeq, ifneq",
233 conditional_type
234 ),
235 line: 1,
236 context: "add_conditional".to_string(),
237 }],
238 }));
239 }
240
241 let mut builder = GreenNodeBuilder::new();
242 builder.start_node(CONDITIONAL.into());
243
244 // Build CONDITIONAL_IF
245 builder.start_node(CONDITIONAL_IF.into());
246 builder.token(IDENTIFIER.into(), conditional_type);
247 builder.token(WHITESPACE.into(), " ");
248
249 // Wrap condition in EXPR node
250 builder.start_node(EXPR.into());
251 builder.token(IDENTIFIER.into(), condition);
252 builder.finish_node();
253
254 builder.token(NEWLINE.into(), "\n");
255 builder.finish_node();
256
257 // Add if body content
258 if !if_body.is_empty() {
259 for line in if_body.lines() {
260 if !line.is_empty() {
261 builder.token(IDENTIFIER.into(), line);
262 }
263 builder.token(NEWLINE.into(), "\n");
264 }
265 // Add final newline if if_body doesn't end with one
266 if !if_body.ends_with('\n') && !if_body.is_empty() {
267 builder.token(NEWLINE.into(), "\n");
268 }
269 }
270
271 // Add else clause if provided
272 if let Some(else_content) = else_body {
273 builder.start_node(CONDITIONAL_ELSE.into());
274 builder.token(IDENTIFIER.into(), "else");
275 builder.token(NEWLINE.into(), "\n");
276 builder.finish_node();
277
278 // Add else body content
279 if !else_content.is_empty() {
280 for line in else_content.lines() {
281 if !line.is_empty() {
282 builder.token(IDENTIFIER.into(), line);
283 }
284 builder.token(NEWLINE.into(), "\n");
285 }
286 // Add final newline if else_content doesn't end with one
287 if !else_content.ends_with('\n') && !else_content.is_empty() {
288 builder.token(NEWLINE.into(), "\n");
289 }
290 }
291 }
292
293 // Build CONDITIONAL_ENDIF
294 builder.start_node(CONDITIONAL_ENDIF.into());
295 builder.token(IDENTIFIER.into(), "endif");
296 builder.token(NEWLINE.into(), "\n");
297 builder.finish_node();
298
299 builder.finish_node();
300
301 let syntax = SyntaxNode::new_root_mut(builder.finish());
302 let pos = self.syntax().children_with_tokens().count();
303
304 // Add a blank line before the new conditional if there are existing elements
305 let needs_blank_line = self
306 .syntax()
307 .children()
308 .any(|c| c.kind() == RULE || c.kind() == VARIABLE || c.kind() == CONDITIONAL);
309
310 if needs_blank_line {
311 // Create a BLANK_LINE node
312 let mut bl_builder = GreenNodeBuilder::new();
313 bl_builder.start_node(BLANK_LINE.into());
314 bl_builder.token(NEWLINE.into(), "\n");
315 bl_builder.finish_node();
316 let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
317
318 self.syntax()
319 .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
320 } else {
321 self.syntax().splice_children(pos..pos, vec![syntax.into()]);
322 }
323
324 // Return the newly added conditional
325 Ok(Conditional::cast(self.syntax().children().last().unwrap()).unwrap())
326 }
327
328 /// Add a new conditional to the makefile with typed items
329 ///
330 /// This is a more type-safe alternative to `add_conditional` that accepts iterators of
331 /// `MakefileItem` instead of raw strings.
332 ///
333 /// # Arguments
334 /// * `conditional_type` - The type of conditional: "ifdef", "ifndef", "ifeq", or "ifneq"
335 /// * `condition` - The condition expression (e.g., "DEBUG" for ifdef/ifndef, or "(a,b)" for ifeq/ifneq)
336 /// * `if_items` - Items for the if branch
337 /// * `else_items` - Optional items for the else branch
338 ///
339 /// # Example
340 /// ```
341 /// use makefile_lossless::{Makefile, MakefileItem};
342 /// let mut makefile = Makefile::new();
343 /// let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
344 /// let var1 = temp1.variable_definitions().next().unwrap();
345 /// let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
346 /// let var2 = temp2.variable_definitions().next().unwrap();
347 /// makefile.add_conditional_with_items(
348 /// "ifdef",
349 /// "DEBUG",
350 /// vec![MakefileItem::Variable(var1)],
351 /// Some(vec![MakefileItem::Variable(var2)])
352 /// ).unwrap();
353 /// assert!(makefile.to_string().contains("ifdef DEBUG"));
354 /// assert!(makefile.to_string().contains("CFLAGS = -g"));
355 /// assert!(makefile.to_string().contains("CFLAGS = -O2"));
356 /// ```
357 pub fn add_conditional_with_items<I1, I2>(
358 &mut self,
359 conditional_type: &str,
360 condition: &str,
361 if_items: I1,
362 else_items: Option<I2>,
363 ) -> Result<Conditional, Error>
364 where
365 I1: IntoIterator<Item = MakefileItem>,
366 I2: IntoIterator<Item = MakefileItem>,
367 {
368 // Validate conditional type
369 if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&conditional_type) {
370 return Err(Error::Parse(ParseError {
371 errors: vec![ErrorInfo {
372 message: format!(
373 "Invalid conditional type: {}. Must be one of: ifdef, ifndef, ifeq, ifneq",
374 conditional_type
375 ),
376 line: 1,
377 context: "add_conditional_with_items".to_string(),
378 }],
379 }));
380 }
381
382 let mut builder = GreenNodeBuilder::new();
383 builder.start_node(CONDITIONAL.into());
384
385 // Build CONDITIONAL_IF
386 builder.start_node(CONDITIONAL_IF.into());
387 builder.token(IDENTIFIER.into(), conditional_type);
388 builder.token(WHITESPACE.into(), " ");
389
390 // Wrap condition in EXPR node
391 builder.start_node(EXPR.into());
392 builder.token(IDENTIFIER.into(), condition);
393 builder.finish_node();
394
395 builder.token(NEWLINE.into(), "\n");
396 builder.finish_node();
397
398 // Add if branch items
399 for item in if_items {
400 // Clone the item's syntax tree into our builder
401 let item_text = item.syntax().to_string();
402 // Parse it again to get green nodes
403 builder.token(IDENTIFIER.into(), item_text.trim());
404 builder.token(NEWLINE.into(), "\n");
405 }
406
407 // Add else clause if provided
408 if let Some(else_iter) = else_items {
409 builder.start_node(CONDITIONAL_ELSE.into());
410 builder.token(IDENTIFIER.into(), "else");
411 builder.token(NEWLINE.into(), "\n");
412 builder.finish_node();
413
414 // Add else branch items
415 for item in else_iter {
416 let item_text = item.syntax().to_string();
417 builder.token(IDENTIFIER.into(), item_text.trim());
418 builder.token(NEWLINE.into(), "\n");
419 }
420 }
421
422 // Build CONDITIONAL_ENDIF
423 builder.start_node(CONDITIONAL_ENDIF.into());
424 builder.token(IDENTIFIER.into(), "endif");
425 builder.token(NEWLINE.into(), "\n");
426 builder.finish_node();
427
428 builder.finish_node();
429
430 let syntax = SyntaxNode::new_root_mut(builder.finish());
431 let pos = self.syntax().children_with_tokens().count();
432
433 // Add a blank line before the new conditional if there are existing elements
434 let needs_blank_line = self
435 .syntax()
436 .children()
437 .any(|c| c.kind() == RULE || c.kind() == VARIABLE || c.kind() == CONDITIONAL);
438
439 if needs_blank_line {
440 // Create a BLANK_LINE node
441 let mut bl_builder = GreenNodeBuilder::new();
442 bl_builder.start_node(BLANK_LINE.into());
443 bl_builder.token(NEWLINE.into(), "\n");
444 bl_builder.finish_node();
445 let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
446
447 self.syntax()
448 .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
449 } else {
450 self.syntax().splice_children(pos..pos, vec![syntax.into()]);
451 }
452
453 // Return the newly added conditional
454 Ok(Conditional::cast(self.syntax().children().last().unwrap()).unwrap())
455 }
456
457 /// Read the makefile
458 pub fn from_reader<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
459 let mut buf = String::new();
460 r.read_to_string(&mut buf)?;
461
462 let parsed = parse(&buf, None);
463 if !parsed.errors.is_empty() {
464 Err(Error::Parse(ParseError {
465 errors: parsed.errors,
466 }))
467 } else {
468 Ok(parsed.root())
469 }
470 }
471
472 /// Replace rule at given index with a new rule
473 ///
474 /// # Example
475 /// ```
476 /// use makefile_lossless::Makefile;
477 /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
478 /// let new_rule: makefile_lossless::Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
479 /// makefile.replace_rule(0, new_rule).unwrap();
480 /// assert!(makefile.rules().any(|r| r.targets().any(|t| t == "new_rule")));
481 /// ```
482 pub fn replace_rule(&mut self, index: usize, new_rule: Rule) -> Result<(), Error> {
483 let rules: Vec<_> = self
484 .syntax()
485 .children()
486 .filter(|n| n.kind() == RULE)
487 .collect();
488
489 if rules.is_empty() {
490 return Err(Error::Parse(ParseError {
491 errors: vec![ErrorInfo {
492 message: "Cannot replace rule in empty makefile".to_string(),
493 line: 1,
494 context: "replace_rule".to_string(),
495 }],
496 }));
497 }
498
499 if index >= rules.len() {
500 return Err(Error::Parse(ParseError {
501 errors: vec![ErrorInfo {
502 message: format!(
503 "Rule index {} out of bounds (max {})",
504 index,
505 rules.len() - 1
506 ),
507 line: 1,
508 context: "replace_rule".to_string(),
509 }],
510 }));
511 }
512
513 let target_node = &rules[index];
514 let target_index = target_node.index();
515
516 // Replace the rule at the target index
517 self.syntax().splice_children(
518 target_index..target_index + 1,
519 vec![new_rule.syntax().clone().into()],
520 );
521 Ok(())
522 }
523
524 /// Remove rule at given index
525 ///
526 /// # Example
527 /// ```
528 /// use makefile_lossless::Makefile;
529 /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
530 /// let removed = makefile.remove_rule(0).unwrap();
531 /// assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule1"]);
532 /// assert_eq!(makefile.rules().count(), 1);
533 /// ```
534 pub fn remove_rule(&mut self, index: usize) -> Result<Rule, Error> {
535 let rules: Vec<_> = self
536 .syntax()
537 .children()
538 .filter(|n| n.kind() == RULE)
539 .collect();
540
541 if rules.is_empty() {
542 return Err(Error::Parse(ParseError {
543 errors: vec![ErrorInfo {
544 message: "Cannot remove rule from empty makefile".to_string(),
545 line: 1,
546 context: "remove_rule".to_string(),
547 }],
548 }));
549 }
550
551 if index >= rules.len() {
552 return Err(Error::Parse(ParseError {
553 errors: vec![ErrorInfo {
554 message: format!(
555 "Rule index {} out of bounds (max {})",
556 index,
557 rules.len() - 1
558 ),
559 line: 1,
560 context: "remove_rule".to_string(),
561 }],
562 }));
563 }
564
565 let target_node = rules[index].clone();
566 let target_index = target_node.index();
567
568 // Remove the rule at the target index
569 self.syntax()
570 .splice_children(target_index..target_index + 1, vec![]);
571 Ok(Rule::cast(target_node).unwrap())
572 }
573
574 /// Insert rule at given position
575 ///
576 /// # Example
577 /// ```
578 /// use makefile_lossless::Makefile;
579 /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
580 /// let new_rule: makefile_lossless::Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
581 /// makefile.insert_rule(1, new_rule).unwrap();
582 /// let targets: Vec<_> = makefile.rules().flat_map(|r| r.targets().collect::<Vec<_>>()).collect();
583 /// assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
584 /// ```
585 pub fn insert_rule(&mut self, index: usize, new_rule: Rule) -> Result<(), Error> {
586 let rules: Vec<_> = self
587 .syntax()
588 .children()
589 .filter(|n| n.kind() == RULE)
590 .collect();
591
592 if index > rules.len() {
593 return Err(Error::Parse(ParseError {
594 errors: vec![ErrorInfo {
595 message: format!("Rule index {} out of bounds (max {})", index, rules.len()),
596 line: 1,
597 context: "insert_rule".to_string(),
598 }],
599 }));
600 }
601
602 let target_index = if index == rules.len() {
603 // Insert at the end
604 self.syntax().children_with_tokens().count()
605 } else {
606 // Insert before the rule at the given index
607 rules[index].index()
608 };
609
610 // Build the nodes to insert
611 let mut nodes_to_insert = Vec::new();
612
613 // Determine if we need to add blank lines to maintain formatting consistency
614 if index == 0 && !rules.is_empty() {
615 // Inserting before the first rule - check if first rule has a blank line before it
616 // If so, we should add one after our new rule instead
617 // For now, just add the rule without a blank line before it
618 nodes_to_insert.push(new_rule.syntax().clone().into());
619
620 // Add a blank line after the new rule
621 let mut bl_builder = GreenNodeBuilder::new();
622 bl_builder.start_node(BLANK_LINE.into());
623 bl_builder.token(NEWLINE.into(), "\n");
624 bl_builder.finish_node();
625 let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
626 nodes_to_insert.push(blank_line.into());
627 } else if index < rules.len() {
628 // Inserting in the middle (before an existing rule)
629 // The syntax tree structure is: ... [maybe BLANK_LINE] RULE(target) ...
630 // We're inserting right before RULE(target)
631
632 // If there's a BLANK_LINE immediately before the target rule,
633 // it will stay there and separate the previous rule from our new rule.
634 // We don't need to add a BLANK_LINE before our new rule in that case.
635
636 // But we DO need to add a BLANK_LINE after our new rule to separate it
637 // from the target rule (which we're inserting before).
638
639 // Check if there's a blank line immediately before target_index
640 let has_blank_before = if target_index > 0 {
641 self.syntax()
642 .children_with_tokens()
643 .nth(target_index - 1)
644 .and_then(|n| n.as_node().map(|node| node.kind() == BLANK_LINE))
645 .unwrap_or(false)
646 } else {
647 false
648 };
649
650 // Only add a blank before if there isn't one already and we're not at the start
651 if !has_blank_before && index > 0 {
652 let mut bl_builder = GreenNodeBuilder::new();
653 bl_builder.start_node(BLANK_LINE.into());
654 bl_builder.token(NEWLINE.into(), "\n");
655 bl_builder.finish_node();
656 let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
657 nodes_to_insert.push(blank_line.into());
658 }
659
660 // Add the new rule
661 nodes_to_insert.push(new_rule.syntax().clone().into());
662
663 // Always add a blank line after the new rule to separate it from the next rule
664 let mut bl_builder = GreenNodeBuilder::new();
665 bl_builder.start_node(BLANK_LINE.into());
666 bl_builder.token(NEWLINE.into(), "\n");
667 bl_builder.finish_node();
668 let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
669 nodes_to_insert.push(blank_line.into());
670 } else {
671 // Inserting at the end when there are existing rules
672 // Add a blank line before the new rule
673 let mut bl_builder = GreenNodeBuilder::new();
674 bl_builder.start_node(BLANK_LINE.into());
675 bl_builder.token(NEWLINE.into(), "\n");
676 bl_builder.finish_node();
677 let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
678 nodes_to_insert.push(blank_line.into());
679
680 // Add the new rule
681 nodes_to_insert.push(new_rule.syntax().clone().into());
682 }
683
684 // Insert all nodes at the target index
685 self.syntax()
686 .splice_children(target_index..target_index, nodes_to_insert);
687 Ok(())
688 }
689
690 /// Get all include directives in the makefile
691 ///
692 /// # Example
693 /// ```
694 /// use makefile_lossless::Makefile;
695 /// let makefile: Makefile = "include config.mk\n-include .env\n".parse().unwrap();
696 /// let includes = makefile.includes().collect::<Vec<_>>();
697 /// assert_eq!(includes.len(), 2);
698 /// ```
699 pub fn includes(&self) -> impl Iterator<Item = Include> {
700 self.syntax().children().filter_map(Include::cast)
701 }
702
703 /// Get all included file paths
704 ///
705 /// # Example
706 /// ```
707 /// use makefile_lossless::Makefile;
708 /// let makefile: Makefile = "include config.mk\n-include .env\n".parse().unwrap();
709 /// let paths = makefile.included_files().collect::<Vec<_>>();
710 /// assert_eq!(paths, vec!["config.mk", ".env"]);
711 /// ```
712 pub fn included_files(&self) -> impl Iterator<Item = String> + '_ {
713 // We need to collect all Include nodes from anywhere in the syntax tree,
714 // not just direct children of the root, to handle includes in conditionals
715 fn collect_includes(node: &SyntaxNode) -> Vec<Include> {
716 let mut includes = Vec::new();
717
718 // First check if this node itself is an Include
719 if let Some(include) = Include::cast(node.clone()) {
720 includes.push(include);
721 }
722
723 // Then recurse into all children
724 for child in node.children() {
725 includes.extend(collect_includes(&child));
726 }
727
728 includes
729 }
730
731 // Start collection from the root node
732 let includes = collect_includes(self.syntax());
733
734 // Convert to an iterator of paths
735 includes.into_iter().map(|include| {
736 include
737 .syntax()
738 .children()
739 .find(|node| node.kind() == EXPR)
740 .map(|expr| expr.text().to_string().trim().to_string())
741 .unwrap_or_default()
742 })
743 }
744
745 /// Find the first rule with a specific target name
746 ///
747 /// # Example
748 /// ```
749 /// use makefile_lossless::Makefile;
750 /// let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
751 /// let rule = makefile.find_rule_by_target("rule2");
752 /// assert!(rule.is_some());
753 /// assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
754 /// ```
755 pub fn find_rule_by_target(&self, target: &str) -> Option<Rule> {
756 self.rules()
757 .find(|rule| rule.targets().any(|t| t == target))
758 }
759
760 /// Find all rules with a specific target name
761 ///
762 /// # Example
763 /// ```
764 /// use makefile_lossless::Makefile;
765 /// let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n".parse().unwrap();
766 /// let rules: Vec<_> = makefile.find_rules_by_target("rule1").collect();
767 /// assert_eq!(rules.len(), 2);
768 /// ```
769 pub fn find_rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator<Item = Rule> + 'a {
770 self.rules_by_target(target)
771 }
772
773 /// Find the first rule whose target matches the given pattern
774 ///
775 /// Supports make-style pattern matching where `%` in a rule's target acts as a wildcard.
776 /// For example, a rule with target `%.o` will match `foo.o`, `bar.o`, etc.
777 ///
778 /// # Example
779 /// ```
780 /// use makefile_lossless::Makefile;
781 /// let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
782 /// let rule = makefile.find_rule_by_target_pattern("foo.o");
783 /// assert!(rule.is_some());
784 /// ```
785 pub fn find_rule_by_target_pattern(&self, target: &str) -> Option<Rule> {
786 self.rules()
787 .find(|rule| rule.targets().any(|t| matches_pattern(&t, target)))
788 }
789
790 /// Find all rules whose targets match the given pattern
791 ///
792 /// Supports make-style pattern matching where `%` in a rule's target acts as a wildcard.
793 /// For example, a rule with target `%.o` will match `foo.o`, `bar.o`, etc.
794 ///
795 /// # Example
796 /// ```
797 /// use makefile_lossless::Makefile;
798 /// let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n".parse().unwrap();
799 /// let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
800 /// assert_eq!(rules.len(), 2);
801 /// ```
802 pub fn find_rules_by_target_pattern<'a>(
803 &'a self,
804 target: &'a str,
805 ) -> impl Iterator<Item = Rule> + 'a {
806 self.rules()
807 .filter(move |rule| rule.targets().any(|t| matches_pattern(&t, target)))
808 }
809
810 /// Add a target to .PHONY (creates .PHONY rule if it doesn't exist)
811 ///
812 /// # Example
813 /// ```
814 /// use makefile_lossless::Makefile;
815 /// let mut makefile = Makefile::new();
816 /// makefile.add_phony_target("clean").unwrap();
817 /// assert!(makefile.is_phony("clean"));
818 /// ```
819 pub fn add_phony_target(&mut self, target: &str) -> Result<(), Error> {
820 // Find existing .PHONY rule
821 if let Some(mut phony_rule) = self.find_rule_by_target(".PHONY") {
822 // Check if target is already in prerequisites
823 if !phony_rule.prerequisites().any(|p| p == target) {
824 phony_rule.add_prerequisite(target)?;
825 }
826 } else {
827 // Create new .PHONY rule
828 let mut phony_rule = self.add_rule(".PHONY");
829 phony_rule.add_prerequisite(target)?;
830 }
831 Ok(())
832 }
833
834 /// Remove a target from .PHONY (removes .PHONY rule if it becomes empty)
835 ///
836 /// Returns `true` if the target was found and removed, `false` if it wasn't in .PHONY.
837 /// If there are multiple .PHONY rules, it removes the target from the first rule that contains it.
838 ///
839 /// # Example
840 /// ```
841 /// use makefile_lossless::Makefile;
842 /// let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
843 /// assert!(makefile.remove_phony_target("clean").unwrap());
844 /// assert!(!makefile.is_phony("clean"));
845 /// assert!(makefile.is_phony("test"));
846 /// ```
847 pub fn remove_phony_target(&mut self, target: &str) -> Result<bool, Error> {
848 // Find the first .PHONY rule that contains the target
849 let mut phony_rule = None;
850 for rule in self.rules_by_target(".PHONY") {
851 if rule.prerequisites().any(|p| p == target) {
852 phony_rule = Some(rule);
853 break;
854 }
855 }
856
857 let mut phony_rule = match phony_rule {
858 Some(rule) => rule,
859 None => return Ok(false),
860 };
861
862 // Count prerequisites before removal
863 let prereq_count = phony_rule.prerequisites().count();
864
865 // Remove the prerequisite
866 phony_rule.remove_prerequisite(target)?;
867
868 // Check if .PHONY has no more prerequisites, if so remove the rule
869 if prereq_count == 1 {
870 // We just removed the last prerequisite, so remove the entire rule
871 phony_rule.remove()?;
872 }
873
874 Ok(true)
875 }
876
877 /// Check if a target is marked as phony
878 ///
879 /// # Example
880 /// ```
881 /// use makefile_lossless::Makefile;
882 /// let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
883 /// assert!(makefile.is_phony("clean"));
884 /// assert!(makefile.is_phony("test"));
885 /// assert!(!makefile.is_phony("build"));
886 /// ```
887 pub fn is_phony(&self, target: &str) -> bool {
888 // Check all .PHONY rules since there can be multiple
889 self.rules_by_target(".PHONY")
890 .any(|rule| rule.prerequisites().any(|p| p == target))
891 }
892
893 /// Get all phony targets
894 ///
895 /// # Example
896 /// ```
897 /// use makefile_lossless::Makefile;
898 /// let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
899 /// let phony_targets: Vec<_> = makefile.phony_targets().collect();
900 /// assert_eq!(phony_targets, vec!["clean", "test", "build"]);
901 /// ```
902 pub fn phony_targets(&self) -> impl Iterator<Item = String> + '_ {
903 // Collect from all .PHONY rules since there can be multiple
904 self.rules_by_target(".PHONY")
905 .flat_map(|rule| rule.prerequisites().collect::<Vec<_>>())
906 }
907}