Skip to main content

makefile_lossless/
lossless.rs

1use crate::lex::lex;
2use crate::MakefileVariant;
3use crate::SyntaxKind;
4use crate::SyntaxKind::*;
5use rowan::ast::AstNode;
6use std::str::FromStr;
7
8#[derive(Debug)]
9/// An error that can occur when parsing a makefile
10pub enum Error {
11    /// An I/O error occurred
12    Io(std::io::Error),
13
14    /// A parse error occurred
15    Parse(ParseError),
16}
17
18impl std::fmt::Display for Error {
19    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
20        match &self {
21            Error::Io(e) => write!(f, "IO error: {}", e),
22            Error::Parse(e) => write!(f, "Parse error: {}", e),
23        }
24    }
25}
26
27impl From<std::io::Error> for Error {
28    fn from(e: std::io::Error) -> Self {
29        Error::Io(e)
30    }
31}
32
33impl std::error::Error for Error {}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36/// An error that occurred while parsing a makefile
37pub struct ParseError {
38    /// The list of individual parsing errors
39    pub errors: Vec<ErrorInfo>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43/// Information about a specific parsing error
44pub struct ErrorInfo {
45    /// The error message
46    pub message: String,
47    /// The line number where the error occurred
48    pub line: usize,
49    /// The context around the error
50    pub context: String,
51}
52
53impl std::fmt::Display for ParseError {
54    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
55        for err in &self.errors {
56            writeln!(f, "Error at line {}: {}", err.line, err.message)?;
57            writeln!(f, "{}| {}", err.line, err.context)?;
58        }
59        Ok(())
60    }
61}
62
63impl std::error::Error for ParseError {}
64
65impl From<ParseError> for Error {
66    fn from(e: ParseError) -> Self {
67        Error::Parse(e)
68    }
69}
70
71/// A positioned parse error containing location information.
72#[derive(Debug, Clone, PartialEq, Eq, Hash)]
73pub struct PositionedParseError {
74    /// The error message
75    pub message: String,
76    /// The text range where the error occurred
77    pub range: rowan::TextRange,
78    /// Optional error code for categorization
79    pub code: Option<String>,
80}
81
82impl std::fmt::Display for PositionedParseError {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}", self.message)
85    }
86}
87
88impl std::error::Error for PositionedParseError {}
89
90/// these two SyntaxKind types, allowing for a nicer SyntaxNode API where
91/// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
93pub enum Lang {}
94impl rowan::Language for Lang {
95    type Kind = SyntaxKind;
96    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
97        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
98    }
99    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
100        kind.into()
101    }
102}
103
104/// GreenNode is an immutable tree, which is cheap to change,
105/// but doesn't contain offsets and parent pointers.
106use rowan::GreenNode;
107
108/// You can construct GreenNodes by hand, but a builder
109/// is helpful for top-down parsers: it maintains a stack
110/// of currently in-progress nodes
111use rowan::GreenNodeBuilder;
112
113/// The parse results are stored as a "green tree".
114/// We'll discuss working with the results later
115#[derive(Debug)]
116pub(crate) struct Parse {
117    pub(crate) green_node: GreenNode,
118    pub(crate) errors: Vec<ErrorInfo>,
119    pub(crate) positioned_errors: Vec<PositionedParseError>,
120}
121
122pub(crate) fn parse(text: &str, variant: Option<MakefileVariant>) -> Parse {
123    struct Parser {
124        /// input tokens, including whitespace,
125        /// in *reverse* order.
126        tokens: Vec<(SyntaxKind, String)>,
127        /// the in-progress tree.
128        builder: GreenNodeBuilder<'static>,
129        /// the list of syntax errors we've accumulated
130        /// so far.
131        errors: Vec<ErrorInfo>,
132        /// positioned errors with location information
133        positioned_errors: Vec<PositionedParseError>,
134        /// Token positions (start, end) in forward order, indexed by forward token index
135        token_positions: Vec<(rowan::TextSize, rowan::TextSize)>,
136        /// current token index into token_positions (counting from the end since tokens are in reverse)
137        current_token_index: usize,
138        /// The original text
139        original_text: String,
140        /// The makefile variant
141        variant: Option<MakefileVariant>,
142    }
143
144    impl Parser {
145        fn error(&mut self, msg: String) {
146            self.builder.start_node(ERROR.into());
147
148            let (line, context) = if self.current() == Some(INDENT) {
149                // For indented lines, report the error on the next line
150                let lines: Vec<&str> = self.original_text.lines().collect();
151                let tab_line = lines
152                    .iter()
153                    .enumerate()
154                    .find(|(_, line)| line.starts_with('\t'))
155                    .map(|(i, _)| i + 1)
156                    .unwrap_or(1);
157
158                // Use the next line as context if available
159                let next_line = tab_line + 1;
160                if next_line <= lines.len() {
161                    (next_line, lines[next_line - 1].to_string())
162                } else {
163                    (tab_line, lines[tab_line - 1].to_string())
164                }
165            } else {
166                let line = self.get_line_number_for_position(self.tokens.len());
167                (line, self.get_context_for_line(line))
168            };
169
170            let message = if self.current() == Some(INDENT) && !msg.contains("indented") {
171                if !self.tokens.is_empty() && self.tokens[self.tokens.len() - 1].0 == IDENTIFIER {
172                    "expected ':'".to_string()
173                } else {
174                    "indented line not part of a rule".to_string()
175                }
176            } else {
177                msg
178            };
179
180            self.errors.push(ErrorInfo {
181                message: message.clone(),
182                line,
183                context,
184            });
185
186            self.add_positioned_error(message, None);
187
188            if self.current().is_some() {
189                self.bump();
190            }
191            self.builder.finish_node();
192        }
193
194        /// Add a positioned error at the current token position
195        fn add_positioned_error(&mut self, message: String, code: Option<String>) {
196            let range = if self.current_token_index < self.token_positions.len() {
197                let (start, end) = self.token_positions[self.current_token_index];
198                rowan::TextRange::new(start, end)
199            } else {
200                // Default to end of text if no current token
201                let end = self
202                    .token_positions
203                    .last()
204                    .map(|(_, end)| *end)
205                    .unwrap_or_else(|| rowan::TextSize::from(0));
206                rowan::TextRange::new(end, end)
207            };
208
209            self.positioned_errors.push(PositionedParseError {
210                message,
211                range,
212                code,
213            });
214        }
215
216        fn get_line_number_for_position(&self, position: usize) -> usize {
217            if position >= self.tokens.len() {
218                return self.original_text.matches('\n').count() + 1;
219            }
220
221            // Count newlines in the processed text up to this position
222            self.tokens[0..position]
223                .iter()
224                .filter(|(kind, _)| *kind == NEWLINE)
225                .count()
226                + 1
227        }
228
229        fn get_context_for_line(&self, line_number: usize) -> String {
230            self.original_text
231                .lines()
232                .nth(line_number - 1)
233                .unwrap_or("")
234                .to_string()
235        }
236
237        fn parse_recipe_line(&mut self) {
238            self.builder.start_node(RECIPE.into());
239
240            // Check for and consume the indent
241            if self.current() != Some(INDENT) {
242                self.error("recipe line must start with a tab".to_string());
243                self.builder.finish_node();
244                return;
245            }
246            self.bump();
247
248            // Parse the recipe content, handling line continuations (backslash at end of line)
249            loop {
250                let mut last_text_content: Option<String> = None;
251
252                // Consume all tokens until newline, tracking the last TEXT token's content
253                while self.current().is_some() && self.current() != Some(NEWLINE) {
254                    // Save the text content if this is a TEXT token
255                    if self.current() == Some(TEXT) {
256                        if let Some((_kind, text)) = self.tokens.last() {
257                            last_text_content = Some(text.clone());
258                        }
259                    }
260                    self.bump();
261                }
262
263                // Consume the newline
264                if self.current() == Some(NEWLINE) {
265                    self.bump();
266                }
267
268                // Check if the last TEXT token ended with a backslash (continuation)
269                let is_continuation = last_text_content
270                    .as_ref()
271                    .map(|text| text.trim_end().ends_with('\\'))
272                    .unwrap_or(false);
273
274                if is_continuation {
275                    // This is a continuation line - consume the indent of the next line and continue
276                    if self.current() == Some(INDENT) {
277                        self.bump();
278                        // Continue parsing the next line
279                        continue;
280                    } else {
281                        // If there's no indent after a backslash, that's unusual but we'll stop here
282                        break;
283                    }
284                } else {
285                    // No continuation - we're done
286                    break;
287                }
288            }
289
290            self.builder.finish_node();
291        }
292
293        fn parse_rule_target(&mut self) -> bool {
294            match self.current() {
295                Some(IDENTIFIER) => {
296                    // Check if this is an archive member (e.g., libfoo.a(bar.o))
297                    if self.is_archive_member() {
298                        self.parse_archive_member();
299                    } else {
300                        self.bump();
301                    }
302                    true
303                }
304                Some(DOLLAR) => {
305                    self.parse_variable_reference();
306                    true
307                }
308                _ => {
309                    self.error("expected rule target".to_string());
310                    false
311                }
312            }
313        }
314
315        fn is_archive_member(&self) -> bool {
316            // Check if the current identifier is followed by a parenthesis
317            // Pattern: archive.a(member.o)
318            if self.tokens.len() < 2 {
319                return false;
320            }
321
322            // Look for pattern: IDENTIFIER LPAREN
323            let current_is_identifier = self.current() == Some(IDENTIFIER);
324            let next_is_lparen =
325                self.tokens.len() > 1 && self.tokens[self.tokens.len() - 2].0 == LPAREN;
326
327            current_is_identifier && next_is_lparen
328        }
329
330        fn parse_archive_member(&mut self) {
331            // We're parsing something like: libfoo.a(bar.o baz.o)
332            // Structure will be:
333            // - IDENTIFIER: libfoo.a
334            // - LPAREN
335            // - ARCHIVE_MEMBERS
336            //   - ARCHIVE_MEMBER: bar.o
337            //   - ARCHIVE_MEMBER: baz.o
338            // - RPAREN
339
340            // Parse archive name
341            if self.current() == Some(IDENTIFIER) {
342                self.bump();
343            }
344
345            // Parse opening parenthesis
346            if self.current() == Some(LPAREN) {
347                self.bump();
348
349                // Start the ARCHIVE_MEMBERS container for just the members
350                self.builder.start_node(ARCHIVE_MEMBERS.into());
351
352                // Parse member name(s) - each as an ARCHIVE_MEMBER node
353                while self.current().is_some() && self.current() != Some(RPAREN) {
354                    match self.current() {
355                        Some(IDENTIFIER) | Some(TEXT) => {
356                            // Start an individual member node
357                            self.builder.start_node(ARCHIVE_MEMBER.into());
358                            self.bump();
359                            self.builder.finish_node();
360                        }
361                        Some(WHITESPACE) => self.bump(),
362                        Some(DOLLAR) => {
363                            // Variable reference can also be a member
364                            self.builder.start_node(ARCHIVE_MEMBER.into());
365                            self.parse_variable_reference();
366                            self.builder.finish_node();
367                        }
368                        _ => break,
369                    }
370                }
371
372                // Finish the ARCHIVE_MEMBERS container
373                self.builder.finish_node();
374
375                // Parse closing parenthesis
376                if self.current() == Some(RPAREN) {
377                    self.bump();
378                } else {
379                    self.error("expected ')' to close archive member".to_string());
380                }
381            }
382        }
383
384        fn parse_rule_dependencies(&mut self) {
385            self.builder.start_node(PREREQUISITES.into());
386
387            while self.current().is_some() && self.current() != Some(NEWLINE) {
388                match self.current() {
389                    Some(WHITESPACE) => {
390                        self.bump(); // Consume whitespace between prerequisites
391                    }
392                    Some(IDENTIFIER) => {
393                        // Start a new prerequisite node
394                        self.builder.start_node(PREREQUISITE.into());
395
396                        if self.is_archive_member() {
397                            self.parse_archive_member();
398                        } else {
399                            self.bump(); // Simple identifier
400                        }
401
402                        self.builder.finish_node(); // End PREREQUISITE
403                    }
404                    Some(DOLLAR) => {
405                        // Variable reference - parse it within a PREREQUISITE node
406                        self.builder.start_node(PREREQUISITE.into());
407                        self.parse_variable_reference();
408                        self.builder.finish_node(); // End PREREQUISITE
409                    }
410                    _ => {
411                        // Other tokens (like comments) - just consume them
412                        self.bump();
413                    }
414                }
415            }
416
417            self.builder.finish_node(); // End PREREQUISITES
418        }
419
420        fn parse_rule_recipes(&mut self) {
421            // Track how many levels deep we are in conditionals that started in this rule
422            let mut conditional_depth = 0;
423            // Also track consecutive newlines to detect blank lines
424            let mut newline_count = 0;
425
426            loop {
427                match self.current() {
428                    Some(INDENT) => {
429                        newline_count = 0;
430                        self.parse_recipe_line();
431                    }
432                    Some(NEWLINE) => {
433                        newline_count += 1;
434                        self.bump();
435                    }
436                    Some(COMMENT) => {
437                        // Comments after blank lines should not be part of the rule
438                        if conditional_depth == 0 && newline_count >= 1 {
439                            break;
440                        }
441                        newline_count = 0;
442                        self.parse_comment();
443                    }
444                    Some(IDENTIFIER) => {
445                        let token = &self.tokens.last().unwrap().1.clone();
446                        // Check if this is a starting conditional directive
447                        if (token == "ifdef"
448                            || token == "ifndef"
449                            || token == "ifeq"
450                            || token == "ifneq")
451                            && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
452                        {
453                            // If we're not inside a conditional (depth == 0) and there's a blank line,
454                            // this is a top-level conditional, not part of the rule
455                            if conditional_depth == 0 && newline_count >= 1 {
456                                break;
457                            }
458                            newline_count = 0;
459                            conditional_depth += 1;
460                            self.parse_conditional();
461                            // parse_conditional() handles the entire conditional including endif,
462                            // so we need to decrement after it returns
463                            conditional_depth -= 1;
464                        } else if token == "include" || token == "-include" || token == "sinclude" {
465                            // Includes can appear in rules, with same blank line logic
466                            if conditional_depth == 0 && newline_count >= 1 {
467                                break;
468                            }
469                            newline_count = 0;
470                            self.parse_include();
471                        } else if token == "else" || token == "endif" {
472                            // These should only appear if we're inside a conditional
473                            // If we see them at depth 0, something is wrong, so break
474                            break;
475                        } else {
476                            // Any other identifier at depth 0 means the rule is over
477                            if conditional_depth == 0 {
478                                break;
479                            }
480                            // Otherwise, it's content inside a conditional (variable assignment, etc.)
481                            // Let it be handled by parse_normal_content
482                            break;
483                        }
484                    }
485                    _ => break,
486                }
487            }
488        }
489
490        fn find_and_consume_colon(&mut self) -> bool {
491            // Skip whitespace before colon
492            self.skip_ws();
493
494            // Check if we're at a colon or double-colon
495            if self.current() == Some(OPERATOR)
496                && matches!(self.tokens.last().unwrap().1.as_str(), ":" | "::")
497            {
498                self.bump();
499                return true;
500            }
501
502            // Look ahead for a colon on the same line
503            let has_colon = self
504                .tokens
505                .iter()
506                .rev()
507                .take_while(|(kind, _)| *kind != NEWLINE)
508                .any(|(kind, text)| *kind == OPERATOR && (text == ":" || text == "::"));
509
510            if has_colon {
511                // Consume tokens until we find the colon (staying on same line)
512                while self.current().is_some() && self.current() != Some(NEWLINE) {
513                    if self.current() == Some(OPERATOR)
514                        && matches!(
515                            self.tokens.last().map(|(_, text)| text.as_str()),
516                            Some(":" | "::")
517                        )
518                    {
519                        self.bump();
520                        return true;
521                    }
522                    self.bump();
523                }
524            }
525
526            self.error("expected ':'".to_string());
527            false
528        }
529
530        fn parse_rule(&mut self) {
531            self.builder.start_node(RULE.into());
532
533            // Parse targets in a TARGETS node
534            self.skip_ws();
535            self.builder.start_node(TARGETS.into());
536            let has_target = self.parse_rule_targets();
537            self.builder.finish_node();
538
539            // Find and consume the colon
540            let has_colon = if has_target {
541                self.find_and_consume_colon()
542            } else {
543                false
544            };
545
546            // Parse dependencies if we found both target and colon
547            if has_target && has_colon {
548                self.skip_ws();
549                self.parse_rule_dependencies();
550                self.expect_eol();
551
552                // Parse recipe lines
553                self.parse_rule_recipes();
554            }
555
556            self.builder.finish_node();
557        }
558
559        fn parse_rule_targets(&mut self) -> bool {
560            // Parse first target
561            let has_first_target = self.parse_rule_target();
562
563            if !has_first_target {
564                return false;
565            }
566
567            // Parse additional targets until we hit the colon
568            loop {
569                self.skip_ws();
570
571                // Check if we're at a colon
572                if self.current() == Some(OPERATOR) && self.tokens.last().unwrap().1 == ":" {
573                    break;
574                }
575
576                // Try to parse another target
577                match self.current() {
578                    Some(IDENTIFIER) | Some(DOLLAR) => {
579                        if !self.parse_rule_target() {
580                            break;
581                        }
582                    }
583                    _ => break,
584                }
585            }
586
587            true
588        }
589
590        fn parse_comment(&mut self) {
591            if self.current() == Some(COMMENT) {
592                self.bump(); // Consume the comment token
593
594                // Handle end of line or file after comment
595                if self.current() == Some(NEWLINE) {
596                    self.bump(); // Consume the newline
597                } else if self.current() == Some(WHITESPACE) {
598                    // For whitespace after a comment, just consume it
599                    self.skip_ws();
600                    if self.current() == Some(NEWLINE) {
601                        self.bump();
602                    }
603                }
604                // If we're at EOF after a comment, that's fine
605            } else {
606                self.error("expected comment".to_string());
607            }
608        }
609
610        fn parse_assignment(&mut self) {
611            self.builder.start_node(VARIABLE.into());
612
613            // Handle export prefix if present
614            self.skip_ws();
615            if self.current() == Some(IDENTIFIER) && self.tokens.last().unwrap().1 == "export" {
616                self.bump();
617                self.skip_ws();
618            }
619
620            // Parse variable name
621            match self.current() {
622                Some(IDENTIFIER) => self.bump(),
623                Some(DOLLAR) => self.parse_variable_reference(),
624                _ => {
625                    self.error("expected variable name".to_string());
626                    self.builder.finish_node();
627                    return;
628                }
629            }
630
631            // Skip whitespace and parse operator
632            self.skip_ws();
633            match self.current() {
634                Some(OPERATOR) => {
635                    let op = &self.tokens.last().unwrap().1;
636                    if ["=", ":=", "::=", ":::=", "+=", "?=", "!="].contains(&op.as_str()) {
637                        self.bump();
638                        self.skip_ws();
639
640                        // Parse value, creating nested EXPR nodes for variable references
641                        self.builder.start_node(EXPR.into());
642                        while self.current().is_some() && self.current() != Some(NEWLINE) {
643                            if self.current() == Some(DOLLAR) {
644                                self.parse_variable_reference();
645                            } else {
646                                self.bump();
647                            }
648                        }
649                        self.builder.finish_node();
650
651                        // Expect newline
652                        if self.current() == Some(NEWLINE) {
653                            self.bump();
654                        } else {
655                            self.error("expected newline after variable value".to_string());
656                        }
657                    } else {
658                        self.error(format!("invalid assignment operator: {}", op));
659                    }
660                }
661                // Bare "export VARNAME" without assignment operator is valid GNU Make
662                Some(NEWLINE) => {
663                    self.bump();
664                }
665                None => {
666                    // EOF after export VARNAME is fine
667                }
668                _ => self.error("expected assignment operator".to_string()),
669            }
670
671            self.builder.finish_node();
672        }
673
674        fn parse_variable_reference(&mut self) {
675            self.builder.start_node(EXPR.into());
676            self.bump(); // Consume $
677
678            if self.current() == Some(LPAREN) || self.current() == Some(LBRACE) {
679                let is_brace = self.current() == Some(LBRACE);
680                self.bump(); // Consume ( or {
681
682                if is_brace {
683                    // For ${...}, consume until matching }
684                    while self.current().is_some() && self.current() != Some(RBRACE) {
685                        if self.current() == Some(DOLLAR) {
686                            self.parse_variable_reference();
687                        } else {
688                            self.bump();
689                        }
690                    }
691                    if self.current() == Some(RBRACE) {
692                        self.bump(); // Consume }
693                    }
694                } else {
695                    // Start by checking if this is a function like $(shell ...)
696                    let mut is_function = false;
697
698                    if self.current() == Some(IDENTIFIER) {
699                        let function_name = &self.tokens.last().unwrap().1;
700                        // Common makefile functions
701                        let known_functions = [
702                            "shell", "wildcard", "call", "eval", "file", "abspath", "dir",
703                        ];
704                        if known_functions.contains(&function_name.as_str()) {
705                            is_function = true;
706                        }
707                    }
708
709                    if is_function {
710                        // Preserve the function name
711                        self.bump();
712
713                        // Parse the rest of the function call, handling nested variable references
714                        self.consume_balanced_parens(1);
715                    } else {
716                        // Handle regular variable references
717                        self.parse_parenthesized_expr_internal(true);
718                    }
719                }
720            } else if self.current().is_some() && self.current() != Some(NEWLINE) {
721                // Single character variable like $X or $$
722                self.bump();
723            } else {
724                self.error("expected variable name after $".to_string());
725            }
726
727            self.builder.finish_node();
728        }
729
730        // Helper method to parse a conditional comparison (ifeq/ifneq)
731        // Supports both syntaxes: (arg1,arg2) and "arg1" "arg2"
732        fn parse_parenthesized_expr(&mut self) {
733            self.builder.start_node(EXPR.into());
734
735            // Check if we have parenthesized or quoted syntax
736            if self.current() == Some(LPAREN) {
737                // Parenthesized syntax: ifeq (arg1,arg2)
738                self.bump(); // Consume opening paren
739                self.parse_parenthesized_expr_internal(false);
740            } else if self.current() == Some(QUOTE) {
741                // Quoted syntax: ifeq "arg1" "arg2" or ifeq 'arg1' 'arg2'
742                self.parse_quoted_comparison();
743            } else {
744                self.error("expected opening parenthesis or quote".to_string());
745            }
746
747            self.builder.finish_node();
748        }
749
750        // Internal helper to parse parenthesized expressions
751        fn parse_parenthesized_expr_internal(&mut self, is_variable_ref: bool) {
752            let mut paren_count = 1;
753
754            while paren_count > 0 && self.current().is_some() {
755                match self.current() {
756                    Some(LPAREN) => {
757                        paren_count += 1;
758                        self.bump();
759                        // Start a new expression node for nested parentheses
760                        self.builder.start_node(EXPR.into());
761                    }
762                    Some(RPAREN) => {
763                        paren_count -= 1;
764                        self.bump();
765                        if paren_count > 0 {
766                            self.builder.finish_node();
767                        }
768                    }
769                    Some(QUOTE) => {
770                        // Handle quoted strings
771                        self.parse_quoted_string();
772                    }
773                    Some(DOLLAR) => {
774                        // Handle variable references
775                        self.parse_variable_reference();
776                    }
777                    Some(_) => self.bump(),
778                    None => {
779                        self.error(if is_variable_ref {
780                            "unclosed variable reference".to_string()
781                        } else {
782                            "unclosed parenthesis".to_string()
783                        });
784                        break;
785                    }
786                }
787            }
788
789            if !is_variable_ref {
790                self.skip_ws();
791                self.expect_eol();
792            }
793        }
794
795        // Helper method to parse quoted comparison for ifeq/ifneq
796        // Handles: "arg1" "arg2" or 'arg1' 'arg2'
797        fn parse_quoted_comparison(&mut self) {
798            // First quoted string - lexer already tokenized the entire string
799            if self.current() == Some(QUOTE) {
800                self.bump(); // Consume the entire first quoted string token
801            } else {
802                self.error("expected first quoted argument".to_string());
803            }
804
805            // Skip whitespace between the two arguments
806            self.skip_ws();
807
808            // Second quoted string - lexer already tokenized the entire string
809            if self.current() == Some(QUOTE) {
810                self.bump(); // Consume the entire second quoted string token
811            } else {
812                self.error("expected second quoted argument".to_string());
813            }
814
815            // Skip trailing whitespace and expect end of line
816            self.skip_ws();
817            self.expect_eol();
818        }
819
820        // Handle parsing a quoted string. The lexer emits the entire quoted
821        // string (including both delimiters) as a single QUOTE token, so we
822        // just consume that one token.
823        fn parse_quoted_string(&mut self) {
824            if self.current() == Some(QUOTE) {
825                self.bump();
826            }
827        }
828
829        fn parse_conditional_keyword(&mut self) -> Option<String> {
830            if self.current() != Some(IDENTIFIER) {
831                self.error(
832                    "expected conditional keyword (ifdef, ifndef, ifeq, or ifneq)".to_string(),
833                );
834                return None;
835            }
836
837            let token = self.tokens.last().unwrap().1.clone();
838            if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&token.as_str()) {
839                self.error(format!("unknown conditional directive: {}", token));
840                return None;
841            }
842
843            self.bump();
844            Some(token)
845        }
846
847        fn parse_simple_condition(&mut self) {
848            self.builder.start_node(EXPR.into());
849
850            // Skip any leading whitespace
851            self.skip_ws();
852
853            // Collect variable names
854            let mut found_var = false;
855
856            while !self.is_at_eof() && self.current() != Some(NEWLINE) {
857                match self.current() {
858                    Some(WHITESPACE) => self.skip_ws(),
859                    Some(DOLLAR) => {
860                        found_var = true;
861                        self.parse_variable_reference();
862                    }
863                    Some(_) => {
864                        // Accept any token as part of condition
865                        found_var = true;
866                        self.bump();
867                    }
868                    None => break,
869                }
870            }
871
872            if !found_var {
873                // Empty condition is an error in GNU Make
874                self.error("expected condition after conditional directive".to_string());
875            }
876
877            self.builder.finish_node();
878
879            // Expect end of line
880            if self.current() == Some(NEWLINE) {
881                self.bump();
882            } else if !self.is_at_eof() {
883                self.skip_until_newline();
884            }
885        }
886
887        // Helper to check if a token is a conditional directive
888        fn is_conditional_directive(&self, token: &str) -> bool {
889            token == "ifdef"
890                || token == "ifndef"
891                || token == "ifeq"
892                || token == "ifneq"
893                || token == "else"
894                || token == "endif"
895        }
896
897        // Helper method to handle conditional token
898        fn handle_conditional_token(&mut self, token: &str, depth: &mut usize) -> bool {
899            match token {
900                "ifdef" | "ifndef" | "ifeq" | "ifneq"
901                    if matches!(self.variant, None | Some(MakefileVariant::GNUMake)) =>
902                {
903                    // Don't increment depth here - parse_conditional manages its own depth internally
904                    // Incrementing here causes the outer conditional to never exit its loop
905                    self.parse_conditional();
906                    true
907                }
908                "else" => {
909                    // Not valid outside of a conditional
910                    if *depth == 0 {
911                        self.error("else without matching if".to_string());
912                        // Always consume a token to guarantee progress
913                        self.bump();
914                        false
915                    } else {
916                        // Start CONDITIONAL_ELSE node
917                        self.builder.start_node(CONDITIONAL_ELSE.into());
918
919                        // Consume the 'else' token
920                        self.bump();
921                        self.skip_ws();
922
923                        // Check if this is "else <conditional>" (else ifdef, else ifeq, etc.)
924                        if self.current() == Some(IDENTIFIER) {
925                            let next_token = &self.tokens.last().unwrap().1;
926                            if next_token == "ifdef"
927                                || next_token == "ifndef"
928                                || next_token == "ifeq"
929                                || next_token == "ifneq"
930                            {
931                                // This is "else ifdef", "else ifeq", etc.
932                                // Parse the conditional part
933                                match next_token.as_str() {
934                                    "ifdef" | "ifndef" => {
935                                        self.bump(); // Consume the directive token
936                                        self.skip_ws();
937                                        self.parse_simple_condition();
938                                    }
939                                    "ifeq" | "ifneq" => {
940                                        self.bump(); // Consume the directive token
941                                        self.skip_ws();
942                                        self.parse_parenthesized_expr();
943                                    }
944                                    _ => unreachable!(),
945                                }
946                                // The newline will be consumed by the conditional body loop
947                            } else {
948                                // Plain 'else' with something else after it (not a conditional keyword)
949                                // The newline will be consumed by the conditional body loop
950                            }
951                        } else {
952                            // Plain 'else' - the newline will be consumed by the conditional body loop
953                        }
954
955                        self.builder.finish_node(); // finish CONDITIONAL_ELSE
956                        true
957                    }
958                }
959                "endif" => {
960                    // Not valid outside of a conditional
961                    if *depth == 0 {
962                        self.error("endif without matching if".to_string());
963                        // Always consume a token to guarantee progress
964                        self.bump();
965                        false
966                    } else {
967                        *depth -= 1;
968
969                        // Start CONDITIONAL_ENDIF node
970                        self.builder.start_node(CONDITIONAL_ENDIF.into());
971
972                        // Consume the endif
973                        self.bump();
974
975                        // Be more permissive with what follows endif
976                        self.skip_ws();
977
978                        // Handle common patterns after endif:
979                        // 1. Comments: endif # comment
980                        // 2. Whitespace at end of file
981                        // 3. Newlines
982                        if self.current() == Some(COMMENT) {
983                            self.parse_comment();
984                        } else if self.current() == Some(NEWLINE) {
985                            self.bump();
986                        } else if self.current() == Some(WHITESPACE) {
987                            // Skip whitespace without an error
988                            self.skip_ws();
989                            if self.current() == Some(NEWLINE) {
990                                self.bump();
991                            }
992                            // If we're at EOF after whitespace, that's fine too
993                        } else if !self.is_at_eof() {
994                            // For any other tokens, be lenient and just consume until EOL
995                            // This makes the parser more resilient to various "endif" formattings
996                            while !self.is_at_eof() && self.current() != Some(NEWLINE) {
997                                self.bump();
998                            }
999                            if self.current() == Some(NEWLINE) {
1000                                self.bump();
1001                            }
1002                        }
1003                        // If we're at EOF after endif, that's fine
1004
1005                        self.builder.finish_node(); // finish CONDITIONAL_ENDIF
1006                        true
1007                    }
1008                }
1009                _ => false,
1010            }
1011        }
1012
1013        fn parse_conditional(&mut self) {
1014            self.builder.start_node(CONDITIONAL.into());
1015
1016            // Start the initial conditional (ifdef/ifndef/ifeq/ifneq)
1017            self.builder.start_node(CONDITIONAL_IF.into());
1018
1019            // Parse the conditional keyword
1020            let Some(token) = self.parse_conditional_keyword() else {
1021                self.skip_until_newline();
1022                self.builder.finish_node(); // finish CONDITIONAL_IF
1023                self.builder.finish_node(); // finish CONDITIONAL
1024                return;
1025            };
1026
1027            // Skip whitespace after keyword
1028            self.skip_ws();
1029
1030            // Parse the condition based on keyword type
1031            match token.as_str() {
1032                "ifdef" | "ifndef" => {
1033                    self.parse_simple_condition();
1034                }
1035                "ifeq" | "ifneq" => {
1036                    self.parse_parenthesized_expr();
1037                }
1038                _ => unreachable!("Invalid conditional token"),
1039            }
1040
1041            // Skip any trailing whitespace and check for inline comments
1042            self.skip_ws();
1043            if self.current() == Some(COMMENT) {
1044                self.parse_comment();
1045            }
1046            // Note: expect_eol is already called by parse_simple_condition() and parse_parenthesized_expr()
1047
1048            self.builder.finish_node(); // finish CONDITIONAL_IF
1049
1050            // Parse the conditional body
1051            let mut depth = 1;
1052
1053            // More reliable loop detection
1054            let mut position_count = std::collections::HashMap::<usize, usize>::new();
1055            let max_repetitions = 15; // Permissive but safe limit
1056
1057            while depth > 0 && !self.is_at_eof() {
1058                // Track position to detect infinite loops
1059                let current_pos = self.tokens.len();
1060                *position_count.entry(current_pos).or_insert(0) += 1;
1061
1062                // If we've seen the same position too many times, break
1063                // This prevents infinite loops while allowing complex parsing
1064                if position_count.get(&current_pos).unwrap() > &max_repetitions {
1065                    // Instead of adding an error, just break out silently
1066                    // to avoid breaking tests that expect no errors
1067                    break;
1068                }
1069
1070                match self.current() {
1071                    None => {
1072                        self.error("unterminated conditional (missing endif)".to_string());
1073                        break;
1074                    }
1075                    Some(IDENTIFIER) => {
1076                        let token = self.tokens.last().unwrap().1.clone();
1077                        if !self.handle_conditional_token(&token, &mut depth) {
1078                            if token == "include" || token == "-include" || token == "sinclude" {
1079                                self.parse_include();
1080                            } else {
1081                                self.parse_normal_content();
1082                            }
1083                        }
1084                    }
1085                    Some(INDENT) => self.parse_recipe_line(),
1086                    Some(WHITESPACE) => self.bump(),
1087                    Some(COMMENT) => self.parse_comment(),
1088                    Some(NEWLINE) => self.bump(),
1089                    Some(DOLLAR) => self.parse_normal_content(),
1090                    Some(QUOTE) => self.parse_quoted_string(),
1091                    Some(_) => {
1092                        // Be more tolerant of unexpected tokens in conditionals
1093                        self.bump();
1094                    }
1095                }
1096            }
1097
1098            self.builder.finish_node();
1099        }
1100
1101        // Helper to parse normal content (either assignment or rule)
1102        fn parse_normal_content(&mut self) {
1103            // Skip any leading whitespace
1104            self.skip_ws();
1105
1106            // Check if this could be a variable assignment
1107            if self.is_assignment_line() {
1108                self.parse_assignment();
1109            } else {
1110                // Try to handle as a rule
1111                self.parse_rule();
1112            }
1113        }
1114
1115        fn parse_include(&mut self) {
1116            self.builder.start_node(INCLUDE.into());
1117
1118            // Consume include keyword variant
1119            if self.current() != Some(IDENTIFIER)
1120                || (!["include", "-include", "sinclude"]
1121                    .contains(&self.tokens.last().unwrap().1.as_str()))
1122            {
1123                self.error("expected include directive".to_string());
1124                self.builder.finish_node();
1125                return;
1126            }
1127            self.bump();
1128            self.skip_ws();
1129
1130            // Parse file paths
1131            self.builder.start_node(EXPR.into());
1132            let mut found_path = false;
1133
1134            while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1135                match self.current() {
1136                    Some(WHITESPACE) => self.skip_ws(),
1137                    Some(DOLLAR) => {
1138                        found_path = true;
1139                        self.parse_variable_reference();
1140                    }
1141                    Some(_) => {
1142                        // Accept any token as part of the path
1143                        found_path = true;
1144                        self.bump();
1145                    }
1146                    None => break,
1147                }
1148            }
1149
1150            if !found_path {
1151                self.error("expected file path after include".to_string());
1152            }
1153
1154            self.builder.finish_node();
1155
1156            // Expect newline
1157            if self.current() == Some(NEWLINE) {
1158                self.bump();
1159            } else if !self.is_at_eof() {
1160                self.error("expected newline after include".to_string());
1161                self.skip_until_newline();
1162            }
1163
1164            self.builder.finish_node();
1165        }
1166
1167        fn parse_identifier_token(&mut self) -> bool {
1168            let token = &self.tokens.last().unwrap().1;
1169
1170            // Handle special cases first
1171            if token.starts_with("%") {
1172                self.parse_rule();
1173                return true;
1174            }
1175
1176            if token.starts_with("if")
1177                && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1178            {
1179                self.parse_conditional();
1180                return true;
1181            }
1182
1183            if token == "include" || token == "-include" || token == "sinclude" {
1184                self.parse_include();
1185                return true;
1186            }
1187
1188            // Handle normal content (assignment or rule)
1189            self.parse_normal_content();
1190            true
1191        }
1192
1193        fn parse_token(&mut self) -> bool {
1194            match self.current() {
1195                None => false,
1196                Some(IDENTIFIER) => {
1197                    let token = &self.tokens.last().unwrap().1;
1198                    if self.is_conditional_directive(token)
1199                        && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1200                    {
1201                        self.parse_conditional();
1202                        true
1203                    } else {
1204                        self.parse_identifier_token()
1205                    }
1206                }
1207                Some(DOLLAR) => {
1208                    self.parse_normal_content();
1209                    true
1210                }
1211                Some(NEWLINE) => {
1212                    self.builder.start_node(BLANK_LINE.into());
1213                    self.bump();
1214                    self.builder.finish_node();
1215                    true
1216                }
1217                Some(COMMENT) => {
1218                    self.parse_comment();
1219                    true
1220                }
1221                Some(WHITESPACE) => {
1222                    // Special case for trailing whitespace
1223                    if self.is_end_of_file_or_newline_after_whitespace() {
1224                        // If the whitespace is just before EOF or a newline, consume it all without errors
1225                        // to be more lenient with final whitespace
1226                        self.skip_ws();
1227                        return true;
1228                    }
1229
1230                    // Special case for indented lines that might be part of help text or documentation
1231                    // Look ahead to see what comes after the whitespace
1232                    let look_ahead_pos = self.tokens.len().saturating_sub(1);
1233                    let mut is_documentation_or_help = false;
1234
1235                    if look_ahead_pos > 0 {
1236                        let next_token = &self.tokens[look_ahead_pos - 1];
1237                        // Consider this documentation if it's an identifier starting with @, a comment,
1238                        // or any reasonable text
1239                        if next_token.0 == IDENTIFIER
1240                            || next_token.0 == COMMENT
1241                            || next_token.0 == TEXT
1242                        {
1243                            is_documentation_or_help = true;
1244                        }
1245                    }
1246
1247                    if is_documentation_or_help {
1248                        // For documentation/help text lines, just consume all tokens until newline
1249                        // without generating errors
1250                        self.skip_ws();
1251                        while self.current().is_some() && self.current() != Some(NEWLINE) {
1252                            self.bump();
1253                        }
1254                        if self.current() == Some(NEWLINE) {
1255                            self.bump();
1256                        }
1257                    } else {
1258                        self.skip_ws();
1259                    }
1260                    true
1261                }
1262                Some(INDENT) => {
1263                    // We'll consume the INDENT token
1264                    self.bump();
1265
1266                    // Consume the rest of the line
1267                    while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1268                        self.bump();
1269                    }
1270                    if self.current() == Some(NEWLINE) {
1271                        self.bump();
1272                    }
1273                    true
1274                }
1275                Some(kind) => {
1276                    self.error(format!("unexpected token {:?}", kind));
1277                    self.bump();
1278                    true
1279                }
1280            }
1281        }
1282
1283        fn parse(mut self) -> Parse {
1284            self.builder.start_node(ROOT.into());
1285
1286            while self.parse_token() {}
1287
1288            self.builder.finish_node();
1289
1290            Parse {
1291                green_node: self.builder.finish(),
1292                errors: self.errors,
1293                positioned_errors: self.positioned_errors,
1294            }
1295        }
1296
1297        // Simplify the is_assignment_line method by making it more direct
1298        fn is_assignment_line(&mut self) -> bool {
1299            let assignment_ops = ["=", ":=", "::=", ":::=", "+=", "?=", "!="];
1300            let mut pos = self.tokens.len().saturating_sub(1);
1301            let mut seen_identifier = false;
1302            let mut seen_export = false;
1303
1304            while pos > 0 {
1305                let (kind, text) = &self.tokens[pos];
1306
1307                match kind {
1308                    NEWLINE => break,
1309                    IDENTIFIER if text == "export" => seen_export = true,
1310                    IDENTIFIER if !seen_identifier => seen_identifier = true,
1311                    OPERATOR if assignment_ops.contains(&text.as_str()) => {
1312                        return seen_identifier || seen_export
1313                    }
1314                    OPERATOR if text == ":" || text == "::" => return false, // It's a rule if we see a colon first
1315                    WHITESPACE => (),
1316                    _ if seen_export => return true, // Everything after export is part of the assignment
1317                    _ => return false,
1318                }
1319                pos = pos.saturating_sub(1);
1320            }
1321            // Bare "export VARNAME" (without assignment operator) is a valid GNU Make directive
1322            seen_export
1323        }
1324
1325        /// Advance one token, adding it to the current branch of the tree builder.
1326        fn bump(&mut self) {
1327            let (kind, text) = self.tokens.pop().unwrap();
1328            self.builder.token(kind.into(), text.as_str());
1329            if self.current_token_index > 0 {
1330                self.current_token_index -= 1;
1331            }
1332        }
1333        /// Peek at the first unprocessed token
1334        fn current(&self) -> Option<SyntaxKind> {
1335            self.tokens.last().map(|(kind, _)| *kind)
1336        }
1337
1338        fn expect_eol(&mut self) {
1339            // Skip any whitespace before looking for a newline
1340            self.skip_ws();
1341
1342            match self.current() {
1343                Some(NEWLINE) => {
1344                    self.bump();
1345                }
1346                None => {
1347                    // End of file is also acceptable
1348                }
1349                n => {
1350                    self.error(format!("expected newline, got {:?}", n));
1351                    // Try to recover by skipping to the next newline
1352                    self.skip_until_newline();
1353                }
1354            }
1355        }
1356
1357        // Helper to check if we're at EOF
1358        fn is_at_eof(&self) -> bool {
1359            self.current().is_none()
1360        }
1361
1362        // Helper to check if we're at EOF or there's only whitespace left
1363        fn is_at_eof_or_only_whitespace(&self) -> bool {
1364            if self.is_at_eof() {
1365                return true;
1366            }
1367
1368            // Check if only whitespace and newlines remain
1369            self.tokens
1370                .iter()
1371                .rev()
1372                .all(|(kind, _)| matches!(*kind, WHITESPACE | NEWLINE))
1373        }
1374
1375        fn skip_ws(&mut self) {
1376            while self.current() == Some(WHITESPACE) {
1377                self.bump()
1378            }
1379        }
1380
1381        fn skip_until_newline(&mut self) {
1382            while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1383                self.bump();
1384            }
1385            if self.current() == Some(NEWLINE) {
1386                self.bump();
1387            }
1388        }
1389
1390        // Helper to handle nested parentheses and collect tokens until matching closing parenthesis
1391        fn consume_balanced_parens(&mut self, start_paren_count: usize) -> usize {
1392            let mut paren_count = start_paren_count;
1393
1394            while paren_count > 0 && self.current().is_some() {
1395                match self.current() {
1396                    Some(LPAREN) => {
1397                        paren_count += 1;
1398                        self.bump();
1399                    }
1400                    Some(RPAREN) => {
1401                        paren_count -= 1;
1402                        self.bump();
1403                        if paren_count == 0 {
1404                            break;
1405                        }
1406                    }
1407                    Some(DOLLAR) => {
1408                        // Handle nested variable references
1409                        self.parse_variable_reference();
1410                    }
1411                    Some(_) => self.bump(),
1412                    None => {
1413                        self.error("unclosed parenthesis".to_string());
1414                        break;
1415                    }
1416                }
1417            }
1418
1419            paren_count
1420        }
1421
1422        // Helper to check if we're near the end of the file with just whitespace
1423        fn is_end_of_file_or_newline_after_whitespace(&self) -> bool {
1424            // Use our new helper method
1425            if self.is_at_eof_or_only_whitespace() {
1426                return true;
1427            }
1428
1429            // If there are 1 or 0 tokens left, we're at EOF
1430            if self.tokens.len() <= 1 {
1431                return true;
1432            }
1433
1434            false
1435        }
1436    }
1437
1438    let mut tokens = lex(text);
1439
1440    // Build token positions in forward order before reversing
1441    let mut token_positions = Vec::with_capacity(tokens.len());
1442    let mut position = rowan::TextSize::from(0);
1443    for (_kind, text) in &tokens {
1444        let start = position;
1445        let end = start + rowan::TextSize::of(text.as_str());
1446        token_positions.push((start, end));
1447        position = end;
1448    }
1449
1450    let current_token_index = tokens.len().saturating_sub(1);
1451    tokens.reverse();
1452    Parser {
1453        tokens,
1454        builder: GreenNodeBuilder::new(),
1455        errors: Vec::new(),
1456        positioned_errors: Vec::new(),
1457        token_positions,
1458        current_token_index,
1459        original_text: text.to_string(),
1460        variant,
1461    }
1462    .parse()
1463}
1464
1465/// To work with the parse results we need a view into the
1466/// green tree - the Syntax tree.
1467/// It is also immutable, like a GreenNode,
1468/// but it contains parent pointers, offsets, and
1469/// has identity semantics.
1470pub(crate) type SyntaxNode = rowan::SyntaxNode<Lang>;
1471#[allow(unused)]
1472type SyntaxToken = rowan::SyntaxToken<Lang>;
1473#[allow(unused)]
1474pub(crate) type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
1475
1476impl Parse {
1477    fn syntax(&self) -> SyntaxNode {
1478        SyntaxNode::new_root_mut(self.green_node.clone())
1479    }
1480
1481    pub(crate) fn root(&self) -> Makefile {
1482        Makefile::cast(self.syntax()).unwrap()
1483    }
1484}
1485
1486/// Calculate line and column (both 0-indexed) for the given offset in the tree.
1487/// Column is measured in bytes from the start of the line.
1488fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
1489    let root = node.ancestors().last().unwrap_or_else(|| node.clone());
1490    let mut line = 0;
1491    let mut last_newline_offset = rowan::TextSize::from(0);
1492
1493    for element in root.preorder_with_tokens() {
1494        if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
1495            if token.text_range().start() >= offset {
1496                break;
1497            }
1498
1499            // Count newlines and track position of last one
1500            for (idx, _) in token.text().match_indices('\n') {
1501                line += 1;
1502                last_newline_offset =
1503                    token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
1504            }
1505        }
1506    }
1507
1508    let column: usize = (offset - last_newline_offset).into();
1509    (line, column)
1510}
1511
1512macro_rules! ast_node {
1513    ($ast:ident, $kind:ident) => {
1514        #[derive(Clone, PartialEq, Eq, Hash)]
1515        #[repr(transparent)]
1516        /// An AST node for $ast
1517        pub struct $ast(SyntaxNode);
1518
1519        impl AstNode for $ast {
1520            type Language = Lang;
1521
1522            fn can_cast(kind: SyntaxKind) -> bool {
1523                kind == $kind
1524            }
1525
1526            fn cast(syntax: SyntaxNode) -> Option<Self> {
1527                if Self::can_cast(syntax.kind()) {
1528                    Some(Self(syntax))
1529                } else {
1530                    None
1531                }
1532            }
1533
1534            fn syntax(&self) -> &SyntaxNode {
1535                &self.0
1536            }
1537        }
1538
1539        impl $ast {
1540            /// Get the line number (0-indexed) where this node starts.
1541            pub fn line(&self) -> usize {
1542                line_col_at_offset(&self.0, self.0.text_range().start()).0
1543            }
1544
1545            /// Get the column number (0-indexed, in bytes) where this node starts.
1546            pub fn column(&self) -> usize {
1547                line_col_at_offset(&self.0, self.0.text_range().start()).1
1548            }
1549
1550            /// Get both line and column (0-indexed) where this node starts.
1551            /// Returns (line, column) where column is measured in bytes from the start of the line.
1552            pub fn line_col(&self) -> (usize, usize) {
1553                line_col_at_offset(&self.0, self.0.text_range().start())
1554            }
1555        }
1556
1557        impl core::fmt::Display for $ast {
1558            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1559                write!(f, "{}", self.0.text())
1560            }
1561        }
1562    };
1563}
1564
1565ast_node!(Makefile, ROOT);
1566ast_node!(Rule, RULE);
1567ast_node!(Recipe, RECIPE);
1568ast_node!(Identifier, IDENTIFIER);
1569ast_node!(VariableDefinition, VARIABLE);
1570ast_node!(Include, INCLUDE);
1571ast_node!(ArchiveMembers, ARCHIVE_MEMBERS);
1572ast_node!(ArchiveMember, ARCHIVE_MEMBER);
1573ast_node!(Conditional, CONDITIONAL);
1574
1575/// A reference to a variable in the makefile, e.g. `$(FOO)` or `${BAR}`.
1576///
1577/// This wraps an EXPR syntax node whose first token is `$` followed by `(` or `{`.
1578#[derive(Clone, PartialEq, Eq, Hash)]
1579pub struct VariableReference(SyntaxNode);
1580
1581impl VariableReference {
1582    /// Try to cast a syntax node into a VariableReference.
1583    ///
1584    /// Returns `Some` if the node is an EXPR whose first token is `$` followed by
1585    /// `(`, `{`, or an identifier (for single-character variables like `$X`).
1586    pub fn cast(syntax: SyntaxNode) -> Option<Self> {
1587        if syntax.kind() != EXPR {
1588            return None;
1589        }
1590        let mut tokens = syntax
1591            .children_with_tokens()
1592            .filter_map(|it| it.into_token());
1593        let first = tokens.next()?;
1594        if first.kind() != DOLLAR {
1595            return None;
1596        }
1597        // Accept $(...), ${...}, or $X (single-char)
1598        tokens.next()?;
1599        Some(Self(syntax))
1600    }
1601
1602    /// Get the syntax node backing this variable reference.
1603    pub fn syntax(&self) -> &SyntaxNode {
1604        &self.0
1605    }
1606
1607    /// Get the name of the referenced variable.
1608    ///
1609    /// For simple references like `$(FOO)`, returns `"FOO"`.
1610    /// For function calls like `$(wildcard *.c)`, returns `"wildcard"`.
1611    ///
1612    /// Note: Variable references inside recipes are not parsed into the syntax tree
1613    /// (recipes are stored as raw text). This only finds references in variable values,
1614    /// prerequisites, and targets.
1615    ///
1616    /// # Example
1617    /// ```
1618    /// use makefile_lossless::Makefile;
1619    /// let makefile: Makefile = "CFLAGS = $(BASE_FLAGS) -Wall\n".parse().unwrap();
1620    /// let refs: Vec<_> = makefile.variable_references().collect();
1621    /// assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
1622    /// ```
1623    pub fn name(&self) -> Option<String> {
1624        // After $ and (, the first IDENTIFIER token is the variable/function name
1625        self.0
1626            .children_with_tokens()
1627            .filter_map(|it| it.into_token())
1628            .find(|t| t.kind() == IDENTIFIER)
1629            .map(|t| t.text().to_string())
1630    }
1631
1632    /// Check if this is a function call rather than a simple variable reference.
1633    ///
1634    /// Returns `true` if the content after the function name contains whitespace
1635    /// or commas, indicating arguments (e.g. `$(subst a,b,text)` vs `$(CC)`).
1636    ///
1637    /// # Example
1638    /// ```
1639    /// use makefile_lossless::Makefile;
1640    /// let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
1641    /// let refs: Vec<_> = makefile.variable_references().collect();
1642    /// assert!(refs[0].is_function_call());
1643    /// ```
1644    pub fn is_function_call(&self) -> bool {
1645        let mut tokens = self
1646            .0
1647            .children_with_tokens()
1648            .filter_map(|it| it.into_token());
1649
1650        // Skip $ and opening paren/brace
1651        let Some(dollar) = tokens.next() else {
1652            return false;
1653        };
1654        if dollar.kind() != DOLLAR {
1655            return false;
1656        }
1657        let Some(open) = tokens.next() else {
1658            return false;
1659        };
1660        if open.kind() != LPAREN && open.kind() != LBRACE {
1661            return false;
1662        }
1663
1664        // Skip the function name (first IDENTIFIER)
1665        let Some(ident) = tokens.next() else {
1666            return false;
1667        };
1668        if ident.kind() != IDENTIFIER {
1669            return false;
1670        }
1671
1672        // If the next token is whitespace or comma, it's a function call
1673        match tokens.next() {
1674            Some(t) => t.kind() == WHITESPACE || t.kind() == COMMA,
1675            None => false,
1676        }
1677    }
1678
1679    /// Count the number of comma-separated arguments in a function call.
1680    ///
1681    /// Returns 0 for simple variable references. For function calls, counts
1682    /// the commas at depth 0 (not inside nested parentheses) plus 1.
1683    ///
1684    /// # Example
1685    /// ```
1686    /// use makefile_lossless::Makefile;
1687    /// let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
1688    /// let refs: Vec<_> = makefile.variable_references().collect();
1689    /// assert_eq!(refs[0].argument_count(), 3);
1690    /// ```
1691    pub fn argument_count(&self) -> usize {
1692        if !self.is_function_call() {
1693            return 0;
1694        }
1695
1696        let mut commas = 0;
1697        let mut depth = 0;
1698        let mut past_name = false;
1699
1700        for element in self.0.children_with_tokens() {
1701            let Some(token) = element.as_token() else {
1702                // Child nodes (nested EXPR) don't contain top-level commas
1703                continue;
1704            };
1705            match token.kind() {
1706                IDENTIFIER if !past_name => {
1707                    past_name = true;
1708                }
1709                DOLLAR | LPAREN | LBRACE if !past_name => {}
1710                LPAREN => depth += 1,
1711                RPAREN if depth > 0 => depth -= 1,
1712                COMMA if depth == 0 && past_name => commas += 1,
1713                _ => {}
1714            }
1715        }
1716
1717        if past_name {
1718            commas + 1
1719        } else {
1720            0
1721        }
1722    }
1723
1724    /// Determine which argument (0-based) the given byte offset falls into.
1725    ///
1726    /// Returns `None` if the offset is not inside this reference or if this
1727    /// is not a function call.
1728    ///
1729    /// # Example
1730    /// ```
1731    /// use makefile_lossless::Makefile;
1732    /// let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
1733    /// let refs: Vec<_> = makefile.variable_references().collect();
1734    /// // offset 12 is 'a' (first arg), offset 14 is 'b' (second arg), offset 16 is 't' (third arg)
1735    /// assert_eq!(refs[0].argument_index_at_offset(12), Some(0));
1736    /// assert_eq!(refs[0].argument_index_at_offset(14), Some(1));
1737    /// assert_eq!(refs[0].argument_index_at_offset(16), Some(2));
1738    /// ```
1739    pub fn argument_index_at_offset(&self, offset: usize) -> Option<usize> {
1740        if !self.is_function_call() {
1741            return None;
1742        }
1743
1744        let ref_start: usize = self.0.text_range().start().into();
1745        let ref_end: usize = self.0.text_range().end().into();
1746        if offset < ref_start || offset > ref_end {
1747            return None;
1748        }
1749
1750        let mut arg_index = 0;
1751        let mut depth = 0;
1752        let mut past_name = false;
1753
1754        for element in self.0.children_with_tokens() {
1755            let Some(token) = element.as_token() else {
1756                continue;
1757            };
1758            let token_end: usize = token.text_range().end().into();
1759
1760            match token.kind() {
1761                IDENTIFIER if !past_name => {
1762                    past_name = true;
1763                }
1764                DOLLAR | LPAREN | LBRACE if !past_name => {}
1765                LPAREN => depth += 1,
1766                RPAREN if depth > 0 => depth -= 1,
1767                COMMA if depth == 0 && past_name => {
1768                    if offset < token_end {
1769                        return Some(arg_index);
1770                    }
1771                    arg_index += 1;
1772                }
1773                _ => {}
1774            }
1775        }
1776
1777        if past_name {
1778            Some(arg_index)
1779        } else {
1780            None
1781        }
1782    }
1783
1784    /// Get the line number (0-indexed) where this reference starts.
1785    pub fn line(&self) -> usize {
1786        line_col_at_offset(&self.0, self.0.text_range().start()).0
1787    }
1788
1789    /// Get the column number (0-indexed, in bytes) where this reference starts.
1790    pub fn column(&self) -> usize {
1791        line_col_at_offset(&self.0, self.0.text_range().start()).1
1792    }
1793
1794    /// Get both line and column (0-indexed) where this reference starts.
1795    pub fn line_col(&self) -> (usize, usize) {
1796        line_col_at_offset(&self.0, self.0.text_range().start())
1797    }
1798
1799    /// Get the text range of this reference in the source.
1800    pub fn text_range(&self) -> rowan::TextRange {
1801        self.0.text_range()
1802    }
1803}
1804
1805impl core::fmt::Display for VariableReference {
1806    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1807        write!(f, "{}", self.0.text())
1808    }
1809}
1810
1811impl Recipe {
1812    /// Get the text content of this recipe line (the command to execute)
1813    ///
1814    /// For single-line recipes, this returns the command text excluding the
1815    /// leading tab and trailing newline.
1816    ///
1817    /// For multi-line recipes (with backslash continuations), this returns the
1818    /// full text including the internal newlines and continuation-line indentation,
1819    /// but still excluding the leading tab of the first line and the final newline.
1820    /// This preserves the exact content needed for a lossless round-trip.
1821    ///
1822    /// For comment-only lines, this returns an empty string.
1823    pub fn text(&self) -> String {
1824        let tokens: Vec<_> = self
1825            .syntax()
1826            .children_with_tokens()
1827            .filter_map(|it| it.as_token().cloned())
1828            .collect();
1829
1830        if tokens.is_empty() {
1831            return String::new();
1832        }
1833
1834        // Skip the first token if it's the leading INDENT
1835        let start = if tokens.first().map(|t| t.kind()) == Some(INDENT) {
1836            1
1837        } else {
1838            0
1839        };
1840
1841        // Skip the last token if it's the trailing NEWLINE
1842        let end = if tokens.last().map(|t| t.kind()) == Some(NEWLINE) {
1843            tokens.len() - 1
1844        } else {
1845            tokens.len()
1846        };
1847
1848        // Include TEXT, NEWLINE (internal continuation), and INDENT (continuation indent) tokens,
1849        // but skip COMMENT tokens (those are returned by comment()).
1850        // For INDENT tokens after a continuation newline, strip the leading tab character.
1851        let mut after_newline = false;
1852        tokens[start..end]
1853            .iter()
1854            .filter_map(|t| match t.kind() {
1855                TEXT => {
1856                    after_newline = false;
1857                    Some(t.text().to_string())
1858                }
1859                NEWLINE => {
1860                    after_newline = true;
1861                    Some(t.text().to_string())
1862                }
1863                INDENT if after_newline => {
1864                    after_newline = false;
1865                    // Strip the leading tab from continuation-line indentation
1866                    let text = t.text();
1867                    Some(text.strip_prefix('\t').unwrap_or(text).to_string())
1868                }
1869                _ => None,
1870            })
1871            .collect()
1872    }
1873
1874    /// Get the indentation string of this recipe line.
1875    ///
1876    /// Returns the leading indentation (typically a tab character) of this recipe line,
1877    /// or `None` if no indent token is present.
1878    ///
1879    /// # Example
1880    /// ```
1881    /// use makefile_lossless::Makefile;
1882    ///
1883    /// let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
1884    /// let rule = makefile.rules().next().unwrap();
1885    /// let recipe = rule.recipe_nodes().next().unwrap();
1886    /// assert_eq!(recipe.indent(), Some("\t".to_string()));
1887    /// ```
1888    pub fn indent(&self) -> Option<String> {
1889        self.syntax().children_with_tokens().find_map(|it| {
1890            if let Some(token) = it.as_token() {
1891                if token.kind() == INDENT {
1892                    return Some(token.text().to_string());
1893                }
1894            }
1895            None
1896        })
1897    }
1898
1899    /// Get the comment content of this recipe line, if any
1900    ///
1901    /// Returns the comment text (including the '#' character) if this recipe
1902    /// line contains a comment, or None if there is no comment.
1903    ///
1904    /// # Example
1905    /// ```
1906    /// use makefile_lossless::Makefile;
1907    ///
1908    /// let makefile: Makefile = "all:\n\t# This is a comment\n\techo hello\n".parse().unwrap();
1909    /// let rule = makefile.rules().next().unwrap();
1910    /// let recipes: Vec<_> = rule.recipe_nodes().collect();
1911    /// assert_eq!(recipes[0].comment(), Some("# This is a comment".to_string()));
1912    /// assert_eq!(recipes[1].comment(), None);
1913    /// ```
1914    pub fn comment(&self) -> Option<String> {
1915        self.syntax()
1916            .children_with_tokens()
1917            .filter_map(|it| {
1918                if let Some(token) = it.as_token() {
1919                    if token.kind() == COMMENT {
1920                        return Some(token.text().to_string());
1921                    }
1922                }
1923                None
1924            })
1925            .next()
1926    }
1927
1928    /// Get the full content of this recipe line
1929    ///
1930    /// Returns all content including command text, comments, and internal whitespace,
1931    /// but excluding the leading indent. This is useful for getting the complete
1932    /// content of a recipe line regardless of whether it's a command, comment, or both.
1933    ///
1934    /// # Example
1935    /// ```
1936    /// use makefile_lossless::Makefile;
1937    ///
1938    /// let makefile: Makefile = "all:\n\techo hello # inline comment\n".parse().unwrap();
1939    /// let rule = makefile.rules().next().unwrap();
1940    /// let recipe = rule.recipe_nodes().next().unwrap();
1941    /// assert_eq!(recipe.full(), "echo hello # inline comment");
1942    /// ```
1943    pub fn full(&self) -> String {
1944        self.syntax()
1945            .children_with_tokens()
1946            .filter_map(|it| {
1947                if let Some(token) = it.as_token() {
1948                    // Include TEXT and COMMENT tokens, but skip INDENT and NEWLINE
1949                    if token.kind() == TEXT || token.kind() == COMMENT {
1950                        return Some(token.text().to_string());
1951                    }
1952                }
1953                None
1954            })
1955            .collect::<Vec<_>>()
1956            .join("")
1957    }
1958
1959    /// Get the parent rule containing this recipe
1960    ///
1961    /// # Example
1962    /// ```
1963    /// use makefile_lossless::Makefile;
1964    ///
1965    /// let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
1966    /// let rule = makefile.rules().next().unwrap();
1967    /// let recipe = rule.recipe_nodes().next().unwrap();
1968    /// let parent = recipe.parent().unwrap();
1969    /// assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
1970    /// ```
1971    pub fn parent(&self) -> Option<Rule> {
1972        self.syntax().parent().and_then(Rule::cast)
1973    }
1974
1975    /// Get the source range of this recipe node.
1976    pub fn text_range(&self) -> rowan::TextRange {
1977        self.syntax().text_range()
1978    }
1979
1980    /// Check if this recipe has the silent prefix (@)
1981    ///
1982    /// # Example
1983    /// ```
1984    /// use makefile_lossless::Makefile;
1985    ///
1986    /// let makefile: Makefile = "all:\n\t@echo hello\n\techo world\n".parse().unwrap();
1987    /// let rule = makefile.rules().next().unwrap();
1988    /// let recipes: Vec<_> = rule.recipe_nodes().collect();
1989    /// assert!(recipes[0].is_silent());
1990    /// assert!(!recipes[1].is_silent());
1991    /// ```
1992    pub fn is_silent(&self) -> bool {
1993        let text = self.text();
1994        text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1995    }
1996
1997    /// Check if this recipe has the ignore-errors prefix (-)
1998    ///
1999    /// # Example
2000    /// ```
2001    /// use makefile_lossless::Makefile;
2002    ///
2003    /// let makefile: Makefile = "all:\n\t-echo hello\n\techo world\n".parse().unwrap();
2004    /// let rule = makefile.rules().next().unwrap();
2005    /// let recipes: Vec<_> = rule.recipe_nodes().collect();
2006    /// assert!(recipes[0].is_ignore_errors());
2007    /// assert!(!recipes[1].is_ignore_errors());
2008    /// ```
2009    pub fn is_ignore_errors(&self) -> bool {
2010        let text = self.text();
2011        text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
2012    }
2013
2014    /// Set the command prefix for this recipe
2015    ///
2016    /// The prefix can contain `@` (silent), `-` (ignore errors), and/or `+` (always execute).
2017    /// Pass an empty string to remove all prefixes.
2018    ///
2019    /// # Example
2020    /// ```
2021    /// use makefile_lossless::Makefile;
2022    ///
2023    /// let mut makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
2024    /// let rule = makefile.rules().next().unwrap();
2025    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2026    /// recipe.set_prefix("@");
2027    /// assert_eq!(recipe.text(), "@echo hello");
2028    /// assert!(recipe.is_silent());
2029    /// ```
2030    pub fn set_prefix(&mut self, prefix: &str) {
2031        let text = self.text();
2032
2033        // Strip existing prefix characters
2034        let stripped = text.trim_start_matches(['@', '-', '+']);
2035
2036        // Build new text with the new prefix
2037        let new_text = format!("{}{}", prefix, stripped);
2038
2039        self.replace_text(&new_text);
2040    }
2041
2042    /// Replace the text content of this recipe line
2043    ///
2044    /// # Example
2045    /// ```
2046    /// use makefile_lossless::Makefile;
2047    ///
2048    /// let mut makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
2049    /// let rule = makefile.rules().next().unwrap();
2050    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2051    /// recipe.replace_text("echo world");
2052    /// assert_eq!(recipe.text(), "echo world");
2053    /// ```
2054    pub fn replace_text(&mut self, new_text: &str) {
2055        let node = self.syntax();
2056        let parent = node.parent().expect("Recipe node must have a parent");
2057        let node_index = node.index();
2058
2059        // Build a new RECIPE node with the new text
2060        let mut builder = GreenNodeBuilder::new();
2061        builder.start_node(RECIPE.into());
2062
2063        // Preserve the existing INDENT token if present
2064        if let Some(indent_token) = node
2065            .children_with_tokens()
2066            .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
2067        {
2068            builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
2069        } else {
2070            builder.token(INDENT.into(), "\t");
2071        }
2072
2073        builder.token(TEXT.into(), new_text);
2074
2075        // Preserve the existing NEWLINE token if present
2076        if let Some(newline_token) = node
2077            .children_with_tokens()
2078            .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
2079        {
2080            builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
2081        } else {
2082            builder.token(NEWLINE.into(), "\n");
2083        }
2084
2085        builder.finish_node();
2086        let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2087
2088        // Replace the old node with the new one
2089        parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
2090
2091        // Update self to point to the new node
2092        // Note: index() returns position among all siblings (nodes + tokens)
2093        // so we need to use children_with_tokens() and filter for the node
2094        *self = parent
2095            .children_with_tokens()
2096            .nth(node_index)
2097            .and_then(|element| element.into_node())
2098            .and_then(Recipe::cast)
2099            .expect("New recipe node should exist at the same index");
2100    }
2101
2102    /// Insert a new recipe line before this one
2103    ///
2104    /// # Example
2105    /// ```
2106    /// use makefile_lossless::Makefile;
2107    ///
2108    /// let mut makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
2109    /// let mut rule = makefile.rules().next().unwrap();
2110    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2111    /// recipe.insert_before("echo hello");
2112    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo hello", "echo world"]);
2113    /// ```
2114    pub fn insert_before(&self, text: &str) {
2115        let node = self.syntax();
2116        let parent = node.parent().expect("Recipe node must have a parent");
2117        let node_index = node.index();
2118
2119        // Build a new RECIPE node
2120        let mut builder = GreenNodeBuilder::new();
2121        builder.start_node(RECIPE.into());
2122        builder.token(INDENT.into(), "\t");
2123        builder.token(TEXT.into(), text);
2124        builder.token(NEWLINE.into(), "\n");
2125        builder.finish_node();
2126        let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2127
2128        // Insert before this recipe
2129        parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
2130    }
2131
2132    /// Insert a new recipe line after this one
2133    ///
2134    /// # Example
2135    /// ```
2136    /// use makefile_lossless::Makefile;
2137    ///
2138    /// let mut makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
2139    /// let mut rule = makefile.rules().next().unwrap();
2140    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2141    /// recipe.insert_after("echo world");
2142    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo hello", "echo world"]);
2143    /// ```
2144    pub fn insert_after(&self, text: &str) {
2145        let node = self.syntax();
2146        let parent = node.parent().expect("Recipe node must have a parent");
2147        let node_index = node.index();
2148
2149        // Build a new RECIPE node
2150        let mut builder = GreenNodeBuilder::new();
2151        builder.start_node(RECIPE.into());
2152        builder.token(INDENT.into(), "\t");
2153        builder.token(TEXT.into(), text);
2154        builder.token(NEWLINE.into(), "\n");
2155        builder.finish_node();
2156        let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2157
2158        // Insert after this recipe
2159        parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
2160    }
2161
2162    /// Remove this recipe line from its parent
2163    ///
2164    /// # Example
2165    /// ```
2166    /// use makefile_lossless::Makefile;
2167    ///
2168    /// let mut makefile: Makefile = "all:\n\techo hello\n\techo world\n".parse().unwrap();
2169    /// let mut rule = makefile.rules().next().unwrap();
2170    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2171    /// recipe.remove();
2172    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
2173    /// ```
2174    pub fn remove(&self) {
2175        let node = self.syntax();
2176        let parent = node.parent().expect("Recipe node must have a parent");
2177        let node_index = node.index();
2178
2179        // Remove this recipe node from its parent
2180        parent.splice_children(node_index..node_index + 1, vec![]);
2181    }
2182}
2183
2184///
2185/// This removes trailing NEWLINE tokens from the end of a RULE node to avoid
2186/// extra blank lines at the end of a file when the last rule is removed.
2187pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
2188    // Collect all trailing NEWLINE tokens at the end of the rule and within RECIPE nodes
2189    let mut newlines_to_remove = vec![];
2190    let mut current = node.last_child_or_token();
2191
2192    while let Some(element) = current {
2193        match &element {
2194            rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2195                newlines_to_remove.push(token.clone());
2196                current = token.prev_sibling_or_token();
2197            }
2198            rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
2199                // Also check for trailing newlines in the RECIPE node
2200                let mut recipe_current = n.last_child_or_token();
2201                while let Some(recipe_element) = recipe_current {
2202                    match &recipe_element {
2203                        rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2204                            newlines_to_remove.push(token.clone());
2205                            recipe_current = token.prev_sibling_or_token();
2206                        }
2207                        _ => break,
2208                    }
2209                }
2210                break; // Stop after checking the last RECIPE node
2211            }
2212            _ => break,
2213        }
2214    }
2215
2216    // Remove all but one trailing newline (keep at least one)
2217    // Remove from highest index to lowest to avoid index shifts
2218    if newlines_to_remove.len() > 1 {
2219        // Sort by index descending
2220        newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
2221
2222        for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
2223            let parent = token.parent().unwrap();
2224            let idx = token.index();
2225            parent.splice_children(idx..idx + 1, vec![]);
2226        }
2227    }
2228}
2229
2230/// Helper function to remove a node along with its preceding comments and up to 1 empty line.
2231///
2232/// This walks backward from the node, removing:
2233/// - The node itself
2234/// - All preceding comments (COMMENT tokens)
2235/// - Up to 1 empty line (consecutive NEWLINE tokens)
2236/// - Any WHITESPACE tokens between these elements
2237pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
2238    let mut collected_elements = vec![];
2239    let mut found_comment = false;
2240
2241    // Walk backward to collect preceding comments, newlines, and whitespace
2242    let mut current = node.prev_sibling_or_token();
2243    while let Some(element) = current {
2244        match &element {
2245            rowan::NodeOrToken::Token(token) => match token.kind() {
2246                COMMENT => {
2247                    if token.text().starts_with("#!") {
2248                        break; // Don't remove shebang lines
2249                    }
2250                    found_comment = true;
2251                    collected_elements.push(element.clone());
2252                }
2253                NEWLINE | WHITESPACE => {
2254                    collected_elements.push(element.clone());
2255                }
2256                _ => break, // Hit something else, stop
2257            },
2258            rowan::NodeOrToken::Node(n) => {
2259                // Handle BLANK_LINE nodes which wrap newlines
2260                if n.kind() == BLANK_LINE {
2261                    collected_elements.push(element.clone());
2262                } else {
2263                    break; // Hit another node type, stop
2264                }
2265            }
2266        }
2267        current = element.prev_sibling_or_token();
2268    }
2269
2270    // Determine which preceding elements to remove
2271    // If we found comments, remove them along with up to 1 blank line
2272    let mut elements_to_remove = vec![];
2273    let mut consecutive_newlines = 0;
2274    for element in collected_elements.iter().rev() {
2275        let should_remove = match element {
2276            rowan::NodeOrToken::Token(token) => match token.kind() {
2277                COMMENT => {
2278                    consecutive_newlines = 0;
2279                    found_comment
2280                }
2281                NEWLINE => {
2282                    consecutive_newlines += 1;
2283                    found_comment && consecutive_newlines <= 1
2284                }
2285                WHITESPACE => found_comment,
2286                _ => false,
2287            },
2288            rowan::NodeOrToken::Node(n) => {
2289                // Handle BLANK_LINE nodes (count as newlines)
2290                if n.kind() == BLANK_LINE {
2291                    consecutive_newlines += 1;
2292                    found_comment && consecutive_newlines <= 1
2293                } else {
2294                    false
2295                }
2296            }
2297        };
2298
2299        if should_remove {
2300            elements_to_remove.push(element.clone());
2301        }
2302    }
2303
2304    // Remove elements in reverse order (from highest index to lowest) to avoid index shifts
2305    // Start with the node itself, then preceding elements
2306    let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
2307    all_to_remove.extend(elements_to_remove.into_iter().rev());
2308
2309    // Sort by index in descending order
2310    all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
2311
2312    for element in all_to_remove {
2313        let idx = element.index();
2314        parent.splice_children(idx..idx + 1, vec![]);
2315    }
2316}
2317
2318impl FromStr for Rule {
2319    type Err = crate::Error;
2320
2321    fn from_str(s: &str) -> Result<Self, Self::Err> {
2322        Rule::parse(s).to_rule_result()
2323    }
2324}
2325
2326impl FromStr for Makefile {
2327    type Err = crate::Error;
2328
2329    fn from_str(s: &str) -> Result<Self, Self::Err> {
2330        Makefile::parse(s).to_result()
2331    }
2332}
2333
2334#[cfg(test)]
2335mod tests {
2336    use super::*;
2337    use crate::ast::makefile::MakefileItem;
2338    use crate::pattern::matches_pattern;
2339
2340    #[test]
2341    fn test_conditionals() {
2342        // We'll use relaxed parsing for conditionals
2343
2344        // Basic conditionals - ifdef/ifndef
2345        let code = "ifdef DEBUG\n    DEBUG_FLAG := 1\nendif\n";
2346        let mut buf = code.as_bytes();
2347        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
2348        assert!(makefile.code().contains("DEBUG_FLAG"));
2349
2350        // Basic conditionals - ifeq/ifneq
2351        let code =
2352            "ifeq ($(OS),Windows_NT)\n    RESULT := windows\nelse\n    RESULT := unix\nendif\n";
2353        let mut buf = code.as_bytes();
2354        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
2355        assert!(makefile.code().contains("RESULT"));
2356        assert!(makefile.code().contains("windows"));
2357
2358        // Nested conditionals with else
2359        let code = "ifdef DEBUG\n    CFLAGS += -g\n    ifdef VERBOSE\n        CFLAGS += -v\n    endif\nelse\n    CFLAGS += -O2\nendif\n";
2360        let mut buf = code.as_bytes();
2361        let makefile = Makefile::read_relaxed(&mut buf)
2362            .expect("Failed to parse nested conditionals with else");
2363        assert!(makefile.code().contains("CFLAGS"));
2364        assert!(makefile.code().contains("VERBOSE"));
2365
2366        // Empty conditionals
2367        let code = "ifdef DEBUG\nendif\n";
2368        let mut buf = code.as_bytes();
2369        let makefile =
2370            Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
2371        assert!(makefile.code().contains("ifdef DEBUG"));
2372
2373        // Conditionals with else ifeq
2374        let code = "ifeq ($(OS),Windows)\n    EXT := .exe\nelse ifeq ($(OS),Linux)\n    EXT := .bin\nelse\n    EXT := .out\nendif\n";
2375        let mut buf = code.as_bytes();
2376        let makefile =
2377            Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
2378        assert!(makefile.code().contains("EXT"));
2379
2380        // Invalid conditionals - this should generate parse errors but still produce a Makefile
2381        let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
2382        let mut buf = code.as_bytes();
2383        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
2384        assert!(makefile.code().contains("DEBUG"));
2385
2386        // Missing condition - this should also generate parse errors but still produce a Makefile
2387        let code = "ifdef \nDEBUG := 1\nendif\n";
2388        let mut buf = code.as_bytes();
2389        let makefile = Makefile::read_relaxed(&mut buf)
2390            .expect("Failed to parse with recovery - missing condition");
2391        assert!(makefile.code().contains("DEBUG"));
2392    }
2393
2394    #[test]
2395    fn test_parse_simple() {
2396        const SIMPLE: &str = r#"VARIABLE = value
2397
2398rule: dependency
2399	command
2400"#;
2401        let parsed = parse(SIMPLE, None);
2402        assert!(parsed.errors.is_empty());
2403        let node = parsed.syntax();
2404        assert_eq!(
2405            format!("{:#?}", node),
2406            r#"ROOT@0..44
2407  VARIABLE@0..17
2408    IDENTIFIER@0..8 "VARIABLE"
2409    WHITESPACE@8..9 " "
2410    OPERATOR@9..10 "="
2411    WHITESPACE@10..11 " "
2412    EXPR@11..16
2413      IDENTIFIER@11..16 "value"
2414    NEWLINE@16..17 "\n"
2415  BLANK_LINE@17..18
2416    NEWLINE@17..18 "\n"
2417  RULE@18..44
2418    TARGETS@18..22
2419      IDENTIFIER@18..22 "rule"
2420    OPERATOR@22..23 ":"
2421    WHITESPACE@23..24 " "
2422    PREREQUISITES@24..34
2423      PREREQUISITE@24..34
2424        IDENTIFIER@24..34 "dependency"
2425    NEWLINE@34..35 "\n"
2426    RECIPE@35..44
2427      INDENT@35..36 "\t"
2428      TEXT@36..43 "command"
2429      NEWLINE@43..44 "\n"
2430"#
2431        );
2432
2433        let root = parsed.root();
2434
2435        let mut rules = root.rules().collect::<Vec<_>>();
2436        assert_eq!(rules.len(), 1);
2437        let rule = rules.pop().unwrap();
2438        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2439        assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
2440        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2441
2442        let mut variables = root.variable_definitions().collect::<Vec<_>>();
2443        assert_eq!(variables.len(), 1);
2444        let variable = variables.pop().unwrap();
2445        assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2446        assert_eq!(variable.raw_value(), Some("value".to_string()));
2447    }
2448
2449    #[test]
2450    fn test_parse_export_assign() {
2451        const EXPORT: &str = r#"export VARIABLE := value
2452"#;
2453        let parsed = parse(EXPORT, None);
2454        assert!(parsed.errors.is_empty());
2455        let node = parsed.syntax();
2456        assert_eq!(
2457            format!("{:#?}", node),
2458            r#"ROOT@0..25
2459  VARIABLE@0..25
2460    IDENTIFIER@0..6 "export"
2461    WHITESPACE@6..7 " "
2462    IDENTIFIER@7..15 "VARIABLE"
2463    WHITESPACE@15..16 " "
2464    OPERATOR@16..18 ":="
2465    WHITESPACE@18..19 " "
2466    EXPR@19..24
2467      IDENTIFIER@19..24 "value"
2468    NEWLINE@24..25 "\n"
2469"#
2470        );
2471
2472        let root = parsed.root();
2473
2474        let mut variables = root.variable_definitions().collect::<Vec<_>>();
2475        assert_eq!(variables.len(), 1);
2476        let variable = variables.pop().unwrap();
2477        assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2478        assert_eq!(variable.raw_value(), Some("value".to_string()));
2479    }
2480
2481    #[test]
2482    fn test_parse_multiple_prerequisites() {
2483        const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2484	command
2485
2486"#;
2487        let parsed = parse(MULTIPLE_PREREQUISITES, None);
2488        assert!(parsed.errors.is_empty());
2489        let node = parsed.syntax();
2490        assert_eq!(
2491            format!("{:#?}", node),
2492            r#"ROOT@0..40
2493  RULE@0..40
2494    TARGETS@0..4
2495      IDENTIFIER@0..4 "rule"
2496    OPERATOR@4..5 ":"
2497    WHITESPACE@5..6 " "
2498    PREREQUISITES@6..29
2499      PREREQUISITE@6..17
2500        IDENTIFIER@6..17 "dependency1"
2501      WHITESPACE@17..18 " "
2502      PREREQUISITE@18..29
2503        IDENTIFIER@18..29 "dependency2"
2504    NEWLINE@29..30 "\n"
2505    RECIPE@30..39
2506      INDENT@30..31 "\t"
2507      TEXT@31..38 "command"
2508      NEWLINE@38..39 "\n"
2509    NEWLINE@39..40 "\n"
2510"#
2511        );
2512        let root = parsed.root();
2513
2514        let rule = root.rules().next().unwrap();
2515        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2516        assert_eq!(
2517            rule.prerequisites().collect::<Vec<_>>(),
2518            vec!["dependency1", "dependency2"]
2519        );
2520        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2521    }
2522
2523    #[test]
2524    fn test_add_rule() {
2525        let mut makefile = Makefile::new();
2526        let rule = makefile.add_rule("rule");
2527        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2528        assert_eq!(
2529            rule.prerequisites().collect::<Vec<_>>(),
2530            Vec::<String>::new()
2531        );
2532
2533        assert_eq!(makefile.to_string(), "rule:\n");
2534    }
2535
2536    #[test]
2537    fn test_add_rule_with_shebang() {
2538        // Regression test for bug where add_rule() panics on makefiles with shebangs
2539        let content = r#"#!/usr/bin/make -f
2540
2541build: blah
2542	$(MAKE) install
2543
2544clean:
2545	dh_clean
2546"#;
2547
2548        let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2549        let initial_count = makefile.rules().count();
2550        assert_eq!(initial_count, 2);
2551
2552        // This should not panic
2553        let rule = makefile.add_rule("build-indep");
2554        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2555
2556        // Should have one more rule now
2557        assert_eq!(makefile.rules().count(), initial_count + 1);
2558    }
2559
2560    #[test]
2561    fn test_add_rule_formatting() {
2562        // Regression test for formatting issues when adding rules
2563        let content = r#"build: blah
2564	$(MAKE) install
2565
2566clean:
2567	dh_clean
2568"#;
2569
2570        let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2571        let mut rule = makefile.add_rule("build-indep");
2572        rule.add_prerequisite("build").unwrap();
2573
2574        let expected = r#"build: blah
2575	$(MAKE) install
2576
2577clean:
2578	dh_clean
2579
2580build-indep: build
2581"#;
2582
2583        assert_eq!(makefile.to_string(), expected);
2584    }
2585
2586    #[test]
2587    fn test_push_command() {
2588        let mut makefile = Makefile::new();
2589        let mut rule = makefile.add_rule("rule");
2590
2591        // Add commands in place to the rule
2592        rule.push_command("command");
2593        rule.push_command("command2");
2594
2595        // Check the commands in the rule
2596        assert_eq!(
2597            rule.recipes().collect::<Vec<_>>(),
2598            vec!["command", "command2"]
2599        );
2600
2601        // Add a third command
2602        rule.push_command("command3");
2603        assert_eq!(
2604            rule.recipes().collect::<Vec<_>>(),
2605            vec!["command", "command2", "command3"]
2606        );
2607
2608        // Check if the makefile was modified
2609        assert_eq!(
2610            makefile.to_string(),
2611            "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2612        );
2613
2614        // The rule should have the same string representation
2615        assert_eq!(
2616            rule.to_string(),
2617            "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2618        );
2619    }
2620
2621    #[test]
2622    fn test_replace_command() {
2623        let mut makefile = Makefile::new();
2624        let mut rule = makefile.add_rule("rule");
2625
2626        // Add commands in place
2627        rule.push_command("command");
2628        rule.push_command("command2");
2629
2630        // Check the commands in the rule
2631        assert_eq!(
2632            rule.recipes().collect::<Vec<_>>(),
2633            vec!["command", "command2"]
2634        );
2635
2636        // Replace the first command
2637        rule.replace_command(0, "new command");
2638        assert_eq!(
2639            rule.recipes().collect::<Vec<_>>(),
2640            vec!["new command", "command2"]
2641        );
2642
2643        // Check if the makefile was modified
2644        assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2645
2646        // The rule should have the same string representation
2647        assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2648    }
2649
2650    #[test]
2651    fn test_replace_command_with_comments() {
2652        // Regression test for bug where replace_command() inserts instead of replacing
2653        // when the rule contains comments
2654        let content = b"override_dh_strip:\n\t# no longer necessary after buster\n\tdh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'\n";
2655
2656        let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2657
2658        let mut rule = makefile.rules().next().unwrap();
2659
2660        // Before replacement, there should be 2 recipe nodes (comment + command)
2661        assert_eq!(rule.recipe_nodes().count(), 2);
2662        let recipes: Vec<_> = rule.recipe_nodes().collect();
2663        assert_eq!(recipes[0].text(), ""); // comment-only
2664        assert_eq!(
2665            recipes[1].text(),
2666            "dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"
2667        );
2668
2669        // Replace the second recipe (index 1, the actual command)
2670        assert!(rule.replace_command(1, "dh_strip"));
2671
2672        // After replacement, there should still be 2 recipe nodes
2673        assert_eq!(rule.recipe_nodes().count(), 2);
2674        let recipes: Vec<_> = rule.recipe_nodes().collect();
2675        assert_eq!(recipes[0].text(), ""); // comment still there
2676        assert_eq!(recipes[1].text(), "dh_strip");
2677    }
2678
2679    #[test]
2680    fn test_parse_rule_without_newline() {
2681        let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2682        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2683        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2684        let rule = "rule: dependency".parse::<Rule>().unwrap();
2685        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2686        assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2687    }
2688
2689    #[test]
2690    fn test_parse_makefile_without_newline() {
2691        let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2692        assert_eq!(makefile.rules().count(), 1);
2693    }
2694
2695    #[test]
2696    fn test_from_reader() {
2697        let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2698        assert_eq!(makefile.rules().count(), 1);
2699    }
2700
2701    #[test]
2702    fn test_parse_with_tab_after_last_newline() {
2703        let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2704        assert_eq!(makefile.rules().count(), 1);
2705    }
2706
2707    #[test]
2708    fn test_parse_with_space_after_last_newline() {
2709        let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2710        assert_eq!(makefile.rules().count(), 1);
2711    }
2712
2713    #[test]
2714    fn test_parse_with_comment_after_last_newline() {
2715        let makefile =
2716            Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2717        assert_eq!(makefile.rules().count(), 1);
2718    }
2719
2720    #[test]
2721    fn test_parse_with_variable_rule() {
2722        let makefile =
2723            Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2724                .unwrap();
2725
2726        // Check variable definition
2727        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2728        assert_eq!(vars.len(), 1);
2729        assert_eq!(vars[0].name(), Some("RULE".to_string()));
2730        assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2731
2732        // Check rule
2733        let rules = makefile.rules().collect::<Vec<_>>();
2734        assert_eq!(rules.len(), 1);
2735        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2736        assert_eq!(
2737            rules[0].prerequisites().collect::<Vec<_>>(),
2738            vec!["dependency"]
2739        );
2740        assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2741    }
2742
2743    #[test]
2744    fn test_parse_with_variable_dependency() {
2745        let makefile =
2746            Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2747
2748        // Check variable definition
2749        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2750        assert_eq!(vars.len(), 1);
2751        assert_eq!(vars[0].name(), Some("DEP".to_string()));
2752        assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2753
2754        // Check rule
2755        let rules = makefile.rules().collect::<Vec<_>>();
2756        assert_eq!(rules.len(), 1);
2757        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2758        assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2759        assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2760    }
2761
2762    #[test]
2763    fn test_parse_with_variable_command() {
2764        let makefile =
2765            Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2766
2767        // Check variable definition
2768        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2769        assert_eq!(vars.len(), 1);
2770        assert_eq!(vars[0].name(), Some("COM".to_string()));
2771        assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2772
2773        // Check rule
2774        let rules = makefile.rules().collect::<Vec<_>>();
2775        assert_eq!(rules.len(), 1);
2776        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2777        assert_eq!(
2778            rules[0].prerequisites().collect::<Vec<_>>(),
2779            vec!["dependency"]
2780        );
2781        assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2782    }
2783
2784    #[test]
2785    fn test_regular_line_error_reporting() {
2786        let input = "rule target\n\tcommand";
2787
2788        // Test both APIs with one input
2789        let parsed = parse(input, None);
2790        let direct_error = &parsed.errors[0];
2791
2792        // Verify error is detected with correct details
2793        assert_eq!(direct_error.line, 2);
2794        assert!(
2795            direct_error.message.contains("expected"),
2796            "Error message should contain 'expected': {}",
2797            direct_error.message
2798        );
2799        assert_eq!(direct_error.context, "\tcommand");
2800
2801        // Check public API
2802        let reader_result = Makefile::from_reader(input.as_bytes());
2803        let parse_error = match reader_result {
2804            Ok(_) => panic!("Expected Parse error from from_reader"),
2805            Err(err) => match err {
2806                self::Error::Parse(parse_err) => parse_err,
2807                _ => panic!("Expected Parse error"),
2808            },
2809        };
2810
2811        // Verify formatting includes line number and context
2812        let error_text = parse_error.to_string();
2813        assert!(error_text.contains("Error at line 2:"));
2814        assert!(error_text.contains("2| \tcommand"));
2815    }
2816
2817    #[test]
2818    fn test_parsing_error_context_with_bad_syntax() {
2819        // Input with unusual characters to ensure they're preserved
2820        let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2821
2822        // With our relaxed parsing, verify we either get a proper error or parse successfully
2823        match Makefile::from_reader(input.as_bytes()) {
2824            Ok(makefile) => {
2825                // If it parses successfully, our parser is robust enough to handle unusual characters
2826                assert_eq!(
2827                    makefile.rules().count(),
2828                    0,
2829                    "Should not have found any rules"
2830                );
2831            }
2832            Err(err) => match err {
2833                self::Error::Parse(error) => {
2834                    // Verify error details are properly reported
2835                    assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2836                    assert!(
2837                        !error.errors[0].context.is_empty(),
2838                        "Error context should not be empty"
2839                    );
2840                }
2841                _ => panic!("Unexpected error type"),
2842            },
2843        };
2844    }
2845
2846    #[test]
2847    fn test_error_message_format() {
2848        // Test the error formatter directly
2849        let parse_error = ParseError {
2850            errors: vec![ErrorInfo {
2851                message: "test error".to_string(),
2852                line: 42,
2853                context: "some problematic code".to_string(),
2854            }],
2855        };
2856
2857        let error_text = parse_error.to_string();
2858        assert!(error_text.contains("Error at line 42: test error"));
2859        assert!(error_text.contains("42| some problematic code"));
2860    }
2861
2862    #[test]
2863    fn test_line_number_calculation() {
2864        // Test inputs for various error locations
2865        let test_cases = [
2866            ("rule dependency\n\tcommand", 2),             // Missing colon
2867            ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2),              // Strange characters
2868            ("var = value\n#comment\n\tindented line", 3), // Indented line not part of a rule
2869        ];
2870
2871        for (input, expected_line) in test_cases {
2872            // Attempt to parse the input
2873            match input.parse::<Makefile>() {
2874                Ok(_) => {
2875                    // If the parser succeeds, that's fine - our parser is more robust
2876                    // Skip assertions when there's no error to check
2877                    continue;
2878                }
2879                Err(err) => {
2880                    if let Error::Parse(parse_err) = err {
2881                        // Verify error line number matches expected line
2882                        assert_eq!(
2883                            parse_err.errors[0].line, expected_line,
2884                            "Line number should match the expected line"
2885                        );
2886
2887                        // If the error is about indentation, check that the context includes the tab
2888                        if parse_err.errors[0].message.contains("indented") {
2889                            assert!(
2890                                parse_err.errors[0].context.starts_with('\t'),
2891                                "Context for indentation errors should include the tab character"
2892                            );
2893                        }
2894                    } else {
2895                        panic!("Expected parse error, got: {:?}", err);
2896                    }
2897                }
2898            }
2899        }
2900    }
2901
2902    #[test]
2903    fn test_conditional_features() {
2904        // Simple use of variables in conditionals
2905        let code = r#"
2906# Set variables based on DEBUG flag
2907ifdef DEBUG
2908    CFLAGS += -g -DDEBUG
2909else
2910    CFLAGS = -O2
2911endif
2912
2913# Define a build rule
2914all: $(OBJS)
2915	$(CC) $(CFLAGS) -o $@ $^
2916"#;
2917
2918        let mut buf = code.as_bytes();
2919        let makefile =
2920            Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2921
2922        // Instead of checking for variable definitions which might not get created
2923        // due to conditionals, let's verify that we can parse the content without errors
2924        assert!(!makefile.code().is_empty(), "Makefile has content");
2925
2926        // Check that we detected a rule
2927        let rules = makefile.rules().collect::<Vec<_>>();
2928        assert!(!rules.is_empty(), "Should have found rules");
2929
2930        // Verify conditional presence in the original code
2931        assert!(code.contains("ifdef DEBUG"));
2932        assert!(code.contains("endif"));
2933
2934        // Also try with an explicitly defined variable
2935        let code_with_var = r#"
2936# Define a variable first
2937CC = gcc
2938
2939ifdef DEBUG
2940    CFLAGS += -g -DDEBUG
2941else
2942    CFLAGS = -O2
2943endif
2944
2945all: $(OBJS)
2946	$(CC) $(CFLAGS) -o $@ $^
2947"#;
2948
2949        let mut buf = code_with_var.as_bytes();
2950        let makefile =
2951            Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2952
2953        // Now we should definitely find at least the CC variable
2954        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2955        assert!(
2956            !vars.is_empty(),
2957            "Should have found at least the CC variable definition"
2958        );
2959    }
2960
2961    #[test]
2962    fn test_include_directive() {
2963        let parsed = parse(
2964            "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2965            None,
2966        );
2967        assert!(parsed.errors.is_empty());
2968        let node = parsed.syntax();
2969        assert!(format!("{:#?}", node).contains("INCLUDE@"));
2970    }
2971
2972    #[test]
2973    fn test_export_variables() {
2974        let parsed = parse("export SHELL := /bin/bash\n", None);
2975        assert!(parsed.errors.is_empty());
2976        let makefile = parsed.root();
2977        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2978        assert_eq!(vars.len(), 1);
2979        let shell_var = vars
2980            .iter()
2981            .find(|v| v.name() == Some("SHELL".to_string()))
2982            .unwrap();
2983        assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2984    }
2985
2986    #[test]
2987    fn test_bare_export_variable() {
2988        // "export VARNAME" without assignment operator is a valid GNU Make directive
2989        // that exports a previously-defined variable.
2990        let parsed = parse(
2991            "DEB_CFLAGS_MAINT_APPEND = -Wno-error\nexport DEB_CFLAGS_MAINT_APPEND\n\n%:\n\tdh $@\n",
2992            None,
2993        );
2994        assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2995        let makefile = parsed.root();
2996        // The bare export should be parsed as a variable, not a rule
2997        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2998        assert_eq!(vars.len(), 2);
2999        // The pattern rule should be found
3000        let rules = makefile.rules().collect::<Vec<_>>();
3001        assert_eq!(rules.len(), 1);
3002        assert!(rules[0].targets().any(|t| t == "%"));
3003        // build-arch should match via the pattern rule
3004        assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
3005    }
3006
3007    #[test]
3008    fn test_bare_export_at_eof() {
3009        // Bare "export VARNAME" at end of file (no trailing newline)
3010        let parsed = parse("VAR = value\nexport VAR", None);
3011        assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3012        let makefile = parsed.root();
3013        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3014        assert_eq!(vars.len(), 2);
3015        assert_eq!(makefile.rules().count(), 0);
3016    }
3017
3018    #[test]
3019    fn test_bare_export_does_not_eat_include() {
3020        // Bare "export VARNAME" must not consume subsequent include directives
3021        let parsed = parse("VAR = value\nexport VAR\ninclude other.mk\n", None);
3022        assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3023        let makefile = parsed.root();
3024        assert_eq!(makefile.includes().count(), 1);
3025        assert_eq!(
3026            makefile.included_files().collect::<Vec<_>>(),
3027            vec!["other.mk"]
3028        );
3029    }
3030
3031    #[test]
3032    fn test_bare_export_multiple() {
3033        // Multiple bare exports in a row
3034        let parsed = parse(
3035            "A = 1\nB = 2\nexport A\nexport B\n\nall:\n\techo done\n",
3036            None,
3037        );
3038        assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3039        let makefile = parsed.root();
3040        assert_eq!(makefile.variable_definitions().count(), 4);
3041        let rules = makefile.rules().collect::<Vec<_>>();
3042        assert_eq!(rules.len(), 1);
3043        assert!(rules[0].targets().any(|t| t == "all"));
3044    }
3045
3046    #[test]
3047    fn test_parse_error_does_not_cross_lines() {
3048        // A line that fails to parse as a rule (no colon) must not
3049        // consume tokens from subsequent lines.
3050        let parsed = parse("notarule\n\nbuild-arch:\n\techo arch\n", None);
3051        let makefile = parsed.root();
3052        let rules = makefile.rules().collect::<Vec<_>>();
3053        // The "notarule" line may produce an error, but build-arch must still be found
3054        assert!(
3055            rules.iter().any(|r| r.targets().any(|t| t == "build-arch")),
3056            "build-arch rule should be parsed despite earlier error; rules: {:?}",
3057            rules
3058                .iter()
3059                .map(|r| r.targets().collect::<Vec<_>>())
3060                .collect::<Vec<_>>()
3061        );
3062    }
3063
3064    #[test]
3065    fn test_pyfai_rules_full() {
3066        // Real-world pyFAI debian/rules that triggered #1131043
3067        let input = "\
3068#!/usr/bin/make -f
3069
3070export DH_VERBOSE=1
3071export PYBUILD_NAME=pyfai
3072
3073DEB_CFLAGS_MAINT_APPEND = -Wno-error=incompatible-pointer-types
3074export DEB_CFLAGS_MAINT_APPEND
3075
3076PY3VER := $(shell py3versions -dv)
3077
3078include /usr/share/dpkg/pkg-info.mk # sets SOURCE_DATE_EPOCH
3079
3080%:
3081\tdh $@ --buildsystem=pybuild
3082
3083override_dh_auto_build-arch:
3084\tPYBUILD_BUILD_ARGS=\"-Ccompile-args=--verbose\" dh_auto_build
3085
3086override_dh_auto_build-indep: override_dh_auto_build-arch
3087\tsphinx-build -N -bhtml doc/source build/html
3088
3089override_dh_auto_test:
3090
3091execute_after_dh_auto_install:
3092\tdh_install -p pyfai debian/python3-pyfai/usr/bin /usr
3093";
3094        let parsed = parse(input, None);
3095        let makefile = parsed.root();
3096
3097        // Include must be detected
3098        assert_eq!(makefile.includes().count(), 1);
3099
3100        // Pattern rule must be found
3101        assert!(
3102            makefile.find_rule_by_target_pattern("build-arch").is_some(),
3103            "build-arch should match via %: pattern rule"
3104        );
3105        assert!(
3106            makefile
3107                .find_rule_by_target_pattern("build-indep")
3108                .is_some(),
3109            "build-indep should match via %: pattern rule"
3110        );
3111
3112        // All override/execute_after rules must be found
3113        let rule_targets: Vec<Vec<String>> =
3114            makefile.rules().map(|r| r.targets().collect()).collect();
3115        assert!(
3116            rule_targets.iter().any(|t| t.contains(&"%".to_string())),
3117            "missing %: rule; got: {:?}",
3118            rule_targets
3119        );
3120        assert!(
3121            rule_targets
3122                .iter()
3123                .any(|t| t.contains(&"override_dh_auto_build-arch".to_string())),
3124            "missing override_dh_auto_build-arch; got: {:?}",
3125            rule_targets
3126        );
3127        assert!(
3128            rule_targets
3129                .iter()
3130                .any(|t| t.contains(&"override_dh_auto_test".to_string())),
3131            "missing override_dh_auto_test; got: {:?}",
3132            rule_targets
3133        );
3134        assert!(
3135            rule_targets
3136                .iter()
3137                .any(|t| t.contains(&"execute_after_dh_auto_install".to_string())),
3138            "missing execute_after_dh_auto_install; got: {:?}",
3139            rule_targets
3140        );
3141    }
3142
3143    #[test]
3144    fn test_variable_scopes() {
3145        let parsed = parse(
3146            "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
3147            None,
3148        );
3149        assert!(parsed.errors.is_empty());
3150        let makefile = parsed.root();
3151        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3152        assert_eq!(vars.len(), 4);
3153        let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
3154        assert!(var_names.contains(&"SIMPLE".to_string()));
3155        assert!(var_names.contains(&"IMMEDIATE".to_string()));
3156        assert!(var_names.contains(&"CONDITIONAL".to_string()));
3157        assert!(var_names.contains(&"APPEND".to_string()));
3158    }
3159
3160    #[test]
3161    fn test_pattern_rule_parsing() {
3162        let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
3163        assert!(parsed.errors.is_empty());
3164        let makefile = parsed.root();
3165        let rules = makefile.rules().collect::<Vec<_>>();
3166        assert_eq!(rules.len(), 1);
3167        assert_eq!(rules[0].targets().next().unwrap(), "%.o");
3168        assert!(rules[0].recipes().next().unwrap().contains("$@"));
3169    }
3170
3171    #[test]
3172    fn test_include_variants() {
3173        // Test all variants of include directives
3174        let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
3175        let parsed = parse(makefile_str, None);
3176        assert!(parsed.errors.is_empty());
3177
3178        // Get the syntax tree for inspection
3179        let node = parsed.syntax();
3180        let debug_str = format!("{:#?}", node);
3181
3182        // Check that all includes are correctly parsed as INCLUDE nodes
3183        assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
3184
3185        // Check that we can access the includes through the AST
3186        let makefile = parsed.root();
3187
3188        // Count all child nodes that are INCLUDE kind
3189        let include_count = makefile
3190            .syntax()
3191            .children()
3192            .filter(|child| child.kind() == INCLUDE)
3193            .count();
3194        assert_eq!(include_count, 4);
3195
3196        // Test variable expansion in include paths
3197        assert!(makefile
3198            .included_files()
3199            .any(|path| path.contains("$(VAR)")));
3200    }
3201
3202    #[test]
3203    fn test_include_api() {
3204        // Test the API for working with include directives
3205        let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
3206        let makefile: Makefile = makefile_str.parse().unwrap();
3207
3208        // Test the includes method
3209        let includes: Vec<_> = makefile.includes().collect();
3210        assert_eq!(includes.len(), 3);
3211
3212        // Test the is_optional method
3213        assert!(!includes[0].is_optional()); // include
3214        assert!(includes[1].is_optional()); // -include
3215        assert!(includes[2].is_optional()); // sinclude
3216
3217        // Test the included_files method
3218        let files: Vec<_> = makefile.included_files().collect();
3219        assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
3220
3221        // Test the path method on Include
3222        assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
3223        assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
3224        assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
3225    }
3226
3227    #[test]
3228    fn test_include_integration() {
3229        // Test include directives in realistic makefile contexts
3230
3231        // Case 1: With .PHONY (which was a source of the original issue)
3232        let phony_makefile = Makefile::from_reader(
3233            ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3234            .as_bytes()
3235        ).unwrap();
3236
3237        // We expect 2 rules: .PHONY and rule
3238        assert_eq!(phony_makefile.rules().count(), 2);
3239
3240        // But only one non-special rule (not starting with '.')
3241        let normal_rules_count = phony_makefile
3242            .rules()
3243            .filter(|r| !r.targets().any(|t| t.starts_with('.')))
3244            .count();
3245        assert_eq!(normal_rules_count, 1);
3246
3247        // Verify we have the include directive
3248        assert_eq!(phony_makefile.includes().count(), 1);
3249        assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
3250
3251        // Case 2: Without .PHONY, just a regular rule and include
3252        let simple_makefile = Makefile::from_reader(
3253            "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3254                .as_bytes(),
3255        )
3256        .unwrap();
3257        assert_eq!(simple_makefile.rules().count(), 1);
3258        assert_eq!(simple_makefile.includes().count(), 1);
3259    }
3260
3261    #[test]
3262    fn test_real_conditional_directives() {
3263        // Basic if/else conditional
3264        let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
3265        let mut buf = conditional.as_bytes();
3266        let makefile =
3267            Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
3268        let code = makefile.code();
3269        assert!(code.contains("ifdef DEBUG"));
3270        assert!(code.contains("else"));
3271        assert!(code.contains("endif"));
3272
3273        // ifdef with nested ifdef
3274        let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
3275        let mut buf = nested.as_bytes();
3276        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
3277        let code = makefile.code();
3278        assert!(code.contains("ifdef DEBUG"));
3279        assert!(code.contains("ifdef VERBOSE"));
3280
3281        // ifeq form
3282        let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
3283        let mut buf = ifeq.as_bytes();
3284        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
3285        let code = makefile.code();
3286        assert!(code.contains("ifeq"));
3287        assert!(code.contains("Windows_NT"));
3288    }
3289
3290    #[test]
3291    fn test_indented_text_outside_rules() {
3292        // Simple help target with echo commands
3293        let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \"  help     show help\"\n";
3294        let parsed = parse(help_text, None);
3295        assert!(parsed.errors.is_empty());
3296
3297        // Verify recipes are correctly parsed
3298        let root = parsed.root();
3299        let rules = root.rules().collect::<Vec<_>>();
3300        assert_eq!(rules.len(), 1);
3301
3302        let help_rule = &rules[0];
3303        let recipes = help_rule.recipes().collect::<Vec<_>>();
3304        assert_eq!(recipes.len(), 2);
3305        assert!(recipes[0].contains("Available targets"));
3306        assert!(recipes[1].contains("help"));
3307    }
3308
3309    #[test]
3310    fn test_comment_handling_in_recipes() {
3311        // Create a recipe with a comment line
3312        let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
3313
3314        // Parse the recipe
3315        let parsed = parse(recipe_comment, None);
3316
3317        // Verify no parsing errors
3318        assert!(
3319            parsed.errors.is_empty(),
3320            "Should parse recipe with comments without errors"
3321        );
3322
3323        // Check rule structure
3324        let root = parsed.root();
3325        let rules = root.rules().collect::<Vec<_>>();
3326        assert_eq!(rules.len(), 1, "Should find exactly one rule");
3327
3328        // Check the rule has the correct name
3329        let build_rule = &rules[0];
3330        assert_eq!(
3331            build_rule.targets().collect::<Vec<_>>(),
3332            vec!["build"],
3333            "Rule should have 'build' as target"
3334        );
3335
3336        // Check recipes are parsed correctly
3337        // recipes() now returns all recipe nodes including comment-only lines
3338        let recipes = build_rule.recipe_nodes().collect::<Vec<_>>();
3339        assert_eq!(recipes.len(), 2, "Should find two recipe nodes");
3340
3341        // First recipe should be comment-only
3342        assert_eq!(recipes[0].text(), "");
3343        assert_eq!(
3344            recipes[0].comment(),
3345            Some("# This is a comment".to_string())
3346        );
3347
3348        // Second recipe should be the command
3349        assert_eq!(recipes[1].text(), "gcc -o app main.c");
3350        assert_eq!(recipes[1].comment(), None);
3351    }
3352
3353    #[test]
3354    fn test_multiline_variables() {
3355        // Simple multiline variable test
3356        let multiline = "SOURCES = main.c \\\n          util.c\n";
3357
3358        // Parse the multiline variable
3359        let parsed = parse(multiline, None);
3360
3361        // We can extract the variable even with errors (since backslash handling is not perfect)
3362        let root = parsed.root();
3363        let vars = root.variable_definitions().collect::<Vec<_>>();
3364        assert!(!vars.is_empty(), "Should find at least one variable");
3365
3366        // Test other multiline variable forms
3367
3368        // := assignment operator
3369        let operators = "CFLAGS := -Wall \\\n         -Werror\n";
3370        let parsed_operators = parse(operators, None);
3371
3372        // Extract variable with := operator
3373        let root = parsed_operators.root();
3374        let vars = root.variable_definitions().collect::<Vec<_>>();
3375        assert!(
3376            !vars.is_empty(),
3377            "Should find at least one variable with := operator"
3378        );
3379
3380        // += assignment operator
3381        let append = "LDFLAGS += -L/usr/lib \\\n          -lm\n";
3382        let parsed_append = parse(append, None);
3383
3384        // Extract variable with += operator
3385        let root = parsed_append.root();
3386        let vars = root.variable_definitions().collect::<Vec<_>>();
3387        assert!(
3388            !vars.is_empty(),
3389            "Should find at least one variable with += operator"
3390        );
3391    }
3392
3393    #[test]
3394    fn test_whitespace_and_eof_handling() {
3395        // Test 1: File ending with blank lines
3396        let blank_lines = "VAR = value\n\n\n";
3397
3398        let parsed_blank = parse(blank_lines, None);
3399
3400        // We should be able to extract the variable definition
3401        let root = parsed_blank.root();
3402        let vars = root.variable_definitions().collect::<Vec<_>>();
3403        assert_eq!(
3404            vars.len(),
3405            1,
3406            "Should find one variable in blank lines test"
3407        );
3408
3409        // Test 2: File ending with space
3410        let trailing_space = "VAR = value \n";
3411
3412        let parsed_space = parse(trailing_space, None);
3413
3414        // We should be able to extract the variable definition
3415        let root = parsed_space.root();
3416        let vars = root.variable_definitions().collect::<Vec<_>>();
3417        assert_eq!(
3418            vars.len(),
3419            1,
3420            "Should find one variable in trailing space test"
3421        );
3422
3423        // Test 3: No final newline
3424        let no_newline = "VAR = value";
3425
3426        let parsed_no_newline = parse(no_newline, None);
3427
3428        // Regardless of parsing errors, we should be able to extract the variable
3429        let root = parsed_no_newline.root();
3430        let vars = root.variable_definitions().collect::<Vec<_>>();
3431        assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
3432        assert_eq!(
3433            vars[0].name(),
3434            Some("VAR".to_string()),
3435            "Variable name should be VAR"
3436        );
3437    }
3438
3439    #[test]
3440    fn test_complex_variable_references() {
3441        // Simple function call
3442        let wildcard = "SOURCES = $(wildcard *.c)\n";
3443        let parsed = parse(wildcard, None);
3444        assert!(parsed.errors.is_empty());
3445
3446        // Nested variable reference
3447        let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3448        let parsed = parse(nested, None);
3449        assert!(parsed.errors.is_empty());
3450
3451        // Function with complex arguments
3452        let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3453        let parsed = parse(patsubst, None);
3454        assert!(parsed.errors.is_empty());
3455    }
3456
3457    #[test]
3458    fn test_complex_variable_references_minimal() {
3459        // Simple function call
3460        let wildcard = "SOURCES = $(wildcard *.c)\n";
3461        let parsed = parse(wildcard, None);
3462        assert!(parsed.errors.is_empty());
3463
3464        // Nested variable reference
3465        let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3466        let parsed = parse(nested, None);
3467        assert!(parsed.errors.is_empty());
3468
3469        // Function with complex arguments
3470        let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3471        let parsed = parse(patsubst, None);
3472        assert!(parsed.errors.is_empty());
3473    }
3474
3475    #[test]
3476    fn test_multiline_variable_with_backslash() {
3477        let content = r#"
3478LONG_VAR = This is a long variable \
3479    that continues on the next line \
3480    and even one more line
3481"#;
3482
3483        // For now, we'll use relaxed parsing since the backslash handling isn't fully implemented
3484        let mut buf = content.as_bytes();
3485        let makefile =
3486            Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
3487
3488        // Check that we can extract the variable even with errors
3489        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3490        assert_eq!(
3491            vars.len(),
3492            1,
3493            "Expected 1 variable but found {}",
3494            vars.len()
3495        );
3496        let var_value = vars[0].raw_value();
3497        assert!(var_value.is_some(), "Variable value is None");
3498
3499        // The value might not be perfect due to relaxed parsing, but it should contain most of the content
3500        let value_str = var_value.unwrap();
3501        assert!(
3502            value_str.contains("long variable"),
3503            "Value doesn't contain expected content"
3504        );
3505    }
3506
3507    #[test]
3508    fn test_multiline_variable_with_mixed_operators() {
3509        let content = r#"
3510PREFIX ?= /usr/local
3511CFLAGS := -Wall -O2 \
3512    -I$(PREFIX)/include \
3513    -DDEBUG
3514"#;
3515        // Use relaxed parsing for now
3516        let mut buf = content.as_bytes();
3517        let makefile = Makefile::read_relaxed(&mut buf)
3518            .expect("Failed to parse multiline variable with operators");
3519
3520        // Check that we can extract variables even with errors
3521        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3522        assert!(
3523            !vars.is_empty(),
3524            "Expected at least 1 variable, found {}",
3525            vars.len()
3526        );
3527
3528        // Check PREFIX variable
3529        let prefix_var = vars
3530            .iter()
3531            .find(|v| v.name().unwrap_or_default() == "PREFIX");
3532        assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
3533        assert!(
3534            prefix_var.unwrap().raw_value().is_some(),
3535            "PREFIX variable has no value"
3536        );
3537
3538        // CFLAGS may be parsed incompletely but should exist in some form
3539        let cflags_var = vars
3540            .iter()
3541            .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
3542        assert!(
3543            cflags_var.is_some(),
3544            "Expected to find CFLAGS variable (or part of it)"
3545        );
3546    }
3547
3548    #[test]
3549    fn test_indented_help_text() {
3550        let content = r#"
3551.PHONY: help
3552help:
3553	@echo "Available targets:"
3554	@echo "  build  - Build the project"
3555	@echo "  test   - Run tests"
3556	@echo "  clean  - Remove build artifacts"
3557"#;
3558        // Use relaxed parsing for now
3559        let mut buf = content.as_bytes();
3560        let makefile =
3561            Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
3562
3563        // Check that we can extract rules even with errors
3564        let rules = makefile.rules().collect::<Vec<_>>();
3565        assert!(!rules.is_empty(), "Expected at least one rule");
3566
3567        // Find help rule
3568        let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
3569        assert!(help_rule.is_some(), "Expected to find help rule");
3570
3571        // Check recipes - they might not be perfectly parsed but should exist
3572        let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
3573        assert!(
3574            !recipes.is_empty(),
3575            "Expected at least one recipe line in help rule"
3576        );
3577        assert!(
3578            recipes.iter().any(|r| r.contains("Available targets")),
3579            "Expected to find 'Available targets' in recipes"
3580        );
3581    }
3582
3583    #[test]
3584    fn test_indented_lines_in_conditionals() {
3585        let content = r#"
3586ifdef DEBUG
3587    CFLAGS += -g -DDEBUG
3588    # This is a comment inside conditional
3589    ifdef VERBOSE
3590        CFLAGS += -v
3591    endif
3592endif
3593"#;
3594        // Use relaxed parsing for conditionals with indented lines
3595        let mut buf = content.as_bytes();
3596        let makefile = Makefile::read_relaxed(&mut buf)
3597            .expect("Failed to parse indented lines in conditionals");
3598
3599        // Check that we detected conditionals
3600        let code = makefile.code();
3601        assert!(code.contains("ifdef DEBUG"));
3602        assert!(code.contains("ifdef VERBOSE"));
3603        assert!(code.contains("endif"));
3604    }
3605
3606    #[test]
3607    fn test_recipe_with_colon() {
3608        let content = r#"
3609build:
3610	@echo "Building at: $(shell date)"
3611	gcc -o program main.c
3612"#;
3613        let parsed = parse(content, None);
3614        assert!(
3615            parsed.errors.is_empty(),
3616            "Failed to parse recipe with colon: {:?}",
3617            parsed.errors
3618        );
3619    }
3620
3621    #[test]
3622    fn test_double_colon_rules() {
3623        let content = r#"
3624%.o :: %.c
3625	$(CC) -c $< -o $@
3626
3627# Double colon allows multiple rules for same target
3628all:: prerequisite1
3629	@echo "First rule for all"
3630
3631all:: prerequisite2
3632	@echo "Second rule for all"
3633"#;
3634        let parsed = parse(content, None);
3635        assert!(
3636            parsed.errors.is_empty(),
3637            "Failed to parse double colon rules: {:?}",
3638            parsed.errors
3639        );
3640
3641        let makefile = parsed.root();
3642        let rules: Vec<_> = makefile.rules().collect();
3643        assert_eq!(rules.len(), 3);
3644
3645        // All rules should be double-colon
3646        for rule in &rules {
3647            assert!(rule.is_double_colon());
3648        }
3649
3650        // Check targets
3651        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["%.o"]);
3652        assert_eq!(rules[1].targets().collect::<Vec<_>>(), vec!["all"]);
3653        assert_eq!(rules[2].targets().collect::<Vec<_>>(), vec!["all"]);
3654
3655        // Check prerequisites
3656        assert_eq!(
3657            rules[1].prerequisites().collect::<Vec<_>>(),
3658            vec!["prerequisite1"]
3659        );
3660        assert_eq!(
3661            rules[2].prerequisites().collect::<Vec<_>>(),
3662            vec!["prerequisite2"]
3663        );
3664    }
3665
3666    #[test]
3667    fn test_else_conditional_directives() {
3668        // Test else ifeq
3669        let content = r#"
3670ifeq ($(OS),Windows_NT)
3671    TARGET = windows
3672else ifeq ($(OS),Darwin)
3673    TARGET = macos
3674else ifeq ($(OS),Linux)
3675    TARGET = linux
3676else
3677    TARGET = unknown
3678endif
3679"#;
3680        let mut buf = content.as_bytes();
3681        let makefile =
3682            Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3683        assert!(makefile.code().contains("else ifeq"));
3684        assert!(makefile.code().contains("TARGET"));
3685
3686        // Test else ifdef
3687        let content = r#"
3688ifdef WINDOWS
3689    TARGET = windows
3690else ifdef DARWIN
3691    TARGET = macos
3692else ifdef LINUX
3693    TARGET = linux
3694else
3695    TARGET = unknown
3696endif
3697"#;
3698        let mut buf = content.as_bytes();
3699        let makefile =
3700            Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3701        assert!(makefile.code().contains("else ifdef"));
3702
3703        // Test else ifndef
3704        let content = r#"
3705ifndef NOWINDOWS
3706    TARGET = windows
3707else ifndef NODARWIN
3708    TARGET = macos
3709else
3710    TARGET = linux
3711endif
3712"#;
3713        let mut buf = content.as_bytes();
3714        let makefile =
3715            Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3716        assert!(makefile.code().contains("else ifndef"));
3717
3718        // Test else ifneq
3719        let content = r#"
3720ifneq ($(OS),Windows_NT)
3721    TARGET = not_windows
3722else ifneq ($(OS),Darwin)
3723    TARGET = not_macos
3724else
3725    TARGET = darwin
3726endif
3727"#;
3728        let mut buf = content.as_bytes();
3729        let makefile =
3730            Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3731        assert!(makefile.code().contains("else ifneq"));
3732    }
3733
3734    #[test]
3735    fn test_complex_else_conditionals() {
3736        // Test complex nested else conditionals with mixed types
3737        let content = r#"VAR1 := foo
3738VAR2 := bar
3739
3740ifeq ($(VAR1),foo)
3741    RESULT := foo_matched
3742else ifdef VAR2
3743    RESULT := var2_defined
3744else ifndef VAR3
3745    RESULT := var3_not_defined
3746else
3747    RESULT := final_else
3748endif
3749
3750all:
3751	@echo $(RESULT)
3752"#;
3753        let mut buf = content.as_bytes();
3754        let makefile =
3755            Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3756
3757        // Verify the structure is preserved
3758        let code = makefile.code();
3759        assert!(code.contains("ifeq ($(VAR1),foo)"));
3760        assert!(code.contains("else ifdef VAR2"));
3761        assert!(code.contains("else ifndef VAR3"));
3762        assert!(code.contains("else"));
3763        assert!(code.contains("endif"));
3764        assert!(code.contains("RESULT"));
3765
3766        // Verify rules are still parsed correctly
3767        let rules: Vec<_> = makefile.rules().collect();
3768        assert_eq!(rules.len(), 1);
3769        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3770    }
3771
3772    #[test]
3773    fn test_conditional_token_structure() {
3774        // Test that conditionals have proper token structure
3775        let content = r#"ifdef VAR1
3776X := 1
3777else ifdef VAR2
3778X := 2
3779else
3780X := 3
3781endif
3782"#;
3783        let mut buf = content.as_bytes();
3784        let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3785
3786        // Check that we can traverse the syntax tree
3787        let syntax = makefile.syntax();
3788
3789        // Find CONDITIONAL nodes
3790        let mut found_conditional = false;
3791        let mut found_conditional_if = false;
3792        let mut found_conditional_else = false;
3793        let mut found_conditional_endif = false;
3794
3795        fn check_node(
3796            node: &SyntaxNode,
3797            found_cond: &mut bool,
3798            found_if: &mut bool,
3799            found_else: &mut bool,
3800            found_endif: &mut bool,
3801        ) {
3802            match node.kind() {
3803                SyntaxKind::CONDITIONAL => *found_cond = true,
3804                SyntaxKind::CONDITIONAL_IF => *found_if = true,
3805                SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3806                SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3807                _ => {}
3808            }
3809
3810            for child in node.children() {
3811                check_node(&child, found_cond, found_if, found_else, found_endif);
3812            }
3813        }
3814
3815        check_node(
3816            syntax,
3817            &mut found_conditional,
3818            &mut found_conditional_if,
3819            &mut found_conditional_else,
3820            &mut found_conditional_endif,
3821        );
3822
3823        assert!(found_conditional, "Should have CONDITIONAL node");
3824        assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3825        assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3826        assert!(
3827            found_conditional_endif,
3828            "Should have CONDITIONAL_ENDIF node"
3829        );
3830    }
3831
3832    #[test]
3833    fn test_ambiguous_assignment_vs_rule() {
3834        // Test case: Variable assignment with equals sign
3835        const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3836
3837        let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3838        let makefile =
3839            Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3840
3841        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3842        let rules = makefile.rules().collect::<Vec<_>>();
3843
3844        assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3845        assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3846
3847        assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3848
3849        // Test case: Simple rule with colon
3850        const SIMPLE_RULE: &str = "target: dependency\n";
3851
3852        let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3853        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3854
3855        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3856        let rules = makefile.rules().collect::<Vec<_>>();
3857
3858        assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3859        assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3860
3861        let rule = &rules[0];
3862        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3863    }
3864
3865    #[test]
3866    fn test_nested_conditionals() {
3867        let content = r#"
3868ifdef RELEASE
3869    CFLAGS += -O3
3870    ifndef DEBUG
3871        ifneq ($(ARCH),arm)
3872            CFLAGS += -march=native
3873        else
3874            CFLAGS += -mcpu=cortex-a72
3875        endif
3876    endif
3877endif
3878"#;
3879        // Use relaxed parsing for nested conditionals test
3880        let mut buf = content.as_bytes();
3881        let makefile =
3882            Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3883
3884        // Check that we detected conditionals
3885        let code = makefile.code();
3886        assert!(code.contains("ifdef RELEASE"));
3887        assert!(code.contains("ifndef DEBUG"));
3888        assert!(code.contains("ifneq"));
3889    }
3890
3891    #[test]
3892    fn test_space_indented_recipes() {
3893        // This test is expected to fail with current implementation
3894        // It should pass once the parser is more flexible with indentation
3895        let content = r#"
3896build:
3897    @echo "Building with spaces instead of tabs"
3898    gcc -o program main.c
3899"#;
3900        // Use relaxed parsing for now
3901        let mut buf = content.as_bytes();
3902        let makefile =
3903            Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3904
3905        // Check that we can extract rules even with errors
3906        let rules = makefile.rules().collect::<Vec<_>>();
3907        assert!(!rules.is_empty(), "Expected at least one rule");
3908
3909        // Find build rule
3910        let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3911        assert!(build_rule.is_some(), "Expected to find build rule");
3912    }
3913
3914    #[test]
3915    fn test_complex_variable_functions() {
3916        let content = r#"
3917FILES := $(shell find . -name "*.c")
3918OBJS := $(patsubst %.c,%.o,$(FILES))
3919NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3920HEADERS := ${wildcard *.h}
3921"#;
3922        let parsed = parse(content, None);
3923        assert!(
3924            parsed.errors.is_empty(),
3925            "Failed to parse complex variable functions: {:?}",
3926            parsed.errors
3927        );
3928    }
3929
3930    #[test]
3931    fn test_nested_variable_expansions() {
3932        let content = r#"
3933VERSION = 1.0
3934PACKAGE = myapp
3935TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3936INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3937"#;
3938        let parsed = parse(content, None);
3939        assert!(
3940            parsed.errors.is_empty(),
3941            "Failed to parse nested variable expansions: {:?}",
3942            parsed.errors
3943        );
3944    }
3945
3946    #[test]
3947    fn test_special_directives() {
3948        let content = r#"
3949# Special makefile directives
3950.PHONY: all clean
3951.SUFFIXES: .c .o
3952.DEFAULT: all
3953
3954# Variable definition and export directive
3955export PATH := /usr/bin:/bin
3956"#;
3957        // Use relaxed parsing to allow for special directives
3958        let mut buf = content.as_bytes();
3959        let makefile =
3960            Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3961
3962        // Check that we can extract rules even with errors
3963        let rules = makefile.rules().collect::<Vec<_>>();
3964
3965        // Find phony rule
3966        let phony_rule = rules
3967            .iter()
3968            .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3969        assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3970
3971        // Check that variables can be extracted
3972        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3973        assert!(!vars.is_empty(), "Expected to find at least one variable");
3974    }
3975
3976    // Comprehensive Test combining multiple issues
3977
3978    #[test]
3979    fn test_comprehensive_real_world_makefile() {
3980        // Simple makefile with basic elements
3981        let content = r#"
3982# Basic variable assignment
3983VERSION = 1.0.0
3984
3985# Phony target
3986.PHONY: all clean
3987
3988# Simple rule
3989all:
3990	echo "Building version $(VERSION)"
3991
3992# Another rule with dependencies
3993clean:
3994	rm -f *.o
3995"#;
3996
3997        // Parse the content
3998        let parsed = parse(content, None);
3999
4000        // Check that parsing succeeded
4001        assert!(parsed.errors.is_empty(), "Expected no parsing errors");
4002
4003        // Check that we found variables
4004        let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
4005        assert!(!variables.is_empty(), "Expected at least one variable");
4006        assert_eq!(
4007            variables[0].name(),
4008            Some("VERSION".to_string()),
4009            "Expected VERSION variable"
4010        );
4011
4012        // Check that we found rules
4013        let rules = parsed.root().rules().collect::<Vec<_>>();
4014        assert!(!rules.is_empty(), "Expected at least one rule");
4015
4016        // Check for specific rules
4017        let rule_targets: Vec<String> = rules
4018            .iter()
4019            .flat_map(|r| r.targets().collect::<Vec<_>>())
4020            .collect();
4021        assert!(
4022            rule_targets.contains(&".PHONY".to_string()),
4023            "Expected .PHONY rule"
4024        );
4025        assert!(
4026            rule_targets.contains(&"all".to_string()),
4027            "Expected 'all' rule"
4028        );
4029        assert!(
4030            rule_targets.contains(&"clean".to_string()),
4031            "Expected 'clean' rule"
4032        );
4033    }
4034
4035    #[test]
4036    fn test_indented_help_text_outside_rules() {
4037        // Create test content with indented help text
4038        let content = r#"
4039# Targets with help text
4040help:
4041    @echo "Available targets:"
4042    @echo "  build      build the project"
4043    @echo "  test       run tests"
4044    @echo "  clean      clean build artifacts"
4045
4046# Another target
4047clean:
4048	rm -rf build/
4049"#;
4050
4051        // Parse the content
4052        let parsed = parse(content, None);
4053
4054        // Verify parsing succeeded
4055        assert!(
4056            parsed.errors.is_empty(),
4057            "Failed to parse indented help text"
4058        );
4059
4060        // Check that we found the expected rules
4061        let rules = parsed.root().rules().collect::<Vec<_>>();
4062        assert_eq!(rules.len(), 2, "Expected to find two rules");
4063
4064        // Find the rules by target
4065        let help_rule = rules
4066            .iter()
4067            .find(|r| r.targets().any(|t| t == "help"))
4068            .expect("Expected to find help rule");
4069
4070        let clean_rule = rules
4071            .iter()
4072            .find(|r| r.targets().any(|t| t == "clean"))
4073            .expect("Expected to find clean rule");
4074
4075        // Check help rule has expected recipe lines
4076        let help_recipes = help_rule.recipes().collect::<Vec<_>>();
4077        assert!(
4078            !help_recipes.is_empty(),
4079            "Help rule should have recipe lines"
4080        );
4081        assert!(
4082            help_recipes
4083                .iter()
4084                .any(|line| line.contains("Available targets")),
4085            "Help recipes should include 'Available targets' line"
4086        );
4087
4088        // Check clean rule has expected recipe
4089        let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
4090        assert!(
4091            !clean_recipes.is_empty(),
4092            "Clean rule should have recipe lines"
4093        );
4094        assert!(
4095            clean_recipes.iter().any(|line| line.contains("rm -rf")),
4096            "Clean recipes should include 'rm -rf' command"
4097        );
4098    }
4099
4100    #[test]
4101    fn test_makefile1_phony_pattern() {
4102        // Replicate the specific pattern in Makefile_1 that caused issues
4103        let content = "#line 2145\n.PHONY: $(PHONY)\n";
4104
4105        // Parse the content
4106        let result = parse(content, None);
4107
4108        // Verify no parsing errors
4109        assert!(
4110            result.errors.is_empty(),
4111            "Failed to parse .PHONY: $(PHONY) pattern"
4112        );
4113
4114        // Check that the rule was parsed correctly
4115        let rules = result.root().rules().collect::<Vec<_>>();
4116        assert_eq!(rules.len(), 1, "Expected 1 rule");
4117        assert_eq!(
4118            rules[0].targets().next().unwrap(),
4119            ".PHONY",
4120            "Expected .PHONY rule"
4121        );
4122
4123        // Check that the prerequisite contains the variable reference
4124        let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
4125        assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
4126        assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
4127    }
4128
4129    #[test]
4130    fn test_skip_until_newline_behavior() {
4131        // Test the skip_until_newline function to cover the != vs == mutant
4132        let input = "text without newline";
4133        let parsed = parse(input, None);
4134        // This should handle gracefully without infinite loops
4135        assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4136
4137        let input_with_newline = "text\nafter newline";
4138        let parsed2 = parse(input_with_newline, None);
4139        assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
4140    }
4141
4142    #[test]
4143    #[ignore] // Ignored until proper handling of orphaned indented lines is implemented
4144    fn test_error_with_indent_token() {
4145        // Test the error logic with INDENT token to cover the ! deletion mutant
4146        let input = "\tinvalid indented line";
4147        let parsed = parse(input, None);
4148        // Should produce an error about indented line not part of a rule
4149        assert!(!parsed.errors.is_empty());
4150
4151        let error_msg = &parsed.errors[0].message;
4152        assert!(error_msg.contains("recipe commences before first target"));
4153    }
4154
4155    #[test]
4156    fn test_conditional_token_handling() {
4157        // Test conditional token handling to cover the == vs != mutant
4158        let input = r#"
4159ifndef VAR
4160    CFLAGS = -DTEST
4161endif
4162"#;
4163        let parsed = parse(input, None);
4164        // Test that parsing doesn't panic and produces some result
4165        let makefile = parsed.root();
4166        let _vars = makefile.variable_definitions().collect::<Vec<_>>();
4167        // Should handle conditionals, possibly with errors but without crashing
4168
4169        // Test with nested conditionals
4170        let nested = r#"
4171ifdef DEBUG
4172    ifndef RELEASE
4173        CFLAGS = -g
4174    endif
4175endif
4176"#;
4177        let parsed_nested = parse(nested, None);
4178        // Test that parsing doesn't panic
4179        let _makefile = parsed_nested.root();
4180    }
4181
4182    #[test]
4183    fn test_include_vs_conditional_logic() {
4184        // Test the include vs conditional logic to cover the == vs != mutant at line 743
4185        let input = r#"
4186include file.mk
4187ifdef VAR
4188    VALUE = 1
4189endif
4190"#;
4191        let parsed = parse(input, None);
4192        // Test that parsing doesn't panic and produces some result
4193        let makefile = parsed.root();
4194        let includes = makefile.includes().collect::<Vec<_>>();
4195        // Should recognize include directive
4196        assert!(!includes.is_empty() || !parsed.errors.is_empty());
4197
4198        // Test with -include
4199        let optional_include = r#"
4200-include optional.mk
4201ifndef VAR
4202    VALUE = default
4203endif
4204"#;
4205        let parsed2 = parse(optional_include, None);
4206        // Test that parsing doesn't panic
4207        let _makefile = parsed2.root();
4208    }
4209
4210    #[test]
4211    fn test_balanced_parens_counting() {
4212        // Test balanced parentheses parsing to cover the += vs -= mutant
4213        let input = r#"
4214VAR = $(call func,$(nested,arg),extra)
4215COMPLEX = $(if $(condition),$(then_val),$(else_val))
4216"#;
4217        let parsed = parse(input, None);
4218        assert!(parsed.errors.is_empty());
4219
4220        let makefile = parsed.root();
4221        let vars = makefile.variable_definitions().collect::<Vec<_>>();
4222        assert_eq!(vars.len(), 2);
4223    }
4224
4225    #[test]
4226    fn test_documentation_lookahead() {
4227        // Test the documentation lookahead logic to cover the - vs + mutant at line 895
4228        let input = r#"
4229# Documentation comment
4230help:
4231	@echo "Usage instructions"
4232	@echo "More help text"
4233"#;
4234        let parsed = parse(input, None);
4235        assert!(parsed.errors.is_empty());
4236
4237        let makefile = parsed.root();
4238        let rules = makefile.rules().collect::<Vec<_>>();
4239        assert_eq!(rules.len(), 1);
4240        assert_eq!(rules[0].targets().next().unwrap(), "help");
4241    }
4242
4243    #[test]
4244    fn test_edge_case_empty_input() {
4245        // Test with empty input
4246        let parsed = parse("", None);
4247        assert!(parsed.errors.is_empty());
4248
4249        // Test with only whitespace
4250        let parsed2 = parse("   \n  \n", None);
4251        // Some parsers might report warnings/errors for whitespace-only input
4252        // Just ensure it doesn't crash
4253        let _makefile = parsed2.root();
4254    }
4255
4256    #[test]
4257    fn test_malformed_conditional_recovery() {
4258        // Test parser recovery from malformed conditionals
4259        let input = r#"
4260ifdef
4261    # Missing condition variable
4262endif
4263"#;
4264        let parsed = parse(input, None);
4265        // Parser should either handle gracefully or report appropriate errors
4266        // Not checking for specific error since parsing strategy may vary
4267        assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4268    }
4269
4270    #[test]
4271    fn test_replace_rule() {
4272        let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4273        let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4274
4275        makefile.replace_rule(0, new_rule).unwrap();
4276
4277        let targets: Vec<_> = makefile
4278            .rules()
4279            .flat_map(|r| r.targets().collect::<Vec<_>>())
4280            .collect();
4281        assert_eq!(targets, vec!["new_rule", "rule2"]);
4282
4283        let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
4284        assert_eq!(recipes, vec!["new_command"]);
4285    }
4286
4287    #[test]
4288    fn test_replace_rule_out_of_bounds() {
4289        let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4290        let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4291
4292        let result = makefile.replace_rule(5, new_rule);
4293        assert!(result.is_err());
4294    }
4295
4296    #[test]
4297    fn test_remove_rule() {
4298        let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
4299            .parse()
4300            .unwrap();
4301
4302        let removed = makefile.remove_rule(1).unwrap();
4303        assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
4304
4305        let remaining_targets: Vec<_> = makefile
4306            .rules()
4307            .flat_map(|r| r.targets().collect::<Vec<_>>())
4308            .collect();
4309        assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
4310        assert_eq!(makefile.rules().count(), 2);
4311    }
4312
4313    #[test]
4314    fn test_remove_rule_out_of_bounds() {
4315        let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4316
4317        let result = makefile.remove_rule(5);
4318        assert!(result.is_err());
4319    }
4320
4321    #[test]
4322    fn test_insert_rule() {
4323        let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4324        let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
4325
4326        makefile.insert_rule(1, new_rule).unwrap();
4327
4328        let targets: Vec<_> = makefile
4329            .rules()
4330            .flat_map(|r| r.targets().collect::<Vec<_>>())
4331            .collect();
4332        assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
4333        assert_eq!(makefile.rules().count(), 3);
4334    }
4335
4336    #[test]
4337    fn test_insert_rule_at_end() {
4338        let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4339        let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
4340
4341        makefile.insert_rule(1, new_rule).unwrap();
4342
4343        let targets: Vec<_> = makefile
4344            .rules()
4345            .flat_map(|r| r.targets().collect::<Vec<_>>())
4346            .collect();
4347        assert_eq!(targets, vec!["rule1", "end_rule"]);
4348    }
4349
4350    #[test]
4351    fn test_insert_rule_out_of_bounds() {
4352        let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4353        let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4354
4355        let result = makefile.insert_rule(5, new_rule);
4356        assert!(result.is_err());
4357    }
4358
4359    #[test]
4360    fn test_insert_rule_preserves_blank_line_spacing_at_end() {
4361        // Test that inserting at the end preserves blank line spacing
4362        let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
4363        let mut makefile: Makefile = input.parse().unwrap();
4364        let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4365
4366        makefile.insert_rule(2, new_rule).unwrap();
4367
4368        let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4369        assert_eq!(makefile.to_string(), expected);
4370    }
4371
4372    #[test]
4373    fn test_insert_rule_adds_blank_lines_when_missing() {
4374        // Test that inserting adds blank lines even when input has none
4375        let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
4376        let mut makefile: Makefile = input.parse().unwrap();
4377        let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4378
4379        makefile.insert_rule(2, new_rule).unwrap();
4380
4381        let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4382        assert_eq!(makefile.to_string(), expected);
4383    }
4384
4385    #[test]
4386    fn test_remove_command() {
4387        let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4388            .parse()
4389            .unwrap();
4390
4391        rule.remove_command(1);
4392        let recipes: Vec<_> = rule.recipes().collect();
4393        assert_eq!(recipes, vec!["command1", "command3"]);
4394        assert_eq!(rule.recipe_count(), 2);
4395    }
4396
4397    #[test]
4398    fn test_remove_command_out_of_bounds() {
4399        let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4400
4401        let result = rule.remove_command(5);
4402        assert!(!result);
4403    }
4404
4405    #[test]
4406    fn test_insert_command() {
4407        let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
4408
4409        rule.insert_command(1, "command2");
4410        let recipes: Vec<_> = rule.recipes().collect();
4411        assert_eq!(recipes, vec!["command1", "command2", "command3"]);
4412    }
4413
4414    #[test]
4415    fn test_insert_command_at_end() {
4416        let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4417
4418        rule.insert_command(1, "command2");
4419        let recipes: Vec<_> = rule.recipes().collect();
4420        assert_eq!(recipes, vec!["command1", "command2"]);
4421    }
4422
4423    #[test]
4424    fn test_insert_command_in_empty_rule() {
4425        let mut rule: Rule = "rule:\n".parse().unwrap();
4426
4427        rule.insert_command(0, "new_command");
4428        let recipes: Vec<_> = rule.recipes().collect();
4429        assert_eq!(recipes, vec!["new_command"]);
4430    }
4431
4432    #[test]
4433    fn test_recipe_count() {
4434        let rule1: Rule = "rule:\n".parse().unwrap();
4435        assert_eq!(rule1.recipe_count(), 0);
4436
4437        let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
4438        assert_eq!(rule2.recipe_count(), 2);
4439    }
4440
4441    #[test]
4442    fn test_clear_commands() {
4443        let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4444            .parse()
4445            .unwrap();
4446
4447        rule.clear_commands();
4448        assert_eq!(rule.recipe_count(), 0);
4449
4450        let recipes: Vec<_> = rule.recipes().collect();
4451        assert_eq!(recipes, Vec::<String>::new());
4452
4453        // Rule target should still be preserved
4454        let targets: Vec<_> = rule.targets().collect();
4455        assert_eq!(targets, vec!["rule"]);
4456    }
4457
4458    #[test]
4459    fn test_clear_commands_empty_rule() {
4460        let mut rule: Rule = "rule:\n".parse().unwrap();
4461
4462        rule.clear_commands();
4463        assert_eq!(rule.recipe_count(), 0);
4464
4465        let targets: Vec<_> = rule.targets().collect();
4466        assert_eq!(targets, vec!["rule"]);
4467    }
4468
4469    #[test]
4470    fn test_rule_manipulation_preserves_structure() {
4471        // Test that makefile structure (comments, variables, etc.) is preserved during rule manipulation
4472        let input = r#"# Comment
4473VAR = value
4474
4475rule1:
4476	command1
4477
4478# Another comment
4479rule2:
4480	command2
4481
4482VAR2 = value2
4483"#;
4484
4485        let mut makefile: Makefile = input.parse().unwrap();
4486        let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4487
4488        // Insert rule in the middle
4489        makefile.insert_rule(1, new_rule).unwrap();
4490
4491        // Check that rules are correct
4492        let targets: Vec<_> = makefile
4493            .rules()
4494            .flat_map(|r| r.targets().collect::<Vec<_>>())
4495            .collect();
4496        assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
4497
4498        // Check that variables are preserved
4499        let vars: Vec<_> = makefile.variable_definitions().collect();
4500        assert_eq!(vars.len(), 2);
4501
4502        // The structure should be preserved in the output
4503        let output = makefile.code();
4504        assert!(output.contains("# Comment"));
4505        assert!(output.contains("VAR = value"));
4506        assert!(output.contains("# Another comment"));
4507        assert!(output.contains("VAR2 = value2"));
4508    }
4509
4510    #[test]
4511    fn test_replace_rule_with_multiple_targets() {
4512        let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
4513        let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
4514
4515        makefile.replace_rule(0, new_rule).unwrap();
4516
4517        let targets: Vec<_> = makefile
4518            .rules()
4519            .flat_map(|r| r.targets().collect::<Vec<_>>())
4520            .collect();
4521        assert_eq!(targets, vec!["new_target"]);
4522    }
4523
4524    #[test]
4525    fn test_empty_makefile_operations() {
4526        let mut makefile = Makefile::new();
4527
4528        // Test operations on empty makefile
4529        assert!(makefile
4530            .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
4531            .is_err());
4532        assert!(makefile.remove_rule(0).is_err());
4533
4534        // Insert into empty makefile should work
4535        let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
4536        makefile.insert_rule(0, new_rule).unwrap();
4537        assert_eq!(makefile.rules().count(), 1);
4538    }
4539
4540    #[test]
4541    fn test_command_operations_preserve_indentation() {
4542        let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
4543            .parse()
4544            .unwrap();
4545
4546        rule.insert_command(1, "middle_command");
4547        let recipes: Vec<_> = rule.recipes().collect();
4548        assert_eq!(
4549            recipes,
4550            vec!["\tdeep_indent", "middle_command", "shallow_indent"]
4551        );
4552    }
4553
4554    #[test]
4555    fn test_rule_operations_with_variables_and_includes() {
4556        let input = r#"VAR1 = value1
4557include common.mk
4558
4559rule1:
4560	command1
4561
4562VAR2 = value2
4563include other.mk
4564
4565rule2:
4566	command2
4567"#;
4568
4569        let mut makefile: Makefile = input.parse().unwrap();
4570
4571        // Remove middle rule
4572        makefile.remove_rule(0).unwrap();
4573
4574        // Verify structure is preserved
4575        let output = makefile.code();
4576        assert!(output.contains("VAR1 = value1"));
4577        assert!(output.contains("include common.mk"));
4578        assert!(output.contains("VAR2 = value2"));
4579        assert!(output.contains("include other.mk"));
4580
4581        // Only rule2 should remain
4582        assert_eq!(makefile.rules().count(), 1);
4583        let remaining_targets: Vec<_> = makefile
4584            .rules()
4585            .flat_map(|r| r.targets().collect::<Vec<_>>())
4586            .collect();
4587        assert_eq!(remaining_targets, vec!["rule2"]);
4588    }
4589
4590    #[test]
4591    fn test_command_manipulation_edge_cases() {
4592        // Test with rule that has no commands
4593        let mut empty_rule: Rule = "empty:\n".parse().unwrap();
4594        assert_eq!(empty_rule.recipe_count(), 0);
4595
4596        empty_rule.insert_command(0, "first_command");
4597        assert_eq!(empty_rule.recipe_count(), 1);
4598
4599        // Test clearing already empty rule
4600        let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
4601        empty_rule2.clear_commands();
4602        assert_eq!(empty_rule2.recipe_count(), 0);
4603    }
4604
4605    #[test]
4606    fn test_large_makefile_performance() {
4607        // Create a makefile with many rules to test performance doesn't degrade
4608        let mut makefile = Makefile::new();
4609
4610        // Add 100 rules
4611        for i in 0..100 {
4612            let rule_name = format!("rule{}", i);
4613            makefile
4614                .add_rule(&rule_name)
4615                .push_command(&format!("command{}", i));
4616        }
4617
4618        assert_eq!(makefile.rules().count(), 100);
4619
4620        // Replace rule in the middle - should be efficient
4621        let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
4622        makefile.replace_rule(50, new_rule).unwrap();
4623
4624        // Verify the change
4625        let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
4626        assert_eq!(rule_50_targets, vec!["middle_rule"]);
4627
4628        assert_eq!(makefile.rules().count(), 100); // Count unchanged
4629    }
4630
4631    #[test]
4632    fn test_complex_recipe_manipulation() {
4633        let mut complex_rule: Rule = r#"complex:
4634	@echo "Starting build"
4635	$(CC) $(CFLAGS) -o $@ $<
4636	@echo "Build complete"
4637	chmod +x $@
4638"#
4639        .parse()
4640        .unwrap();
4641
4642        assert_eq!(complex_rule.recipe_count(), 4);
4643
4644        // Remove the echo statements, keep the actual build commands
4645        complex_rule.remove_command(0); // Remove first echo
4646        complex_rule.remove_command(1); // Remove second echo (now at index 1, not 2)
4647
4648        let final_recipes: Vec<_> = complex_rule.recipes().collect();
4649        assert_eq!(final_recipes.len(), 2);
4650        assert!(final_recipes[0].contains("$(CC)"));
4651        assert!(final_recipes[1].contains("chmod"));
4652    }
4653
4654    #[test]
4655    fn test_variable_definition_remove() {
4656        let makefile: Makefile = r#"VAR1 = value1
4657VAR2 = value2
4658VAR3 = value3
4659"#
4660        .parse()
4661        .unwrap();
4662
4663        // Verify we have 3 variables
4664        assert_eq!(makefile.variable_definitions().count(), 3);
4665
4666        // Remove the second variable
4667        let mut var2 = makefile
4668            .variable_definitions()
4669            .nth(1)
4670            .expect("Should have second variable");
4671        assert_eq!(var2.name(), Some("VAR2".to_string()));
4672        var2.remove();
4673
4674        // Verify we now have 2 variables and VAR2 is gone
4675        assert_eq!(makefile.variable_definitions().count(), 2);
4676        let var_names: Vec<_> = makefile
4677            .variable_definitions()
4678            .filter_map(|v| v.name())
4679            .collect();
4680        assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4681    }
4682
4683    #[test]
4684    fn test_variable_definition_set_value() {
4685        let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4686
4687        let mut var = makefile
4688            .variable_definitions()
4689            .next()
4690            .expect("Should have variable");
4691        assert_eq!(var.raw_value(), Some("old_value".to_string()));
4692
4693        // Change the value
4694        var.set_value("new_value");
4695
4696        // Verify the value changed
4697        assert_eq!(var.raw_value(), Some("new_value".to_string()));
4698        assert!(makefile.code().contains("VAR = new_value"));
4699    }
4700
4701    #[test]
4702    fn test_variable_definition_set_value_preserves_format() {
4703        let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4704
4705        let mut var = makefile
4706            .variable_definitions()
4707            .next()
4708            .expect("Should have variable");
4709        assert_eq!(var.raw_value(), Some("old_value".to_string()));
4710
4711        // Change the value
4712        var.set_value("new_value");
4713
4714        // Verify the value changed but format preserved
4715        assert_eq!(var.raw_value(), Some("new_value".to_string()));
4716        let code = makefile.code();
4717        assert!(code.contains("export"), "Should preserve export prefix");
4718        assert!(code.contains(":="), "Should preserve := operator");
4719        assert!(code.contains("new_value"), "Should have new value");
4720    }
4721
4722    #[test]
4723    fn test_makefile_find_variable() {
4724        let makefile: Makefile = r#"VAR1 = value1
4725VAR2 = value2
4726VAR3 = value3
4727"#
4728        .parse()
4729        .unwrap();
4730
4731        // Find existing variable
4732        let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4733        assert_eq!(vars.len(), 1);
4734        assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4735        assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4736
4737        // Try to find non-existent variable
4738        assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4739    }
4740
4741    #[test]
4742    fn test_makefile_find_variable_with_export() {
4743        let makefile: Makefile = r#"VAR1 = value1
4744export VAR2 := value2
4745VAR3 = value3
4746"#
4747        .parse()
4748        .unwrap();
4749
4750        // Find exported variable
4751        let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4752        assert_eq!(vars.len(), 1);
4753        assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4754        assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4755    }
4756
4757    #[test]
4758    fn test_variable_definition_is_export() {
4759        let makefile: Makefile = r#"VAR1 = value1
4760export VAR2 := value2
4761export VAR3 = value3
4762VAR4 := value4
4763"#
4764        .parse()
4765        .unwrap();
4766
4767        let vars: Vec<_> = makefile.variable_definitions().collect();
4768        assert_eq!(vars.len(), 4);
4769
4770        assert!(!vars[0].is_export());
4771        assert!(vars[1].is_export());
4772        assert!(vars[2].is_export());
4773        assert!(!vars[3].is_export());
4774    }
4775
4776    #[test]
4777    fn test_makefile_find_variable_multiple() {
4778        let makefile: Makefile = r#"VAR1 = value1
4779VAR1 = value2
4780VAR2 = other
4781VAR1 = value3
4782"#
4783        .parse()
4784        .unwrap();
4785
4786        // Find all VAR1 definitions
4787        let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4788        assert_eq!(vars.len(), 3);
4789        assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4790        assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4791        assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4792
4793        // Find VAR2
4794        let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4795        assert_eq!(var2s.len(), 1);
4796        assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4797    }
4798
4799    #[test]
4800    fn test_variable_remove_and_find() {
4801        let makefile: Makefile = r#"VAR1 = value1
4802VAR2 = value2
4803VAR3 = value3
4804"#
4805        .parse()
4806        .unwrap();
4807
4808        // Find and remove VAR2
4809        let mut var2 = makefile
4810            .find_variable("VAR2")
4811            .next()
4812            .expect("Should find VAR2");
4813        var2.remove();
4814
4815        // Verify VAR2 is gone
4816        assert_eq!(makefile.find_variable("VAR2").count(), 0);
4817
4818        // Verify other variables still exist
4819        assert_eq!(makefile.find_variable("VAR1").count(), 1);
4820        assert_eq!(makefile.find_variable("VAR3").count(), 1);
4821    }
4822
4823    #[test]
4824    fn test_variable_remove_with_comment() {
4825        let makefile: Makefile = r#"VAR1 = value1
4826# This is a comment about VAR2
4827VAR2 = value2
4828VAR3 = value3
4829"#
4830        .parse()
4831        .unwrap();
4832
4833        // Remove VAR2
4834        let mut var2 = makefile
4835            .variable_definitions()
4836            .nth(1)
4837            .expect("Should have second variable");
4838        assert_eq!(var2.name(), Some("VAR2".to_string()));
4839        var2.remove();
4840
4841        // Verify the comment is also removed
4842        assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4843    }
4844
4845    #[test]
4846    fn test_variable_remove_with_multiple_comments() {
4847        let makefile: Makefile = r#"VAR1 = value1
4848# Comment line 1
4849# Comment line 2
4850# Comment line 3
4851VAR2 = value2
4852VAR3 = value3
4853"#
4854        .parse()
4855        .unwrap();
4856
4857        // Remove VAR2
4858        let mut var2 = makefile
4859            .variable_definitions()
4860            .nth(1)
4861            .expect("Should have second variable");
4862        var2.remove();
4863
4864        // Verify all comments are removed
4865        assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4866    }
4867
4868    #[test]
4869    fn test_variable_remove_with_empty_line() {
4870        let makefile: Makefile = r#"VAR1 = value1
4871
4872# Comment about VAR2
4873VAR2 = value2
4874VAR3 = value3
4875"#
4876        .parse()
4877        .unwrap();
4878
4879        // Remove VAR2
4880        let mut var2 = makefile
4881            .variable_definitions()
4882            .nth(1)
4883            .expect("Should have second variable");
4884        var2.remove();
4885
4886        // Verify comment and up to 1 empty line are removed
4887        // Should have VAR1, then newline, then VAR3 (empty line removed)
4888        assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4889    }
4890
4891    #[test]
4892    fn test_variable_remove_with_multiple_empty_lines() {
4893        let makefile: Makefile = r#"VAR1 = value1
4894
4895
4896# Comment about VAR2
4897VAR2 = value2
4898VAR3 = value3
4899"#
4900        .parse()
4901        .unwrap();
4902
4903        // Remove VAR2
4904        let mut var2 = makefile
4905            .variable_definitions()
4906            .nth(1)
4907            .expect("Should have second variable");
4908        var2.remove();
4909
4910        // Verify comment and only 1 empty line are removed (one empty line preserved)
4911        // Should preserve one empty line before where VAR2 was
4912        assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4913    }
4914
4915    #[test]
4916    fn test_rule_remove_with_comment() {
4917        let makefile: Makefile = r#"rule1:
4918	command1
4919
4920# Comment about rule2
4921rule2:
4922	command2
4923rule3:
4924	command3
4925"#
4926        .parse()
4927        .unwrap();
4928
4929        // Remove rule2
4930        let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4931        rule2.remove().unwrap();
4932
4933        // Verify the comment is removed
4934        // Note: The empty line after rule1 is part of rule1's text, not a sibling, so it's preserved
4935        assert_eq!(
4936            makefile.code(),
4937            "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4938        );
4939    }
4940
4941    #[test]
4942    fn test_variable_remove_preserves_shebang() {
4943        let makefile: Makefile = r#"#!/usr/bin/make -f
4944# This is a regular comment
4945VAR1 = value1
4946VAR2 = value2
4947"#
4948        .parse()
4949        .unwrap();
4950
4951        // Remove VAR1
4952        let mut var1 = makefile.variable_definitions().next().unwrap();
4953        var1.remove();
4954
4955        // Verify the shebang is preserved but regular comment is removed
4956        let code = makefile.code();
4957        assert!(code.starts_with("#!/usr/bin/make -f"));
4958        assert!(!code.contains("regular comment"));
4959        assert!(!code.contains("VAR1"));
4960        assert!(code.contains("VAR2"));
4961    }
4962
4963    #[test]
4964    fn test_variable_remove_preserves_subsequent_comments() {
4965        let makefile: Makefile = r#"VAR1 = value1
4966# Comment about VAR2
4967VAR2 = value2
4968
4969# Comment about VAR3
4970VAR3 = value3
4971"#
4972        .parse()
4973        .unwrap();
4974
4975        // Remove VAR2
4976        let mut var2 = makefile
4977            .variable_definitions()
4978            .nth(1)
4979            .expect("Should have second variable");
4980        var2.remove();
4981
4982        // Verify preceding comment is removed but subsequent comment/empty line are preserved
4983        let code = makefile.code();
4984        assert_eq!(
4985            code,
4986            "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4987        );
4988    }
4989
4990    #[test]
4991    fn test_variable_remove_after_shebang_preserves_empty_line() {
4992        let makefile: Makefile = r#"#!/usr/bin/make -f
4993export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4994
4995%:
4996	dh $@
4997"#
4998        .parse()
4999        .unwrap();
5000
5001        // Remove the variable
5002        let mut var = makefile.variable_definitions().next().unwrap();
5003        var.remove();
5004
5005        // Verify shebang is preserved and empty line after variable is preserved
5006        assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
5007    }
5008
5009    #[test]
5010    fn test_rule_add_prerequisite() {
5011        let mut rule: Rule = "target: dep1\n".parse().unwrap();
5012        rule.add_prerequisite("dep2").unwrap();
5013        assert_eq!(
5014            rule.prerequisites().collect::<Vec<_>>(),
5015            vec!["dep1", "dep2"]
5016        );
5017        // Verify proper spacing
5018        assert_eq!(rule.to_string(), "target: dep1 dep2\n");
5019    }
5020
5021    #[test]
5022    fn test_rule_add_prerequisite_to_rule_without_prereqs() {
5023        // Regression test for missing space after colon when adding first prerequisite
5024        let mut rule: Rule = "target:\n".parse().unwrap();
5025        rule.add_prerequisite("dep1").unwrap();
5026        assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
5027        // Should have space after colon
5028        assert_eq!(rule.to_string(), "target: dep1\n");
5029    }
5030
5031    #[test]
5032    fn test_rule_remove_prerequisite() {
5033        let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
5034        assert!(rule.remove_prerequisite("dep2").unwrap());
5035        assert_eq!(
5036            rule.prerequisites().collect::<Vec<_>>(),
5037            vec!["dep1", "dep3"]
5038        );
5039        assert!(!rule.remove_prerequisite("nonexistent").unwrap());
5040    }
5041
5042    #[test]
5043    fn test_rule_set_prerequisites() {
5044        let mut rule: Rule = "target: old_dep\n".parse().unwrap();
5045        rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
5046            .unwrap();
5047        assert_eq!(
5048            rule.prerequisites().collect::<Vec<_>>(),
5049            vec!["new_dep1", "new_dep2"]
5050        );
5051    }
5052
5053    #[test]
5054    fn test_rule_set_prerequisites_empty() {
5055        let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
5056        rule.set_prerequisites(vec![]).unwrap();
5057        assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
5058    }
5059
5060    #[test]
5061    fn test_rule_add_target() {
5062        let mut rule: Rule = "target1: dep1\n".parse().unwrap();
5063        rule.add_target("target2").unwrap();
5064        assert_eq!(
5065            rule.targets().collect::<Vec<_>>(),
5066            vec!["target1", "target2"]
5067        );
5068    }
5069
5070    #[test]
5071    fn test_rule_set_targets() {
5072        let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5073        rule.set_targets(vec!["new_target1", "new_target2"])
5074            .unwrap();
5075        assert_eq!(
5076            rule.targets().collect::<Vec<_>>(),
5077            vec!["new_target1", "new_target2"]
5078        );
5079    }
5080
5081    #[test]
5082    fn test_rule_set_targets_empty() {
5083        let mut rule: Rule = "target: dep1\n".parse().unwrap();
5084        let result = rule.set_targets(vec![]);
5085        assert!(result.is_err());
5086        // Verify target wasn't changed
5087        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
5088    }
5089
5090    #[test]
5091    fn test_rule_has_target() {
5092        let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
5093        assert!(rule.has_target("target1"));
5094        assert!(rule.has_target("target2"));
5095        assert!(!rule.has_target("target3"));
5096        assert!(!rule.has_target("nonexistent"));
5097    }
5098
5099    #[test]
5100    fn test_rule_rename_target() {
5101        let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5102        assert!(rule.rename_target("old_target", "new_target").unwrap());
5103        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
5104        // Try renaming non-existent target
5105        assert!(!rule.rename_target("nonexistent", "something").unwrap());
5106    }
5107
5108    #[test]
5109    fn test_rule_rename_target_multiple() {
5110        let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5111        assert!(rule.rename_target("target2", "renamed_target").unwrap());
5112        assert_eq!(
5113            rule.targets().collect::<Vec<_>>(),
5114            vec!["target1", "renamed_target", "target3"]
5115        );
5116    }
5117
5118    #[test]
5119    fn test_rule_remove_target() {
5120        let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5121        assert!(rule.remove_target("target2").unwrap());
5122        assert_eq!(
5123            rule.targets().collect::<Vec<_>>(),
5124            vec!["target1", "target3"]
5125        );
5126        // Try removing non-existent target
5127        assert!(!rule.remove_target("nonexistent").unwrap());
5128    }
5129
5130    #[test]
5131    fn test_rule_remove_target_last() {
5132        let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
5133        let result = rule.remove_target("single_target");
5134        assert!(result.is_err());
5135        // Verify target wasn't removed
5136        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
5137    }
5138
5139    #[test]
5140    fn test_rule_target_manipulation_preserves_prerequisites() {
5141        let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
5142
5143        // Remove a target
5144        rule.remove_target("target1").unwrap();
5145        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
5146        assert_eq!(
5147            rule.prerequisites().collect::<Vec<_>>(),
5148            vec!["dep1", "dep2"]
5149        );
5150        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5151
5152        // Add a target
5153        rule.add_target("target3").unwrap();
5154        assert_eq!(
5155            rule.targets().collect::<Vec<_>>(),
5156            vec!["target2", "target3"]
5157        );
5158        assert_eq!(
5159            rule.prerequisites().collect::<Vec<_>>(),
5160            vec!["dep1", "dep2"]
5161        );
5162        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5163
5164        // Rename a target
5165        rule.rename_target("target2", "renamed").unwrap();
5166        assert_eq!(
5167            rule.targets().collect::<Vec<_>>(),
5168            vec!["renamed", "target3"]
5169        );
5170        assert_eq!(
5171            rule.prerequisites().collect::<Vec<_>>(),
5172            vec!["dep1", "dep2"]
5173        );
5174        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5175    }
5176
5177    #[test]
5178    fn test_rule_remove() {
5179        let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5180        let rule = makefile.find_rule_by_target("rule1").unwrap();
5181        rule.remove().unwrap();
5182        assert_eq!(makefile.rules().count(), 1);
5183        assert!(makefile.find_rule_by_target("rule1").is_none());
5184        assert!(makefile.find_rule_by_target("rule2").is_some());
5185    }
5186
5187    #[test]
5188    fn test_rule_remove_last_trims_blank_lines() {
5189        // Regression test for bug where removing the last rule left trailing blank lines
5190        let makefile: Makefile =
5191            "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
5192                .parse()
5193                .unwrap();
5194
5195        // Remove the last rule (override_dh_missing)
5196        let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
5197        rule.remove().unwrap();
5198
5199        // Should not have trailing blank line
5200        assert_eq!(makefile.code(), "%:\n\tdh $@\n");
5201        assert_eq!(makefile.rules().count(), 1);
5202    }
5203
5204    #[test]
5205    fn test_makefile_find_rule_by_target() {
5206        let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5207        let rule = makefile.find_rule_by_target("rule2");
5208        assert!(rule.is_some());
5209        assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
5210        assert!(makefile.find_rule_by_target("nonexistent").is_none());
5211    }
5212
5213    #[test]
5214    fn test_makefile_find_rules_by_target() {
5215        let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
5216            .parse()
5217            .unwrap();
5218        assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
5219        assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
5220        assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
5221    }
5222
5223    #[test]
5224    fn test_makefile_find_rule_by_target_pattern_simple() {
5225        let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5226        let rule = makefile.find_rule_by_target_pattern("foo.o");
5227        assert!(rule.is_some());
5228        assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
5229    }
5230
5231    #[test]
5232    fn test_makefile_find_rule_by_target_pattern_no_match() {
5233        let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5234        let rule = makefile.find_rule_by_target_pattern("foo.c");
5235        assert!(rule.is_none());
5236    }
5237
5238    #[test]
5239    fn test_makefile_find_rule_by_target_pattern_exact() {
5240        let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5241        let rule = makefile.find_rule_by_target_pattern("foo.o");
5242        assert!(rule.is_some());
5243        assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
5244    }
5245
5246    #[test]
5247    fn test_makefile_find_rule_by_target_pattern_prefix() {
5248        let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5249        let rule = makefile.find_rule_by_target_pattern("libfoo.a");
5250        assert!(rule.is_some());
5251        assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
5252    }
5253
5254    #[test]
5255    fn test_makefile_find_rule_by_target_pattern_suffix() {
5256        let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5257        let rule = makefile.find_rule_by_target_pattern("foo_test.o");
5258        assert!(rule.is_some());
5259        assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
5260    }
5261
5262    #[test]
5263    fn test_makefile_find_rule_by_target_pattern_middle() {
5264        let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5265        let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
5266        assert!(rule.is_some());
5267        assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
5268    }
5269
5270    #[test]
5271    fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
5272        let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
5273        let rule = makefile.find_rule_by_target_pattern("anything");
5274        assert!(rule.is_some());
5275        assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
5276    }
5277
5278    #[test]
5279    fn test_makefile_find_rules_by_target_pattern_multiple() {
5280        let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
5281            .parse()
5282            .unwrap();
5283        let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5284        assert_eq!(rules.len(), 2);
5285    }
5286
5287    #[test]
5288    fn test_makefile_find_rules_by_target_pattern_mixed() {
5289        let makefile: Makefile =
5290            "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
5291                .parse()
5292                .unwrap();
5293        let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5294        assert_eq!(rules.len(), 2); // Matches both %.o and foo.o
5295        let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
5296        assert_eq!(rules.len(), 1); // Only exact match
5297    }
5298
5299    #[test]
5300    fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
5301        let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5302        let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5303        assert_eq!(rules.len(), 1);
5304        let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
5305        assert_eq!(rules.len(), 0);
5306    }
5307
5308    #[test]
5309    fn test_matches_pattern_exact() {
5310        assert!(matches_pattern("foo.o", "foo.o"));
5311        assert!(!matches_pattern("foo.o", "bar.o"));
5312    }
5313
5314    #[test]
5315    fn test_matches_pattern_suffix() {
5316        assert!(matches_pattern("%.o", "foo.o"));
5317        assert!(matches_pattern("%.o", "bar.o"));
5318        assert!(matches_pattern("%.o", "baz/qux.o"));
5319        assert!(!matches_pattern("%.o", "foo.c"));
5320    }
5321
5322    #[test]
5323    fn test_matches_pattern_prefix() {
5324        assert!(matches_pattern("lib%.a", "libfoo.a"));
5325        assert!(matches_pattern("lib%.a", "libbar.a"));
5326        assert!(!matches_pattern("lib%.a", "foo.a"));
5327        assert!(!matches_pattern("lib%.a", "lib.a"));
5328    }
5329
5330    #[test]
5331    fn test_matches_pattern_middle() {
5332        assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
5333        assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
5334        assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
5335        assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
5336    }
5337
5338    #[test]
5339    fn test_matches_pattern_wildcard_only() {
5340        assert!(matches_pattern("%", "anything"));
5341        assert!(matches_pattern("%", "foo.o"));
5342        // GNU make: stem must be non-empty, so "%" does NOT match ""
5343        assert!(!matches_pattern("%", ""));
5344    }
5345
5346    #[test]
5347    fn test_matches_pattern_empty_stem() {
5348        // GNU make: stem must be non-empty
5349        assert!(!matches_pattern("%.o", ".o")); // stem would be empty
5350        assert!(!matches_pattern("lib%", "lib")); // stem would be empty
5351        assert!(!matches_pattern("lib%.a", "lib.a")); // stem would be empty
5352    }
5353
5354    #[test]
5355    fn test_matches_pattern_multiple_wildcards_not_supported() {
5356        // GNU make does NOT support multiple % in pattern rules
5357        // These should not match (fall back to exact match)
5358        assert!(!matches_pattern("%foo%bar", "xfooybarz"));
5359        assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
5360    }
5361
5362    #[test]
5363    fn test_makefile_add_phony_target() {
5364        let mut makefile = Makefile::new();
5365        makefile.add_phony_target("clean").unwrap();
5366        assert!(makefile.is_phony("clean"));
5367        assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
5368    }
5369
5370    #[test]
5371    fn test_makefile_add_phony_target_existing() {
5372        let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
5373        makefile.add_phony_target("clean").unwrap();
5374        assert!(makefile.is_phony("test"));
5375        assert!(makefile.is_phony("clean"));
5376        let targets: Vec<_> = makefile.phony_targets().collect();
5377        assert!(targets.contains(&"test".to_string()));
5378        assert!(targets.contains(&"clean".to_string()));
5379    }
5380
5381    #[test]
5382    fn test_makefile_remove_phony_target() {
5383        let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5384        assert!(makefile.remove_phony_target("clean").unwrap());
5385        assert!(!makefile.is_phony("clean"));
5386        assert!(makefile.is_phony("test"));
5387        assert!(!makefile.remove_phony_target("nonexistent").unwrap());
5388    }
5389
5390    #[test]
5391    fn test_makefile_remove_phony_target_last() {
5392        let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
5393        assert!(makefile.remove_phony_target("clean").unwrap());
5394        assert!(!makefile.is_phony("clean"));
5395        // .PHONY rule should be removed entirely
5396        assert!(makefile.find_rule_by_target(".PHONY").is_none());
5397    }
5398
5399    #[test]
5400    fn test_makefile_is_phony() {
5401        let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5402        assert!(makefile.is_phony("clean"));
5403        assert!(makefile.is_phony("test"));
5404        assert!(!makefile.is_phony("build"));
5405    }
5406
5407    #[test]
5408    fn test_makefile_phony_targets() {
5409        let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5410        let phony_targets: Vec<_> = makefile.phony_targets().collect();
5411        assert_eq!(phony_targets, vec!["clean", "test", "build"]);
5412    }
5413
5414    #[test]
5415    fn test_makefile_phony_targets_empty() {
5416        let makefile = Makefile::new();
5417        assert_eq!(makefile.phony_targets().count(), 0);
5418    }
5419
5420    #[test]
5421    fn test_makefile_remove_first_phony_target_no_extra_space() {
5422        let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5423        assert!(makefile.remove_phony_target("clean").unwrap());
5424        let result = makefile.to_string();
5425        assert_eq!(result, ".PHONY: test build\n");
5426    }
5427
5428    #[test]
5429    fn test_recipe_with_leading_comments_and_blank_lines() {
5430        // Regression test for bug where recipes with leading comments and blank lines
5431        // were not parsed correctly. The parser would stop parsing recipes when it
5432        // encountered a newline, missing subsequent recipe lines.
5433        let makefile_text = r#"#!/usr/bin/make
5434
5435%:
5436	dh $@
5437
5438override_dh_build:
5439	# The next line is empty
5440
5441	dh_python3
5442"#;
5443        let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
5444
5445        let rules: Vec<_> = makefile.rules().collect();
5446        assert_eq!(rules.len(), 2, "Expected 2 rules");
5447
5448        // First rule: %
5449        let rule0 = &rules[0];
5450        assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
5451        assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
5452
5453        // Second rule: override_dh_build
5454        let rule1 = &rules[1];
5455        assert_eq!(
5456            rule1.targets().collect::<Vec<_>>(),
5457            vec!["override_dh_build"]
5458        );
5459
5460        // The key assertion: we should have at least the actual command recipe
5461        let recipes: Vec<_> = rule1.recipes().collect();
5462        assert!(
5463            !recipes.is_empty(),
5464            "Expected at least one recipe for override_dh_build, got none"
5465        );
5466        assert!(
5467            recipes.contains(&"dh_python3".to_string()),
5468            "Expected 'dh_python3' in recipes, got: {:?}",
5469            recipes
5470        );
5471    }
5472
5473    #[test]
5474    fn test_rule_parse_preserves_trailing_blank_lines() {
5475        // Regression test: ensure that trailing blank lines are preserved
5476        // when parsing a rule and using it with replace_rule()
5477        let input = r#"override_dh_systemd_enable:
5478	dh_systemd_enable -pracoon
5479
5480override_dh_install:
5481	dh_install
5482"#;
5483
5484        let mut mf: Makefile = input.parse().unwrap();
5485
5486        // Get first rule and convert to string
5487        let rule = mf.rules().next().unwrap();
5488        let rule_text = rule.to_string();
5489
5490        // Should include trailing blank line
5491        assert_eq!(
5492            rule_text,
5493            "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
5494        );
5495
5496        // Modify the text
5497        let modified =
5498            rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
5499
5500        // Parse back - should preserve trailing blank line
5501        let new_rule: Rule = modified.parse().unwrap();
5502        assert_eq!(
5503            new_rule.to_string(),
5504            "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
5505        );
5506
5507        // Replace in makefile
5508        mf.replace_rule(0, new_rule).unwrap();
5509
5510        // Verify blank line is still present in output
5511        let output = mf.to_string();
5512        assert!(
5513            output.contains(
5514                "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
5515            ),
5516            "Blank line between rules should be preserved. Got: {:?}",
5517            output
5518        );
5519    }
5520
5521    #[test]
5522    fn test_rule_parse_round_trip_with_trailing_newlines() {
5523        // Test that parsing and stringifying a rule preserves exact trailing newlines
5524        let test_cases = vec![
5525            "rule:\n\tcommand\n",     // One newline
5526            "rule:\n\tcommand\n\n",   // Two newlines (blank line)
5527            "rule:\n\tcommand\n\n\n", // Three newlines (two blank lines)
5528        ];
5529
5530        for rule_text in test_cases {
5531            let rule: Rule = rule_text.parse().unwrap();
5532            let result = rule.to_string();
5533            assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
5534        }
5535    }
5536
5537    #[test]
5538    fn test_rule_clone() {
5539        // Test that Rule can be cloned and produces an identical copy
5540        let rule_text = "rule:\n\tcommand\n\n";
5541        let rule: Rule = rule_text.parse().unwrap();
5542        let cloned = rule.clone();
5543
5544        // Both should produce the same string representation
5545        assert_eq!(rule.to_string(), cloned.to_string());
5546        assert_eq!(rule.to_string(), rule_text);
5547        assert_eq!(cloned.to_string(), rule_text);
5548
5549        // Verify targets and recipes are the same
5550        assert_eq!(
5551            rule.targets().collect::<Vec<_>>(),
5552            cloned.targets().collect::<Vec<_>>()
5553        );
5554        assert_eq!(
5555            rule.recipes().collect::<Vec<_>>(),
5556            cloned.recipes().collect::<Vec<_>>()
5557        );
5558    }
5559
5560    #[test]
5561    fn test_makefile_clone() {
5562        // Test that Makefile and other AST nodes can be cloned
5563        let input = "VAR = value\n\nrule:\n\tcommand\n";
5564        let makefile: Makefile = input.parse().unwrap();
5565        let cloned = makefile.clone();
5566
5567        // Both should produce the same string representation
5568        assert_eq!(makefile.to_string(), cloned.to_string());
5569        assert_eq!(makefile.to_string(), input);
5570
5571        // Verify rule count is the same
5572        assert_eq!(makefile.rules().count(), cloned.rules().count());
5573
5574        // Verify variable definitions are the same
5575        assert_eq!(
5576            makefile.variable_definitions().count(),
5577            cloned.variable_definitions().count()
5578        );
5579    }
5580
5581    #[test]
5582    fn test_conditional_with_recipe_line() {
5583        // Test that conditionals with recipe lines (tab-indented) work correctly
5584        let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
5585        let parsed = parse(input, None);
5586
5587        // Should parse without errors
5588        assert!(
5589            parsed.errors.is_empty(),
5590            "Expected no parse errors, but got: {:?}",
5591            parsed.errors
5592        );
5593
5594        // Should preserve the code
5595        let mf = parsed.root();
5596        assert_eq!(mf.code(), input);
5597    }
5598
5599    #[test]
5600    fn test_conditional_in_rule_recipe() {
5601        // Test conditional inside a rule's recipe section
5602        let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
5603        let parsed = parse(input, None);
5604
5605        // Should parse without errors
5606        assert!(
5607            parsed.errors.is_empty(),
5608            "Expected no parse errors, but got: {:?}",
5609            parsed.errors
5610        );
5611
5612        // Should preserve the code
5613        let mf = parsed.root();
5614        assert_eq!(mf.code(), input);
5615
5616        // Should have exactly one rule
5617        assert_eq!(mf.rules().count(), 1);
5618    }
5619
5620    #[test]
5621    fn test_rule_items() {
5622        use crate::RuleItem;
5623
5624        // Test rule with both recipes and conditionals
5625        let input = r#"test:
5626	echo "before"
5627ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
5628	./run-tests
5629endif
5630	echo "after"
5631"#;
5632        let rule: Rule = input.parse().unwrap();
5633
5634        let items: Vec<_> = rule.items().collect();
5635        assert_eq!(
5636            items.len(),
5637            3,
5638            "Expected 3 items: recipe, conditional, recipe"
5639        );
5640
5641        // Check first item is a recipe
5642        match &items[0] {
5643            RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
5644            RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5645        }
5646
5647        // Check second item is a conditional
5648        match &items[1] {
5649            RuleItem::Conditional(c) => {
5650                assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5651            }
5652            RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
5653        }
5654
5655        // Check third item is a recipe
5656        match &items[2] {
5657            RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5658            RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5659        }
5660
5661        // Test rule with only recipes (no conditionals)
5662        let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5663        let simple_items: Vec<_> = simple_rule.items().collect();
5664        assert_eq!(simple_items.len(), 2);
5665
5666        match &simple_items[0] {
5667            RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5668            _ => panic!("Expected recipe"),
5669        }
5670
5671        match &simple_items[1] {
5672            RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5673            _ => panic!("Expected recipe"),
5674        }
5675
5676        // Test rule with only conditional (no plain recipes)
5677        let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5678            .parse()
5679            .unwrap();
5680        let cond_items: Vec<_> = cond_only.items().collect();
5681        assert_eq!(cond_items.len(), 1);
5682
5683        match &cond_items[0] {
5684            RuleItem::Conditional(c) => {
5685                assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5686            }
5687            _ => panic!("Expected conditional"),
5688        }
5689    }
5690
5691    #[test]
5692    fn test_conditionals_iterator() {
5693        let makefile: Makefile = r#"ifdef DEBUG
5694VAR = debug
5695endif
5696
5697ifndef RELEASE
5698OTHER = dev
5699endif
5700"#
5701        .parse()
5702        .unwrap();
5703
5704        let conditionals: Vec<_> = makefile.conditionals().collect();
5705        assert_eq!(conditionals.len(), 2);
5706
5707        assert_eq!(
5708            conditionals[0].conditional_type(),
5709            Some("ifdef".to_string())
5710        );
5711        assert_eq!(
5712            conditionals[1].conditional_type(),
5713            Some("ifndef".to_string())
5714        );
5715    }
5716
5717    #[test]
5718    fn test_conditional_type_and_condition() {
5719        let makefile: Makefile = r#"ifdef DEBUG
5720VAR = debug
5721endif
5722"#
5723        .parse()
5724        .unwrap();
5725
5726        let conditional = makefile.conditionals().next().unwrap();
5727        assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5728        assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5729    }
5730
5731    #[test]
5732    fn test_conditional_has_else() {
5733        let makefile_with_else: Makefile = r#"ifdef DEBUG
5734VAR = debug
5735else
5736VAR = release
5737endif
5738"#
5739        .parse()
5740        .unwrap();
5741
5742        let conditional = makefile_with_else.conditionals().next().unwrap();
5743        assert!(conditional.has_else());
5744
5745        let makefile_without_else: Makefile = r#"ifdef DEBUG
5746VAR = debug
5747endif
5748"#
5749        .parse()
5750        .unwrap();
5751
5752        let conditional = makefile_without_else.conditionals().next().unwrap();
5753        assert!(!conditional.has_else());
5754    }
5755
5756    #[test]
5757    fn test_conditional_if_body() {
5758        let makefile: Makefile = r#"ifdef DEBUG
5759VAR = debug
5760endif
5761"#
5762        .parse()
5763        .unwrap();
5764
5765        let conditional = makefile.conditionals().next().unwrap();
5766        let if_body = conditional.if_body();
5767        assert!(if_body.is_some());
5768        assert!(if_body.unwrap().contains("VAR = debug"));
5769    }
5770
5771    #[test]
5772    fn test_conditional_else_body() {
5773        let makefile: Makefile = r#"ifdef DEBUG
5774VAR = debug
5775else
5776VAR = release
5777endif
5778"#
5779        .parse()
5780        .unwrap();
5781
5782        let conditional = makefile.conditionals().next().unwrap();
5783        let else_body = conditional.else_body();
5784        assert!(else_body.is_some());
5785        assert!(else_body.unwrap().contains("VAR = release"));
5786    }
5787
5788    #[test]
5789    fn test_add_conditional_ifdef() {
5790        let mut makefile = Makefile::new();
5791        let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5792        assert!(result.is_ok());
5793
5794        let code = makefile.to_string();
5795        assert!(code.contains("ifdef DEBUG"));
5796        assert!(code.contains("VAR = debug"));
5797        assert!(code.contains("endif"));
5798    }
5799
5800    #[test]
5801    fn test_add_conditional_with_else() {
5802        let mut makefile = Makefile::new();
5803        let result =
5804            makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5805        assert!(result.is_ok());
5806
5807        let code = makefile.to_string();
5808        assert!(code.contains("ifdef DEBUG"));
5809        assert!(code.contains("VAR = debug"));
5810        assert!(code.contains("else"));
5811        assert!(code.contains("VAR = release"));
5812        assert!(code.contains("endif"));
5813    }
5814
5815    #[test]
5816    fn test_add_conditional_invalid_type() {
5817        let mut makefile = Makefile::new();
5818        let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5819        assert!(result.is_err());
5820    }
5821
5822    #[test]
5823    fn test_add_conditional_formatting() {
5824        let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5825        let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5826        assert!(result.is_ok());
5827
5828        let code = makefile.to_string();
5829        // Should have a blank line before the conditional
5830        assert!(code.contains("\n\nifdef DEBUG"));
5831    }
5832
5833    #[test]
5834    fn test_conditional_remove() {
5835        let makefile: Makefile = r#"ifdef DEBUG
5836VAR = debug
5837endif
5838
5839VAR2 = value2
5840"#
5841        .parse()
5842        .unwrap();
5843
5844        let mut conditional = makefile.conditionals().next().unwrap();
5845        let result = conditional.remove();
5846        assert!(result.is_ok());
5847
5848        let code = makefile.to_string();
5849        assert!(!code.contains("ifdef DEBUG"));
5850        assert!(!code.contains("VAR = debug"));
5851        assert!(code.contains("VAR2 = value2"));
5852    }
5853
5854    #[test]
5855    fn test_add_conditional_ifndef() {
5856        let mut makefile = Makefile::new();
5857        let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5858        assert!(result.is_ok());
5859
5860        let code = makefile.to_string();
5861        assert!(code.contains("ifndef NDEBUG"));
5862        assert!(code.contains("VAR = enabled"));
5863        assert!(code.contains("endif"));
5864    }
5865
5866    #[test]
5867    fn test_add_conditional_ifeq() {
5868        let mut makefile = Makefile::new();
5869        let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5870        assert!(result.is_ok());
5871
5872        let code = makefile.to_string();
5873        assert!(code.contains("ifeq ($(OS),Linux)"));
5874        assert!(code.contains("VAR = linux"));
5875        assert!(code.contains("endif"));
5876    }
5877
5878    #[test]
5879    fn test_add_conditional_ifneq() {
5880        let mut makefile = Makefile::new();
5881        let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5882        assert!(result.is_ok());
5883
5884        let code = makefile.to_string();
5885        assert!(code.contains("ifneq ($(OS),Windows)"));
5886        assert!(code.contains("VAR = unix"));
5887        assert!(code.contains("endif"));
5888    }
5889
5890    #[test]
5891    fn test_conditional_api_integration() {
5892        // Create a makefile with a rule and a variable
5893        let mut makefile: Makefile = r#"VAR1 = value1
5894
5895rule1:
5896	command1
5897"#
5898        .parse()
5899        .unwrap();
5900
5901        // Add a conditional
5902        makefile
5903            .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5904            .unwrap();
5905
5906        // Verify the conditional was added
5907        assert_eq!(makefile.conditionals().count(), 1);
5908        let conditional = makefile.conditionals().next().unwrap();
5909        assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5910        assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5911        assert!(conditional.has_else());
5912
5913        // Verify the original content is preserved
5914        assert_eq!(makefile.variable_definitions().count(), 1);
5915        assert_eq!(makefile.rules().count(), 1);
5916    }
5917
5918    #[test]
5919    fn test_conditional_if_items() {
5920        let makefile: Makefile = r#"ifdef DEBUG
5921VAR = debug
5922rule:
5923	command
5924endif
5925"#
5926        .parse()
5927        .unwrap();
5928
5929        let cond = makefile.conditionals().next().unwrap();
5930        let items: Vec<_> = cond.if_items().collect();
5931        assert_eq!(items.len(), 2); // One variable, one rule
5932
5933        match &items[0] {
5934            MakefileItem::Variable(v) => {
5935                assert_eq!(v.name(), Some("VAR".to_string()));
5936            }
5937            _ => panic!("Expected variable"),
5938        }
5939
5940        match &items[1] {
5941            MakefileItem::Rule(r) => {
5942                assert!(r.targets().any(|t| t == "rule"));
5943            }
5944            _ => panic!("Expected rule"),
5945        }
5946    }
5947
5948    #[test]
5949    fn test_conditional_else_items() {
5950        let makefile: Makefile = r#"ifdef DEBUG
5951VAR = debug
5952else
5953VAR2 = release
5954rule2:
5955	command
5956endif
5957"#
5958        .parse()
5959        .unwrap();
5960
5961        let cond = makefile.conditionals().next().unwrap();
5962        let items: Vec<_> = cond.else_items().collect();
5963        assert_eq!(items.len(), 2); // One variable, one rule
5964
5965        match &items[0] {
5966            MakefileItem::Variable(v) => {
5967                assert_eq!(v.name(), Some("VAR2".to_string()));
5968            }
5969            _ => panic!("Expected variable"),
5970        }
5971
5972        match &items[1] {
5973            MakefileItem::Rule(r) => {
5974                assert!(r.targets().any(|t| t == "rule2"));
5975            }
5976            _ => panic!("Expected rule"),
5977        }
5978    }
5979
5980    #[test]
5981    fn test_conditional_add_if_item() {
5982        let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5983        let mut cond = makefile.conditionals().next().unwrap();
5984
5985        // Parse a variable from a temporary makefile
5986        let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5987        let var = temp.variable_definitions().next().unwrap();
5988        cond.add_if_item(MakefileItem::Variable(var));
5989
5990        let code = makefile.to_string();
5991        assert!(code.contains("CFLAGS = -g"));
5992
5993        // Verify it's in the if branch
5994        let cond = makefile.conditionals().next().unwrap();
5995        assert_eq!(cond.if_items().count(), 1);
5996    }
5997
5998    #[test]
5999    fn test_conditional_add_else_item() {
6000        let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
6001        let mut cond = makefile.conditionals().next().unwrap();
6002
6003        // Parse a variable from a temporary makefile
6004        let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
6005        let var = temp.variable_definitions().next().unwrap();
6006        cond.add_else_item(MakefileItem::Variable(var));
6007
6008        let code = makefile.to_string();
6009        assert!(code.contains("else"));
6010        assert!(code.contains("CFLAGS = -O2"));
6011
6012        // Verify it's in the else branch
6013        let cond = makefile.conditionals().next().unwrap();
6014        assert_eq!(cond.else_items().count(), 1);
6015    }
6016
6017    #[test]
6018    fn test_add_conditional_with_items() {
6019        let mut makefile = Makefile::new();
6020
6021        // Parse items from temporary makefiles
6022        let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
6023        let var1 = temp1.variable_definitions().next().unwrap();
6024
6025        let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
6026        let var2 = temp2.variable_definitions().next().unwrap();
6027
6028        let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
6029        let rule1 = temp3.rules().next().unwrap();
6030
6031        let result = makefile.add_conditional_with_items(
6032            "ifdef",
6033            "DEBUG",
6034            vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
6035            Some(vec![MakefileItem::Variable(var2)]),
6036        );
6037
6038        assert!(result.is_ok());
6039
6040        let code = makefile.to_string();
6041        assert!(code.contains("ifdef DEBUG"));
6042        assert!(code.contains("CFLAGS = -g"));
6043        assert!(code.contains("debug:"));
6044        assert!(code.contains("else"));
6045        assert!(code.contains("CFLAGS = -O2"));
6046    }
6047
6048    #[test]
6049    fn test_conditional_items_with_nested_conditional() {
6050        let makefile: Makefile = r#"ifdef DEBUG
6051VAR = debug
6052ifdef VERBOSE
6053	VAR2 = verbose
6054endif
6055endif
6056"#
6057        .parse()
6058        .unwrap();
6059
6060        let cond = makefile.conditionals().next().unwrap();
6061        let items: Vec<_> = cond.if_items().collect();
6062        assert_eq!(items.len(), 2); // One variable, one nested conditional
6063
6064        match &items[0] {
6065            MakefileItem::Variable(v) => {
6066                assert_eq!(v.name(), Some("VAR".to_string()));
6067            }
6068            _ => panic!("Expected variable"),
6069        }
6070
6071        match &items[1] {
6072            MakefileItem::Conditional(c) => {
6073                assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6074            }
6075            _ => panic!("Expected conditional"),
6076        }
6077    }
6078
6079    #[test]
6080    fn test_conditional_items_with_include() {
6081        let makefile: Makefile = r#"ifdef DEBUG
6082include debug.mk
6083VAR = debug
6084endif
6085"#
6086        .parse()
6087        .unwrap();
6088
6089        let cond = makefile.conditionals().next().unwrap();
6090        let items: Vec<_> = cond.if_items().collect();
6091        assert_eq!(items.len(), 2); // One include, one variable
6092
6093        match &items[0] {
6094            MakefileItem::Include(i) => {
6095                assert_eq!(i.path(), Some("debug.mk".to_string()));
6096            }
6097            _ => panic!("Expected include"),
6098        }
6099
6100        match &items[1] {
6101            MakefileItem::Variable(v) => {
6102                assert_eq!(v.name(), Some("VAR".to_string()));
6103            }
6104            _ => panic!("Expected variable"),
6105        }
6106    }
6107
6108    #[test]
6109    fn test_makefile_items_iterator() {
6110        let makefile: Makefile = r#"VAR = value
6111ifdef DEBUG
6112CFLAGS = -g
6113endif
6114rule:
6115	command
6116include common.mk
6117"#
6118        .parse()
6119        .unwrap();
6120
6121        // First verify we can find each type individually
6122        // variable_definitions() is recursive, so it finds VAR and CFLAGS (inside conditional)
6123        assert_eq!(makefile.variable_definitions().count(), 2);
6124        assert_eq!(makefile.conditionals().count(), 1);
6125        assert_eq!(makefile.rules().count(), 1);
6126
6127        let items: Vec<_> = makefile.items().collect();
6128        // Note: include directives might not be at top level, need to check
6129        assert!(
6130            items.len() >= 3,
6131            "Expected at least 3 items, got {}",
6132            items.len()
6133        );
6134
6135        match &items[0] {
6136            MakefileItem::Variable(v) => {
6137                assert_eq!(v.name(), Some("VAR".to_string()));
6138            }
6139            _ => panic!("Expected variable at position 0"),
6140        }
6141
6142        match &items[1] {
6143            MakefileItem::Conditional(c) => {
6144                assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6145            }
6146            _ => panic!("Expected conditional at position 1"),
6147        }
6148
6149        match &items[2] {
6150            MakefileItem::Rule(r) => {
6151                let targets: Vec<_> = r.targets().collect();
6152                assert_eq!(targets, vec!["rule"]);
6153            }
6154            _ => panic!("Expected rule at position 2"),
6155        }
6156    }
6157
6158    #[test]
6159    fn test_conditional_unwrap() {
6160        let makefile: Makefile = r#"ifdef DEBUG
6161VAR = debug
6162rule:
6163	command
6164endif
6165"#
6166        .parse()
6167        .unwrap();
6168
6169        let mut cond = makefile.conditionals().next().unwrap();
6170        cond.unwrap().unwrap();
6171
6172        let code = makefile.to_string();
6173        let expected = "VAR = debug\nrule:\n\tcommand\n";
6174        assert_eq!(code, expected);
6175
6176        // Should have no conditionals now
6177        assert_eq!(makefile.conditionals().count(), 0);
6178
6179        // Should still have the variable and rule
6180        assert_eq!(makefile.variable_definitions().count(), 1);
6181        assert_eq!(makefile.rules().count(), 1);
6182    }
6183
6184    #[test]
6185    fn test_conditional_unwrap_with_else_fails() {
6186        let makefile: Makefile = r#"ifdef DEBUG
6187VAR = debug
6188else
6189VAR = release
6190endif
6191"#
6192        .parse()
6193        .unwrap();
6194
6195        let mut cond = makefile.conditionals().next().unwrap();
6196        let result = cond.unwrap();
6197
6198        assert!(result.is_err());
6199        assert!(result
6200            .unwrap_err()
6201            .to_string()
6202            .contains("Cannot unwrap conditional with else clause"));
6203    }
6204
6205    #[test]
6206    fn test_conditional_unwrap_nested() {
6207        let makefile: Makefile = r#"ifdef OUTER
6208VAR = outer
6209ifdef INNER
6210VAR2 = inner
6211endif
6212endif
6213"#
6214        .parse()
6215        .unwrap();
6216
6217        // Unwrap the outer conditional
6218        let mut outer_cond = makefile.conditionals().next().unwrap();
6219        outer_cond.unwrap().unwrap();
6220
6221        let code = makefile.to_string();
6222        let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
6223        assert_eq!(code, expected);
6224    }
6225
6226    #[test]
6227    fn test_conditional_unwrap_empty() {
6228        let makefile: Makefile = r#"ifdef DEBUG
6229endif
6230"#
6231        .parse()
6232        .unwrap();
6233
6234        let mut cond = makefile.conditionals().next().unwrap();
6235        cond.unwrap().unwrap();
6236
6237        let code = makefile.to_string();
6238        assert_eq!(code, "");
6239    }
6240
6241    #[test]
6242    fn test_rule_parent() {
6243        let makefile: Makefile = r#"all:
6244	echo "test"
6245"#
6246        .parse()
6247        .unwrap();
6248
6249        let rule = makefile.rules().next().unwrap();
6250        let parent = rule.parent();
6251        // Parent is ROOT node which doesn't cast to MakefileItem
6252        assert!(parent.is_none());
6253    }
6254
6255    #[test]
6256    fn test_item_parent_in_conditional() {
6257        let makefile: Makefile = r#"ifdef DEBUG
6258VAR = debug
6259rule:
6260	command
6261endif
6262"#
6263        .parse()
6264        .unwrap();
6265
6266        let cond = makefile.conditionals().next().unwrap();
6267
6268        // Get items from the conditional
6269        let items: Vec<_> = cond.if_items().collect();
6270        assert_eq!(items.len(), 2);
6271
6272        // Check variable parent is the conditional
6273        if let MakefileItem::Variable(var) = &items[0] {
6274            let parent = var.parent();
6275            assert!(parent.is_some());
6276            if let Some(MakefileItem::Conditional(_)) = parent {
6277                // Expected - parent is a conditional
6278            } else {
6279                panic!("Expected variable parent to be a Conditional");
6280            }
6281        } else {
6282            panic!("Expected first item to be a Variable");
6283        }
6284
6285        // Check rule parent is the conditional
6286        if let MakefileItem::Rule(rule) = &items[1] {
6287            let parent = rule.parent();
6288            assert!(parent.is_some());
6289            if let Some(MakefileItem::Conditional(_)) = parent {
6290                // Expected - parent is a conditional
6291            } else {
6292                panic!("Expected rule parent to be a Conditional");
6293            }
6294        } else {
6295            panic!("Expected second item to be a Rule");
6296        }
6297    }
6298
6299    #[test]
6300    fn test_nested_conditional_parent() {
6301        let makefile: Makefile = r#"ifdef OUTER
6302VAR = outer
6303ifdef INNER
6304VAR2 = inner
6305endif
6306endif
6307"#
6308        .parse()
6309        .unwrap();
6310
6311        let outer_cond = makefile.conditionals().next().unwrap();
6312
6313        // Get inner conditional from outer conditional's items
6314        let items: Vec<_> = outer_cond.if_items().collect();
6315
6316        // Find the nested conditional
6317        let inner_cond = items
6318            .iter()
6319            .find_map(|item| {
6320                if let MakefileItem::Conditional(c) = item {
6321                    Some(c)
6322                } else {
6323                    None
6324                }
6325            })
6326            .unwrap();
6327
6328        // Inner conditional's parent should be the outer conditional
6329        let parent = inner_cond.parent();
6330        assert!(parent.is_some());
6331        if let Some(MakefileItem::Conditional(_)) = parent {
6332            // Expected - parent is a conditional
6333        } else {
6334            panic!("Expected inner conditional's parent to be a Conditional");
6335        }
6336    }
6337
6338    #[test]
6339    fn test_line_col() {
6340        let text = r#"# Comment at line 0
6341VAR1 = value1
6342VAR2 = value2
6343
6344rule1: dep1 dep2
6345	command1
6346	command2
6347
6348rule2:
6349	command3
6350
6351ifdef DEBUG
6352CFLAGS = -g
6353endif
6354"#;
6355        let makefile: Makefile = text.parse().unwrap();
6356
6357        // Test variable definition line numbers
6358        // variable_definitions() is recursive, so it finds VAR1, VAR2, and CFLAGS (inside conditional)
6359        let vars: Vec<_> = makefile.variable_definitions().collect();
6360        assert_eq!(vars.len(), 3);
6361
6362        // VAR1 starts at line 1
6363        assert_eq!(vars[0].line(), 1);
6364        assert_eq!(vars[0].column(), 0);
6365        assert_eq!(vars[0].line_col(), (1, 0));
6366
6367        // VAR2 starts at line 2
6368        assert_eq!(vars[1].line(), 2);
6369        assert_eq!(vars[1].column(), 0);
6370
6371        // CFLAGS starts at line 12 (inside ifdef DEBUG)
6372        assert_eq!(vars[2].line(), 12);
6373        assert_eq!(vars[2].column(), 0);
6374
6375        // Test rule line numbers
6376        let rules: Vec<_> = makefile.rules().collect();
6377        assert_eq!(rules.len(), 2);
6378
6379        // rule1 starts at line 4
6380        assert_eq!(rules[0].line(), 4);
6381        assert_eq!(rules[0].column(), 0);
6382        assert_eq!(rules[0].line_col(), (4, 0));
6383
6384        // rule2 starts at line 8
6385        assert_eq!(rules[1].line(), 8);
6386        assert_eq!(rules[1].column(), 0);
6387
6388        // Test conditional line numbers
6389        let conditionals: Vec<_> = makefile.conditionals().collect();
6390        assert_eq!(conditionals.len(), 1);
6391
6392        // ifdef DEBUG starts at line 11
6393        assert_eq!(conditionals[0].line(), 11);
6394        assert_eq!(conditionals[0].column(), 0);
6395        assert_eq!(conditionals[0].line_col(), (11, 0));
6396    }
6397
6398    #[test]
6399    fn test_line_col_multiline() {
6400        let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
6401        let makefile: Makefile = text.parse().unwrap();
6402
6403        // Variable definition starts at line 0
6404        let vars: Vec<_> = makefile.variable_definitions().collect();
6405        assert_eq!(vars.len(), 1);
6406        assert_eq!(vars[0].line(), 0);
6407        assert_eq!(vars[0].column(), 0);
6408
6409        // Rule starts at line 4
6410        let rules: Vec<_> = makefile.rules().collect();
6411        assert_eq!(rules.len(), 1);
6412        assert_eq!(rules[0].line(), 4);
6413        assert_eq!(rules[0].column(), 0);
6414    }
6415
6416    #[test]
6417    fn test_line_col_includes() {
6418        let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
6419        let makefile: Makefile = text.parse().unwrap();
6420
6421        // Variable at line 0
6422        let vars: Vec<_> = makefile.variable_definitions().collect();
6423        assert_eq!(vars[0].line(), 0);
6424
6425        // Includes at lines 2 and 3
6426        let includes: Vec<_> = makefile.includes().collect();
6427        assert_eq!(includes.len(), 2);
6428        assert_eq!(includes[0].line(), 2);
6429        assert_eq!(includes[0].column(), 0);
6430        assert_eq!(includes[1].line(), 3);
6431        assert_eq!(includes[1].column(), 0);
6432    }
6433
6434    #[test]
6435    fn test_conditional_in_rule_vs_toplevel() {
6436        // Conditional immediately after rule (no blank line) - part of rule
6437        let text1 = r#"rule:
6438	command
6439ifeq (,$(X))
6440	test
6441endif
6442"#;
6443        let makefile: Makefile = text1.parse().unwrap();
6444        let rules: Vec<_> = makefile.rules().collect();
6445        let conditionals: Vec<_> = makefile.conditionals().collect();
6446
6447        assert_eq!(rules.len(), 1);
6448        assert_eq!(
6449            conditionals.len(),
6450            0,
6451            "Conditional should be part of rule, not top-level"
6452        );
6453
6454        // Conditional after blank line - top-level
6455        let text2 = r#"rule:
6456	command
6457
6458ifeq (,$(X))
6459	test
6460endif
6461"#;
6462        let makefile: Makefile = text2.parse().unwrap();
6463        let rules: Vec<_> = makefile.rules().collect();
6464        let conditionals: Vec<_> = makefile.conditionals().collect();
6465
6466        assert_eq!(rules.len(), 1);
6467        assert_eq!(
6468            conditionals.len(),
6469            1,
6470            "Conditional after blank line should be top-level"
6471        );
6472        assert_eq!(conditionals[0].line(), 3);
6473    }
6474
6475    #[test]
6476    fn test_nested_conditionals_line_tracking() {
6477        let text = r#"ifdef OUTER
6478VAR1 = value1
6479ifdef INNER
6480VAR2 = value2
6481endif
6482VAR3 = value3
6483endif
6484"#;
6485        let makefile: Makefile = text.parse().unwrap();
6486
6487        let conditionals: Vec<_> = makefile.conditionals().collect();
6488        assert_eq!(
6489            conditionals.len(),
6490            1,
6491            "Only outer conditional should be top-level"
6492        );
6493        assert_eq!(conditionals[0].line(), 0);
6494        assert_eq!(conditionals[0].column(), 0);
6495    }
6496
6497    #[test]
6498    fn test_conditional_else_line_tracking() {
6499        let text = r#"VAR1 = before
6500
6501ifdef DEBUG
6502DEBUG_FLAGS = -g
6503else
6504DEBUG_FLAGS = -O2
6505endif
6506
6507VAR2 = after
6508"#;
6509        let makefile: Makefile = text.parse().unwrap();
6510
6511        let conditionals: Vec<_> = makefile.conditionals().collect();
6512        assert_eq!(conditionals.len(), 1);
6513        assert_eq!(conditionals[0].line(), 2);
6514        assert_eq!(conditionals[0].column(), 0);
6515    }
6516
6517    #[test]
6518    fn test_broken_conditional_endif_without_if() {
6519        // endif without matching if - parser should handle gracefully
6520        let text = "VAR = value\nendif\n";
6521        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6522
6523        // Should parse without crashing
6524        let vars: Vec<_> = makefile.variable_definitions().collect();
6525        assert_eq!(vars.len(), 1);
6526        assert_eq!(vars[0].line(), 0);
6527    }
6528
6529    #[test]
6530    fn test_broken_conditional_else_without_if() {
6531        // else without matching if
6532        let text = "VAR = value\nelse\nVAR2 = other\n";
6533        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6534
6535        // Should parse without crashing
6536        let vars: Vec<_> = makefile.variable_definitions().collect();
6537        assert!(!vars.is_empty(), "Should parse at least the first variable");
6538        assert_eq!(vars[0].line(), 0);
6539    }
6540
6541    #[test]
6542    fn test_broken_conditional_missing_endif() {
6543        // ifdef without matching endif
6544        let text = r#"ifdef DEBUG
6545DEBUG_FLAGS = -g
6546VAR = value
6547"#;
6548        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6549
6550        // Should parse without crashing
6551        assert!(makefile.code().contains("ifdef DEBUG"));
6552    }
6553
6554    #[test]
6555    fn test_multiple_conditionals_line_tracking() {
6556        let text = r#"ifdef A
6557VAR_A = a
6558endif
6559
6560ifdef B
6561VAR_B = b
6562endif
6563
6564ifdef C
6565VAR_C = c
6566endif
6567"#;
6568        let makefile: Makefile = text.parse().unwrap();
6569
6570        let conditionals: Vec<_> = makefile.conditionals().collect();
6571        assert_eq!(conditionals.len(), 3);
6572        assert_eq!(conditionals[0].line(), 0);
6573        assert_eq!(conditionals[1].line(), 4);
6574        assert_eq!(conditionals[2].line(), 8);
6575    }
6576
6577    #[test]
6578    fn test_conditional_with_multiple_else_ifeq() {
6579        let text = r#"ifeq ($(OS),Windows)
6580EXT = .exe
6581else ifeq ($(OS),Linux)
6582EXT = .bin
6583else
6584EXT = .out
6585endif
6586"#;
6587        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6588
6589        let conditionals: Vec<_> = makefile.conditionals().collect();
6590        assert_eq!(conditionals.len(), 1);
6591        assert_eq!(conditionals[0].line(), 0);
6592        assert_eq!(conditionals[0].column(), 0);
6593    }
6594
6595    #[test]
6596    fn test_conditional_types_line_tracking() {
6597        let text = r#"ifdef VAR1
6598A = 1
6599endif
6600
6601ifndef VAR2
6602B = 2
6603endif
6604
6605ifeq ($(X),y)
6606C = 3
6607endif
6608
6609ifneq ($(Y),n)
6610D = 4
6611endif
6612"#;
6613        let makefile: Makefile = text.parse().unwrap();
6614
6615        let conditionals: Vec<_> = makefile.conditionals().collect();
6616        assert_eq!(conditionals.len(), 4);
6617
6618        assert_eq!(conditionals[0].line(), 0); // ifdef
6619        assert_eq!(
6620            conditionals[0].conditional_type(),
6621            Some("ifdef".to_string())
6622        );
6623
6624        assert_eq!(conditionals[1].line(), 4); // ifndef
6625        assert_eq!(
6626            conditionals[1].conditional_type(),
6627            Some("ifndef".to_string())
6628        );
6629
6630        assert_eq!(conditionals[2].line(), 8); // ifeq
6631        assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
6632
6633        assert_eq!(conditionals[3].line(), 12); // ifneq
6634        assert_eq!(
6635            conditionals[3].conditional_type(),
6636            Some("ifneq".to_string())
6637        );
6638    }
6639
6640    #[test]
6641    fn test_conditional_in_rule_with_recipes() {
6642        let text = r#"test:
6643	echo "start"
6644ifdef VERBOSE
6645	echo "verbose mode"
6646endif
6647	echo "end"
6648"#;
6649        let makefile: Makefile = text.parse().unwrap();
6650
6651        let rules: Vec<_> = makefile.rules().collect();
6652        let conditionals: Vec<_> = makefile.conditionals().collect();
6653
6654        assert_eq!(rules.len(), 1);
6655        assert_eq!(rules[0].line(), 0);
6656        // Conditional is part of the rule, not top-level
6657        assert_eq!(conditionals.len(), 0);
6658    }
6659
6660    #[test]
6661    fn test_broken_conditional_double_else() {
6662        // Two else clauses in one conditional
6663        let text = r#"ifdef DEBUG
6664A = 1
6665else
6666B = 2
6667else
6668C = 3
6669endif
6670"#;
6671        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6672
6673        // Should parse without crashing, though it's malformed
6674        assert!(makefile.code().contains("ifdef DEBUG"));
6675    }
6676
6677    #[test]
6678    fn test_broken_conditional_mismatched_nesting() {
6679        // Mismatched nesting - more endifs than ifs
6680        let text = r#"ifdef A
6681VAR = value
6682endif
6683endif
6684"#;
6685        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6686
6687        // Should parse without crashing
6688        // The extra endif will be parsed separately, so we may get more than 1 item
6689        let conditionals: Vec<_> = makefile.conditionals().collect();
6690        assert!(
6691            !conditionals.is_empty(),
6692            "Should parse at least the first conditional"
6693        );
6694    }
6695
6696    #[test]
6697    fn test_conditional_with_comment_line_tracking() {
6698        let text = r#"# This is a comment
6699ifdef DEBUG
6700# Another comment
6701CFLAGS = -g
6702endif
6703# Final comment
6704"#;
6705        let makefile: Makefile = text.parse().unwrap();
6706
6707        let conditionals: Vec<_> = makefile.conditionals().collect();
6708        assert_eq!(conditionals.len(), 1);
6709        assert_eq!(conditionals[0].line(), 1);
6710        assert_eq!(conditionals[0].column(), 0);
6711    }
6712
6713    #[test]
6714    fn test_conditional_after_variable_with_blank_lines() {
6715        let text = r#"VAR1 = value1
6716
6717
6718ifdef DEBUG
6719VAR2 = value2
6720endif
6721"#;
6722        let makefile: Makefile = text.parse().unwrap();
6723
6724        let vars: Vec<_> = makefile.variable_definitions().collect();
6725        let conditionals: Vec<_> = makefile.conditionals().collect();
6726
6727        // variable_definitions() is recursive, so it finds VAR1 and VAR2 (inside conditional)
6728        assert_eq!(vars.len(), 2);
6729        assert_eq!(vars[0].line(), 0); // VAR1
6730        assert_eq!(vars[1].line(), 4); // VAR2
6731
6732        assert_eq!(conditionals.len(), 1);
6733        assert_eq!(conditionals[0].line(), 3);
6734    }
6735
6736    #[test]
6737    fn test_empty_conditional_line_tracking() {
6738        let text = r#"ifdef DEBUG
6739endif
6740
6741ifndef RELEASE
6742endif
6743"#;
6744        let makefile: Makefile = text.parse().unwrap();
6745
6746        let conditionals: Vec<_> = makefile.conditionals().collect();
6747        assert_eq!(conditionals.len(), 2);
6748        assert_eq!(conditionals[0].line(), 0);
6749        assert_eq!(conditionals[1].line(), 3);
6750    }
6751
6752    #[test]
6753    fn test_recipe_line_tracking() {
6754        let text = r#"build:
6755	echo "Building..."
6756	gcc -o app main.c
6757	echo "Done"
6758
6759test:
6760	./run-tests
6761"#;
6762        let makefile: Makefile = text.parse().unwrap();
6763
6764        // Test first rule's recipes
6765        let rule1 = makefile.rules().next().expect("Should have first rule");
6766        let recipes: Vec<_> = rule1.recipe_nodes().collect();
6767        assert_eq!(recipes.len(), 3);
6768
6769        assert_eq!(recipes[0].text(), "echo \"Building...\"");
6770        assert_eq!(recipes[0].line(), 1);
6771        assert_eq!(recipes[0].column(), 0);
6772
6773        assert_eq!(recipes[1].text(), "gcc -o app main.c");
6774        assert_eq!(recipes[1].line(), 2);
6775        assert_eq!(recipes[1].column(), 0);
6776
6777        assert_eq!(recipes[2].text(), "echo \"Done\"");
6778        assert_eq!(recipes[2].line(), 3);
6779        assert_eq!(recipes[2].column(), 0);
6780
6781        // Test second rule's recipes
6782        let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6783        let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6784        assert_eq!(recipes2.len(), 1);
6785
6786        assert_eq!(recipes2[0].text(), "./run-tests");
6787        assert_eq!(recipes2[0].line(), 6);
6788        assert_eq!(recipes2[0].column(), 0);
6789    }
6790
6791    #[test]
6792    fn test_recipe_with_variables_line_tracking() {
6793        let text = r#"install:
6794	mkdir -p $(DESTDIR)
6795	cp $(BINARY) $(DESTDIR)/
6796"#;
6797        let makefile: Makefile = text.parse().unwrap();
6798        let rule = makefile.rules().next().expect("Should have rule");
6799        let recipes: Vec<_> = rule.recipe_nodes().collect();
6800
6801        assert_eq!(recipes.len(), 2);
6802        assert_eq!(recipes[0].line(), 1);
6803        assert_eq!(recipes[1].line(), 2);
6804    }
6805
6806    #[test]
6807    fn test_recipe_text_no_leading_tab() {
6808        // Test that Recipe::text() does not include the leading tab
6809        let text = "test:\n\techo hello\n\t\techo nested\n\t  echo with spaces\n";
6810        let makefile: Makefile = text.parse().unwrap();
6811        let rule = makefile.rules().next().expect("Should have rule");
6812        let recipes: Vec<_> = rule.recipe_nodes().collect();
6813
6814        assert_eq!(recipes.len(), 3);
6815
6816        // Debug: print syntax tree for the first recipe
6817        eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6818
6819        // First recipe: single tab
6820        assert_eq!(recipes[0].text(), "echo hello");
6821
6822        // Second recipe: double tab (nested)
6823        eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6824        assert_eq!(recipes[1].text(), "\techo nested");
6825
6826        // Third recipe: tab followed by spaces
6827        eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6828        assert_eq!(recipes[2].text(), "  echo with spaces");
6829    }
6830
6831    #[test]
6832    fn test_recipe_parent() {
6833        let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6834        let rule = makefile.rules().next().unwrap();
6835        let recipe = rule.recipe_nodes().next().unwrap();
6836
6837        let parent = recipe.parent().expect("Recipe should have parent");
6838        assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6839        assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6840    }
6841
6842    #[test]
6843    fn test_recipe_is_silent_various_prefixes() {
6844        let makefile: Makefile = r#"test:
6845	@echo silent
6846	-echo ignore
6847	+echo always
6848	@-echo silent_ignore
6849	-@echo ignore_silent
6850	+@echo always_silent
6851	echo normal
6852"#
6853        .parse()
6854        .unwrap();
6855
6856        let rule = makefile.rules().next().unwrap();
6857        let recipes: Vec<_> = rule.recipe_nodes().collect();
6858
6859        assert_eq!(recipes.len(), 7);
6860        assert!(recipes[0].is_silent(), "@echo should be silent");
6861        assert!(!recipes[1].is_silent(), "-echo should not be silent");
6862        assert!(!recipes[2].is_silent(), "+echo should not be silent");
6863        assert!(recipes[3].is_silent(), "@-echo should be silent");
6864        assert!(recipes[4].is_silent(), "-@echo should be silent");
6865        assert!(recipes[5].is_silent(), "+@echo should be silent");
6866        assert!(!recipes[6].is_silent(), "echo should not be silent");
6867    }
6868
6869    #[test]
6870    fn test_recipe_is_ignore_errors_various_prefixes() {
6871        let makefile: Makefile = r#"test:
6872	@echo silent
6873	-echo ignore
6874	+echo always
6875	@-echo silent_ignore
6876	-@echo ignore_silent
6877	+-echo always_ignore
6878	echo normal
6879"#
6880        .parse()
6881        .unwrap();
6882
6883        let rule = makefile.rules().next().unwrap();
6884        let recipes: Vec<_> = rule.recipe_nodes().collect();
6885
6886        assert_eq!(recipes.len(), 7);
6887        assert!(
6888            !recipes[0].is_ignore_errors(),
6889            "@echo should not ignore errors"
6890        );
6891        assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6892        assert!(
6893            !recipes[2].is_ignore_errors(),
6894            "+echo should not ignore errors"
6895        );
6896        assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6897        assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6898        assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6899        assert!(
6900            !recipes[6].is_ignore_errors(),
6901            "echo should not ignore errors"
6902        );
6903    }
6904
6905    #[test]
6906    fn test_recipe_set_prefix_add() {
6907        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6908        let rule = makefile.rules().next().unwrap();
6909        let mut recipe = rule.recipe_nodes().next().unwrap();
6910
6911        recipe.set_prefix("@");
6912        assert_eq!(recipe.text(), "@echo hello");
6913        assert!(recipe.is_silent());
6914    }
6915
6916    #[test]
6917    fn test_recipe_set_prefix_change() {
6918        let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6919        let rule = makefile.rules().next().unwrap();
6920        let mut recipe = rule.recipe_nodes().next().unwrap();
6921
6922        recipe.set_prefix("-");
6923        assert_eq!(recipe.text(), "-echo hello");
6924        assert!(!recipe.is_silent());
6925        assert!(recipe.is_ignore_errors());
6926    }
6927
6928    #[test]
6929    fn test_recipe_set_prefix_remove() {
6930        let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
6931        let rule = makefile.rules().next().unwrap();
6932        let mut recipe = rule.recipe_nodes().next().unwrap();
6933
6934        recipe.set_prefix("");
6935        assert_eq!(recipe.text(), "echo hello");
6936        assert!(!recipe.is_silent());
6937        assert!(!recipe.is_ignore_errors());
6938    }
6939
6940    #[test]
6941    fn test_recipe_set_prefix_combinations() {
6942        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6943        let rule = makefile.rules().next().unwrap();
6944        let mut recipe = rule.recipe_nodes().next().unwrap();
6945
6946        recipe.set_prefix("@-");
6947        assert_eq!(recipe.text(), "@-echo hello");
6948        assert!(recipe.is_silent());
6949        assert!(recipe.is_ignore_errors());
6950
6951        recipe.set_prefix("-@");
6952        assert_eq!(recipe.text(), "-@echo hello");
6953        assert!(recipe.is_silent());
6954        assert!(recipe.is_ignore_errors());
6955    }
6956
6957    #[test]
6958    fn test_recipe_replace_text_basic() {
6959        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6960        let rule = makefile.rules().next().unwrap();
6961        let mut recipe = rule.recipe_nodes().next().unwrap();
6962
6963        recipe.replace_text("echo world");
6964        assert_eq!(recipe.text(), "echo world");
6965
6966        // Verify it's still accessible from the rule
6967        let rule = makefile.rules().next().unwrap();
6968        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6969    }
6970
6971    #[test]
6972    fn test_recipe_replace_text_with_prefix() {
6973        let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6974        let rule = makefile.rules().next().unwrap();
6975        let mut recipe = rule.recipe_nodes().next().unwrap();
6976
6977        recipe.replace_text("@echo goodbye");
6978        assert_eq!(recipe.text(), "@echo goodbye");
6979        assert!(recipe.is_silent());
6980    }
6981
6982    #[test]
6983    fn test_recipe_insert_before_single() {
6984        let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6985        let rule = makefile.rules().next().unwrap();
6986        let recipe = rule.recipe_nodes().next().unwrap();
6987
6988        recipe.insert_before("echo hello");
6989
6990        let rule = makefile.rules().next().unwrap();
6991        let recipes: Vec<_> = rule.recipes().collect();
6992        assert_eq!(recipes, vec!["echo hello", "echo world"]);
6993    }
6994
6995    #[test]
6996    fn test_recipe_insert_before_multiple() {
6997        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6998            .parse()
6999            .unwrap();
7000        let rule = makefile.rules().next().unwrap();
7001        let recipes: Vec<_> = rule.recipe_nodes().collect();
7002
7003        // Insert before the second recipe
7004        recipes[1].insert_before("echo middle");
7005
7006        let rule = makefile.rules().next().unwrap();
7007        let new_recipes: Vec<_> = rule.recipes().collect();
7008        assert_eq!(
7009            new_recipes,
7010            vec!["echo one", "echo middle", "echo two", "echo three"]
7011        );
7012    }
7013
7014    #[test]
7015    fn test_recipe_insert_before_first() {
7016        let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7017        let rule = makefile.rules().next().unwrap();
7018        let recipes: Vec<_> = rule.recipe_nodes().collect();
7019
7020        recipes[0].insert_before("echo zero");
7021
7022        let rule = makefile.rules().next().unwrap();
7023        let new_recipes: Vec<_> = rule.recipes().collect();
7024        assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
7025    }
7026
7027    #[test]
7028    fn test_recipe_insert_after_single() {
7029        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7030        let rule = makefile.rules().next().unwrap();
7031        let recipe = rule.recipe_nodes().next().unwrap();
7032
7033        recipe.insert_after("echo world");
7034
7035        let rule = makefile.rules().next().unwrap();
7036        let recipes: Vec<_> = rule.recipes().collect();
7037        assert_eq!(recipes, vec!["echo hello", "echo world"]);
7038    }
7039
7040    #[test]
7041    fn test_recipe_insert_after_multiple() {
7042        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7043            .parse()
7044            .unwrap();
7045        let rule = makefile.rules().next().unwrap();
7046        let recipes: Vec<_> = rule.recipe_nodes().collect();
7047
7048        // Insert after the second recipe
7049        recipes[1].insert_after("echo middle");
7050
7051        let rule = makefile.rules().next().unwrap();
7052        let new_recipes: Vec<_> = rule.recipes().collect();
7053        assert_eq!(
7054            new_recipes,
7055            vec!["echo one", "echo two", "echo middle", "echo three"]
7056        );
7057    }
7058
7059    #[test]
7060    fn test_recipe_insert_after_last() {
7061        let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7062        let rule = makefile.rules().next().unwrap();
7063        let recipes: Vec<_> = rule.recipe_nodes().collect();
7064
7065        recipes[1].insert_after("echo three");
7066
7067        let rule = makefile.rules().next().unwrap();
7068        let new_recipes: Vec<_> = rule.recipes().collect();
7069        assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
7070    }
7071
7072    #[test]
7073    fn test_recipe_remove_single() {
7074        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7075        let rule = makefile.rules().next().unwrap();
7076        let recipe = rule.recipe_nodes().next().unwrap();
7077
7078        recipe.remove();
7079
7080        let rule = makefile.rules().next().unwrap();
7081        assert_eq!(rule.recipes().count(), 0);
7082    }
7083
7084    #[test]
7085    fn test_recipe_remove_first() {
7086        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7087            .parse()
7088            .unwrap();
7089        let rule = makefile.rules().next().unwrap();
7090        let recipes: Vec<_> = rule.recipe_nodes().collect();
7091
7092        recipes[0].remove();
7093
7094        let rule = makefile.rules().next().unwrap();
7095        let new_recipes: Vec<_> = rule.recipes().collect();
7096        assert_eq!(new_recipes, vec!["echo two", "echo three"]);
7097    }
7098
7099    #[test]
7100    fn test_recipe_remove_middle() {
7101        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7102            .parse()
7103            .unwrap();
7104        let rule = makefile.rules().next().unwrap();
7105        let recipes: Vec<_> = rule.recipe_nodes().collect();
7106
7107        recipes[1].remove();
7108
7109        let rule = makefile.rules().next().unwrap();
7110        let new_recipes: Vec<_> = rule.recipes().collect();
7111        assert_eq!(new_recipes, vec!["echo one", "echo three"]);
7112    }
7113
7114    #[test]
7115    fn test_recipe_remove_last() {
7116        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7117            .parse()
7118            .unwrap();
7119        let rule = makefile.rules().next().unwrap();
7120        let recipes: Vec<_> = rule.recipe_nodes().collect();
7121
7122        recipes[2].remove();
7123
7124        let rule = makefile.rules().next().unwrap();
7125        let new_recipes: Vec<_> = rule.recipes().collect();
7126        assert_eq!(new_recipes, vec!["echo one", "echo two"]);
7127    }
7128
7129    #[test]
7130    fn test_recipe_multiple_operations() {
7131        let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7132        let rule = makefile.rules().next().unwrap();
7133        let mut recipe = rule.recipe_nodes().next().unwrap();
7134
7135        // Replace text
7136        recipe.replace_text("echo modified");
7137        assert_eq!(recipe.text(), "echo modified");
7138
7139        // Add prefix
7140        recipe.set_prefix("@");
7141        assert_eq!(recipe.text(), "@echo modified");
7142
7143        // Insert after
7144        recipe.insert_after("echo three");
7145
7146        // Verify all changes
7147        let rule = makefile.rules().next().unwrap();
7148        let recipes: Vec<_> = rule.recipes().collect();
7149        assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
7150    }
7151
7152    #[test]
7153    fn test_from_str_relaxed_valid() {
7154        let input = "all: foo\n\tfoo bar\n";
7155        let (makefile, errors) = Makefile::from_str_relaxed(input);
7156        assert!(errors.is_empty());
7157        assert_eq!(makefile.rules().count(), 1);
7158        assert_eq!(makefile.to_string(), input);
7159    }
7160
7161    #[test]
7162    fn test_from_str_relaxed_with_errors() {
7163        // "rule target\n\tcommand" produces a parse error (missing colon)
7164        let input = "rule target\n\tcommand\n";
7165        let (makefile, errors) = Makefile::from_str_relaxed(input);
7166        assert!(!errors.is_empty());
7167        // Round-trip preserves all text
7168        assert_eq!(makefile.to_string(), input);
7169    }
7170
7171    #[test]
7172    fn test_positioned_errors_have_valid_ranges() {
7173        let input = "rule target\n\tcommand\n";
7174        let parsed = Makefile::parse(input);
7175        assert!(!parsed.ok());
7176
7177        let positioned = parsed.positioned_errors();
7178        assert!(!positioned.is_empty());
7179
7180        for err in positioned {
7181            // Range should be within the input
7182            let start: u32 = err.range.start().into();
7183            let end: u32 = err.range.end().into();
7184            assert!(start <= end);
7185            assert!((end as usize) <= input.len());
7186        }
7187    }
7188
7189    #[test]
7190    fn test_positioned_errors_point_to_error_location() {
7191        let input = "rule target\n\tcommand\n";
7192        let parsed = Makefile::parse(input);
7193        assert!(!parsed.ok());
7194
7195        let positioned = parsed.positioned_errors();
7196        assert!(!positioned.is_empty());
7197
7198        let err = &positioned[0];
7199        let start: usize = err.range.start().into();
7200        let end: usize = err.range.end().into();
7201        // The error should point somewhere in the input
7202        let error_text = &input[start..end];
7203        assert!(!error_text.is_empty());
7204
7205        // Tree should still be accessible
7206        let tree = parsed.tree();
7207        assert_eq!(tree.to_string(), input);
7208    }
7209
7210    #[test]
7211    fn test_tree_with_errors_preserves_text() {
7212        let input = "rule target\n\tcommand\nVAR = value\n";
7213        let parsed = Makefile::parse(input);
7214        assert!(!parsed.ok());
7215
7216        let tree = parsed.tree();
7217        assert_eq!(tree.to_string(), input);
7218
7219        // Valid parts should still be accessible
7220        assert_eq!(tree.variable_definitions().count(), 1);
7221    }
7222}
7223
7224#[cfg(test)]
7225mod test_continuation {
7226    use super::*;
7227
7228    #[test]
7229    fn test_recipe_continuation_lines() {
7230        let makefile_content = r#"override_dh_autoreconf:
7231	set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \
7232	  dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \
7233	  sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs
7234	dh_autoreconf
7235"#;
7236
7237        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7238        let rule = makefile.rules().next().unwrap();
7239
7240        let recipes: Vec<_> = rule.recipe_nodes().collect();
7241
7242        // Should have 2 recipe nodes: one multi-line command and one single-line
7243        assert_eq!(recipes.len(), 2);
7244
7245        // First recipe should contain all three physical lines with newlines preserved,
7246        // and the leading tab stripped from each continuation line
7247        let expected_first = "set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \\\n  dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \\\n  sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs";
7248        assert_eq!(recipes[0].text(), expected_first);
7249
7250        // Second recipe should be the standalone dh_autoreconf line
7251        assert_eq!(recipes[1].text(), "dh_autoreconf");
7252    }
7253
7254    #[test]
7255    fn test_simple_continuation() {
7256        let makefile_content = "test:\n\techo hello && \\\n\t  echo world\n";
7257
7258        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7259        let rule = makefile.rules().next().unwrap();
7260        let recipes: Vec<_> = rule.recipe_nodes().collect();
7261
7262        assert_eq!(recipes.len(), 1);
7263        assert_eq!(recipes[0].text(), "echo hello && \\\n  echo world");
7264    }
7265
7266    #[test]
7267    fn test_multiple_continuations() {
7268        let makefile_content = "test:\n\techo line1 && \\\n\t  echo line2 && \\\n\t  echo line3 && \\\n\t  echo line4\n";
7269
7270        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7271        let rule = makefile.rules().next().unwrap();
7272        let recipes: Vec<_> = rule.recipe_nodes().collect();
7273
7274        assert_eq!(recipes.len(), 1);
7275        assert_eq!(
7276            recipes[0].text(),
7277            "echo line1 && \\\n  echo line2 && \\\n  echo line3 && \\\n  echo line4"
7278        );
7279    }
7280
7281    #[test]
7282    fn test_continuation_round_trip() {
7283        let makefile_content = "test:\n\techo hello && \\\n\t  echo world\n\techo done\n";
7284
7285        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7286        let output = makefile.to_string();
7287
7288        // Should preserve the exact content
7289        assert_eq!(output, makefile_content);
7290    }
7291
7292    #[test]
7293    fn test_continuation_with_silent_prefix() {
7294        let makefile_content = "test:\n\t@echo hello && \\\n\t  echo world\n";
7295
7296        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7297        let rule = makefile.rules().next().unwrap();
7298        let recipes: Vec<_> = rule.recipe_nodes().collect();
7299
7300        assert_eq!(recipes.len(), 1);
7301        assert_eq!(recipes[0].text(), "@echo hello && \\\n  echo world");
7302        assert!(recipes[0].is_silent());
7303    }
7304
7305    #[test]
7306    fn test_mixed_continued_and_non_continued() {
7307        let makefile_content = r#"test:
7308	echo first
7309	echo second && \
7310	  echo third
7311	echo fourth
7312"#;
7313
7314        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7315        let rule = makefile.rules().next().unwrap();
7316        let recipes: Vec<_> = rule.recipe_nodes().collect();
7317
7318        assert_eq!(recipes.len(), 3);
7319        assert_eq!(recipes[0].text(), "echo first");
7320        assert_eq!(recipes[1].text(), "echo second && \\\n  echo third");
7321        assert_eq!(recipes[2].text(), "echo fourth");
7322    }
7323
7324    #[test]
7325    fn test_continuation_replace_command() {
7326        let makefile_content = "test:\n\techo hello && \\\n\t  echo world\n\techo done\n";
7327
7328        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7329        let mut rule = makefile.rules().next().unwrap();
7330
7331        // Replace the multi-line command
7332        rule.replace_command(0, "echo replaced");
7333
7334        let recipes: Vec<_> = rule.recipe_nodes().collect();
7335        assert_eq!(recipes.len(), 2);
7336        assert_eq!(recipes[0].text(), "echo replaced");
7337        assert_eq!(recipes[1].text(), "echo done");
7338    }
7339
7340    #[test]
7341    fn test_continuation_count() {
7342        let makefile_content = "test:\n\techo hello && \\\n\t  echo world\n\techo done\n";
7343
7344        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7345        let rule = makefile.rules().next().unwrap();
7346
7347        // Even though there are 3 physical lines, there should be 2 logical recipe nodes
7348        assert_eq!(rule.recipe_count(), 2);
7349        assert_eq!(rule.recipe_nodes().count(), 2);
7350
7351        // recipes() should return one string per logical recipe node
7352        let recipes_list: Vec<_> = rule.recipes().collect();
7353        assert_eq!(
7354            recipes_list,
7355            vec!["echo hello && \\\n  echo world", "echo done"]
7356        );
7357    }
7358
7359    #[test]
7360    fn test_backslash_in_middle_of_line() {
7361        // Backslash not at end should not trigger continuation
7362        let makefile_content = "test:\n\techo hello\\nworld\n\techo done\n";
7363
7364        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7365        let rule = makefile.rules().next().unwrap();
7366        let recipes: Vec<_> = rule.recipe_nodes().collect();
7367
7368        assert_eq!(recipes.len(), 2);
7369        assert_eq!(recipes[0].text(), "echo hello\\nworld");
7370        assert_eq!(recipes[1].text(), "echo done");
7371    }
7372
7373    #[test]
7374    fn test_shell_for_loop_with_continuation() {
7375        // Regression test for Debian bug #1128608 / GitHub issue (if any)
7376        // Ensures shell for loops with backslash continuations are treated as
7377        // a single recipe node and preserve the 'done' statement
7378        let makefile_content = r#"override_dh_installman:
7379	for i in foo bar; do \
7380		pod2man --section=1 $$i ; \
7381	done
7382"#;
7383
7384        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7385        let rule = makefile.rules().next().unwrap();
7386
7387        // Should have exactly 1 recipe node containing the entire for loop
7388        let recipes: Vec<_> = rule.recipe_nodes().collect();
7389        assert_eq!(recipes.len(), 1);
7390
7391        // The recipe text should contain the complete for loop including 'done'
7392        let recipe_text = recipes[0].text();
7393        let expected_recipe = "for i in foo bar; do \\\n\tpod2man --section=1 $$i ; \\\ndone";
7394        assert_eq!(recipe_text, expected_recipe);
7395
7396        // Round-trip should preserve the complete structure
7397        let output = makefile.to_string();
7398        assert_eq!(output, makefile_content);
7399    }
7400
7401    #[test]
7402    fn test_shell_for_loop_remove_command() {
7403        // Regression test: removing other commands shouldn't affect 'done'
7404        // This simulates lintian-brush modifying debian/rules files
7405        let makefile_content = r#"override_dh_installman:
7406	for i in foo bar; do \
7407		pod2man --section=1 $$i ; \
7408	done
7409	echo "Done with man pages"
7410"#;
7411
7412        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7413        let mut rule = makefile.rules().next().unwrap();
7414
7415        // Should have 2 recipe nodes: the for loop and the echo
7416        assert_eq!(rule.recipe_count(), 2);
7417
7418        // Remove the second command (the echo)
7419        rule.remove_command(1);
7420
7421        // Should now have only the for loop
7422        let recipes: Vec<_> = rule.recipe_nodes().collect();
7423        assert_eq!(recipes.len(), 1);
7424
7425        // The for loop should still be complete with 'done'
7426        let output = makefile.to_string();
7427        let expected_output = r#"override_dh_installman:
7428	for i in foo bar; do \
7429		pod2man --section=1 $$i ; \
7430	done
7431"#;
7432        assert_eq!(output, expected_output);
7433    }
7434
7435    #[test]
7436    fn test_variable_reference_paren() {
7437        let makefile: Makefile = "CFLAGS = $(BASE_FLAGS) -Wall\n".parse().unwrap();
7438        let refs: Vec<_> = makefile.variable_references().collect();
7439        assert_eq!(refs.len(), 1);
7440        assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7441        assert_eq!(refs[0].to_string(), "$(BASE_FLAGS)");
7442    }
7443
7444    #[test]
7445    fn test_variable_reference_brace() {
7446        let makefile: Makefile = "CFLAGS = ${BASE_FLAGS} -Wall\n".parse().unwrap();
7447        let refs: Vec<_> = makefile.variable_references().collect();
7448        assert_eq!(refs.len(), 1);
7449        assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7450        assert_eq!(refs[0].to_string(), "${BASE_FLAGS}");
7451    }
7452
7453    #[test]
7454    fn test_variable_reference_in_prerequisites() {
7455        let makefile: Makefile = "all: $(TARGETS)\n".parse().unwrap();
7456        let refs: Vec<_> = makefile.variable_references().collect();
7457        let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7458        assert!(names.contains(&"TARGETS".to_string()));
7459    }
7460
7461    #[test]
7462    fn test_variable_reference_multiple() {
7463        let makefile: Makefile =
7464            "CFLAGS = $(BASE_FLAGS) -Wall\nLDFLAGS = $(BASE_LDFLAGS) -lm\nall: $(TARGETS)\n"
7465                .parse()
7466                .unwrap();
7467        let refs: Vec<_> = makefile.variable_references().collect();
7468        let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7469        assert!(names.contains(&"BASE_FLAGS".to_string()));
7470        assert!(names.contains(&"BASE_LDFLAGS".to_string()));
7471        assert!(names.contains(&"TARGETS".to_string()));
7472    }
7473
7474    #[test]
7475    fn test_variable_reference_nested() {
7476        let makefile: Makefile = "FOO = $($(INNER))\n".parse().unwrap();
7477        let refs: Vec<_> = makefile.variable_references().collect();
7478        let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7479        assert!(names.contains(&"INNER".to_string()));
7480    }
7481
7482    #[test]
7483    fn test_variable_reference_line_col() {
7484        let makefile: Makefile = "A = 1\nB = $(FOO)\n".parse().unwrap();
7485        let refs: Vec<_> = makefile.variable_references().collect();
7486        assert_eq!(refs.len(), 1);
7487        assert_eq!(refs[0].name(), Some("FOO".to_string()));
7488        assert_eq!(refs[0].line(), 1);
7489        assert_eq!(refs[0].column(), 4);
7490        assert_eq!(refs[0].line_col(), (1, 4));
7491    }
7492
7493    #[test]
7494    fn test_variable_reference_no_refs() {
7495        let makefile: Makefile = "A = hello\nall:\n\techo done\n".parse().unwrap();
7496        let refs: Vec<_> = makefile.variable_references().collect();
7497        assert_eq!(refs.len(), 0);
7498    }
7499
7500    #[test]
7501    fn test_variable_reference_mixed_styles() {
7502        let makefile: Makefile = "A = $(FOO) ${BAR}\n".parse().unwrap();
7503        let refs: Vec<_> = makefile.variable_references().collect();
7504        let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7505        assert_eq!(names.len(), 2);
7506        assert!(names.contains(&"FOO".to_string()));
7507        assert!(names.contains(&"BAR".to_string()));
7508    }
7509
7510    #[test]
7511    fn test_brace_variable_in_prerequisites() {
7512        let makefile: Makefile = "all: ${OBJS}\n".parse().unwrap();
7513        let refs: Vec<_> = makefile.variable_references().collect();
7514        assert_eq!(refs.len(), 1);
7515        assert_eq!(refs[0].name(), Some("OBJS".to_string()));
7516    }
7517
7518    #[test]
7519    fn test_parse_brace_variable_roundtrip() {
7520        let input = "CFLAGS = ${BASE_FLAGS} -Wall\n";
7521        let makefile: Makefile = input.parse().unwrap();
7522        assert_eq!(makefile.to_string(), input);
7523    }
7524
7525    #[test]
7526    fn test_parse_nested_variable_in_value_roundtrip() {
7527        let input = "FOO = $(BAR) baz $(QUUX)\n";
7528        let makefile: Makefile = input.parse().unwrap();
7529        assert_eq!(makefile.to_string(), input);
7530    }
7531
7532    #[test]
7533    fn test_is_function_call() {
7534        let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7535        let refs: Vec<_> = makefile.variable_references().collect();
7536        assert_eq!(refs.len(), 1);
7537        assert!(refs[0].is_function_call());
7538    }
7539
7540    #[test]
7541    fn test_is_function_call_simple_variable() {
7542        let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7543        let refs: Vec<_> = makefile.variable_references().collect();
7544        assert_eq!(refs.len(), 1);
7545        assert!(!refs[0].is_function_call());
7546    }
7547
7548    #[test]
7549    fn test_is_function_call_with_commas() {
7550        let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7551        let refs: Vec<_> = makefile.variable_references().collect();
7552        assert_eq!(refs.len(), 1);
7553        assert!(refs[0].is_function_call());
7554    }
7555
7556    #[test]
7557    fn test_is_function_call_braces() {
7558        let makefile: Makefile = "FILES = ${wildcard *.c}\n".parse().unwrap();
7559        let refs: Vec<_> = makefile.variable_references().collect();
7560        assert_eq!(refs.len(), 1);
7561        assert!(refs[0].is_function_call());
7562    }
7563
7564    #[test]
7565    fn test_argument_count_simple_variable() {
7566        let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7567        let refs: Vec<_> = makefile.variable_references().collect();
7568        assert_eq!(refs[0].argument_count(), 0);
7569    }
7570
7571    #[test]
7572    fn test_argument_count_one_arg() {
7573        let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7574        let refs: Vec<_> = makefile.variable_references().collect();
7575        assert_eq!(refs[0].argument_count(), 1);
7576    }
7577
7578    #[test]
7579    fn test_argument_count_three_args() {
7580        let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7581        let refs: Vec<_> = makefile.variable_references().collect();
7582        assert_eq!(refs[0].argument_count(), 3);
7583    }
7584
7585    #[test]
7586    fn test_argument_index_at_offset_subst() {
7587        let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7588        let refs: Vec<_> = makefile.variable_references().collect();
7589        // "X = $(subst a,b,text)"
7590        //  0123456789012345678901
7591        //              ^first arg (offset 12)
7592        //                ^second arg (offset 14)
7593        //                  ^third arg (offset 16)
7594        assert_eq!(refs[0].argument_index_at_offset(12), Some(0));
7595        assert_eq!(refs[0].argument_index_at_offset(14), Some(1));
7596        assert_eq!(refs[0].argument_index_at_offset(16), Some(2));
7597    }
7598
7599    #[test]
7600    fn test_argument_index_at_offset_outside() {
7601        let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7602        let refs: Vec<_> = makefile.variable_references().collect();
7603        // Before the reference
7604        assert_eq!(refs[0].argument_index_at_offset(0), None);
7605        // After the reference
7606        assert_eq!(refs[0].argument_index_at_offset(22), None);
7607    }
7608
7609    #[test]
7610    fn test_argument_index_at_offset_simple_variable() {
7611        let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7612        let refs: Vec<_> = makefile.variable_references().collect();
7613        assert_eq!(refs[0].argument_index_at_offset(11), None);
7614    }
7615
7616    #[test]
7617    fn test_lex_braces() {
7618        use crate::lex::lex;
7619        let tokens = lex("${FOO}");
7620        let kinds: Vec<_> = tokens.iter().map(|(k, _)| *k).collect();
7621        assert!(kinds.contains(&DOLLAR));
7622        assert!(kinds.contains(&LBRACE));
7623        assert!(kinds.contains(&RBRACE));
7624    }
7625
7626    #[test]
7627    fn test_parse_quoted_string_inside_function_call() {
7628        // The lexer emits a balanced quoted string as one QUOTE token, so a
7629        // quoted argument with embedded parentheses must not break paren
7630        // balance tracking inside a $(...) expression. Lone or asymmetric
7631        // quotes (it's, foo'bar) must not swallow the rest of the line.
7632        let cases = [
7633            "X = $(if a,'foo')\n",
7634            "X = $(if a,'foo (bar)')\n",
7635            "X = $(if a,'(')\n",
7636            "X = $(if a,')')\n",
7637            "X = $(if $(SKIP),-k 'not ($(call f,$(s),$(SKIP)))')\n",
7638            "X = foo'bar\nY = baz\n",
7639            "X = it's fine\n",
7640            "X = $(if a,it's)\n",
7641            "X = '\nY = bar\n",
7642        ];
7643        for src in cases {
7644            let parsed: Makefile = src.parse().unwrap_or_else(|e| {
7645                panic!("failed to parse {src:?}: {e:?}");
7646            });
7647            assert_eq!(parsed.to_string(), src, "round-trip mismatch for {src:?}");
7648        }
7649    }
7650}