Skip to main content

makefile_lossless/ast/
makefile.rs

1use crate::lossless::{
2    parse, Conditional, Error, ErrorInfo, Include, Makefile, ParseError, Rule, SyntaxNode,
3    VariableDefinition, VariableReference,
4};
5use crate::pattern::matches_pattern;
6use crate::SyntaxKind::*;
7use rowan::ast::AstNode;
8use rowan::GreenNodeBuilder;
9use std::collections::VecDeque;
10
11/// Represents different types of items that can appear in a Makefile
12#[derive(Clone)]
13pub enum MakefileItem {
14    /// A rule definition (e.g., "target: prerequisites")
15    Rule(Rule),
16    /// A variable definition (e.g., "VAR = value")
17    Variable(VariableDefinition),
18    /// An include directive (e.g., "include foo.mk")
19    Include(Include),
20    /// A conditional block (e.g., "ifdef DEBUG ... endif")
21    Conditional(Conditional),
22}
23
24impl MakefileItem {
25    /// Try to cast a syntax node to a MakefileItem
26    pub(crate) fn cast(node: SyntaxNode) -> Option<Self> {
27        if let Some(rule) = Rule::cast(node.clone()) {
28            Some(MakefileItem::Rule(rule))
29        } else if let Some(var) = VariableDefinition::cast(node.clone()) {
30            Some(MakefileItem::Variable(var))
31        } else if let Some(inc) = Include::cast(node.clone()) {
32            Some(MakefileItem::Include(inc))
33        } else {
34            Conditional::cast(node).map(MakefileItem::Conditional)
35        }
36    }
37
38    /// Get the underlying syntax node
39    pub fn syntax(&self) -> &SyntaxNode {
40        match self {
41            MakefileItem::Rule(r) => r.syntax(),
42            MakefileItem::Variable(v) => v.syntax(),
43            MakefileItem::Include(i) => i.syntax(),
44            MakefileItem::Conditional(c) => c.syntax(),
45        }
46    }
47
48    /// Helper to get parent node or return an appropriate error
49    fn get_parent_or_error(&self, action: &str, method: &str) -> Result<SyntaxNode, Error> {
50        self.syntax().parent().ok_or_else(|| {
51            Error::Parse(ParseError {
52                errors: vec![ErrorInfo {
53                    message: format!("Cannot {} item without parent", action),
54                    line: 1,
55                    context: format!("MakefileItem::{}", method),
56                }],
57            })
58        })
59    }
60
61    /// Check if a token is a regular comment (not a shebang)
62    fn is_regular_comment(token: &rowan::SyntaxToken<crate::lossless::Lang>) -> bool {
63        token.kind() == COMMENT && !token.text().starts_with("#!")
64    }
65
66    /// Extract comment text from a comment token, removing '#' prefix
67    fn extract_comment_text(token: &rowan::SyntaxToken<crate::lossless::Lang>) -> String {
68        let text = token.text();
69        text.strip_prefix("# ")
70            .or_else(|| text.strip_prefix('#'))
71            .unwrap_or(text)
72            .to_string()
73    }
74
75    /// Helper to find all preceding comment-related elements up to the first non-comment element
76    ///
77    /// Returns elements in reverse order (from closest to furthest from the item)
78    fn collect_preceding_comment_elements(
79        &self,
80    ) -> Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<crate::lossless::Lang>>> {
81        let mut elements = Vec::new();
82        let mut current = self.syntax().prev_sibling_or_token();
83
84        while let Some(element) = current {
85            match &element {
86                rowan::NodeOrToken::Token(token) if Self::is_regular_comment(token) => {
87                    elements.push(element.clone());
88                }
89                rowan::NodeOrToken::Token(token)
90                    if token.kind() == NEWLINE || token.kind() == WHITESPACE =>
91                {
92                    elements.push(element.clone());
93                }
94                rowan::NodeOrToken::Node(n) if n.kind() == BLANK_LINE => {
95                    elements.push(element.clone());
96                }
97                rowan::NodeOrToken::Token(token) if token.kind() == COMMENT => {
98                    // Hit a shebang, stop here
99                    break;
100                }
101                _ => break,
102            }
103            current = element.prev_sibling_or_token();
104        }
105
106        elements
107    }
108
109    /// Helper to parse comment text and extract properly formatted comment tokens
110    fn parse_comment_tokens(
111        comment_text: &str,
112    ) -> (
113        rowan::SyntaxToken<crate::lossless::Lang>,
114        Option<rowan::SyntaxToken<crate::lossless::Lang>>,
115    ) {
116        let comment_line = format!("# {}\n", comment_text);
117        let temp_makefile = crate::lossless::parse(&comment_line, None);
118        let root = temp_makefile.root();
119
120        let mut comment_token = None;
121        let mut newline_token = None;
122        let mut found_comment = false;
123
124        for element in root.syntax().children_with_tokens() {
125            if let rowan::NodeOrToken::Token(token) = element {
126                if token.kind() == COMMENT {
127                    comment_token = Some(token);
128                    found_comment = true;
129                } else if token.kind() == NEWLINE && found_comment && newline_token.is_none() {
130                    newline_token = Some(token);
131                    break;
132                }
133            }
134        }
135
136        (
137            comment_token.expect("Failed to extract comment token"),
138            newline_token,
139        )
140    }
141
142    /// Replace this MakefileItem with another MakefileItem
143    ///
144    /// This preserves the position of the original item but replaces its content
145    /// with the new item. Preceding comments are preserved.
146    ///
147    /// # Example
148    /// ```
149    /// use makefile_lossless::{Makefile, MakefileItem};
150    /// let mut makefile: Makefile = "VAR1 = old\nrule:\n\tcommand\n".parse().unwrap();
151    /// let temp: Makefile = "VAR2 = new\n".parse().unwrap();
152    /// let new_var = temp.variable_definitions().next().unwrap();
153    /// let mut first_item = makefile.items().next().unwrap();
154    /// first_item.replace(MakefileItem::Variable(new_var)).unwrap();
155    /// assert!(makefile.to_string().contains("VAR2 = new"));
156    /// assert!(!makefile.to_string().contains("VAR1"));
157    /// ```
158    pub fn replace(&mut self, new_item: MakefileItem) -> Result<(), Error> {
159        let parent = self.get_parent_or_error("replace", "replace")?;
160        let current_index = self.syntax().index();
161
162        // Replace the current node with the new item's syntax
163        parent.splice_children(
164            current_index..current_index + 1,
165            vec![new_item.syntax().clone().into()],
166        );
167
168        // Update self to point to the new item
169        *self = new_item;
170
171        Ok(())
172    }
173
174    /// Add a comment before this MakefileItem
175    ///
176    /// The comment text should not include the leading '#' character.
177    /// Multiple comment lines can be added by calling this method multiple times.
178    ///
179    /// # Example
180    /// ```
181    /// use makefile_lossless::Makefile;
182    /// let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
183    /// let mut item = makefile.items().next().unwrap();
184    /// item.add_comment("This is a variable").unwrap();
185    /// assert!(makefile.to_string().contains("# This is a variable"));
186    /// ```
187    pub fn add_comment(&mut self, comment_text: &str) -> Result<(), Error> {
188        let parent = self.get_parent_or_error("add comment to", "add_comment")?;
189        let current_index = self.syntax().index();
190
191        // Get properly formatted comment tokens
192        let (comment_token, newline_token) = Self::parse_comment_tokens(comment_text);
193
194        let mut elements = vec![rowan::NodeOrToken::Token(comment_token)];
195        if let Some(newline) = newline_token {
196            elements.push(rowan::NodeOrToken::Token(newline));
197        }
198
199        // Insert comment and newline before the current item
200        parent.splice_children(current_index..current_index, elements);
201
202        Ok(())
203    }
204
205    /// Get all preceding comments for this MakefileItem
206    ///
207    /// Returns an iterator of comment strings (without the leading '#' and whitespace).
208    ///
209    /// # Example
210    /// ```
211    /// use makefile_lossless::Makefile;
212    /// let makefile: Makefile = "# Comment 1\n# Comment 2\nVAR = value\n".parse().unwrap();
213    /// let item = makefile.items().next().unwrap();
214    /// let comments: Vec<_> = item.preceding_comments().collect();
215    /// assert_eq!(comments.len(), 2);
216    /// assert_eq!(comments[0], "Comment 1");
217    /// assert_eq!(comments[1], "Comment 2");
218    /// ```
219    pub fn preceding_comments(&self) -> impl Iterator<Item = String> {
220        let elements = self.collect_preceding_comment_elements();
221        let mut comments = Vec::new();
222
223        // Process elements in reverse order (furthest to closest)
224        for element in elements.iter().rev() {
225            if let rowan::NodeOrToken::Token(token) = element {
226                if token.kind() == COMMENT {
227                    comments.push(Self::extract_comment_text(token));
228                }
229            }
230        }
231
232        comments.into_iter()
233    }
234
235    /// Remove all preceding comments for this MakefileItem
236    ///
237    /// Returns the number of comments removed.
238    ///
239    /// # Example
240    /// ```
241    /// use makefile_lossless::Makefile;
242    /// let mut makefile: Makefile = "# Comment 1\n# Comment 2\nVAR = value\n".parse().unwrap();
243    /// let mut item = makefile.items().next().unwrap();
244    /// let count = item.remove_comments().unwrap();
245    /// assert_eq!(count, 2);
246    /// assert!(!makefile.to_string().contains("# Comment"));
247    /// ```
248    pub fn remove_comments(&mut self) -> Result<usize, Error> {
249        let parent = self.get_parent_or_error("remove comments from", "remove_comments")?;
250        let collected_elements = self.collect_preceding_comment_elements();
251
252        // Count the comments
253        let mut comment_count = 0;
254        for element in collected_elements.iter() {
255            if let rowan::NodeOrToken::Token(token) = element {
256                if token.kind() == COMMENT {
257                    comment_count += 1;
258                }
259            }
260        }
261
262        // Determine which elements to remove - similar to remove_with_preceding_comments
263        // We remove comments and up to 1 blank line worth of newlines
264        let mut elements_to_remove = Vec::new();
265        let mut consecutive_newlines = 0;
266        for element in collected_elements.iter().rev() {
267            let should_remove = match element {
268                rowan::NodeOrToken::Token(token) if token.kind() == COMMENT => {
269                    consecutive_newlines = 0;
270                    true // Remove comments
271                }
272                rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
273                    consecutive_newlines += 1;
274                    comment_count > 0 && consecutive_newlines <= 1
275                }
276                rowan::NodeOrToken::Token(token) if token.kind() == WHITESPACE => comment_count > 0,
277                rowan::NodeOrToken::Node(n) if n.kind() == BLANK_LINE => {
278                    consecutive_newlines += 1;
279                    comment_count > 0 && consecutive_newlines <= 1
280                }
281                _ => false,
282            };
283
284            if should_remove {
285                elements_to_remove.push(element.clone());
286            }
287        }
288
289        // Remove elements in reverse order (from highest index to lowest)
290        elements_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
291        for element in elements_to_remove {
292            let idx = element.index();
293            parent.splice_children(idx..idx + 1, vec![]);
294        }
295
296        Ok(comment_count)
297    }
298
299    /// Modify the first preceding comment for this MakefileItem
300    ///
301    /// Returns `true` if a comment was found and modified, `false` if no comment exists.
302    /// The comment text should not include the leading '#' character.
303    ///
304    /// # Example
305    /// ```
306    /// use makefile_lossless::Makefile;
307    /// let mut makefile: Makefile = "# Old comment\nVAR = value\n".parse().unwrap();
308    /// let mut item = makefile.items().next().unwrap();
309    /// let modified = item.modify_comment("New comment").unwrap();
310    /// assert!(modified);
311    /// assert!(makefile.to_string().contains("# New comment"));
312    /// assert!(!makefile.to_string().contains("# Old comment"));
313    /// ```
314    pub fn modify_comment(&mut self, new_comment_text: &str) -> Result<bool, Error> {
315        let parent = self.get_parent_or_error("modify comment for", "modify_comment")?;
316
317        // Find the first preceding comment (closest to the item)
318        let collected_elements = self.collect_preceding_comment_elements();
319        let comment_element = collected_elements.iter().find(|element| {
320            if let rowan::NodeOrToken::Token(token) = element {
321                token.kind() == COMMENT
322            } else {
323                false
324            }
325        });
326
327        if let Some(element) = comment_element {
328            let idx = element.index();
329            let (new_comment_token, _) = Self::parse_comment_tokens(new_comment_text);
330            parent.splice_children(
331                idx..idx + 1,
332                vec![rowan::NodeOrToken::Token(new_comment_token)],
333            );
334            Ok(true)
335        } else {
336            Ok(false)
337        }
338    }
339
340    /// Insert a new MakefileItem before this item
341    ///
342    /// This inserts the new item immediately before the current item in the makefile.
343    /// The new item is inserted at the same level as the current item.
344    ///
345    /// # Example
346    /// ```
347    /// use makefile_lossless::{Makefile, MakefileItem};
348    /// let mut makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
349    /// let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
350    /// let new_var = temp.variable_definitions().next().unwrap();
351    /// let mut second_item = makefile.items().nth(1).unwrap();
352    /// second_item.insert_before(MakefileItem::Variable(new_var)).unwrap();
353    /// let result = makefile.to_string();
354    /// assert!(result.contains("VAR1 = first\nVAR_NEW = inserted\nVAR2 = second"));
355    /// ```
356    pub fn insert_before(&mut self, new_item: MakefileItem) -> Result<(), Error> {
357        let parent = self.get_parent_or_error("insert before", "insert_before")?;
358        let current_index = self.syntax().index();
359
360        // Insert the new item before the current item
361        parent.splice_children(
362            current_index..current_index,
363            vec![new_item.syntax().clone().into()],
364        );
365
366        Ok(())
367    }
368
369    /// Insert a new MakefileItem after this item
370    ///
371    /// This inserts the new item immediately after the current item in the makefile.
372    /// The new item is inserted at the same level as the current item.
373    ///
374    /// # Example
375    /// ```
376    /// use makefile_lossless::{Makefile, MakefileItem};
377    /// let mut makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
378    /// let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
379    /// let new_var = temp.variable_definitions().next().unwrap();
380    /// let mut first_item = makefile.items().next().unwrap();
381    /// first_item.insert_after(MakefileItem::Variable(new_var)).unwrap();
382    /// let result = makefile.to_string();
383    /// assert!(result.contains("VAR1 = first\nVAR_NEW = inserted\nVAR2 = second"));
384    /// ```
385    pub fn insert_after(&mut self, new_item: MakefileItem) -> Result<(), Error> {
386        let parent = self.get_parent_or_error("insert after", "insert_after")?;
387        let current_index = self.syntax().index();
388
389        // Insert the new item after the current item
390        parent.splice_children(
391            current_index + 1..current_index + 1,
392            vec![new_item.syntax().clone().into()],
393        );
394
395        Ok(())
396    }
397}
398
399// Internal trait for extracting specific item types from MakefileItem
400trait ExtractFromItem: Sized {
401    fn extract(item: MakefileItem) -> Option<Self>;
402}
403
404impl ExtractFromItem for Rule {
405    fn extract(item: MakefileItem) -> Option<Self> {
406        match item {
407            MakefileItem::Rule(r) => Some(r),
408            _ => None,
409        }
410    }
411}
412
413impl ExtractFromItem for VariableDefinition {
414    fn extract(item: MakefileItem) -> Option<Self> {
415        match item {
416            MakefileItem::Variable(v) => Some(v),
417            _ => None,
418        }
419    }
420}
421
422// Internal stack-based iterator for recursively collecting items from conditionals
423struct RecursiveItemsIter<T> {
424    stack: VecDeque<MakefileItem>,
425    _phantom: std::marker::PhantomData<T>,
426}
427
428impl<T> RecursiveItemsIter<T> {
429    fn new(items: impl Iterator<Item = MakefileItem>) -> Self {
430        Self {
431            stack: items.collect(),
432            _phantom: std::marker::PhantomData,
433        }
434    }
435}
436
437impl<T: ExtractFromItem> Iterator for RecursiveItemsIter<T> {
438    type Item = T;
439
440    fn next(&mut self) -> Option<Self::Item> {
441        while let Some(item) = self.stack.pop_front() {
442            if let MakefileItem::Conditional(ref cond) = item {
443                // Push in natural order since we pop from front
444                self.stack.extend(cond.if_items());
445                self.stack.extend(cond.else_items());
446            }
447            if let Some(extracted) = T::extract(item) {
448                return Some(extracted);
449            }
450        }
451        None
452    }
453}
454
455/// Iterator over blocks of consecutive comment lines in a makefile.
456struct CommentBlockIter {
457    elements: Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<crate::lossless::Lang>>>,
458    pos: usize,
459}
460
461impl CommentBlockIter {
462    fn new(root: &SyntaxNode) -> Self {
463        Self {
464            elements: root.children_with_tokens().collect(),
465            pos: 0,
466        }
467    }
468}
469
470impl Iterator for CommentBlockIter {
471    type Item = rowan::TextRange;
472
473    fn next(&mut self) -> Option<Self::Item> {
474        // Find the start of the next comment
475        while self.pos < self.elements.len() {
476            if let Some(token) = self.elements[self.pos].as_token() {
477                if token.kind() == COMMENT {
478                    break;
479                }
480            }
481            self.pos += 1;
482        }
483
484        if self.pos >= self.elements.len() {
485            return None;
486        }
487
488        let block_start = self.elements[self.pos]
489            .as_token()
490            .unwrap()
491            .text_range()
492            .start();
493        let mut block_end = self.elements[self.pos]
494            .as_token()
495            .unwrap()
496            .text_range()
497            .end();
498        let mut comment_count = 1;
499        self.pos += 1;
500
501        // Extend the block through consecutive comments (with whitespace/newlines/blank lines between)
502        while self.pos < self.elements.len() {
503            match &self.elements[self.pos] {
504                rowan::NodeOrToken::Token(token)
505                    if token.kind() == NEWLINE || token.kind() == WHITESPACE =>
506                {
507                    self.pos += 1;
508                }
509                rowan::NodeOrToken::Node(node) if node.kind() == BLANK_LINE => {
510                    self.pos += 1;
511                }
512                rowan::NodeOrToken::Token(token) if token.kind() == COMMENT => {
513                    block_end = token.text_range().end();
514                    comment_count += 1;
515                    self.pos += 1;
516                }
517                _ => break,
518            }
519        }
520
521        // Only return blocks with 2+ comment lines
522        if comment_count >= 2 {
523            Some(rowan::TextRange::new(block_start, block_end))
524        } else {
525            // Single comment line — skip it and try next
526            self.next()
527        }
528    }
529}
530
531impl Makefile {
532    /// Create a new empty makefile
533    pub fn new() -> Makefile {
534        let mut builder = GreenNodeBuilder::new();
535
536        builder.start_node(ROOT.into());
537        builder.finish_node();
538
539        let syntax = SyntaxNode::new_root_mut(builder.finish());
540        Makefile::cast(syntax).unwrap()
541    }
542
543    /// Parse makefile text, returning a Parse result
544    pub fn parse(text: &str) -> crate::Parse<Makefile> {
545        crate::Parse::<Makefile>::parse_makefile(text)
546    }
547
548    /// Get the text content of the makefile
549    pub fn code(&self) -> String {
550        self.syntax().text().to_string()
551    }
552
553    /// Check if this node is the root of a makefile
554    pub fn is_root(&self) -> bool {
555        self.syntax().kind() == ROOT
556    }
557
558    /// Read a makefile from a reader
559    pub fn read<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
560        let mut buf = String::new();
561        r.read_to_string(&mut buf)?;
562        buf.parse()
563    }
564
565    /// Read makefile from a reader, but allow syntax errors
566    pub fn read_relaxed<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
567        let mut buf = String::new();
568        r.read_to_string(&mut buf)?;
569
570        let parsed = parse(&buf, None);
571        Ok(parsed.root())
572    }
573
574    /// Parse a makefile from a string, allowing syntax errors.
575    ///
576    /// Returns the parsed makefile and a list of errors. The makefile tree is
577    /// always returned, even if there are parse errors, enabling error-resilient
578    /// tooling that can work with partial or invalid input.
579    pub fn from_str_relaxed(s: &str) -> (Self, Vec<ErrorInfo>) {
580        let parsed = parse(s, None);
581        (parsed.root(), parsed.errors)
582    }
583
584    /// Read a makefile from a file path, allowing syntax errors.
585    ///
586    /// Returns the parsed makefile and a list of errors.
587    pub fn from_file_relaxed(
588        path: impl AsRef<std::path::Path>,
589    ) -> Result<(Self, Vec<ErrorInfo>), std::io::Error> {
590        let text = std::fs::read_to_string(path)?;
591        Ok(Self::from_str_relaxed(&text))
592    }
593
594    /// Retrieve the rules in the makefile
595    ///
596    /// # Example
597    /// ```
598    /// use makefile_lossless::Makefile;
599    /// let makefile: Makefile = "rule: dependency\n\tcommand\n".parse().unwrap();
600    /// assert_eq!(makefile.rules().count(), 1);
601    /// ```
602    pub fn rules(&self) -> impl Iterator<Item = Rule> + '_ {
603        RecursiveItemsIter::new(self.items())
604    }
605
606    /// Get all rules that have a specific target
607    pub fn rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator<Item = Rule> + 'a {
608        self.rules()
609            .filter(move |rule| rule.targets().any(|t| t == target))
610    }
611
612    /// Get all variable definitions in the makefile
613    pub fn variable_definitions(&self) -> impl Iterator<Item = VariableDefinition> + '_ {
614        RecursiveItemsIter::new(self.items())
615    }
616
617    /// Get all conditionals in the makefile (top-level only)
618    pub fn conditionals(&self) -> impl Iterator<Item = Conditional> + '_ {
619        self.items().filter_map(|item| match item {
620            MakefileItem::Conditional(c) => Some(c),
621            _ => None,
622        })
623    }
624
625    /// Get all top-level items (rules, variables, includes, conditionals) in the makefile
626    ///
627    /// # Example
628    /// ```
629    /// use makefile_lossless::{Makefile, MakefileItem};
630    /// let makefile: Makefile = r#"VAR = value
631    /// ifdef DEBUG
632    /// CFLAGS = -g
633    /// endif
634    /// rule:
635    /// 	command
636    /// "#.parse().unwrap();
637    /// let items: Vec<_> = makefile.items().collect();
638    /// assert_eq!(items.len(), 3); // VAR, conditional, rule
639    /// ```
640    pub fn items(&self) -> impl Iterator<Item = MakefileItem> + '_ {
641        self.syntax().children().filter_map(MakefileItem::cast)
642    }
643
644    /// Find all variables by name
645    ///
646    /// Returns an iterator over all variable definitions with the given name.
647    /// Makefiles can have multiple definitions of the same variable.
648    ///
649    /// # Example
650    /// ```
651    /// use makefile_lossless::Makefile;
652    /// let makefile: Makefile = "VAR1 = value1\nVAR2 = value2\nVAR1 = value3\n".parse().unwrap();
653    /// let vars: Vec<_> = makefile.find_variable("VAR1").collect();
654    /// assert_eq!(vars.len(), 2);
655    /// assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
656    /// assert_eq!(vars[1].raw_value(), Some("value3".to_string()));
657    /// ```
658    pub fn find_variable<'a>(
659        &'a self,
660        name: &'a str,
661    ) -> impl Iterator<Item = VariableDefinition> + 'a {
662        self.variable_definitions()
663            .filter(move |var| var.name().as_deref() == Some(name))
664    }
665
666    /// Get all variable references in the makefile.
667    ///
668    /// Walks the entire syntax tree to find all `$(VAR)` and `${VAR}` references
669    /// in variable values, prerequisites, and targets.
670    ///
671    /// Note: Variable references inside recipes are not included, since recipes
672    /// are stored as raw text in the syntax tree.
673    ///
674    /// # Example
675    /// ```
676    /// use makefile_lossless::Makefile;
677    /// let makefile: Makefile = "CFLAGS = $(BASE_FLAGS) -Wall\nall: $(TARGETS)\n".parse().unwrap();
678    /// let refs: Vec<_> = makefile.variable_references().collect();
679    /// let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
680    /// assert!(names.contains(&"BASE_FLAGS".to_string()));
681    /// assert!(names.contains(&"TARGETS".to_string()));
682    /// ```
683    pub fn variable_references(&self) -> impl Iterator<Item = VariableReference> + '_ {
684        self.syntax()
685            .descendants()
686            .filter_map(VariableReference::cast)
687    }
688
689    /// Get all top-level items that overlap with the given text range.
690    ///
691    /// Since items are stored in document order, this skips items entirely
692    /// before the range and stops once past it.
693    ///
694    /// # Example
695    /// ```
696    /// use makefile_lossless::{Makefile, TextRange};
697    /// let makefile: Makefile = "CC = gcc\nall: build\n\techo done\n".parse().unwrap();
698    /// let range = TextRange::new(0.into(), 8.into());
699    /// let items: Vec<_> = makefile.items_in_range(range).collect();
700    /// assert_eq!(items.len(), 1);
701    /// ```
702    pub fn items_in_range(
703        &self,
704        range: rowan::TextRange,
705    ) -> impl Iterator<Item = MakefileItem> + '_ {
706        self.items()
707            .skip_while(move |item| item.syntax().text_range().end() <= range.start())
708            .take_while(move |item| item.syntax().text_range().start() < range.end())
709    }
710
711    /// Get all variable references that overlap with the given text range.
712    ///
713    /// Only walks descendants of top-level items that overlap the range,
714    /// rather than scanning the entire tree.
715    pub fn variable_references_in_range(
716        &self,
717        range: rowan::TextRange,
718    ) -> impl Iterator<Item = VariableReference> + '_ {
719        self.items_in_range(range).flat_map(|item| {
720            item.syntax()
721                .descendants()
722                .filter_map(VariableReference::cast)
723                .collect::<Vec<_>>()
724        })
725    }
726
727    /// Get all rules that overlap with the given text range.
728    pub fn rules_in_range(&self, range: rowan::TextRange) -> impl Iterator<Item = Rule> + '_ {
729        self.items_in_range(range).filter_map(|item| match item {
730            MakefileItem::Rule(r) => Some(r),
731            _ => None,
732        })
733    }
734
735    /// Get all variable definitions that overlap with the given text range.
736    pub fn variable_definitions_in_range(
737        &self,
738        range: rowan::TextRange,
739    ) -> impl Iterator<Item = VariableDefinition> + '_ {
740        self.items_in_range(range).filter_map(|item| match item {
741            MakefileItem::Variable(v) => Some(v),
742            _ => None,
743        })
744    }
745
746    /// Get all blocks of consecutive comment lines in the makefile.
747    ///
748    /// Returns the text range of each block. A comment block is two or more
749    /// consecutive comment lines (separated only by whitespace or blank lines).
750    ///
751    /// # Example
752    /// ```
753    /// use makefile_lossless::Makefile;
754    /// let makefile: Makefile = "# line 1\n# line 2\n# line 3\nall:\n\techo done\n".parse().unwrap();
755    /// let blocks: Vec<_> = makefile.comment_blocks().collect();
756    /// assert_eq!(blocks.len(), 1);
757    /// ```
758    pub fn comment_blocks(&self) -> impl Iterator<Item = rowan::TextRange> + '_ {
759        CommentBlockIter::new(self.syntax())
760    }
761
762    /// Add a new rule to the makefile
763    ///
764    /// # Example
765    /// ```
766    /// use makefile_lossless::Makefile;
767    /// let mut makefile = Makefile::new();
768    /// makefile.add_rule("rule");
769    /// assert_eq!(makefile.to_string(), "rule:\n");
770    /// ```
771    pub fn add_rule(&mut self, target: &str) -> Rule {
772        let mut builder = GreenNodeBuilder::new();
773        builder.start_node(RULE.into());
774        builder.token(IDENTIFIER.into(), target);
775        builder.token(OPERATOR.into(), ":");
776        builder.token(NEWLINE.into(), "\n");
777        builder.finish_node();
778
779        let syntax = SyntaxNode::new_root_mut(builder.finish());
780        let pos = self.syntax().children_with_tokens().count();
781
782        // Add a blank line before the new rule if there are existing rules
783        // This maintains standard makefile formatting
784        let needs_blank_line = self.syntax().children().any(|c| c.kind() == RULE);
785
786        if needs_blank_line {
787            // Create a BLANK_LINE node
788            let mut bl_builder = GreenNodeBuilder::new();
789            bl_builder.start_node(BLANK_LINE.into());
790            bl_builder.token(NEWLINE.into(), "\n");
791            bl_builder.finish_node();
792            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
793
794            self.syntax()
795                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
796        } else {
797            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
798        }
799
800        // Use children().count() - 1 to get the last added child node
801        // (not children_with_tokens().count() which includes tokens)
802        Rule::cast(self.syntax().children().last().unwrap()).unwrap()
803    }
804
805    /// Add a new conditional to the makefile
806    ///
807    /// # Arguments
808    /// * `conditional_type` - The type of conditional: "ifdef", "ifndef", "ifeq", or "ifneq"
809    /// * `condition` - The condition expression (e.g., "DEBUG" for ifdef/ifndef, or "(a,b)" for ifeq/ifneq)
810    /// * `if_body` - The content of the if branch
811    /// * `else_body` - Optional content for the else branch
812    ///
813    /// # Example
814    /// ```
815    /// use makefile_lossless::Makefile;
816    /// let mut makefile = Makefile::new();
817    /// makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
818    /// assert!(makefile.to_string().contains("ifdef DEBUG"));
819    /// ```
820    pub fn add_conditional(
821        &mut self,
822        conditional_type: &str,
823        condition: &str,
824        if_body: &str,
825        else_body: Option<&str>,
826    ) -> Result<Conditional, Error> {
827        // Validate conditional type
828        if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&conditional_type) {
829            return Err(Error::Parse(ParseError {
830                errors: vec![ErrorInfo {
831                    message: format!(
832                        "Invalid conditional type: {}. Must be one of: ifdef, ifndef, ifeq, ifneq",
833                        conditional_type
834                    ),
835                    line: 1,
836                    context: "add_conditional".to_string(),
837                }],
838            }));
839        }
840
841        let mut builder = GreenNodeBuilder::new();
842        builder.start_node(CONDITIONAL.into());
843
844        // Build CONDITIONAL_IF
845        builder.start_node(CONDITIONAL_IF.into());
846        builder.token(IDENTIFIER.into(), conditional_type);
847        builder.token(WHITESPACE.into(), " ");
848
849        // Wrap condition in EXPR node
850        builder.start_node(EXPR.into());
851        builder.token(IDENTIFIER.into(), condition);
852        builder.finish_node();
853
854        builder.token(NEWLINE.into(), "\n");
855        builder.finish_node();
856
857        // Add if body content
858        if !if_body.is_empty() {
859            for line in if_body.lines() {
860                if !line.is_empty() {
861                    builder.token(IDENTIFIER.into(), line);
862                }
863                builder.token(NEWLINE.into(), "\n");
864            }
865            // Add final newline if if_body doesn't end with one
866            if !if_body.ends_with('\n') && !if_body.is_empty() {
867                builder.token(NEWLINE.into(), "\n");
868            }
869        }
870
871        // Add else clause if provided
872        if let Some(else_content) = else_body {
873            builder.start_node(CONDITIONAL_ELSE.into());
874            builder.token(IDENTIFIER.into(), "else");
875            builder.token(NEWLINE.into(), "\n");
876            builder.finish_node();
877
878            // Add else body content
879            if !else_content.is_empty() {
880                for line in else_content.lines() {
881                    if !line.is_empty() {
882                        builder.token(IDENTIFIER.into(), line);
883                    }
884                    builder.token(NEWLINE.into(), "\n");
885                }
886                // Add final newline if else_content doesn't end with one
887                if !else_content.ends_with('\n') && !else_content.is_empty() {
888                    builder.token(NEWLINE.into(), "\n");
889                }
890            }
891        }
892
893        // Build CONDITIONAL_ENDIF
894        builder.start_node(CONDITIONAL_ENDIF.into());
895        builder.token(IDENTIFIER.into(), "endif");
896        builder.token(NEWLINE.into(), "\n");
897        builder.finish_node();
898
899        builder.finish_node();
900
901        let syntax = SyntaxNode::new_root_mut(builder.finish());
902        let pos = self.syntax().children_with_tokens().count();
903
904        // Add a blank line before the new conditional if there are existing elements
905        let needs_blank_line = self
906            .syntax()
907            .children()
908            .any(|c| c.kind() == RULE || c.kind() == VARIABLE || c.kind() == CONDITIONAL);
909
910        if needs_blank_line {
911            // Create a BLANK_LINE node
912            let mut bl_builder = GreenNodeBuilder::new();
913            bl_builder.start_node(BLANK_LINE.into());
914            bl_builder.token(NEWLINE.into(), "\n");
915            bl_builder.finish_node();
916            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
917
918            self.syntax()
919                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
920        } else {
921            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
922        }
923
924        // Return the newly added conditional
925        Ok(Conditional::cast(self.syntax().children().last().unwrap()).unwrap())
926    }
927
928    /// Add a new conditional to the makefile with typed items
929    ///
930    /// This is a more type-safe alternative to `add_conditional` that accepts iterators of
931    /// `MakefileItem` instead of raw strings.
932    ///
933    /// # Arguments
934    /// * `conditional_type` - The type of conditional: "ifdef", "ifndef", "ifeq", or "ifneq"
935    /// * `condition` - The condition expression (e.g., "DEBUG" for ifdef/ifndef, or "(a,b)" for ifeq/ifneq)
936    /// * `if_items` - Items for the if branch
937    /// * `else_items` - Optional items for the else branch
938    ///
939    /// # Example
940    /// ```
941    /// use makefile_lossless::{Makefile, MakefileItem};
942    /// let mut makefile = Makefile::new();
943    /// let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
944    /// let var1 = temp1.variable_definitions().next().unwrap();
945    /// let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
946    /// let var2 = temp2.variable_definitions().next().unwrap();
947    /// makefile.add_conditional_with_items(
948    ///     "ifdef",
949    ///     "DEBUG",
950    ///     vec![MakefileItem::Variable(var1)],
951    ///     Some(vec![MakefileItem::Variable(var2)])
952    /// ).unwrap();
953    /// assert!(makefile.to_string().contains("ifdef DEBUG"));
954    /// assert!(makefile.to_string().contains("CFLAGS = -g"));
955    /// assert!(makefile.to_string().contains("CFLAGS = -O2"));
956    /// ```
957    pub fn add_conditional_with_items<I1, I2>(
958        &mut self,
959        conditional_type: &str,
960        condition: &str,
961        if_items: I1,
962        else_items: Option<I2>,
963    ) -> Result<Conditional, Error>
964    where
965        I1: IntoIterator<Item = MakefileItem>,
966        I2: IntoIterator<Item = MakefileItem>,
967    {
968        // Validate conditional type
969        if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&conditional_type) {
970            return Err(Error::Parse(ParseError {
971                errors: vec![ErrorInfo {
972                    message: format!(
973                        "Invalid conditional type: {}. Must be one of: ifdef, ifndef, ifeq, ifneq",
974                        conditional_type
975                    ),
976                    line: 1,
977                    context: "add_conditional_with_items".to_string(),
978                }],
979            }));
980        }
981
982        let mut builder = GreenNodeBuilder::new();
983        builder.start_node(CONDITIONAL.into());
984
985        // Build CONDITIONAL_IF
986        builder.start_node(CONDITIONAL_IF.into());
987        builder.token(IDENTIFIER.into(), conditional_type);
988        builder.token(WHITESPACE.into(), " ");
989
990        // Wrap condition in EXPR node
991        builder.start_node(EXPR.into());
992        builder.token(IDENTIFIER.into(), condition);
993        builder.finish_node();
994
995        builder.token(NEWLINE.into(), "\n");
996        builder.finish_node();
997
998        // Add if branch items
999        for item in if_items {
1000            // Clone the item's syntax tree into our builder
1001            let item_text = item.syntax().to_string();
1002            // Parse it again to get green nodes
1003            builder.token(IDENTIFIER.into(), item_text.trim());
1004            builder.token(NEWLINE.into(), "\n");
1005        }
1006
1007        // Add else clause if provided
1008        if let Some(else_iter) = else_items {
1009            builder.start_node(CONDITIONAL_ELSE.into());
1010            builder.token(IDENTIFIER.into(), "else");
1011            builder.token(NEWLINE.into(), "\n");
1012            builder.finish_node();
1013
1014            // Add else branch items
1015            for item in else_iter {
1016                let item_text = item.syntax().to_string();
1017                builder.token(IDENTIFIER.into(), item_text.trim());
1018                builder.token(NEWLINE.into(), "\n");
1019            }
1020        }
1021
1022        // Build CONDITIONAL_ENDIF
1023        builder.start_node(CONDITIONAL_ENDIF.into());
1024        builder.token(IDENTIFIER.into(), "endif");
1025        builder.token(NEWLINE.into(), "\n");
1026        builder.finish_node();
1027
1028        builder.finish_node();
1029
1030        let syntax = SyntaxNode::new_root_mut(builder.finish());
1031        let pos = self.syntax().children_with_tokens().count();
1032
1033        // Add a blank line before the new conditional if there are existing elements
1034        let needs_blank_line = self
1035            .syntax()
1036            .children()
1037            .any(|c| c.kind() == RULE || c.kind() == VARIABLE || c.kind() == CONDITIONAL);
1038
1039        if needs_blank_line {
1040            // Create a BLANK_LINE node
1041            let mut bl_builder = GreenNodeBuilder::new();
1042            bl_builder.start_node(BLANK_LINE.into());
1043            bl_builder.token(NEWLINE.into(), "\n");
1044            bl_builder.finish_node();
1045            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1046
1047            self.syntax()
1048                .splice_children(pos..pos, vec![blank_line.into(), syntax.into()]);
1049        } else {
1050            self.syntax().splice_children(pos..pos, vec![syntax.into()]);
1051        }
1052
1053        // Return the newly added conditional
1054        Ok(Conditional::cast(self.syntax().children().last().unwrap()).unwrap())
1055    }
1056
1057    /// Read the makefile
1058    pub fn from_reader<R: std::io::Read>(mut r: R) -> Result<Makefile, Error> {
1059        let mut buf = String::new();
1060        r.read_to_string(&mut buf)?;
1061
1062        let parsed = parse(&buf, None);
1063        if !parsed.errors.is_empty() {
1064            Err(Error::Parse(ParseError {
1065                errors: parsed.errors,
1066            }))
1067        } else {
1068            Ok(parsed.root())
1069        }
1070    }
1071
1072    /// Replace rule at given index with a new rule
1073    ///
1074    /// # Example
1075    /// ```
1076    /// use makefile_lossless::Makefile;
1077    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1078    /// let new_rule: makefile_lossless::Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
1079    /// makefile.replace_rule(0, new_rule).unwrap();
1080    /// assert!(makefile.rules().any(|r| r.targets().any(|t| t == "new_rule")));
1081    /// ```
1082    pub fn replace_rule(&mut self, index: usize, new_rule: Rule) -> Result<(), Error> {
1083        let rules: Vec<_> = self
1084            .syntax()
1085            .children()
1086            .filter(|n| n.kind() == RULE)
1087            .collect();
1088
1089        if rules.is_empty() {
1090            return Err(Error::Parse(ParseError {
1091                errors: vec![ErrorInfo {
1092                    message: "Cannot replace rule in empty makefile".to_string(),
1093                    line: 1,
1094                    context: "replace_rule".to_string(),
1095                }],
1096            }));
1097        }
1098
1099        if index >= rules.len() {
1100            return Err(Error::Parse(ParseError {
1101                errors: vec![ErrorInfo {
1102                    message: format!(
1103                        "Rule index {} out of bounds (max {})",
1104                        index,
1105                        rules.len() - 1
1106                    ),
1107                    line: 1,
1108                    context: "replace_rule".to_string(),
1109                }],
1110            }));
1111        }
1112
1113        let target_node = &rules[index];
1114        let target_index = target_node.index();
1115
1116        // Replace the rule at the target index
1117        self.syntax().splice_children(
1118            target_index..target_index + 1,
1119            vec![new_rule.syntax().clone().into()],
1120        );
1121        Ok(())
1122    }
1123
1124    /// Remove rule at given index
1125    ///
1126    /// # Example
1127    /// ```
1128    /// use makefile_lossless::Makefile;
1129    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1130    /// let removed = makefile.remove_rule(0).unwrap();
1131    /// assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule1"]);
1132    /// assert_eq!(makefile.rules().count(), 1);
1133    /// ```
1134    pub fn remove_rule(&mut self, index: usize) -> Result<Rule, Error> {
1135        let rules: Vec<_> = self
1136            .syntax()
1137            .children()
1138            .filter(|n| n.kind() == RULE)
1139            .collect();
1140
1141        if rules.is_empty() {
1142            return Err(Error::Parse(ParseError {
1143                errors: vec![ErrorInfo {
1144                    message: "Cannot remove rule from empty makefile".to_string(),
1145                    line: 1,
1146                    context: "remove_rule".to_string(),
1147                }],
1148            }));
1149        }
1150
1151        if index >= rules.len() {
1152            return Err(Error::Parse(ParseError {
1153                errors: vec![ErrorInfo {
1154                    message: format!(
1155                        "Rule index {} out of bounds (max {})",
1156                        index,
1157                        rules.len() - 1
1158                    ),
1159                    line: 1,
1160                    context: "remove_rule".to_string(),
1161                }],
1162            }));
1163        }
1164
1165        let target_node = rules[index].clone();
1166        let target_index = target_node.index();
1167
1168        // Remove the rule at the target index
1169        self.syntax()
1170            .splice_children(target_index..target_index + 1, vec![]);
1171        Ok(Rule::cast(target_node).unwrap())
1172    }
1173
1174    /// Insert rule at given position
1175    ///
1176    /// # Example
1177    /// ```
1178    /// use makefile_lossless::Makefile;
1179    /// let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1180    /// let new_rule: makefile_lossless::Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
1181    /// makefile.insert_rule(1, new_rule).unwrap();
1182    /// let targets: Vec<_> = makefile.rules().flat_map(|r| r.targets().collect::<Vec<_>>()).collect();
1183    /// assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
1184    /// ```
1185    pub fn insert_rule(&mut self, index: usize, new_rule: Rule) -> Result<(), Error> {
1186        let rules: Vec<_> = self
1187            .syntax()
1188            .children()
1189            .filter(|n| n.kind() == RULE)
1190            .collect();
1191
1192        if index > rules.len() {
1193            return Err(Error::Parse(ParseError {
1194                errors: vec![ErrorInfo {
1195                    message: format!("Rule index {} out of bounds (max {})", index, rules.len()),
1196                    line: 1,
1197                    context: "insert_rule".to_string(),
1198                }],
1199            }));
1200        }
1201
1202        let target_index = if index == rules.len() {
1203            // Insert at the end
1204            self.syntax().children_with_tokens().count()
1205        } else {
1206            // Insert before the rule at the given index
1207            rules[index].index()
1208        };
1209
1210        // Build the nodes to insert
1211        let mut nodes_to_insert = Vec::new();
1212
1213        // Determine if we need to add blank lines to maintain formatting consistency
1214        if index == 0 && !rules.is_empty() {
1215            // Inserting before the first rule - check if first rule has a blank line before it
1216            // If so, we should add one after our new rule instead
1217            // For now, just add the rule without a blank line before it
1218            nodes_to_insert.push(new_rule.syntax().clone().into());
1219
1220            // Add a blank line after the new rule
1221            let mut bl_builder = GreenNodeBuilder::new();
1222            bl_builder.start_node(BLANK_LINE.into());
1223            bl_builder.token(NEWLINE.into(), "\n");
1224            bl_builder.finish_node();
1225            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1226            nodes_to_insert.push(blank_line.into());
1227        } else if index < rules.len() {
1228            // Inserting in the middle (before an existing rule)
1229            // The syntax tree structure is: ... [maybe BLANK_LINE] RULE(target) ...
1230            // We're inserting right before RULE(target)
1231
1232            // If there's a BLANK_LINE immediately before the target rule,
1233            // it will stay there and separate the previous rule from our new rule.
1234            // We don't need to add a BLANK_LINE before our new rule in that case.
1235
1236            // But we DO need to add a BLANK_LINE after our new rule to separate it
1237            // from the target rule (which we're inserting before).
1238
1239            // Check if there's a blank line immediately before target_index
1240            let has_blank_before = if target_index > 0 {
1241                self.syntax()
1242                    .children_with_tokens()
1243                    .nth(target_index - 1)
1244                    .and_then(|n| n.as_node().map(|node| node.kind() == BLANK_LINE))
1245                    .unwrap_or(false)
1246            } else {
1247                false
1248            };
1249
1250            // Only add a blank before if there isn't one already and we're not at the start
1251            if !has_blank_before && index > 0 {
1252                let mut bl_builder = GreenNodeBuilder::new();
1253                bl_builder.start_node(BLANK_LINE.into());
1254                bl_builder.token(NEWLINE.into(), "\n");
1255                bl_builder.finish_node();
1256                let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1257                nodes_to_insert.push(blank_line.into());
1258            }
1259
1260            // Add the new rule
1261            nodes_to_insert.push(new_rule.syntax().clone().into());
1262
1263            // Always add a blank line after the new rule to separate it from the next rule
1264            let mut bl_builder = GreenNodeBuilder::new();
1265            bl_builder.start_node(BLANK_LINE.into());
1266            bl_builder.token(NEWLINE.into(), "\n");
1267            bl_builder.finish_node();
1268            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1269            nodes_to_insert.push(blank_line.into());
1270        } else {
1271            // Inserting at the end when there are existing rules
1272            // Add a blank line before the new rule
1273            let mut bl_builder = GreenNodeBuilder::new();
1274            bl_builder.start_node(BLANK_LINE.into());
1275            bl_builder.token(NEWLINE.into(), "\n");
1276            bl_builder.finish_node();
1277            let blank_line = SyntaxNode::new_root_mut(bl_builder.finish());
1278            nodes_to_insert.push(blank_line.into());
1279
1280            // Add the new rule
1281            nodes_to_insert.push(new_rule.syntax().clone().into());
1282        }
1283
1284        // Insert all nodes at the target index
1285        self.syntax()
1286            .splice_children(target_index..target_index, nodes_to_insert);
1287        Ok(())
1288    }
1289
1290    /// Get all include directives in the makefile
1291    ///
1292    /// # Example
1293    /// ```
1294    /// use makefile_lossless::Makefile;
1295    /// let makefile: Makefile = "include config.mk\n-include .env\n".parse().unwrap();
1296    /// let includes = makefile.includes().collect::<Vec<_>>();
1297    /// assert_eq!(includes.len(), 2);
1298    /// ```
1299    pub fn includes(&self) -> impl Iterator<Item = Include> {
1300        self.syntax().children().filter_map(Include::cast)
1301    }
1302
1303    /// Get all included file paths
1304    ///
1305    /// # Example
1306    /// ```
1307    /// use makefile_lossless::Makefile;
1308    /// let makefile: Makefile = "include config.mk\n-include .env\n".parse().unwrap();
1309    /// let paths = makefile.included_files().collect::<Vec<_>>();
1310    /// assert_eq!(paths, vec!["config.mk", ".env"]);
1311    /// ```
1312    pub fn included_files(&self) -> impl Iterator<Item = String> + '_ {
1313        // We need to collect all Include nodes from anywhere in the syntax tree,
1314        // not just direct children of the root, to handle includes in conditionals
1315        fn collect_includes(node: &SyntaxNode) -> Vec<Include> {
1316            let mut includes = Vec::new();
1317
1318            // First check if this node itself is an Include
1319            if let Some(include) = Include::cast(node.clone()) {
1320                includes.push(include);
1321            }
1322
1323            // Then recurse into all children
1324            for child in node.children() {
1325                includes.extend(collect_includes(&child));
1326            }
1327
1328            includes
1329        }
1330
1331        // Start collection from the root node
1332        let includes = collect_includes(self.syntax());
1333
1334        // Convert to an iterator of paths
1335        includes.into_iter().map(|include| {
1336            include
1337                .syntax()
1338                .children()
1339                .find(|node| node.kind() == EXPR)
1340                .map(|expr| expr.text().to_string().trim().to_string())
1341                .unwrap_or_default()
1342        })
1343    }
1344
1345    /// Find the first rule with a specific target name
1346    ///
1347    /// # Example
1348    /// ```
1349    /// use makefile_lossless::Makefile;
1350    /// let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1351    /// let rule = makefile.find_rule_by_target("rule2");
1352    /// assert!(rule.is_some());
1353    /// assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
1354    /// ```
1355    pub fn find_rule_by_target(&self, target: &str) -> Option<Rule> {
1356        self.rules()
1357            .find(|rule| rule.targets().any(|t| t == target))
1358    }
1359
1360    /// Find all rules with a specific target name
1361    ///
1362    /// # Example
1363    /// ```
1364    /// use makefile_lossless::Makefile;
1365    /// let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n".parse().unwrap();
1366    /// let rules: Vec<_> = makefile.find_rules_by_target("rule1").collect();
1367    /// assert_eq!(rules.len(), 2);
1368    /// ```
1369    pub fn find_rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator<Item = Rule> + 'a {
1370        self.rules_by_target(target)
1371    }
1372
1373    /// Find the first rule whose target matches the given pattern
1374    ///
1375    /// Supports make-style pattern matching where `%` in a rule's target acts as a wildcard.
1376    /// For example, a rule with target `%.o` will match `foo.o`, `bar.o`, etc.
1377    ///
1378    /// # Example
1379    /// ```
1380    /// use makefile_lossless::Makefile;
1381    /// let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
1382    /// let rule = makefile.find_rule_by_target_pattern("foo.o");
1383    /// assert!(rule.is_some());
1384    /// ```
1385    pub fn find_rule_by_target_pattern(&self, target: &str) -> Option<Rule> {
1386        self.rules()
1387            .find(|rule| rule.targets().any(|t| matches_pattern(&t, target)))
1388    }
1389
1390    /// Find all rules whose targets match the given pattern
1391    ///
1392    /// Supports make-style pattern matching where `%` in a rule's target acts as a wildcard.
1393    /// For example, a rule with target `%.o` will match `foo.o`, `bar.o`, etc.
1394    ///
1395    /// # Example
1396    /// ```
1397    /// use makefile_lossless::Makefile;
1398    /// let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n".parse().unwrap();
1399    /// let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
1400    /// assert_eq!(rules.len(), 2);
1401    /// ```
1402    pub fn find_rules_by_target_pattern<'a>(
1403        &'a self,
1404        target: &'a str,
1405    ) -> impl Iterator<Item = Rule> + 'a {
1406        self.rules()
1407            .filter(move |rule| rule.targets().any(|t| matches_pattern(&t, target)))
1408    }
1409
1410    /// Add a target to .PHONY (creates .PHONY rule if it doesn't exist)
1411    ///
1412    /// # Example
1413    /// ```
1414    /// use makefile_lossless::Makefile;
1415    /// let mut makefile = Makefile::new();
1416    /// makefile.add_phony_target("clean").unwrap();
1417    /// assert!(makefile.is_phony("clean"));
1418    /// ```
1419    pub fn add_phony_target(&mut self, target: &str) -> Result<(), Error> {
1420        // Find existing .PHONY rule
1421        if let Some(mut phony_rule) = self.find_rule_by_target(".PHONY") {
1422            // Check if target is already in prerequisites
1423            if !phony_rule.prerequisites().any(|p| p == target) {
1424                phony_rule.add_prerequisite(target)?;
1425            }
1426        } else {
1427            // Create new .PHONY rule
1428            let mut phony_rule = self.add_rule(".PHONY");
1429            phony_rule.add_prerequisite(target)?;
1430        }
1431        Ok(())
1432    }
1433
1434    /// Remove a target from .PHONY (removes .PHONY rule if it becomes empty)
1435    ///
1436    /// Returns `true` if the target was found and removed, `false` if it wasn't in .PHONY.
1437    /// If there are multiple .PHONY rules, it removes the target from the first rule that contains it.
1438    ///
1439    /// # Example
1440    /// ```
1441    /// use makefile_lossless::Makefile;
1442    /// let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
1443    /// assert!(makefile.remove_phony_target("clean").unwrap());
1444    /// assert!(!makefile.is_phony("clean"));
1445    /// assert!(makefile.is_phony("test"));
1446    /// ```
1447    pub fn remove_phony_target(&mut self, target: &str) -> Result<bool, Error> {
1448        // Find the first .PHONY rule that contains the target
1449        let mut phony_rule = None;
1450        for rule in self.rules_by_target(".PHONY") {
1451            if rule.prerequisites().any(|p| p == target) {
1452                phony_rule = Some(rule);
1453                break;
1454            }
1455        }
1456
1457        let mut phony_rule = match phony_rule {
1458            Some(rule) => rule,
1459            None => return Ok(false),
1460        };
1461
1462        // Count prerequisites before removal
1463        let prereq_count = phony_rule.prerequisites().count();
1464
1465        // Remove the prerequisite
1466        phony_rule.remove_prerequisite(target)?;
1467
1468        // Check if .PHONY has no more prerequisites, if so remove the rule
1469        if prereq_count == 1 {
1470            // We just removed the last prerequisite, so remove the entire rule
1471            phony_rule.remove()?;
1472        }
1473
1474        Ok(true)
1475    }
1476
1477    /// Check if a target is marked as phony
1478    ///
1479    /// # Example
1480    /// ```
1481    /// use makefile_lossless::Makefile;
1482    /// let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
1483    /// assert!(makefile.is_phony("clean"));
1484    /// assert!(makefile.is_phony("test"));
1485    /// assert!(!makefile.is_phony("build"));
1486    /// ```
1487    pub fn is_phony(&self, target: &str) -> bool {
1488        // Check all .PHONY rules since there can be multiple
1489        self.rules_by_target(".PHONY")
1490            .any(|rule| rule.prerequisites().any(|p| p == target))
1491    }
1492
1493    /// Get all phony targets
1494    ///
1495    /// # Example
1496    /// ```
1497    /// use makefile_lossless::Makefile;
1498    /// let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
1499    /// let phony_targets: Vec<_> = makefile.phony_targets().collect();
1500    /// assert_eq!(phony_targets, vec!["clean", "test", "build"]);
1501    /// ```
1502    pub fn phony_targets(&self) -> impl Iterator<Item = String> + '_ {
1503        // Collect from all .PHONY rules since there can be multiple
1504        self.rules_by_target(".PHONY")
1505            .flat_map(|rule| rule.prerequisites().collect::<Vec<_>>())
1506    }
1507
1508    /// Add a new include directive at the beginning of the makefile
1509    ///
1510    /// # Arguments
1511    /// * `path` - The file path to include (e.g., "config.mk")
1512    ///
1513    /// # Example
1514    /// ```
1515    /// use makefile_lossless::Makefile;
1516    /// let mut makefile = Makefile::new();
1517    /// makefile.add_include("config.mk");
1518    /// assert_eq!(makefile.included_files().collect::<Vec<_>>(), vec!["config.mk"]);
1519    /// ```
1520    pub fn add_include(&mut self, path: &str) -> Include {
1521        let mut builder = GreenNodeBuilder::new();
1522        builder.start_node(INCLUDE.into());
1523        builder.token(IDENTIFIER.into(), "include");
1524        builder.token(WHITESPACE.into(), " ");
1525
1526        // Wrap path in EXPR node
1527        builder.start_node(EXPR.into());
1528        builder.token(IDENTIFIER.into(), path);
1529        builder.finish_node();
1530
1531        builder.token(NEWLINE.into(), "\n");
1532        builder.finish_node();
1533
1534        let syntax = SyntaxNode::new_root_mut(builder.finish());
1535
1536        // Insert at the beginning (position 0)
1537        self.syntax().splice_children(0..0, vec![syntax.into()]);
1538
1539        // Return the newly added include (first child)
1540        Include::cast(self.syntax().children().next().unwrap()).unwrap()
1541    }
1542
1543    /// Insert an include directive at a specific position
1544    ///
1545    /// The position is relative to other top-level items (rules, variables, includes, conditionals).
1546    ///
1547    /// # Arguments
1548    /// * `index` - The position to insert at (0 = beginning, items().count() = end)
1549    /// * `path` - The file path to include (e.g., "config.mk")
1550    ///
1551    /// # Example
1552    /// ```
1553    /// use makefile_lossless::Makefile;
1554    /// let mut makefile: Makefile = "VAR = value\nrule:\n\tcommand\n".parse().unwrap();
1555    /// makefile.insert_include(1, "config.mk").unwrap();
1556    /// let items: Vec<_> = makefile.items().collect();
1557    /// assert_eq!(items.len(), 3); // VAR, include, rule
1558    /// ```
1559    pub fn insert_include(&mut self, index: usize, path: &str) -> Result<Include, Error> {
1560        let items: Vec<_> = self.syntax().children().collect();
1561
1562        if index > items.len() {
1563            return Err(Error::Parse(ParseError {
1564                errors: vec![ErrorInfo {
1565                    message: format!("Index {} out of bounds (max {})", index, items.len()),
1566                    line: 1,
1567                    context: "insert_include".to_string(),
1568                }],
1569            }));
1570        }
1571
1572        let mut builder = GreenNodeBuilder::new();
1573        builder.start_node(INCLUDE.into());
1574        builder.token(IDENTIFIER.into(), "include");
1575        builder.token(WHITESPACE.into(), " ");
1576
1577        // Wrap path in EXPR node
1578        builder.start_node(EXPR.into());
1579        builder.token(IDENTIFIER.into(), path);
1580        builder.finish_node();
1581
1582        builder.token(NEWLINE.into(), "\n");
1583        builder.finish_node();
1584
1585        let syntax = SyntaxNode::new_root_mut(builder.finish());
1586
1587        let target_index = if index == items.len() {
1588            // Insert at the end
1589            self.syntax().children_with_tokens().count()
1590        } else {
1591            // Insert before the item at the given index
1592            items[index].index()
1593        };
1594
1595        // Insert the include node
1596        self.syntax()
1597            .splice_children(target_index..target_index, vec![syntax.into()]);
1598
1599        // Find and return the newly added include
1600        // It should be at the child index we inserted at
1601        Ok(Include::cast(self.syntax().children().nth(index).unwrap()).unwrap())
1602    }
1603
1604    /// Insert an include directive after a specific MakefileItem
1605    ///
1606    /// This is useful when you want to insert an include relative to another item in the makefile.
1607    ///
1608    /// # Arguments
1609    /// * `after` - The MakefileItem to insert after
1610    /// * `path` - The file path to include (e.g., "config.mk")
1611    ///
1612    /// # Example
1613    /// ```
1614    /// use makefile_lossless::Makefile;
1615    /// let mut makefile: Makefile = "VAR1 = value1\nVAR2 = value2\n".parse().unwrap();
1616    /// let first_var = makefile.items().next().unwrap();
1617    /// makefile.insert_include_after(&first_var, "config.mk").unwrap();
1618    /// let paths: Vec<_> = makefile.included_files().collect();
1619    /// assert_eq!(paths, vec!["config.mk"]);
1620    /// ```
1621    pub fn insert_include_after(
1622        &mut self,
1623        after: &MakefileItem,
1624        path: &str,
1625    ) -> Result<Include, Error> {
1626        let mut builder = GreenNodeBuilder::new();
1627        builder.start_node(INCLUDE.into());
1628        builder.token(IDENTIFIER.into(), "include");
1629        builder.token(WHITESPACE.into(), " ");
1630
1631        // Wrap path in EXPR node
1632        builder.start_node(EXPR.into());
1633        builder.token(IDENTIFIER.into(), path);
1634        builder.finish_node();
1635
1636        builder.token(NEWLINE.into(), "\n");
1637        builder.finish_node();
1638
1639        let syntax = SyntaxNode::new_root_mut(builder.finish());
1640
1641        // Find the position of the item to insert after
1642        let after_syntax = after.syntax();
1643        let target_index = after_syntax.index() + 1;
1644
1645        // Insert the include node after the target item
1646        self.syntax()
1647            .splice_children(target_index..target_index, vec![syntax.into()]);
1648
1649        // Find and return the newly added include
1650        // It should be the child immediately after the 'after' item
1651        let after_child_index = self
1652            .syntax()
1653            .children()
1654            .position(|child| child.text_range() == after_syntax.text_range())
1655            .ok_or_else(|| {
1656                Error::Parse(ParseError {
1657                    errors: vec![ErrorInfo {
1658                        message: "Could not find the reference item".to_string(),
1659                        line: 1,
1660                        context: "insert_include_after".to_string(),
1661                    }],
1662                })
1663            })?;
1664
1665        Ok(Include::cast(self.syntax().children().nth(after_child_index + 1).unwrap()).unwrap())
1666    }
1667}
1668
1669#[cfg(test)]
1670mod tests {
1671    use super::*;
1672
1673    #[test]
1674    fn test_makefile_item_replace_variable_with_variable() {
1675        let makefile: Makefile = "VAR1 = old\nrule:\n\tcommand\n".parse().unwrap();
1676        let temp: Makefile = "VAR2 = new\n".parse().unwrap();
1677        let new_var = temp.variable_definitions().next().unwrap();
1678        let mut first_item = makefile.items().next().unwrap();
1679        first_item.replace(MakefileItem::Variable(new_var)).unwrap();
1680
1681        let result = makefile.to_string();
1682        assert_eq!(result, "VAR2 = new\nrule:\n\tcommand\n");
1683    }
1684
1685    #[test]
1686    fn test_makefile_item_replace_variable_with_rule() {
1687        let makefile: Makefile = "VAR1 = value\nrule1:\n\tcommand1\n".parse().unwrap();
1688        let temp: Makefile = "new_rule:\n\tnew_command\n".parse().unwrap();
1689        let new_rule = temp.rules().next().unwrap();
1690        let mut first_item = makefile.items().next().unwrap();
1691        first_item.replace(MakefileItem::Rule(new_rule)).unwrap();
1692
1693        let result = makefile.to_string();
1694        assert_eq!(result, "new_rule:\n\tnew_command\nrule1:\n\tcommand1\n");
1695    }
1696
1697    #[test]
1698    fn test_makefile_item_replace_preserves_position() {
1699        let makefile: Makefile = "VAR1 = first\nVAR2 = second\nVAR3 = third\n"
1700            .parse()
1701            .unwrap();
1702        let temp: Makefile = "NEW = replacement\n".parse().unwrap();
1703        let new_var = temp.variable_definitions().next().unwrap();
1704
1705        // Replace the second item
1706        let mut second_item = makefile.items().nth(1).unwrap();
1707        second_item
1708            .replace(MakefileItem::Variable(new_var))
1709            .unwrap();
1710
1711        let items: Vec<_> = makefile.variable_definitions().collect();
1712        assert_eq!(items.len(), 3);
1713        assert_eq!(items[0].name(), Some("VAR1".to_string()));
1714        assert_eq!(items[1].name(), Some("NEW".to_string()));
1715        assert_eq!(items[2].name(), Some("VAR3".to_string()));
1716    }
1717
1718    #[test]
1719    fn test_makefile_item_add_comment() {
1720        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1721        let mut item = makefile.items().next().unwrap();
1722        item.add_comment("This is a variable").unwrap();
1723
1724        let result = makefile.to_string();
1725        assert_eq!(result, "# This is a variable\nVAR = value\n");
1726    }
1727
1728    #[test]
1729    fn test_makefile_item_add_multiple_comments() {
1730        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1731        let mut item = makefile.items().next().unwrap();
1732        item.add_comment("Comment 1").unwrap();
1733        // Note: After modifying the tree, we need to get a fresh reference
1734        let mut item = makefile.items().next().unwrap();
1735        item.add_comment("Comment 2").unwrap();
1736
1737        let result = makefile.to_string();
1738        // Comments are added before the item, so adding Comment 2 after Comment 1
1739        // results in Comment 1 appearing first (furthest from item), then Comment 2
1740        assert_eq!(result, "# Comment 1\n# Comment 2\nVAR = value\n");
1741    }
1742
1743    #[test]
1744    fn test_makefile_item_preceding_comments() {
1745        let makefile: Makefile = "# Comment 1\n# Comment 2\nVAR = value\n".parse().unwrap();
1746        let item = makefile.items().next().unwrap();
1747        let comments: Vec<_> = item.preceding_comments().collect();
1748        assert_eq!(comments.len(), 2);
1749        assert_eq!(comments[0], "Comment 1");
1750        assert_eq!(comments[1], "Comment 2");
1751    }
1752
1753    #[test]
1754    fn test_makefile_item_preceding_comments_no_comments() {
1755        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1756        let item = makefile.items().next().unwrap();
1757        let comments: Vec<_> = item.preceding_comments().collect();
1758        assert_eq!(comments.len(), 0);
1759    }
1760
1761    #[test]
1762    fn test_makefile_item_preceding_comments_ignores_shebang() {
1763        let makefile: Makefile = "#!/usr/bin/make\n# Real comment\nVAR = value\n"
1764            .parse()
1765            .unwrap();
1766        let item = makefile.items().next().unwrap();
1767        let comments: Vec<_> = item.preceding_comments().collect();
1768        assert_eq!(comments.len(), 1);
1769        assert_eq!(comments[0], "Real comment");
1770    }
1771
1772    #[test]
1773    fn test_makefile_item_remove_comments() {
1774        let makefile: Makefile = "# Comment 1\n# Comment 2\nVAR = value\n".parse().unwrap();
1775        // Get a fresh reference to the item to ensure we have the current tree state
1776        let mut item = makefile.items().next().unwrap();
1777        let count = item.remove_comments().unwrap();
1778
1779        assert_eq!(count, 2);
1780        let result = makefile.to_string();
1781        assert_eq!(result, "VAR = value\n");
1782    }
1783
1784    #[test]
1785    fn test_makefile_item_remove_comments_no_comments() {
1786        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1787        let mut item = makefile.items().next().unwrap();
1788        let count = item.remove_comments().unwrap();
1789
1790        assert_eq!(count, 0);
1791        assert_eq!(makefile.to_string(), "VAR = value\n");
1792    }
1793
1794    #[test]
1795    fn test_makefile_item_modify_comment() {
1796        let makefile: Makefile = "# Old comment\nVAR = value\n".parse().unwrap();
1797        let mut item = makefile.items().next().unwrap();
1798        let modified = item.modify_comment("New comment").unwrap();
1799
1800        assert!(modified);
1801        let result = makefile.to_string();
1802        assert_eq!(result, "# New comment\nVAR = value\n");
1803    }
1804
1805    #[test]
1806    fn test_makefile_item_modify_comment_no_comment() {
1807        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1808        let mut item = makefile.items().next().unwrap();
1809        let modified = item.modify_comment("New comment").unwrap();
1810
1811        assert!(!modified);
1812        assert_eq!(makefile.to_string(), "VAR = value\n");
1813    }
1814
1815    #[test]
1816    fn test_makefile_item_modify_comment_modifies_closest() {
1817        let makefile: Makefile = "# Comment 1\n# Comment 2\n# Comment 3\nVAR = value\n"
1818            .parse()
1819            .unwrap();
1820        let mut item = makefile.items().next().unwrap();
1821        let modified = item.modify_comment("Modified").unwrap();
1822
1823        assert!(modified);
1824        let result = makefile.to_string();
1825        assert_eq!(
1826            result,
1827            "# Comment 1\n# Comment 2\n# Modified\nVAR = value\n"
1828        );
1829    }
1830
1831    #[test]
1832    fn test_makefile_item_comment_workflow() {
1833        // Test adding, modifying, and removing comments in sequence
1834        let makefile: Makefile = "VAR = value\n".parse().unwrap();
1835        let mut item = makefile.items().next().unwrap();
1836
1837        // Add a comment
1838        item.add_comment("Initial comment").unwrap();
1839        assert_eq!(makefile.to_string(), "# Initial comment\nVAR = value\n");
1840
1841        // Get a fresh reference after modification
1842        let mut item = makefile.items().next().unwrap();
1843        // Modify it
1844        item.modify_comment("Updated comment").unwrap();
1845        assert_eq!(makefile.to_string(), "# Updated comment\nVAR = value\n");
1846
1847        // Get a fresh reference after modification
1848        let mut item = makefile.items().next().unwrap();
1849        // Remove it
1850        let count = item.remove_comments().unwrap();
1851        assert_eq!(count, 1);
1852        assert_eq!(makefile.to_string(), "VAR = value\n");
1853    }
1854
1855    #[test]
1856    fn test_makefile_item_replace_with_comments() {
1857        let makefile: Makefile = "# Comment for VAR1\nVAR1 = old\nrule:\n\tcommand\n"
1858            .parse()
1859            .unwrap();
1860        let temp: Makefile = "VAR2 = new\n".parse().unwrap();
1861        let new_var = temp.variable_definitions().next().unwrap();
1862        let mut first_item = makefile.items().next().unwrap();
1863
1864        // Verify comment exists before replace
1865        let comments: Vec<_> = first_item.preceding_comments().collect();
1866        assert_eq!(comments.len(), 1);
1867        assert_eq!(comments[0], "Comment for VAR1");
1868
1869        // Replace the item
1870        first_item.replace(MakefileItem::Variable(new_var)).unwrap();
1871
1872        let result = makefile.to_string();
1873        // The comment should still be there (replace preserves preceding comments)
1874        assert_eq!(result, "# Comment for VAR1\nVAR2 = new\nrule:\n\tcommand\n");
1875    }
1876
1877    #[test]
1878    fn test_makefile_item_insert_before_variable() {
1879        let makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
1880        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1881        let new_var = temp.variable_definitions().next().unwrap();
1882        let mut second_item = makefile.items().nth(1).unwrap();
1883        second_item
1884            .insert_before(MakefileItem::Variable(new_var))
1885            .unwrap();
1886
1887        let result = makefile.to_string();
1888        assert_eq!(result, "VAR1 = first\nVAR_NEW = inserted\nVAR2 = second\n");
1889    }
1890
1891    #[test]
1892    fn test_makefile_item_insert_after_variable() {
1893        let makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
1894        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1895        let new_var = temp.variable_definitions().next().unwrap();
1896        let mut first_item = makefile.items().next().unwrap();
1897        first_item
1898            .insert_after(MakefileItem::Variable(new_var))
1899            .unwrap();
1900
1901        let result = makefile.to_string();
1902        assert_eq!(result, "VAR1 = first\nVAR_NEW = inserted\nVAR2 = second\n");
1903    }
1904
1905    #[test]
1906    fn test_makefile_item_insert_before_first_item() {
1907        let makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
1908        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1909        let new_var = temp.variable_definitions().next().unwrap();
1910        let mut first_item = makefile.items().next().unwrap();
1911        first_item
1912            .insert_before(MakefileItem::Variable(new_var))
1913            .unwrap();
1914
1915        let result = makefile.to_string();
1916        assert_eq!(result, "VAR_NEW = inserted\nVAR1 = first\nVAR2 = second\n");
1917    }
1918
1919    #[test]
1920    fn test_makefile_item_insert_after_last_item() {
1921        let makefile: Makefile = "VAR1 = first\nVAR2 = second\n".parse().unwrap();
1922        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
1923        let new_var = temp.variable_definitions().next().unwrap();
1924        let mut last_item = makefile.items().nth(1).unwrap();
1925        last_item
1926            .insert_after(MakefileItem::Variable(new_var))
1927            .unwrap();
1928
1929        let result = makefile.to_string();
1930        assert_eq!(result, "VAR1 = first\nVAR2 = second\nVAR_NEW = inserted\n");
1931    }
1932
1933    #[test]
1934    fn test_makefile_item_insert_before_include() {
1935        let makefile: Makefile = "VAR1 = value\nrule:\n\tcommand\n".parse().unwrap();
1936        let temp: Makefile = "include test.mk\n".parse().unwrap();
1937        let new_include = temp.includes().next().unwrap();
1938        let mut first_item = makefile.items().next().unwrap();
1939        first_item
1940            .insert_before(MakefileItem::Include(new_include))
1941            .unwrap();
1942
1943        let result = makefile.to_string();
1944        assert_eq!(result, "include test.mk\nVAR1 = value\nrule:\n\tcommand\n");
1945    }
1946
1947    #[test]
1948    fn test_makefile_item_insert_after_include() {
1949        let makefile: Makefile = "VAR1 = value\nrule:\n\tcommand\n".parse().unwrap();
1950        let temp: Makefile = "include test.mk\n".parse().unwrap();
1951        let new_include = temp.includes().next().unwrap();
1952        let mut first_item = makefile.items().next().unwrap();
1953        first_item
1954            .insert_after(MakefileItem::Include(new_include))
1955            .unwrap();
1956
1957        let result = makefile.to_string();
1958        assert_eq!(result, "VAR1 = value\ninclude test.mk\nrule:\n\tcommand\n");
1959    }
1960
1961    #[test]
1962    fn test_makefile_item_insert_before_rule() {
1963        let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1964        let temp: Makefile = "new_rule:\n\tnew_command\n".parse().unwrap();
1965        let new_rule = temp.rules().next().unwrap();
1966        let mut second_item = makefile.items().nth(1).unwrap();
1967        second_item
1968            .insert_before(MakefileItem::Rule(new_rule))
1969            .unwrap();
1970
1971        let result = makefile.to_string();
1972        assert_eq!(
1973            result,
1974            "rule1:\n\tcommand1\nnew_rule:\n\tnew_command\nrule2:\n\tcommand2\n"
1975        );
1976    }
1977
1978    #[test]
1979    fn test_makefile_item_insert_after_rule() {
1980        let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
1981        let temp: Makefile = "new_rule:\n\tnew_command\n".parse().unwrap();
1982        let new_rule = temp.rules().next().unwrap();
1983        let mut first_item = makefile.items().next().unwrap();
1984        first_item
1985            .insert_after(MakefileItem::Rule(new_rule))
1986            .unwrap();
1987
1988        let result = makefile.to_string();
1989        assert_eq!(
1990            result,
1991            "rule1:\n\tcommand1\nnew_rule:\n\tnew_command\nrule2:\n\tcommand2\n"
1992        );
1993    }
1994
1995    #[test]
1996    fn test_makefile_item_insert_before_with_comments() {
1997        let makefile: Makefile = "# Comment 1\nVAR1 = first\n# Comment 2\nVAR2 = second\n"
1998            .parse()
1999            .unwrap();
2000        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
2001        let new_var = temp.variable_definitions().next().unwrap();
2002        let mut second_item = makefile.items().nth(1).unwrap();
2003        second_item
2004            .insert_before(MakefileItem::Variable(new_var))
2005            .unwrap();
2006
2007        let result = makefile.to_string();
2008        // The new variable should be inserted before Comment 2 (which precedes VAR2)
2009        // This is correct because insert_before inserts before the item and its preceding comments
2010        assert_eq!(
2011            result,
2012            "# Comment 1\nVAR1 = first\n# Comment 2\nVAR_NEW = inserted\nVAR2 = second\n"
2013        );
2014    }
2015
2016    #[test]
2017    fn test_makefile_item_insert_after_with_comments() {
2018        let makefile: Makefile = "# Comment 1\nVAR1 = first\n# Comment 2\nVAR2 = second\n"
2019            .parse()
2020            .unwrap();
2021        let temp: Makefile = "VAR_NEW = inserted\n".parse().unwrap();
2022        let new_var = temp.variable_definitions().next().unwrap();
2023        let mut first_item = makefile.items().next().unwrap();
2024        first_item
2025            .insert_after(MakefileItem::Variable(new_var))
2026            .unwrap();
2027
2028        let result = makefile.to_string();
2029        // The new variable should be inserted between VAR1 and Comment 2/VAR2
2030        assert_eq!(
2031            result,
2032            "# Comment 1\nVAR1 = first\nVAR_NEW = inserted\n# Comment 2\nVAR2 = second\n"
2033        );
2034    }
2035
2036    #[test]
2037    fn test_makefile_item_insert_before_preserves_formatting() {
2038        let makefile: Makefile = "VAR1  =  first\nVAR2  =  second\n".parse().unwrap();
2039        let temp: Makefile = "VAR_NEW  =  inserted\n".parse().unwrap();
2040        let new_var = temp.variable_definitions().next().unwrap();
2041        let mut second_item = makefile.items().nth(1).unwrap();
2042        second_item
2043            .insert_before(MakefileItem::Variable(new_var))
2044            .unwrap();
2045
2046        let result = makefile.to_string();
2047        // Formatting of the new item is preserved from its source
2048        assert_eq!(
2049            result,
2050            "VAR1  =  first\nVAR_NEW  =  inserted\nVAR2  =  second\n"
2051        );
2052    }
2053
2054    #[test]
2055    fn test_makefile_item_insert_multiple_items() {
2056        let makefile: Makefile = "VAR1 = first\nVAR2 = last\n".parse().unwrap();
2057        let temp: Makefile = "VAR_A = a\nVAR_B = b\n".parse().unwrap();
2058        let mut new_vars: Vec<_> = temp.variable_definitions().collect();
2059
2060        let mut target_item = makefile.items().nth(1).unwrap();
2061        target_item
2062            .insert_before(MakefileItem::Variable(new_vars.pop().unwrap()))
2063            .unwrap();
2064
2065        // Get fresh reference after first insertion
2066        let mut target_item = makefile.items().nth(1).unwrap();
2067        target_item
2068            .insert_before(MakefileItem::Variable(new_vars.pop().unwrap()))
2069            .unwrap();
2070
2071        let result = makefile.to_string();
2072        assert_eq!(result, "VAR1 = first\nVAR_A = a\nVAR_B = b\nVAR2 = last\n");
2073    }
2074
2075    #[test]
2076    fn test_rules_after_nested_conditionals() {
2077        // Test for bug where .rules() returns no rules after nested conditionals
2078        // This was reported in https://bugs.debian.org/1126511
2079        let makefile_content = r#"#!/usr/bin/make -f
2080
2081ifeq ($(filter nodoc, $(DEB_BUILD_OPTIONS)),)
2082ifneq ($(shell which valadoc),)
2083  BUILD_DOC:=-Ddocs=true
2084endif
2085endif
2086
2087%:
2088	dh $@
2089
2090override_dh_auto_configure:
2091	echo test
2092"#;
2093        let makefile: Makefile = makefile_content.parse().unwrap();
2094
2095        let rules: Vec<_> = makefile.rules().collect();
2096        let targets: Vec<Vec<_>> = rules.iter().map(|r| r.targets().collect()).collect();
2097        assert_eq!(
2098            rules.len(),
2099            2,
2100            "Expected 2 rules (% and override_dh_auto_configure), got {} rules with targets: {:?}",
2101            rules.len(),
2102            targets
2103        );
2104
2105        assert_eq!(targets[0], vec!["%"]);
2106        assert_eq!(targets[1], vec!["override_dh_auto_configure"]);
2107
2108        // Also test that pattern matching works
2109        assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
2110        assert!(makefile
2111            .find_rule_by_target_pattern("build-indep")
2112            .is_some());
2113    }
2114
2115    #[test]
2116    fn test_items_in_range_single_item() {
2117        let input = "CC = gcc\n\nall: build\n\techo done\n";
2118        let makefile: Makefile = input.parse().unwrap();
2119        // Range covering just the variable definition "CC = gcc\n"
2120        let range = rowan::TextRange::new(0.into(), 8.into());
2121        let items: Vec<_> = makefile.items_in_range(range).collect();
2122        assert_eq!(items.len(), 1);
2123        assert!(matches!(items[0], MakefileItem::Variable(_)));
2124    }
2125
2126    #[test]
2127    fn test_items_in_range_multiple_items() {
2128        let input = "CC = gcc\n\nall: build\n\techo done\n";
2129        let makefile: Makefile = input.parse().unwrap();
2130        // Range covering the whole file
2131        let range = rowan::TextRange::new(0.into(), (input.len() as u32).into());
2132        let items: Vec<_> = makefile.items_in_range(range).collect();
2133        assert_eq!(items.len(), 2);
2134    }
2135
2136    #[test]
2137    fn test_items_in_range_no_overlap() {
2138        let input = "CC = gcc\n\nall: build\n\techo done\n";
2139        let makefile: Makefile = input.parse().unwrap();
2140        // Range in the blank line between items
2141        let range = rowan::TextRange::new(9.into(), 10.into());
2142        let items: Vec<_> = makefile.items_in_range(range).collect();
2143        assert_eq!(items.len(), 0);
2144    }
2145
2146    #[test]
2147    fn test_items_in_range_partial_overlap() {
2148        let input = "CC = gcc\nLD = ld\nall: build\n\techo done\n";
2149        let makefile: Makefile = input.parse().unwrap();
2150        // Range overlapping end of first var and start of second
2151        let range = rowan::TextRange::new(5.into(), 12.into());
2152        let items: Vec<_> = makefile.items_in_range(range).collect();
2153        assert_eq!(items.len(), 2);
2154    }
2155
2156    #[test]
2157    fn test_rules_in_range() {
2158        let input = "CC = gcc\nall: build\n\techo done\nclean:\n\trm -rf build\n";
2159        let makefile: Makefile = input.parse().unwrap();
2160        // Range covering the whole file
2161        let range = rowan::TextRange::new(0.into(), (input.len() as u32).into());
2162        let rules: Vec<_> = makefile.rules_in_range(range).collect();
2163        assert_eq!(rules.len(), 2);
2164
2165        // Range covering only the variable
2166        let range = rowan::TextRange::new(0.into(), 9.into());
2167        let rules: Vec<_> = makefile.rules_in_range(range).collect();
2168        assert_eq!(rules.len(), 0);
2169    }
2170
2171    #[test]
2172    fn test_variable_definitions_in_range() {
2173        let input = "CC = gcc\nLD = ld\nall: build\n\techo done\n";
2174        let makefile: Makefile = input.parse().unwrap();
2175        // Range covering the two variable definitions
2176        let range = rowan::TextRange::new(0.into(), 17.into());
2177        let vars: Vec<_> = makefile.variable_definitions_in_range(range).collect();
2178        assert_eq!(vars.len(), 2);
2179
2180        // Range covering only the rule
2181        let range = rowan::TextRange::new(17.into(), (input.len() as u32).into());
2182        let vars: Vec<_> = makefile.variable_definitions_in_range(range).collect();
2183        assert_eq!(vars.len(), 0);
2184    }
2185
2186    #[test]
2187    fn test_variable_references_in_range() {
2188        let input = "CFLAGS = $(BASE) -Wall\nall: $(TARGETS)\n\techo done\n";
2189        let makefile: Makefile = input.parse().unwrap();
2190
2191        // Full range should find the same refs as variable_references()
2192        let all_refs: Vec<_> = makefile.variable_references().collect();
2193        let range = rowan::TextRange::new(0.into(), (input.len() as u32).into());
2194        let refs: Vec<_> = makefile.variable_references_in_range(range).collect();
2195        assert_eq!(refs.len(), all_refs.len());
2196
2197        // Range covering only the variable definition should find $(BASE)
2198        // Find the actual end of the variable item
2199        let var_item = makefile.items().next().unwrap();
2200        let var_end: u32 = var_item.syntax().text_range().end().into();
2201        let range = rowan::TextRange::new(0.into(), var_end.into());
2202        let refs: Vec<_> = makefile.variable_references_in_range(range).collect();
2203        assert_eq!(refs.len(), 1);
2204        assert_eq!(refs[0].name(), Some("BASE".to_string()));
2205
2206        // Range covering only the rule should find $(TARGETS)
2207        let range = rowan::TextRange::new(var_end.into(), (input.len() as u32).into());
2208        let refs: Vec<_> = makefile.variable_references_in_range(range).collect();
2209        assert_eq!(refs.len(), 1);
2210        assert_eq!(refs[0].name(), Some("TARGETS".to_string()));
2211    }
2212
2213    #[test]
2214    fn test_comment_blocks_single_block() {
2215        let makefile: Makefile = "# line 1\n# line 2\n# line 3\nall:\n\techo done\n"
2216            .parse()
2217            .unwrap();
2218        let blocks: Vec<_> = makefile.comment_blocks().collect();
2219        assert_eq!(blocks.len(), 1);
2220        let text = &makefile.to_string()[std::ops::Range::from(blocks[0])];
2221        assert!(text.contains("# line 1"));
2222        assert!(text.contains("# line 3"));
2223    }
2224
2225    #[test]
2226    fn test_comment_blocks_multiple_blocks() {
2227        let makefile: Makefile =
2228            "# block 1a\n# block 1b\nVAR = value\n# block 2a\n# block 2b\nall:\n\techo done\n"
2229                .parse()
2230                .unwrap();
2231        let blocks: Vec<_> = makefile.comment_blocks().collect();
2232        assert_eq!(blocks.len(), 2);
2233    }
2234
2235    #[test]
2236    fn test_comment_blocks_no_blocks() {
2237        let makefile: Makefile = "VAR = value\nall:\n\techo done\n".parse().unwrap();
2238        let blocks: Vec<_> = makefile.comment_blocks().collect();
2239        assert_eq!(blocks.len(), 0);
2240    }
2241
2242    #[test]
2243    fn test_comment_blocks_single_comment_not_a_block() {
2244        let makefile: Makefile = "# just one line\nVAR = value\n".parse().unwrap();
2245        let blocks: Vec<_> = makefile.comment_blocks().collect();
2246        assert_eq!(blocks.len(), 0);
2247    }
2248
2249    #[test]
2250    fn test_comment_blocks_with_blank_line_between() {
2251        let makefile: Makefile = "# line 1\n\n# line 2\nVAR = value\n".parse().unwrap();
2252        let blocks: Vec<_> = makefile.comment_blocks().collect();
2253        // Blank lines between comments should still form a block
2254        assert_eq!(blocks.len(), 1);
2255    }
2256}