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    /// Check if this recipe has the silent prefix (@)
1976    ///
1977    /// # Example
1978    /// ```
1979    /// use makefile_lossless::Makefile;
1980    ///
1981    /// let makefile: Makefile = "all:\n\t@echo hello\n\techo world\n".parse().unwrap();
1982    /// let rule = makefile.rules().next().unwrap();
1983    /// let recipes: Vec<_> = rule.recipe_nodes().collect();
1984    /// assert!(recipes[0].is_silent());
1985    /// assert!(!recipes[1].is_silent());
1986    /// ```
1987    pub fn is_silent(&self) -> bool {
1988        let text = self.text();
1989        text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1990    }
1991
1992    /// Check if this recipe has the ignore-errors prefix (-)
1993    ///
1994    /// # Example
1995    /// ```
1996    /// use makefile_lossless::Makefile;
1997    ///
1998    /// let makefile: Makefile = "all:\n\t-echo hello\n\techo world\n".parse().unwrap();
1999    /// let rule = makefile.rules().next().unwrap();
2000    /// let recipes: Vec<_> = rule.recipe_nodes().collect();
2001    /// assert!(recipes[0].is_ignore_errors());
2002    /// assert!(!recipes[1].is_ignore_errors());
2003    /// ```
2004    pub fn is_ignore_errors(&self) -> bool {
2005        let text = self.text();
2006        text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
2007    }
2008
2009    /// Set the command prefix for this recipe
2010    ///
2011    /// The prefix can contain `@` (silent), `-` (ignore errors), and/or `+` (always execute).
2012    /// Pass an empty string to remove all prefixes.
2013    ///
2014    /// # Example
2015    /// ```
2016    /// use makefile_lossless::Makefile;
2017    ///
2018    /// let mut makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
2019    /// let rule = makefile.rules().next().unwrap();
2020    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2021    /// recipe.set_prefix("@");
2022    /// assert_eq!(recipe.text(), "@echo hello");
2023    /// assert!(recipe.is_silent());
2024    /// ```
2025    pub fn set_prefix(&mut self, prefix: &str) {
2026        let text = self.text();
2027
2028        // Strip existing prefix characters
2029        let stripped = text.trim_start_matches(['@', '-', '+']);
2030
2031        // Build new text with the new prefix
2032        let new_text = format!("{}{}", prefix, stripped);
2033
2034        self.replace_text(&new_text);
2035    }
2036
2037    /// Replace the text content of this recipe line
2038    ///
2039    /// # Example
2040    /// ```
2041    /// use makefile_lossless::Makefile;
2042    ///
2043    /// let mut makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
2044    /// let rule = makefile.rules().next().unwrap();
2045    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2046    /// recipe.replace_text("echo world");
2047    /// assert_eq!(recipe.text(), "echo world");
2048    /// ```
2049    pub fn replace_text(&mut self, new_text: &str) {
2050        let node = self.syntax();
2051        let parent = node.parent().expect("Recipe node must have a parent");
2052        let node_index = node.index();
2053
2054        // Build a new RECIPE node with the new text
2055        let mut builder = GreenNodeBuilder::new();
2056        builder.start_node(RECIPE.into());
2057
2058        // Preserve the existing INDENT token if present
2059        if let Some(indent_token) = node
2060            .children_with_tokens()
2061            .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
2062        {
2063            builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
2064        } else {
2065            builder.token(INDENT.into(), "\t");
2066        }
2067
2068        builder.token(TEXT.into(), new_text);
2069
2070        // Preserve the existing NEWLINE token if present
2071        if let Some(newline_token) = node
2072            .children_with_tokens()
2073            .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
2074        {
2075            builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
2076        } else {
2077            builder.token(NEWLINE.into(), "\n");
2078        }
2079
2080        builder.finish_node();
2081        let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2082
2083        // Replace the old node with the new one
2084        parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
2085
2086        // Update self to point to the new node
2087        // Note: index() returns position among all siblings (nodes + tokens)
2088        // so we need to use children_with_tokens() and filter for the node
2089        *self = parent
2090            .children_with_tokens()
2091            .nth(node_index)
2092            .and_then(|element| element.into_node())
2093            .and_then(Recipe::cast)
2094            .expect("New recipe node should exist at the same index");
2095    }
2096
2097    /// Insert a new recipe line before this one
2098    ///
2099    /// # Example
2100    /// ```
2101    /// use makefile_lossless::Makefile;
2102    ///
2103    /// let mut makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
2104    /// let mut rule = makefile.rules().next().unwrap();
2105    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2106    /// recipe.insert_before("echo hello");
2107    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo hello", "echo world"]);
2108    /// ```
2109    pub fn insert_before(&self, text: &str) {
2110        let node = self.syntax();
2111        let parent = node.parent().expect("Recipe node must have a parent");
2112        let node_index = node.index();
2113
2114        // Build a new RECIPE node
2115        let mut builder = GreenNodeBuilder::new();
2116        builder.start_node(RECIPE.into());
2117        builder.token(INDENT.into(), "\t");
2118        builder.token(TEXT.into(), text);
2119        builder.token(NEWLINE.into(), "\n");
2120        builder.finish_node();
2121        let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2122
2123        // Insert before this recipe
2124        parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
2125    }
2126
2127    /// Insert a new recipe line after this one
2128    ///
2129    /// # Example
2130    /// ```
2131    /// use makefile_lossless::Makefile;
2132    ///
2133    /// let mut makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
2134    /// let mut rule = makefile.rules().next().unwrap();
2135    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2136    /// recipe.insert_after("echo world");
2137    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo hello", "echo world"]);
2138    /// ```
2139    pub fn insert_after(&self, text: &str) {
2140        let node = self.syntax();
2141        let parent = node.parent().expect("Recipe node must have a parent");
2142        let node_index = node.index();
2143
2144        // Build a new RECIPE node
2145        let mut builder = GreenNodeBuilder::new();
2146        builder.start_node(RECIPE.into());
2147        builder.token(INDENT.into(), "\t");
2148        builder.token(TEXT.into(), text);
2149        builder.token(NEWLINE.into(), "\n");
2150        builder.finish_node();
2151        let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2152
2153        // Insert after this recipe
2154        parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
2155    }
2156
2157    /// Remove this recipe line from its parent
2158    ///
2159    /// # Example
2160    /// ```
2161    /// use makefile_lossless::Makefile;
2162    ///
2163    /// let mut makefile: Makefile = "all:\n\techo hello\n\techo world\n".parse().unwrap();
2164    /// let mut rule = makefile.rules().next().unwrap();
2165    /// let mut recipe = rule.recipe_nodes().next().unwrap();
2166    /// recipe.remove();
2167    /// assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
2168    /// ```
2169    pub fn remove(&self) {
2170        let node = self.syntax();
2171        let parent = node.parent().expect("Recipe node must have a parent");
2172        let node_index = node.index();
2173
2174        // Remove this recipe node from its parent
2175        parent.splice_children(node_index..node_index + 1, vec![]);
2176    }
2177}
2178
2179///
2180/// This removes trailing NEWLINE tokens from the end of a RULE node to avoid
2181/// extra blank lines at the end of a file when the last rule is removed.
2182pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
2183    // Collect all trailing NEWLINE tokens at the end of the rule and within RECIPE nodes
2184    let mut newlines_to_remove = vec![];
2185    let mut current = node.last_child_or_token();
2186
2187    while let Some(element) = current {
2188        match &element {
2189            rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2190                newlines_to_remove.push(token.clone());
2191                current = token.prev_sibling_or_token();
2192            }
2193            rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
2194                // Also check for trailing newlines in the RECIPE node
2195                let mut recipe_current = n.last_child_or_token();
2196                while let Some(recipe_element) = recipe_current {
2197                    match &recipe_element {
2198                        rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2199                            newlines_to_remove.push(token.clone());
2200                            recipe_current = token.prev_sibling_or_token();
2201                        }
2202                        _ => break,
2203                    }
2204                }
2205                break; // Stop after checking the last RECIPE node
2206            }
2207            _ => break,
2208        }
2209    }
2210
2211    // Remove all but one trailing newline (keep at least one)
2212    // Remove from highest index to lowest to avoid index shifts
2213    if newlines_to_remove.len() > 1 {
2214        // Sort by index descending
2215        newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
2216
2217        for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
2218            let parent = token.parent().unwrap();
2219            let idx = token.index();
2220            parent.splice_children(idx..idx + 1, vec![]);
2221        }
2222    }
2223}
2224
2225/// Helper function to remove a node along with its preceding comments and up to 1 empty line.
2226///
2227/// This walks backward from the node, removing:
2228/// - The node itself
2229/// - All preceding comments (COMMENT tokens)
2230/// - Up to 1 empty line (consecutive NEWLINE tokens)
2231/// - Any WHITESPACE tokens between these elements
2232pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
2233    let mut collected_elements = vec![];
2234    let mut found_comment = false;
2235
2236    // Walk backward to collect preceding comments, newlines, and whitespace
2237    let mut current = node.prev_sibling_or_token();
2238    while let Some(element) = current {
2239        match &element {
2240            rowan::NodeOrToken::Token(token) => match token.kind() {
2241                COMMENT => {
2242                    if token.text().starts_with("#!") {
2243                        break; // Don't remove shebang lines
2244                    }
2245                    found_comment = true;
2246                    collected_elements.push(element.clone());
2247                }
2248                NEWLINE | WHITESPACE => {
2249                    collected_elements.push(element.clone());
2250                }
2251                _ => break, // Hit something else, stop
2252            },
2253            rowan::NodeOrToken::Node(n) => {
2254                // Handle BLANK_LINE nodes which wrap newlines
2255                if n.kind() == BLANK_LINE {
2256                    collected_elements.push(element.clone());
2257                } else {
2258                    break; // Hit another node type, stop
2259                }
2260            }
2261        }
2262        current = element.prev_sibling_or_token();
2263    }
2264
2265    // Determine which preceding elements to remove
2266    // If we found comments, remove them along with up to 1 blank line
2267    let mut elements_to_remove = vec![];
2268    let mut consecutive_newlines = 0;
2269    for element in collected_elements.iter().rev() {
2270        let should_remove = match element {
2271            rowan::NodeOrToken::Token(token) => match token.kind() {
2272                COMMENT => {
2273                    consecutive_newlines = 0;
2274                    found_comment
2275                }
2276                NEWLINE => {
2277                    consecutive_newlines += 1;
2278                    found_comment && consecutive_newlines <= 1
2279                }
2280                WHITESPACE => found_comment,
2281                _ => false,
2282            },
2283            rowan::NodeOrToken::Node(n) => {
2284                // Handle BLANK_LINE nodes (count as newlines)
2285                if n.kind() == BLANK_LINE {
2286                    consecutive_newlines += 1;
2287                    found_comment && consecutive_newlines <= 1
2288                } else {
2289                    false
2290                }
2291            }
2292        };
2293
2294        if should_remove {
2295            elements_to_remove.push(element.clone());
2296        }
2297    }
2298
2299    // Remove elements in reverse order (from highest index to lowest) to avoid index shifts
2300    // Start with the node itself, then preceding elements
2301    let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
2302    all_to_remove.extend(elements_to_remove.into_iter().rev());
2303
2304    // Sort by index in descending order
2305    all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
2306
2307    for element in all_to_remove {
2308        let idx = element.index();
2309        parent.splice_children(idx..idx + 1, vec![]);
2310    }
2311}
2312
2313impl FromStr for Rule {
2314    type Err = crate::Error;
2315
2316    fn from_str(s: &str) -> Result<Self, Self::Err> {
2317        Rule::parse(s).to_rule_result()
2318    }
2319}
2320
2321impl FromStr for Makefile {
2322    type Err = crate::Error;
2323
2324    fn from_str(s: &str) -> Result<Self, Self::Err> {
2325        Makefile::parse(s).to_result()
2326    }
2327}
2328
2329#[cfg(test)]
2330mod tests {
2331    use super::*;
2332    use crate::ast::makefile::MakefileItem;
2333    use crate::pattern::matches_pattern;
2334
2335    #[test]
2336    fn test_conditionals() {
2337        // We'll use relaxed parsing for conditionals
2338
2339        // Basic conditionals - ifdef/ifndef
2340        let code = "ifdef DEBUG\n    DEBUG_FLAG := 1\nendif\n";
2341        let mut buf = code.as_bytes();
2342        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
2343        assert!(makefile.code().contains("DEBUG_FLAG"));
2344
2345        // Basic conditionals - ifeq/ifneq
2346        let code =
2347            "ifeq ($(OS),Windows_NT)\n    RESULT := windows\nelse\n    RESULT := unix\nendif\n";
2348        let mut buf = code.as_bytes();
2349        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
2350        assert!(makefile.code().contains("RESULT"));
2351        assert!(makefile.code().contains("windows"));
2352
2353        // Nested conditionals with else
2354        let code = "ifdef DEBUG\n    CFLAGS += -g\n    ifdef VERBOSE\n        CFLAGS += -v\n    endif\nelse\n    CFLAGS += -O2\nendif\n";
2355        let mut buf = code.as_bytes();
2356        let makefile = Makefile::read_relaxed(&mut buf)
2357            .expect("Failed to parse nested conditionals with else");
2358        assert!(makefile.code().contains("CFLAGS"));
2359        assert!(makefile.code().contains("VERBOSE"));
2360
2361        // Empty conditionals
2362        let code = "ifdef DEBUG\nendif\n";
2363        let mut buf = code.as_bytes();
2364        let makefile =
2365            Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
2366        assert!(makefile.code().contains("ifdef DEBUG"));
2367
2368        // Conditionals with else ifeq
2369        let code = "ifeq ($(OS),Windows)\n    EXT := .exe\nelse ifeq ($(OS),Linux)\n    EXT := .bin\nelse\n    EXT := .out\nendif\n";
2370        let mut buf = code.as_bytes();
2371        let makefile =
2372            Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
2373        assert!(makefile.code().contains("EXT"));
2374
2375        // Invalid conditionals - this should generate parse errors but still produce a Makefile
2376        let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
2377        let mut buf = code.as_bytes();
2378        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
2379        assert!(makefile.code().contains("DEBUG"));
2380
2381        // Missing condition - this should also generate parse errors but still produce a Makefile
2382        let code = "ifdef \nDEBUG := 1\nendif\n";
2383        let mut buf = code.as_bytes();
2384        let makefile = Makefile::read_relaxed(&mut buf)
2385            .expect("Failed to parse with recovery - missing condition");
2386        assert!(makefile.code().contains("DEBUG"));
2387    }
2388
2389    #[test]
2390    fn test_parse_simple() {
2391        const SIMPLE: &str = r#"VARIABLE = value
2392
2393rule: dependency
2394	command
2395"#;
2396        let parsed = parse(SIMPLE, None);
2397        assert!(parsed.errors.is_empty());
2398        let node = parsed.syntax();
2399        assert_eq!(
2400            format!("{:#?}", node),
2401            r#"ROOT@0..44
2402  VARIABLE@0..17
2403    IDENTIFIER@0..8 "VARIABLE"
2404    WHITESPACE@8..9 " "
2405    OPERATOR@9..10 "="
2406    WHITESPACE@10..11 " "
2407    EXPR@11..16
2408      IDENTIFIER@11..16 "value"
2409    NEWLINE@16..17 "\n"
2410  BLANK_LINE@17..18
2411    NEWLINE@17..18 "\n"
2412  RULE@18..44
2413    TARGETS@18..22
2414      IDENTIFIER@18..22 "rule"
2415    OPERATOR@22..23 ":"
2416    WHITESPACE@23..24 " "
2417    PREREQUISITES@24..34
2418      PREREQUISITE@24..34
2419        IDENTIFIER@24..34 "dependency"
2420    NEWLINE@34..35 "\n"
2421    RECIPE@35..44
2422      INDENT@35..36 "\t"
2423      TEXT@36..43 "command"
2424      NEWLINE@43..44 "\n"
2425"#
2426        );
2427
2428        let root = parsed.root();
2429
2430        let mut rules = root.rules().collect::<Vec<_>>();
2431        assert_eq!(rules.len(), 1);
2432        let rule = rules.pop().unwrap();
2433        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2434        assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
2435        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2436
2437        let mut variables = root.variable_definitions().collect::<Vec<_>>();
2438        assert_eq!(variables.len(), 1);
2439        let variable = variables.pop().unwrap();
2440        assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2441        assert_eq!(variable.raw_value(), Some("value".to_string()));
2442    }
2443
2444    #[test]
2445    fn test_parse_export_assign() {
2446        const EXPORT: &str = r#"export VARIABLE := value
2447"#;
2448        let parsed = parse(EXPORT, None);
2449        assert!(parsed.errors.is_empty());
2450        let node = parsed.syntax();
2451        assert_eq!(
2452            format!("{:#?}", node),
2453            r#"ROOT@0..25
2454  VARIABLE@0..25
2455    IDENTIFIER@0..6 "export"
2456    WHITESPACE@6..7 " "
2457    IDENTIFIER@7..15 "VARIABLE"
2458    WHITESPACE@15..16 " "
2459    OPERATOR@16..18 ":="
2460    WHITESPACE@18..19 " "
2461    EXPR@19..24
2462      IDENTIFIER@19..24 "value"
2463    NEWLINE@24..25 "\n"
2464"#
2465        );
2466
2467        let root = parsed.root();
2468
2469        let mut variables = root.variable_definitions().collect::<Vec<_>>();
2470        assert_eq!(variables.len(), 1);
2471        let variable = variables.pop().unwrap();
2472        assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2473        assert_eq!(variable.raw_value(), Some("value".to_string()));
2474    }
2475
2476    #[test]
2477    fn test_parse_multiple_prerequisites() {
2478        const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2479	command
2480
2481"#;
2482        let parsed = parse(MULTIPLE_PREREQUISITES, None);
2483        assert!(parsed.errors.is_empty());
2484        let node = parsed.syntax();
2485        assert_eq!(
2486            format!("{:#?}", node),
2487            r#"ROOT@0..40
2488  RULE@0..40
2489    TARGETS@0..4
2490      IDENTIFIER@0..4 "rule"
2491    OPERATOR@4..5 ":"
2492    WHITESPACE@5..6 " "
2493    PREREQUISITES@6..29
2494      PREREQUISITE@6..17
2495        IDENTIFIER@6..17 "dependency1"
2496      WHITESPACE@17..18 " "
2497      PREREQUISITE@18..29
2498        IDENTIFIER@18..29 "dependency2"
2499    NEWLINE@29..30 "\n"
2500    RECIPE@30..39
2501      INDENT@30..31 "\t"
2502      TEXT@31..38 "command"
2503      NEWLINE@38..39 "\n"
2504    NEWLINE@39..40 "\n"
2505"#
2506        );
2507        let root = parsed.root();
2508
2509        let rule = root.rules().next().unwrap();
2510        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2511        assert_eq!(
2512            rule.prerequisites().collect::<Vec<_>>(),
2513            vec!["dependency1", "dependency2"]
2514        );
2515        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2516    }
2517
2518    #[test]
2519    fn test_add_rule() {
2520        let mut makefile = Makefile::new();
2521        let rule = makefile.add_rule("rule");
2522        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2523        assert_eq!(
2524            rule.prerequisites().collect::<Vec<_>>(),
2525            Vec::<String>::new()
2526        );
2527
2528        assert_eq!(makefile.to_string(), "rule:\n");
2529    }
2530
2531    #[test]
2532    fn test_add_rule_with_shebang() {
2533        // Regression test for bug where add_rule() panics on makefiles with shebangs
2534        let content = r#"#!/usr/bin/make -f
2535
2536build: blah
2537	$(MAKE) install
2538
2539clean:
2540	dh_clean
2541"#;
2542
2543        let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2544        let initial_count = makefile.rules().count();
2545        assert_eq!(initial_count, 2);
2546
2547        // This should not panic
2548        let rule = makefile.add_rule("build-indep");
2549        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2550
2551        // Should have one more rule now
2552        assert_eq!(makefile.rules().count(), initial_count + 1);
2553    }
2554
2555    #[test]
2556    fn test_add_rule_formatting() {
2557        // Regression test for formatting issues when adding rules
2558        let content = r#"build: blah
2559	$(MAKE) install
2560
2561clean:
2562	dh_clean
2563"#;
2564
2565        let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2566        let mut rule = makefile.add_rule("build-indep");
2567        rule.add_prerequisite("build").unwrap();
2568
2569        let expected = r#"build: blah
2570	$(MAKE) install
2571
2572clean:
2573	dh_clean
2574
2575build-indep: build
2576"#;
2577
2578        assert_eq!(makefile.to_string(), expected);
2579    }
2580
2581    #[test]
2582    fn test_push_command() {
2583        let mut makefile = Makefile::new();
2584        let mut rule = makefile.add_rule("rule");
2585
2586        // Add commands in place to the rule
2587        rule.push_command("command");
2588        rule.push_command("command2");
2589
2590        // Check the commands in the rule
2591        assert_eq!(
2592            rule.recipes().collect::<Vec<_>>(),
2593            vec!["command", "command2"]
2594        );
2595
2596        // Add a third command
2597        rule.push_command("command3");
2598        assert_eq!(
2599            rule.recipes().collect::<Vec<_>>(),
2600            vec!["command", "command2", "command3"]
2601        );
2602
2603        // Check if the makefile was modified
2604        assert_eq!(
2605            makefile.to_string(),
2606            "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2607        );
2608
2609        // The rule should have the same string representation
2610        assert_eq!(
2611            rule.to_string(),
2612            "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2613        );
2614    }
2615
2616    #[test]
2617    fn test_replace_command() {
2618        let mut makefile = Makefile::new();
2619        let mut rule = makefile.add_rule("rule");
2620
2621        // Add commands in place
2622        rule.push_command("command");
2623        rule.push_command("command2");
2624
2625        // Check the commands in the rule
2626        assert_eq!(
2627            rule.recipes().collect::<Vec<_>>(),
2628            vec!["command", "command2"]
2629        );
2630
2631        // Replace the first command
2632        rule.replace_command(0, "new command");
2633        assert_eq!(
2634            rule.recipes().collect::<Vec<_>>(),
2635            vec!["new command", "command2"]
2636        );
2637
2638        // Check if the makefile was modified
2639        assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2640
2641        // The rule should have the same string representation
2642        assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2643    }
2644
2645    #[test]
2646    fn test_replace_command_with_comments() {
2647        // Regression test for bug where replace_command() inserts instead of replacing
2648        // when the rule contains comments
2649        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";
2650
2651        let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2652
2653        let mut rule = makefile.rules().next().unwrap();
2654
2655        // Before replacement, there should be 2 recipe nodes (comment + command)
2656        assert_eq!(rule.recipe_nodes().count(), 2);
2657        let recipes: Vec<_> = rule.recipe_nodes().collect();
2658        assert_eq!(recipes[0].text(), ""); // comment-only
2659        assert_eq!(
2660            recipes[1].text(),
2661            "dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"
2662        );
2663
2664        // Replace the second recipe (index 1, the actual command)
2665        assert!(rule.replace_command(1, "dh_strip"));
2666
2667        // After replacement, there should still be 2 recipe nodes
2668        assert_eq!(rule.recipe_nodes().count(), 2);
2669        let recipes: Vec<_> = rule.recipe_nodes().collect();
2670        assert_eq!(recipes[0].text(), ""); // comment still there
2671        assert_eq!(recipes[1].text(), "dh_strip");
2672    }
2673
2674    #[test]
2675    fn test_parse_rule_without_newline() {
2676        let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2677        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2678        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2679        let rule = "rule: dependency".parse::<Rule>().unwrap();
2680        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2681        assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2682    }
2683
2684    #[test]
2685    fn test_parse_makefile_without_newline() {
2686        let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2687        assert_eq!(makefile.rules().count(), 1);
2688    }
2689
2690    #[test]
2691    fn test_from_reader() {
2692        let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2693        assert_eq!(makefile.rules().count(), 1);
2694    }
2695
2696    #[test]
2697    fn test_parse_with_tab_after_last_newline() {
2698        let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2699        assert_eq!(makefile.rules().count(), 1);
2700    }
2701
2702    #[test]
2703    fn test_parse_with_space_after_last_newline() {
2704        let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2705        assert_eq!(makefile.rules().count(), 1);
2706    }
2707
2708    #[test]
2709    fn test_parse_with_comment_after_last_newline() {
2710        let makefile =
2711            Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2712        assert_eq!(makefile.rules().count(), 1);
2713    }
2714
2715    #[test]
2716    fn test_parse_with_variable_rule() {
2717        let makefile =
2718            Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2719                .unwrap();
2720
2721        // Check variable definition
2722        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2723        assert_eq!(vars.len(), 1);
2724        assert_eq!(vars[0].name(), Some("RULE".to_string()));
2725        assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2726
2727        // Check rule
2728        let rules = makefile.rules().collect::<Vec<_>>();
2729        assert_eq!(rules.len(), 1);
2730        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2731        assert_eq!(
2732            rules[0].prerequisites().collect::<Vec<_>>(),
2733            vec!["dependency"]
2734        );
2735        assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2736    }
2737
2738    #[test]
2739    fn test_parse_with_variable_dependency() {
2740        let makefile =
2741            Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2742
2743        // Check variable definition
2744        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2745        assert_eq!(vars.len(), 1);
2746        assert_eq!(vars[0].name(), Some("DEP".to_string()));
2747        assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2748
2749        // Check rule
2750        let rules = makefile.rules().collect::<Vec<_>>();
2751        assert_eq!(rules.len(), 1);
2752        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2753        assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2754        assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2755    }
2756
2757    #[test]
2758    fn test_parse_with_variable_command() {
2759        let makefile =
2760            Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2761
2762        // Check variable definition
2763        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2764        assert_eq!(vars.len(), 1);
2765        assert_eq!(vars[0].name(), Some("COM".to_string()));
2766        assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2767
2768        // Check rule
2769        let rules = makefile.rules().collect::<Vec<_>>();
2770        assert_eq!(rules.len(), 1);
2771        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2772        assert_eq!(
2773            rules[0].prerequisites().collect::<Vec<_>>(),
2774            vec!["dependency"]
2775        );
2776        assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2777    }
2778
2779    #[test]
2780    fn test_regular_line_error_reporting() {
2781        let input = "rule target\n\tcommand";
2782
2783        // Test both APIs with one input
2784        let parsed = parse(input, None);
2785        let direct_error = &parsed.errors[0];
2786
2787        // Verify error is detected with correct details
2788        assert_eq!(direct_error.line, 2);
2789        assert!(
2790            direct_error.message.contains("expected"),
2791            "Error message should contain 'expected': {}",
2792            direct_error.message
2793        );
2794        assert_eq!(direct_error.context, "\tcommand");
2795
2796        // Check public API
2797        let reader_result = Makefile::from_reader(input.as_bytes());
2798        let parse_error = match reader_result {
2799            Ok(_) => panic!("Expected Parse error from from_reader"),
2800            Err(err) => match err {
2801                self::Error::Parse(parse_err) => parse_err,
2802                _ => panic!("Expected Parse error"),
2803            },
2804        };
2805
2806        // Verify formatting includes line number and context
2807        let error_text = parse_error.to_string();
2808        assert!(error_text.contains("Error at line 2:"));
2809        assert!(error_text.contains("2| \tcommand"));
2810    }
2811
2812    #[test]
2813    fn test_parsing_error_context_with_bad_syntax() {
2814        // Input with unusual characters to ensure they're preserved
2815        let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2816
2817        // With our relaxed parsing, verify we either get a proper error or parse successfully
2818        match Makefile::from_reader(input.as_bytes()) {
2819            Ok(makefile) => {
2820                // If it parses successfully, our parser is robust enough to handle unusual characters
2821                assert_eq!(
2822                    makefile.rules().count(),
2823                    0,
2824                    "Should not have found any rules"
2825                );
2826            }
2827            Err(err) => match err {
2828                self::Error::Parse(error) => {
2829                    // Verify error details are properly reported
2830                    assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2831                    assert!(
2832                        !error.errors[0].context.is_empty(),
2833                        "Error context should not be empty"
2834                    );
2835                }
2836                _ => panic!("Unexpected error type"),
2837            },
2838        };
2839    }
2840
2841    #[test]
2842    fn test_error_message_format() {
2843        // Test the error formatter directly
2844        let parse_error = ParseError {
2845            errors: vec![ErrorInfo {
2846                message: "test error".to_string(),
2847                line: 42,
2848                context: "some problematic code".to_string(),
2849            }],
2850        };
2851
2852        let error_text = parse_error.to_string();
2853        assert!(error_text.contains("Error at line 42: test error"));
2854        assert!(error_text.contains("42| some problematic code"));
2855    }
2856
2857    #[test]
2858    fn test_line_number_calculation() {
2859        // Test inputs for various error locations
2860        let test_cases = [
2861            ("rule dependency\n\tcommand", 2),             // Missing colon
2862            ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2),              // Strange characters
2863            ("var = value\n#comment\n\tindented line", 3), // Indented line not part of a rule
2864        ];
2865
2866        for (input, expected_line) in test_cases {
2867            // Attempt to parse the input
2868            match input.parse::<Makefile>() {
2869                Ok(_) => {
2870                    // If the parser succeeds, that's fine - our parser is more robust
2871                    // Skip assertions when there's no error to check
2872                    continue;
2873                }
2874                Err(err) => {
2875                    if let Error::Parse(parse_err) = err {
2876                        // Verify error line number matches expected line
2877                        assert_eq!(
2878                            parse_err.errors[0].line, expected_line,
2879                            "Line number should match the expected line"
2880                        );
2881
2882                        // If the error is about indentation, check that the context includes the tab
2883                        if parse_err.errors[0].message.contains("indented") {
2884                            assert!(
2885                                parse_err.errors[0].context.starts_with('\t'),
2886                                "Context for indentation errors should include the tab character"
2887                            );
2888                        }
2889                    } else {
2890                        panic!("Expected parse error, got: {:?}", err);
2891                    }
2892                }
2893            }
2894        }
2895    }
2896
2897    #[test]
2898    fn test_conditional_features() {
2899        // Simple use of variables in conditionals
2900        let code = r#"
2901# Set variables based on DEBUG flag
2902ifdef DEBUG
2903    CFLAGS += -g -DDEBUG
2904else
2905    CFLAGS = -O2
2906endif
2907
2908# Define a build rule
2909all: $(OBJS)
2910	$(CC) $(CFLAGS) -o $@ $^
2911"#;
2912
2913        let mut buf = code.as_bytes();
2914        let makefile =
2915            Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2916
2917        // Instead of checking for variable definitions which might not get created
2918        // due to conditionals, let's verify that we can parse the content without errors
2919        assert!(!makefile.code().is_empty(), "Makefile has content");
2920
2921        // Check that we detected a rule
2922        let rules = makefile.rules().collect::<Vec<_>>();
2923        assert!(!rules.is_empty(), "Should have found rules");
2924
2925        // Verify conditional presence in the original code
2926        assert!(code.contains("ifdef DEBUG"));
2927        assert!(code.contains("endif"));
2928
2929        // Also try with an explicitly defined variable
2930        let code_with_var = r#"
2931# Define a variable first
2932CC = gcc
2933
2934ifdef DEBUG
2935    CFLAGS += -g -DDEBUG
2936else
2937    CFLAGS = -O2
2938endif
2939
2940all: $(OBJS)
2941	$(CC) $(CFLAGS) -o $@ $^
2942"#;
2943
2944        let mut buf = code_with_var.as_bytes();
2945        let makefile =
2946            Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2947
2948        // Now we should definitely find at least the CC variable
2949        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2950        assert!(
2951            !vars.is_empty(),
2952            "Should have found at least the CC variable definition"
2953        );
2954    }
2955
2956    #[test]
2957    fn test_include_directive() {
2958        let parsed = parse(
2959            "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2960            None,
2961        );
2962        assert!(parsed.errors.is_empty());
2963        let node = parsed.syntax();
2964        assert!(format!("{:#?}", node).contains("INCLUDE@"));
2965    }
2966
2967    #[test]
2968    fn test_export_variables() {
2969        let parsed = parse("export SHELL := /bin/bash\n", None);
2970        assert!(parsed.errors.is_empty());
2971        let makefile = parsed.root();
2972        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2973        assert_eq!(vars.len(), 1);
2974        let shell_var = vars
2975            .iter()
2976            .find(|v| v.name() == Some("SHELL".to_string()))
2977            .unwrap();
2978        assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2979    }
2980
2981    #[test]
2982    fn test_bare_export_variable() {
2983        // "export VARNAME" without assignment operator is a valid GNU Make directive
2984        // that exports a previously-defined variable.
2985        let parsed = parse(
2986            "DEB_CFLAGS_MAINT_APPEND = -Wno-error\nexport DEB_CFLAGS_MAINT_APPEND\n\n%:\n\tdh $@\n",
2987            None,
2988        );
2989        assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2990        let makefile = parsed.root();
2991        // The bare export should be parsed as a variable, not a rule
2992        let vars = makefile.variable_definitions().collect::<Vec<_>>();
2993        assert_eq!(vars.len(), 2);
2994        // The pattern rule should be found
2995        let rules = makefile.rules().collect::<Vec<_>>();
2996        assert_eq!(rules.len(), 1);
2997        assert!(rules[0].targets().any(|t| t == "%"));
2998        // build-arch should match via the pattern rule
2999        assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
3000    }
3001
3002    #[test]
3003    fn test_bare_export_at_eof() {
3004        // Bare "export VARNAME" at end of file (no trailing newline)
3005        let parsed = parse("VAR = value\nexport VAR", None);
3006        assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3007        let makefile = parsed.root();
3008        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3009        assert_eq!(vars.len(), 2);
3010        assert_eq!(makefile.rules().count(), 0);
3011    }
3012
3013    #[test]
3014    fn test_bare_export_does_not_eat_include() {
3015        // Bare "export VARNAME" must not consume subsequent include directives
3016        let parsed = parse("VAR = value\nexport VAR\ninclude other.mk\n", None);
3017        assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3018        let makefile = parsed.root();
3019        assert_eq!(makefile.includes().count(), 1);
3020        assert_eq!(
3021            makefile.included_files().collect::<Vec<_>>(),
3022            vec!["other.mk"]
3023        );
3024    }
3025
3026    #[test]
3027    fn test_bare_export_multiple() {
3028        // Multiple bare exports in a row
3029        let parsed = parse(
3030            "A = 1\nB = 2\nexport A\nexport B\n\nall:\n\techo done\n",
3031            None,
3032        );
3033        assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3034        let makefile = parsed.root();
3035        assert_eq!(makefile.variable_definitions().count(), 4);
3036        let rules = makefile.rules().collect::<Vec<_>>();
3037        assert_eq!(rules.len(), 1);
3038        assert!(rules[0].targets().any(|t| t == "all"));
3039    }
3040
3041    #[test]
3042    fn test_parse_error_does_not_cross_lines() {
3043        // A line that fails to parse as a rule (no colon) must not
3044        // consume tokens from subsequent lines.
3045        let parsed = parse("notarule\n\nbuild-arch:\n\techo arch\n", None);
3046        let makefile = parsed.root();
3047        let rules = makefile.rules().collect::<Vec<_>>();
3048        // The "notarule" line may produce an error, but build-arch must still be found
3049        assert!(
3050            rules.iter().any(|r| r.targets().any(|t| t == "build-arch")),
3051            "build-arch rule should be parsed despite earlier error; rules: {:?}",
3052            rules
3053                .iter()
3054                .map(|r| r.targets().collect::<Vec<_>>())
3055                .collect::<Vec<_>>()
3056        );
3057    }
3058
3059    #[test]
3060    fn test_pyfai_rules_full() {
3061        // Real-world pyFAI debian/rules that triggered #1131043
3062        let input = "\
3063#!/usr/bin/make -f
3064
3065export DH_VERBOSE=1
3066export PYBUILD_NAME=pyfai
3067
3068DEB_CFLAGS_MAINT_APPEND = -Wno-error=incompatible-pointer-types
3069export DEB_CFLAGS_MAINT_APPEND
3070
3071PY3VER := $(shell py3versions -dv)
3072
3073include /usr/share/dpkg/pkg-info.mk # sets SOURCE_DATE_EPOCH
3074
3075%:
3076\tdh $@ --buildsystem=pybuild
3077
3078override_dh_auto_build-arch:
3079\tPYBUILD_BUILD_ARGS=\"-Ccompile-args=--verbose\" dh_auto_build
3080
3081override_dh_auto_build-indep: override_dh_auto_build-arch
3082\tsphinx-build -N -bhtml doc/source build/html
3083
3084override_dh_auto_test:
3085
3086execute_after_dh_auto_install:
3087\tdh_install -p pyfai debian/python3-pyfai/usr/bin /usr
3088";
3089        let parsed = parse(input, None);
3090        let makefile = parsed.root();
3091
3092        // Include must be detected
3093        assert_eq!(makefile.includes().count(), 1);
3094
3095        // Pattern rule must be found
3096        assert!(
3097            makefile.find_rule_by_target_pattern("build-arch").is_some(),
3098            "build-arch should match via %: pattern rule"
3099        );
3100        assert!(
3101            makefile
3102                .find_rule_by_target_pattern("build-indep")
3103                .is_some(),
3104            "build-indep should match via %: pattern rule"
3105        );
3106
3107        // All override/execute_after rules must be found
3108        let rule_targets: Vec<Vec<String>> =
3109            makefile.rules().map(|r| r.targets().collect()).collect();
3110        assert!(
3111            rule_targets.iter().any(|t| t.contains(&"%".to_string())),
3112            "missing %: rule; got: {:?}",
3113            rule_targets
3114        );
3115        assert!(
3116            rule_targets
3117                .iter()
3118                .any(|t| t.contains(&"override_dh_auto_build-arch".to_string())),
3119            "missing override_dh_auto_build-arch; got: {:?}",
3120            rule_targets
3121        );
3122        assert!(
3123            rule_targets
3124                .iter()
3125                .any(|t| t.contains(&"override_dh_auto_test".to_string())),
3126            "missing override_dh_auto_test; got: {:?}",
3127            rule_targets
3128        );
3129        assert!(
3130            rule_targets
3131                .iter()
3132                .any(|t| t.contains(&"execute_after_dh_auto_install".to_string())),
3133            "missing execute_after_dh_auto_install; got: {:?}",
3134            rule_targets
3135        );
3136    }
3137
3138    #[test]
3139    fn test_variable_scopes() {
3140        let parsed = parse(
3141            "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
3142            None,
3143        );
3144        assert!(parsed.errors.is_empty());
3145        let makefile = parsed.root();
3146        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3147        assert_eq!(vars.len(), 4);
3148        let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
3149        assert!(var_names.contains(&"SIMPLE".to_string()));
3150        assert!(var_names.contains(&"IMMEDIATE".to_string()));
3151        assert!(var_names.contains(&"CONDITIONAL".to_string()));
3152        assert!(var_names.contains(&"APPEND".to_string()));
3153    }
3154
3155    #[test]
3156    fn test_pattern_rule_parsing() {
3157        let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
3158        assert!(parsed.errors.is_empty());
3159        let makefile = parsed.root();
3160        let rules = makefile.rules().collect::<Vec<_>>();
3161        assert_eq!(rules.len(), 1);
3162        assert_eq!(rules[0].targets().next().unwrap(), "%.o");
3163        assert!(rules[0].recipes().next().unwrap().contains("$@"));
3164    }
3165
3166    #[test]
3167    fn test_include_variants() {
3168        // Test all variants of include directives
3169        let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
3170        let parsed = parse(makefile_str, None);
3171        assert!(parsed.errors.is_empty());
3172
3173        // Get the syntax tree for inspection
3174        let node = parsed.syntax();
3175        let debug_str = format!("{:#?}", node);
3176
3177        // Check that all includes are correctly parsed as INCLUDE nodes
3178        assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
3179
3180        // Check that we can access the includes through the AST
3181        let makefile = parsed.root();
3182
3183        // Count all child nodes that are INCLUDE kind
3184        let include_count = makefile
3185            .syntax()
3186            .children()
3187            .filter(|child| child.kind() == INCLUDE)
3188            .count();
3189        assert_eq!(include_count, 4);
3190
3191        // Test variable expansion in include paths
3192        assert!(makefile
3193            .included_files()
3194            .any(|path| path.contains("$(VAR)")));
3195    }
3196
3197    #[test]
3198    fn test_include_api() {
3199        // Test the API for working with include directives
3200        let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
3201        let makefile: Makefile = makefile_str.parse().unwrap();
3202
3203        // Test the includes method
3204        let includes: Vec<_> = makefile.includes().collect();
3205        assert_eq!(includes.len(), 3);
3206
3207        // Test the is_optional method
3208        assert!(!includes[0].is_optional()); // include
3209        assert!(includes[1].is_optional()); // -include
3210        assert!(includes[2].is_optional()); // sinclude
3211
3212        // Test the included_files method
3213        let files: Vec<_> = makefile.included_files().collect();
3214        assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
3215
3216        // Test the path method on Include
3217        assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
3218        assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
3219        assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
3220    }
3221
3222    #[test]
3223    fn test_include_integration() {
3224        // Test include directives in realistic makefile contexts
3225
3226        // Case 1: With .PHONY (which was a source of the original issue)
3227        let phony_makefile = Makefile::from_reader(
3228            ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3229            .as_bytes()
3230        ).unwrap();
3231
3232        // We expect 2 rules: .PHONY and rule
3233        assert_eq!(phony_makefile.rules().count(), 2);
3234
3235        // But only one non-special rule (not starting with '.')
3236        let normal_rules_count = phony_makefile
3237            .rules()
3238            .filter(|r| !r.targets().any(|t| t.starts_with('.')))
3239            .count();
3240        assert_eq!(normal_rules_count, 1);
3241
3242        // Verify we have the include directive
3243        assert_eq!(phony_makefile.includes().count(), 1);
3244        assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
3245
3246        // Case 2: Without .PHONY, just a regular rule and include
3247        let simple_makefile = Makefile::from_reader(
3248            "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3249                .as_bytes(),
3250        )
3251        .unwrap();
3252        assert_eq!(simple_makefile.rules().count(), 1);
3253        assert_eq!(simple_makefile.includes().count(), 1);
3254    }
3255
3256    #[test]
3257    fn test_real_conditional_directives() {
3258        // Basic if/else conditional
3259        let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
3260        let mut buf = conditional.as_bytes();
3261        let makefile =
3262            Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
3263        let code = makefile.code();
3264        assert!(code.contains("ifdef DEBUG"));
3265        assert!(code.contains("else"));
3266        assert!(code.contains("endif"));
3267
3268        // ifdef with nested ifdef
3269        let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
3270        let mut buf = nested.as_bytes();
3271        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
3272        let code = makefile.code();
3273        assert!(code.contains("ifdef DEBUG"));
3274        assert!(code.contains("ifdef VERBOSE"));
3275
3276        // ifeq form
3277        let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
3278        let mut buf = ifeq.as_bytes();
3279        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
3280        let code = makefile.code();
3281        assert!(code.contains("ifeq"));
3282        assert!(code.contains("Windows_NT"));
3283    }
3284
3285    #[test]
3286    fn test_indented_text_outside_rules() {
3287        // Simple help target with echo commands
3288        let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \"  help     show help\"\n";
3289        let parsed = parse(help_text, None);
3290        assert!(parsed.errors.is_empty());
3291
3292        // Verify recipes are correctly parsed
3293        let root = parsed.root();
3294        let rules = root.rules().collect::<Vec<_>>();
3295        assert_eq!(rules.len(), 1);
3296
3297        let help_rule = &rules[0];
3298        let recipes = help_rule.recipes().collect::<Vec<_>>();
3299        assert_eq!(recipes.len(), 2);
3300        assert!(recipes[0].contains("Available targets"));
3301        assert!(recipes[1].contains("help"));
3302    }
3303
3304    #[test]
3305    fn test_comment_handling_in_recipes() {
3306        // Create a recipe with a comment line
3307        let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
3308
3309        // Parse the recipe
3310        let parsed = parse(recipe_comment, None);
3311
3312        // Verify no parsing errors
3313        assert!(
3314            parsed.errors.is_empty(),
3315            "Should parse recipe with comments without errors"
3316        );
3317
3318        // Check rule structure
3319        let root = parsed.root();
3320        let rules = root.rules().collect::<Vec<_>>();
3321        assert_eq!(rules.len(), 1, "Should find exactly one rule");
3322
3323        // Check the rule has the correct name
3324        let build_rule = &rules[0];
3325        assert_eq!(
3326            build_rule.targets().collect::<Vec<_>>(),
3327            vec!["build"],
3328            "Rule should have 'build' as target"
3329        );
3330
3331        // Check recipes are parsed correctly
3332        // recipes() now returns all recipe nodes including comment-only lines
3333        let recipes = build_rule.recipe_nodes().collect::<Vec<_>>();
3334        assert_eq!(recipes.len(), 2, "Should find two recipe nodes");
3335
3336        // First recipe should be comment-only
3337        assert_eq!(recipes[0].text(), "");
3338        assert_eq!(
3339            recipes[0].comment(),
3340            Some("# This is a comment".to_string())
3341        );
3342
3343        // Second recipe should be the command
3344        assert_eq!(recipes[1].text(), "gcc -o app main.c");
3345        assert_eq!(recipes[1].comment(), None);
3346    }
3347
3348    #[test]
3349    fn test_multiline_variables() {
3350        // Simple multiline variable test
3351        let multiline = "SOURCES = main.c \\\n          util.c\n";
3352
3353        // Parse the multiline variable
3354        let parsed = parse(multiline, None);
3355
3356        // We can extract the variable even with errors (since backslash handling is not perfect)
3357        let root = parsed.root();
3358        let vars = root.variable_definitions().collect::<Vec<_>>();
3359        assert!(!vars.is_empty(), "Should find at least one variable");
3360
3361        // Test other multiline variable forms
3362
3363        // := assignment operator
3364        let operators = "CFLAGS := -Wall \\\n         -Werror\n";
3365        let parsed_operators = parse(operators, None);
3366
3367        // Extract variable with := operator
3368        let root = parsed_operators.root();
3369        let vars = root.variable_definitions().collect::<Vec<_>>();
3370        assert!(
3371            !vars.is_empty(),
3372            "Should find at least one variable with := operator"
3373        );
3374
3375        // += assignment operator
3376        let append = "LDFLAGS += -L/usr/lib \\\n          -lm\n";
3377        let parsed_append = parse(append, None);
3378
3379        // Extract variable with += operator
3380        let root = parsed_append.root();
3381        let vars = root.variable_definitions().collect::<Vec<_>>();
3382        assert!(
3383            !vars.is_empty(),
3384            "Should find at least one variable with += operator"
3385        );
3386    }
3387
3388    #[test]
3389    fn test_whitespace_and_eof_handling() {
3390        // Test 1: File ending with blank lines
3391        let blank_lines = "VAR = value\n\n\n";
3392
3393        let parsed_blank = parse(blank_lines, None);
3394
3395        // We should be able to extract the variable definition
3396        let root = parsed_blank.root();
3397        let vars = root.variable_definitions().collect::<Vec<_>>();
3398        assert_eq!(
3399            vars.len(),
3400            1,
3401            "Should find one variable in blank lines test"
3402        );
3403
3404        // Test 2: File ending with space
3405        let trailing_space = "VAR = value \n";
3406
3407        let parsed_space = parse(trailing_space, None);
3408
3409        // We should be able to extract the variable definition
3410        let root = parsed_space.root();
3411        let vars = root.variable_definitions().collect::<Vec<_>>();
3412        assert_eq!(
3413            vars.len(),
3414            1,
3415            "Should find one variable in trailing space test"
3416        );
3417
3418        // Test 3: No final newline
3419        let no_newline = "VAR = value";
3420
3421        let parsed_no_newline = parse(no_newline, None);
3422
3423        // Regardless of parsing errors, we should be able to extract the variable
3424        let root = parsed_no_newline.root();
3425        let vars = root.variable_definitions().collect::<Vec<_>>();
3426        assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
3427        assert_eq!(
3428            vars[0].name(),
3429            Some("VAR".to_string()),
3430            "Variable name should be VAR"
3431        );
3432    }
3433
3434    #[test]
3435    fn test_complex_variable_references() {
3436        // Simple function call
3437        let wildcard = "SOURCES = $(wildcard *.c)\n";
3438        let parsed = parse(wildcard, None);
3439        assert!(parsed.errors.is_empty());
3440
3441        // Nested variable reference
3442        let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3443        let parsed = parse(nested, None);
3444        assert!(parsed.errors.is_empty());
3445
3446        // Function with complex arguments
3447        let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3448        let parsed = parse(patsubst, None);
3449        assert!(parsed.errors.is_empty());
3450    }
3451
3452    #[test]
3453    fn test_complex_variable_references_minimal() {
3454        // Simple function call
3455        let wildcard = "SOURCES = $(wildcard *.c)\n";
3456        let parsed = parse(wildcard, None);
3457        assert!(parsed.errors.is_empty());
3458
3459        // Nested variable reference
3460        let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3461        let parsed = parse(nested, None);
3462        assert!(parsed.errors.is_empty());
3463
3464        // Function with complex arguments
3465        let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3466        let parsed = parse(patsubst, None);
3467        assert!(parsed.errors.is_empty());
3468    }
3469
3470    #[test]
3471    fn test_multiline_variable_with_backslash() {
3472        let content = r#"
3473LONG_VAR = This is a long variable \
3474    that continues on the next line \
3475    and even one more line
3476"#;
3477
3478        // For now, we'll use relaxed parsing since the backslash handling isn't fully implemented
3479        let mut buf = content.as_bytes();
3480        let makefile =
3481            Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
3482
3483        // Check that we can extract the variable even with errors
3484        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3485        assert_eq!(
3486            vars.len(),
3487            1,
3488            "Expected 1 variable but found {}",
3489            vars.len()
3490        );
3491        let var_value = vars[0].raw_value();
3492        assert!(var_value.is_some(), "Variable value is None");
3493
3494        // The value might not be perfect due to relaxed parsing, but it should contain most of the content
3495        let value_str = var_value.unwrap();
3496        assert!(
3497            value_str.contains("long variable"),
3498            "Value doesn't contain expected content"
3499        );
3500    }
3501
3502    #[test]
3503    fn test_multiline_variable_with_mixed_operators() {
3504        let content = r#"
3505PREFIX ?= /usr/local
3506CFLAGS := -Wall -O2 \
3507    -I$(PREFIX)/include \
3508    -DDEBUG
3509"#;
3510        // Use relaxed parsing for now
3511        let mut buf = content.as_bytes();
3512        let makefile = Makefile::read_relaxed(&mut buf)
3513            .expect("Failed to parse multiline variable with operators");
3514
3515        // Check that we can extract variables even with errors
3516        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3517        assert!(
3518            !vars.is_empty(),
3519            "Expected at least 1 variable, found {}",
3520            vars.len()
3521        );
3522
3523        // Check PREFIX variable
3524        let prefix_var = vars
3525            .iter()
3526            .find(|v| v.name().unwrap_or_default() == "PREFIX");
3527        assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
3528        assert!(
3529            prefix_var.unwrap().raw_value().is_some(),
3530            "PREFIX variable has no value"
3531        );
3532
3533        // CFLAGS may be parsed incompletely but should exist in some form
3534        let cflags_var = vars
3535            .iter()
3536            .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
3537        assert!(
3538            cflags_var.is_some(),
3539            "Expected to find CFLAGS variable (or part of it)"
3540        );
3541    }
3542
3543    #[test]
3544    fn test_indented_help_text() {
3545        let content = r#"
3546.PHONY: help
3547help:
3548	@echo "Available targets:"
3549	@echo "  build  - Build the project"
3550	@echo "  test   - Run tests"
3551	@echo "  clean  - Remove build artifacts"
3552"#;
3553        // Use relaxed parsing for now
3554        let mut buf = content.as_bytes();
3555        let makefile =
3556            Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
3557
3558        // Check that we can extract rules even with errors
3559        let rules = makefile.rules().collect::<Vec<_>>();
3560        assert!(!rules.is_empty(), "Expected at least one rule");
3561
3562        // Find help rule
3563        let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
3564        assert!(help_rule.is_some(), "Expected to find help rule");
3565
3566        // Check recipes - they might not be perfectly parsed but should exist
3567        let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
3568        assert!(
3569            !recipes.is_empty(),
3570            "Expected at least one recipe line in help rule"
3571        );
3572        assert!(
3573            recipes.iter().any(|r| r.contains("Available targets")),
3574            "Expected to find 'Available targets' in recipes"
3575        );
3576    }
3577
3578    #[test]
3579    fn test_indented_lines_in_conditionals() {
3580        let content = r#"
3581ifdef DEBUG
3582    CFLAGS += -g -DDEBUG
3583    # This is a comment inside conditional
3584    ifdef VERBOSE
3585        CFLAGS += -v
3586    endif
3587endif
3588"#;
3589        // Use relaxed parsing for conditionals with indented lines
3590        let mut buf = content.as_bytes();
3591        let makefile = Makefile::read_relaxed(&mut buf)
3592            .expect("Failed to parse indented lines in conditionals");
3593
3594        // Check that we detected conditionals
3595        let code = makefile.code();
3596        assert!(code.contains("ifdef DEBUG"));
3597        assert!(code.contains("ifdef VERBOSE"));
3598        assert!(code.contains("endif"));
3599    }
3600
3601    #[test]
3602    fn test_recipe_with_colon() {
3603        let content = r#"
3604build:
3605	@echo "Building at: $(shell date)"
3606	gcc -o program main.c
3607"#;
3608        let parsed = parse(content, None);
3609        assert!(
3610            parsed.errors.is_empty(),
3611            "Failed to parse recipe with colon: {:?}",
3612            parsed.errors
3613        );
3614    }
3615
3616    #[test]
3617    fn test_double_colon_rules() {
3618        let content = r#"
3619%.o :: %.c
3620	$(CC) -c $< -o $@
3621
3622# Double colon allows multiple rules for same target
3623all:: prerequisite1
3624	@echo "First rule for all"
3625
3626all:: prerequisite2
3627	@echo "Second rule for all"
3628"#;
3629        let parsed = parse(content, None);
3630        assert!(
3631            parsed.errors.is_empty(),
3632            "Failed to parse double colon rules: {:?}",
3633            parsed.errors
3634        );
3635
3636        let makefile = parsed.root();
3637        let rules: Vec<_> = makefile.rules().collect();
3638        assert_eq!(rules.len(), 3);
3639
3640        // All rules should be double-colon
3641        for rule in &rules {
3642            assert!(rule.is_double_colon());
3643        }
3644
3645        // Check targets
3646        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["%.o"]);
3647        assert_eq!(rules[1].targets().collect::<Vec<_>>(), vec!["all"]);
3648        assert_eq!(rules[2].targets().collect::<Vec<_>>(), vec!["all"]);
3649
3650        // Check prerequisites
3651        assert_eq!(
3652            rules[1].prerequisites().collect::<Vec<_>>(),
3653            vec!["prerequisite1"]
3654        );
3655        assert_eq!(
3656            rules[2].prerequisites().collect::<Vec<_>>(),
3657            vec!["prerequisite2"]
3658        );
3659    }
3660
3661    #[test]
3662    fn test_else_conditional_directives() {
3663        // Test else ifeq
3664        let content = r#"
3665ifeq ($(OS),Windows_NT)
3666    TARGET = windows
3667else ifeq ($(OS),Darwin)
3668    TARGET = macos
3669else ifeq ($(OS),Linux)
3670    TARGET = linux
3671else
3672    TARGET = unknown
3673endif
3674"#;
3675        let mut buf = content.as_bytes();
3676        let makefile =
3677            Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3678        assert!(makefile.code().contains("else ifeq"));
3679        assert!(makefile.code().contains("TARGET"));
3680
3681        // Test else ifdef
3682        let content = r#"
3683ifdef WINDOWS
3684    TARGET = windows
3685else ifdef DARWIN
3686    TARGET = macos
3687else ifdef LINUX
3688    TARGET = linux
3689else
3690    TARGET = unknown
3691endif
3692"#;
3693        let mut buf = content.as_bytes();
3694        let makefile =
3695            Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3696        assert!(makefile.code().contains("else ifdef"));
3697
3698        // Test else ifndef
3699        let content = r#"
3700ifndef NOWINDOWS
3701    TARGET = windows
3702else ifndef NODARWIN
3703    TARGET = macos
3704else
3705    TARGET = linux
3706endif
3707"#;
3708        let mut buf = content.as_bytes();
3709        let makefile =
3710            Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3711        assert!(makefile.code().contains("else ifndef"));
3712
3713        // Test else ifneq
3714        let content = r#"
3715ifneq ($(OS),Windows_NT)
3716    TARGET = not_windows
3717else ifneq ($(OS),Darwin)
3718    TARGET = not_macos
3719else
3720    TARGET = darwin
3721endif
3722"#;
3723        let mut buf = content.as_bytes();
3724        let makefile =
3725            Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3726        assert!(makefile.code().contains("else ifneq"));
3727    }
3728
3729    #[test]
3730    fn test_complex_else_conditionals() {
3731        // Test complex nested else conditionals with mixed types
3732        let content = r#"VAR1 := foo
3733VAR2 := bar
3734
3735ifeq ($(VAR1),foo)
3736    RESULT := foo_matched
3737else ifdef VAR2
3738    RESULT := var2_defined
3739else ifndef VAR3
3740    RESULT := var3_not_defined
3741else
3742    RESULT := final_else
3743endif
3744
3745all:
3746	@echo $(RESULT)
3747"#;
3748        let mut buf = content.as_bytes();
3749        let makefile =
3750            Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3751
3752        // Verify the structure is preserved
3753        let code = makefile.code();
3754        assert!(code.contains("ifeq ($(VAR1),foo)"));
3755        assert!(code.contains("else ifdef VAR2"));
3756        assert!(code.contains("else ifndef VAR3"));
3757        assert!(code.contains("else"));
3758        assert!(code.contains("endif"));
3759        assert!(code.contains("RESULT"));
3760
3761        // Verify rules are still parsed correctly
3762        let rules: Vec<_> = makefile.rules().collect();
3763        assert_eq!(rules.len(), 1);
3764        assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3765    }
3766
3767    #[test]
3768    fn test_conditional_token_structure() {
3769        // Test that conditionals have proper token structure
3770        let content = r#"ifdef VAR1
3771X := 1
3772else ifdef VAR2
3773X := 2
3774else
3775X := 3
3776endif
3777"#;
3778        let mut buf = content.as_bytes();
3779        let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3780
3781        // Check that we can traverse the syntax tree
3782        let syntax = makefile.syntax();
3783
3784        // Find CONDITIONAL nodes
3785        let mut found_conditional = false;
3786        let mut found_conditional_if = false;
3787        let mut found_conditional_else = false;
3788        let mut found_conditional_endif = false;
3789
3790        fn check_node(
3791            node: &SyntaxNode,
3792            found_cond: &mut bool,
3793            found_if: &mut bool,
3794            found_else: &mut bool,
3795            found_endif: &mut bool,
3796        ) {
3797            match node.kind() {
3798                SyntaxKind::CONDITIONAL => *found_cond = true,
3799                SyntaxKind::CONDITIONAL_IF => *found_if = true,
3800                SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3801                SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3802                _ => {}
3803            }
3804
3805            for child in node.children() {
3806                check_node(&child, found_cond, found_if, found_else, found_endif);
3807            }
3808        }
3809
3810        check_node(
3811            syntax,
3812            &mut found_conditional,
3813            &mut found_conditional_if,
3814            &mut found_conditional_else,
3815            &mut found_conditional_endif,
3816        );
3817
3818        assert!(found_conditional, "Should have CONDITIONAL node");
3819        assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3820        assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3821        assert!(
3822            found_conditional_endif,
3823            "Should have CONDITIONAL_ENDIF node"
3824        );
3825    }
3826
3827    #[test]
3828    fn test_ambiguous_assignment_vs_rule() {
3829        // Test case: Variable assignment with equals sign
3830        const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3831
3832        let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3833        let makefile =
3834            Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3835
3836        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3837        let rules = makefile.rules().collect::<Vec<_>>();
3838
3839        assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3840        assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3841
3842        assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3843
3844        // Test case: Simple rule with colon
3845        const SIMPLE_RULE: &str = "target: dependency\n";
3846
3847        let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3848        let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3849
3850        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3851        let rules = makefile.rules().collect::<Vec<_>>();
3852
3853        assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3854        assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3855
3856        let rule = &rules[0];
3857        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3858    }
3859
3860    #[test]
3861    fn test_nested_conditionals() {
3862        let content = r#"
3863ifdef RELEASE
3864    CFLAGS += -O3
3865    ifndef DEBUG
3866        ifneq ($(ARCH),arm)
3867            CFLAGS += -march=native
3868        else
3869            CFLAGS += -mcpu=cortex-a72
3870        endif
3871    endif
3872endif
3873"#;
3874        // Use relaxed parsing for nested conditionals test
3875        let mut buf = content.as_bytes();
3876        let makefile =
3877            Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3878
3879        // Check that we detected conditionals
3880        let code = makefile.code();
3881        assert!(code.contains("ifdef RELEASE"));
3882        assert!(code.contains("ifndef DEBUG"));
3883        assert!(code.contains("ifneq"));
3884    }
3885
3886    #[test]
3887    fn test_space_indented_recipes() {
3888        // This test is expected to fail with current implementation
3889        // It should pass once the parser is more flexible with indentation
3890        let content = r#"
3891build:
3892    @echo "Building with spaces instead of tabs"
3893    gcc -o program main.c
3894"#;
3895        // Use relaxed parsing for now
3896        let mut buf = content.as_bytes();
3897        let makefile =
3898            Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3899
3900        // Check that we can extract rules even with errors
3901        let rules = makefile.rules().collect::<Vec<_>>();
3902        assert!(!rules.is_empty(), "Expected at least one rule");
3903
3904        // Find build rule
3905        let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3906        assert!(build_rule.is_some(), "Expected to find build rule");
3907    }
3908
3909    #[test]
3910    fn test_complex_variable_functions() {
3911        let content = r#"
3912FILES := $(shell find . -name "*.c")
3913OBJS := $(patsubst %.c,%.o,$(FILES))
3914NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3915HEADERS := ${wildcard *.h}
3916"#;
3917        let parsed = parse(content, None);
3918        assert!(
3919            parsed.errors.is_empty(),
3920            "Failed to parse complex variable functions: {:?}",
3921            parsed.errors
3922        );
3923    }
3924
3925    #[test]
3926    fn test_nested_variable_expansions() {
3927        let content = r#"
3928VERSION = 1.0
3929PACKAGE = myapp
3930TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3931INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3932"#;
3933        let parsed = parse(content, None);
3934        assert!(
3935            parsed.errors.is_empty(),
3936            "Failed to parse nested variable expansions: {:?}",
3937            parsed.errors
3938        );
3939    }
3940
3941    #[test]
3942    fn test_special_directives() {
3943        let content = r#"
3944# Special makefile directives
3945.PHONY: all clean
3946.SUFFIXES: .c .o
3947.DEFAULT: all
3948
3949# Variable definition and export directive
3950export PATH := /usr/bin:/bin
3951"#;
3952        // Use relaxed parsing to allow for special directives
3953        let mut buf = content.as_bytes();
3954        let makefile =
3955            Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3956
3957        // Check that we can extract rules even with errors
3958        let rules = makefile.rules().collect::<Vec<_>>();
3959
3960        // Find phony rule
3961        let phony_rule = rules
3962            .iter()
3963            .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3964        assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3965
3966        // Check that variables can be extracted
3967        let vars = makefile.variable_definitions().collect::<Vec<_>>();
3968        assert!(!vars.is_empty(), "Expected to find at least one variable");
3969    }
3970
3971    // Comprehensive Test combining multiple issues
3972
3973    #[test]
3974    fn test_comprehensive_real_world_makefile() {
3975        // Simple makefile with basic elements
3976        let content = r#"
3977# Basic variable assignment
3978VERSION = 1.0.0
3979
3980# Phony target
3981.PHONY: all clean
3982
3983# Simple rule
3984all:
3985	echo "Building version $(VERSION)"
3986
3987# Another rule with dependencies
3988clean:
3989	rm -f *.o
3990"#;
3991
3992        // Parse the content
3993        let parsed = parse(content, None);
3994
3995        // Check that parsing succeeded
3996        assert!(parsed.errors.is_empty(), "Expected no parsing errors");
3997
3998        // Check that we found variables
3999        let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
4000        assert!(!variables.is_empty(), "Expected at least one variable");
4001        assert_eq!(
4002            variables[0].name(),
4003            Some("VERSION".to_string()),
4004            "Expected VERSION variable"
4005        );
4006
4007        // Check that we found rules
4008        let rules = parsed.root().rules().collect::<Vec<_>>();
4009        assert!(!rules.is_empty(), "Expected at least one rule");
4010
4011        // Check for specific rules
4012        let rule_targets: Vec<String> = rules
4013            .iter()
4014            .flat_map(|r| r.targets().collect::<Vec<_>>())
4015            .collect();
4016        assert!(
4017            rule_targets.contains(&".PHONY".to_string()),
4018            "Expected .PHONY rule"
4019        );
4020        assert!(
4021            rule_targets.contains(&"all".to_string()),
4022            "Expected 'all' rule"
4023        );
4024        assert!(
4025            rule_targets.contains(&"clean".to_string()),
4026            "Expected 'clean' rule"
4027        );
4028    }
4029
4030    #[test]
4031    fn test_indented_help_text_outside_rules() {
4032        // Create test content with indented help text
4033        let content = r#"
4034# Targets with help text
4035help:
4036    @echo "Available targets:"
4037    @echo "  build      build the project"
4038    @echo "  test       run tests"
4039    @echo "  clean      clean build artifacts"
4040
4041# Another target
4042clean:
4043	rm -rf build/
4044"#;
4045
4046        // Parse the content
4047        let parsed = parse(content, None);
4048
4049        // Verify parsing succeeded
4050        assert!(
4051            parsed.errors.is_empty(),
4052            "Failed to parse indented help text"
4053        );
4054
4055        // Check that we found the expected rules
4056        let rules = parsed.root().rules().collect::<Vec<_>>();
4057        assert_eq!(rules.len(), 2, "Expected to find two rules");
4058
4059        // Find the rules by target
4060        let help_rule = rules
4061            .iter()
4062            .find(|r| r.targets().any(|t| t == "help"))
4063            .expect("Expected to find help rule");
4064
4065        let clean_rule = rules
4066            .iter()
4067            .find(|r| r.targets().any(|t| t == "clean"))
4068            .expect("Expected to find clean rule");
4069
4070        // Check help rule has expected recipe lines
4071        let help_recipes = help_rule.recipes().collect::<Vec<_>>();
4072        assert!(
4073            !help_recipes.is_empty(),
4074            "Help rule should have recipe lines"
4075        );
4076        assert!(
4077            help_recipes
4078                .iter()
4079                .any(|line| line.contains("Available targets")),
4080            "Help recipes should include 'Available targets' line"
4081        );
4082
4083        // Check clean rule has expected recipe
4084        let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
4085        assert!(
4086            !clean_recipes.is_empty(),
4087            "Clean rule should have recipe lines"
4088        );
4089        assert!(
4090            clean_recipes.iter().any(|line| line.contains("rm -rf")),
4091            "Clean recipes should include 'rm -rf' command"
4092        );
4093    }
4094
4095    #[test]
4096    fn test_makefile1_phony_pattern() {
4097        // Replicate the specific pattern in Makefile_1 that caused issues
4098        let content = "#line 2145\n.PHONY: $(PHONY)\n";
4099
4100        // Parse the content
4101        let result = parse(content, None);
4102
4103        // Verify no parsing errors
4104        assert!(
4105            result.errors.is_empty(),
4106            "Failed to parse .PHONY: $(PHONY) pattern"
4107        );
4108
4109        // Check that the rule was parsed correctly
4110        let rules = result.root().rules().collect::<Vec<_>>();
4111        assert_eq!(rules.len(), 1, "Expected 1 rule");
4112        assert_eq!(
4113            rules[0].targets().next().unwrap(),
4114            ".PHONY",
4115            "Expected .PHONY rule"
4116        );
4117
4118        // Check that the prerequisite contains the variable reference
4119        let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
4120        assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
4121        assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
4122    }
4123
4124    #[test]
4125    fn test_skip_until_newline_behavior() {
4126        // Test the skip_until_newline function to cover the != vs == mutant
4127        let input = "text without newline";
4128        let parsed = parse(input, None);
4129        // This should handle gracefully without infinite loops
4130        assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4131
4132        let input_with_newline = "text\nafter newline";
4133        let parsed2 = parse(input_with_newline, None);
4134        assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
4135    }
4136
4137    #[test]
4138    #[ignore] // Ignored until proper handling of orphaned indented lines is implemented
4139    fn test_error_with_indent_token() {
4140        // Test the error logic with INDENT token to cover the ! deletion mutant
4141        let input = "\tinvalid indented line";
4142        let parsed = parse(input, None);
4143        // Should produce an error about indented line not part of a rule
4144        assert!(!parsed.errors.is_empty());
4145
4146        let error_msg = &parsed.errors[0].message;
4147        assert!(error_msg.contains("recipe commences before first target"));
4148    }
4149
4150    #[test]
4151    fn test_conditional_token_handling() {
4152        // Test conditional token handling to cover the == vs != mutant
4153        let input = r#"
4154ifndef VAR
4155    CFLAGS = -DTEST
4156endif
4157"#;
4158        let parsed = parse(input, None);
4159        // Test that parsing doesn't panic and produces some result
4160        let makefile = parsed.root();
4161        let _vars = makefile.variable_definitions().collect::<Vec<_>>();
4162        // Should handle conditionals, possibly with errors but without crashing
4163
4164        // Test with nested conditionals
4165        let nested = r#"
4166ifdef DEBUG
4167    ifndef RELEASE
4168        CFLAGS = -g
4169    endif
4170endif
4171"#;
4172        let parsed_nested = parse(nested, None);
4173        // Test that parsing doesn't panic
4174        let _makefile = parsed_nested.root();
4175    }
4176
4177    #[test]
4178    fn test_include_vs_conditional_logic() {
4179        // Test the include vs conditional logic to cover the == vs != mutant at line 743
4180        let input = r#"
4181include file.mk
4182ifdef VAR
4183    VALUE = 1
4184endif
4185"#;
4186        let parsed = parse(input, None);
4187        // Test that parsing doesn't panic and produces some result
4188        let makefile = parsed.root();
4189        let includes = makefile.includes().collect::<Vec<_>>();
4190        // Should recognize include directive
4191        assert!(!includes.is_empty() || !parsed.errors.is_empty());
4192
4193        // Test with -include
4194        let optional_include = r#"
4195-include optional.mk
4196ifndef VAR
4197    VALUE = default
4198endif
4199"#;
4200        let parsed2 = parse(optional_include, None);
4201        // Test that parsing doesn't panic
4202        let _makefile = parsed2.root();
4203    }
4204
4205    #[test]
4206    fn test_balanced_parens_counting() {
4207        // Test balanced parentheses parsing to cover the += vs -= mutant
4208        let input = r#"
4209VAR = $(call func,$(nested,arg),extra)
4210COMPLEX = $(if $(condition),$(then_val),$(else_val))
4211"#;
4212        let parsed = parse(input, None);
4213        assert!(parsed.errors.is_empty());
4214
4215        let makefile = parsed.root();
4216        let vars = makefile.variable_definitions().collect::<Vec<_>>();
4217        assert_eq!(vars.len(), 2);
4218    }
4219
4220    #[test]
4221    fn test_documentation_lookahead() {
4222        // Test the documentation lookahead logic to cover the - vs + mutant at line 895
4223        let input = r#"
4224# Documentation comment
4225help:
4226	@echo "Usage instructions"
4227	@echo "More help text"
4228"#;
4229        let parsed = parse(input, None);
4230        assert!(parsed.errors.is_empty());
4231
4232        let makefile = parsed.root();
4233        let rules = makefile.rules().collect::<Vec<_>>();
4234        assert_eq!(rules.len(), 1);
4235        assert_eq!(rules[0].targets().next().unwrap(), "help");
4236    }
4237
4238    #[test]
4239    fn test_edge_case_empty_input() {
4240        // Test with empty input
4241        let parsed = parse("", None);
4242        assert!(parsed.errors.is_empty());
4243
4244        // Test with only whitespace
4245        let parsed2 = parse("   \n  \n", None);
4246        // Some parsers might report warnings/errors for whitespace-only input
4247        // Just ensure it doesn't crash
4248        let _makefile = parsed2.root();
4249    }
4250
4251    #[test]
4252    fn test_malformed_conditional_recovery() {
4253        // Test parser recovery from malformed conditionals
4254        let input = r#"
4255ifdef
4256    # Missing condition variable
4257endif
4258"#;
4259        let parsed = parse(input, None);
4260        // Parser should either handle gracefully or report appropriate errors
4261        // Not checking for specific error since parsing strategy may vary
4262        assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4263    }
4264
4265    #[test]
4266    fn test_replace_rule() {
4267        let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4268        let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4269
4270        makefile.replace_rule(0, new_rule).unwrap();
4271
4272        let targets: Vec<_> = makefile
4273            .rules()
4274            .flat_map(|r| r.targets().collect::<Vec<_>>())
4275            .collect();
4276        assert_eq!(targets, vec!["new_rule", "rule2"]);
4277
4278        let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
4279        assert_eq!(recipes, vec!["new_command"]);
4280    }
4281
4282    #[test]
4283    fn test_replace_rule_out_of_bounds() {
4284        let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4285        let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4286
4287        let result = makefile.replace_rule(5, new_rule);
4288        assert!(result.is_err());
4289    }
4290
4291    #[test]
4292    fn test_remove_rule() {
4293        let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
4294            .parse()
4295            .unwrap();
4296
4297        let removed = makefile.remove_rule(1).unwrap();
4298        assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
4299
4300        let remaining_targets: Vec<_> = makefile
4301            .rules()
4302            .flat_map(|r| r.targets().collect::<Vec<_>>())
4303            .collect();
4304        assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
4305        assert_eq!(makefile.rules().count(), 2);
4306    }
4307
4308    #[test]
4309    fn test_remove_rule_out_of_bounds() {
4310        let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4311
4312        let result = makefile.remove_rule(5);
4313        assert!(result.is_err());
4314    }
4315
4316    #[test]
4317    fn test_insert_rule() {
4318        let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4319        let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
4320
4321        makefile.insert_rule(1, new_rule).unwrap();
4322
4323        let targets: Vec<_> = makefile
4324            .rules()
4325            .flat_map(|r| r.targets().collect::<Vec<_>>())
4326            .collect();
4327        assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
4328        assert_eq!(makefile.rules().count(), 3);
4329    }
4330
4331    #[test]
4332    fn test_insert_rule_at_end() {
4333        let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4334        let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
4335
4336        makefile.insert_rule(1, new_rule).unwrap();
4337
4338        let targets: Vec<_> = makefile
4339            .rules()
4340            .flat_map(|r| r.targets().collect::<Vec<_>>())
4341            .collect();
4342        assert_eq!(targets, vec!["rule1", "end_rule"]);
4343    }
4344
4345    #[test]
4346    fn test_insert_rule_out_of_bounds() {
4347        let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4348        let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4349
4350        let result = makefile.insert_rule(5, new_rule);
4351        assert!(result.is_err());
4352    }
4353
4354    #[test]
4355    fn test_insert_rule_preserves_blank_line_spacing_at_end() {
4356        // Test that inserting at the end preserves blank line spacing
4357        let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
4358        let mut makefile: Makefile = input.parse().unwrap();
4359        let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4360
4361        makefile.insert_rule(2, new_rule).unwrap();
4362
4363        let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4364        assert_eq!(makefile.to_string(), expected);
4365    }
4366
4367    #[test]
4368    fn test_insert_rule_adds_blank_lines_when_missing() {
4369        // Test that inserting adds blank lines even when input has none
4370        let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
4371        let mut makefile: Makefile = input.parse().unwrap();
4372        let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4373
4374        makefile.insert_rule(2, new_rule).unwrap();
4375
4376        let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4377        assert_eq!(makefile.to_string(), expected);
4378    }
4379
4380    #[test]
4381    fn test_remove_command() {
4382        let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4383            .parse()
4384            .unwrap();
4385
4386        rule.remove_command(1);
4387        let recipes: Vec<_> = rule.recipes().collect();
4388        assert_eq!(recipes, vec!["command1", "command3"]);
4389        assert_eq!(rule.recipe_count(), 2);
4390    }
4391
4392    #[test]
4393    fn test_remove_command_out_of_bounds() {
4394        let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4395
4396        let result = rule.remove_command(5);
4397        assert!(!result);
4398    }
4399
4400    #[test]
4401    fn test_insert_command() {
4402        let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
4403
4404        rule.insert_command(1, "command2");
4405        let recipes: Vec<_> = rule.recipes().collect();
4406        assert_eq!(recipes, vec!["command1", "command2", "command3"]);
4407    }
4408
4409    #[test]
4410    fn test_insert_command_at_end() {
4411        let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4412
4413        rule.insert_command(1, "command2");
4414        let recipes: Vec<_> = rule.recipes().collect();
4415        assert_eq!(recipes, vec!["command1", "command2"]);
4416    }
4417
4418    #[test]
4419    fn test_insert_command_in_empty_rule() {
4420        let mut rule: Rule = "rule:\n".parse().unwrap();
4421
4422        rule.insert_command(0, "new_command");
4423        let recipes: Vec<_> = rule.recipes().collect();
4424        assert_eq!(recipes, vec!["new_command"]);
4425    }
4426
4427    #[test]
4428    fn test_recipe_count() {
4429        let rule1: Rule = "rule:\n".parse().unwrap();
4430        assert_eq!(rule1.recipe_count(), 0);
4431
4432        let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
4433        assert_eq!(rule2.recipe_count(), 2);
4434    }
4435
4436    #[test]
4437    fn test_clear_commands() {
4438        let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4439            .parse()
4440            .unwrap();
4441
4442        rule.clear_commands();
4443        assert_eq!(rule.recipe_count(), 0);
4444
4445        let recipes: Vec<_> = rule.recipes().collect();
4446        assert_eq!(recipes, Vec::<String>::new());
4447
4448        // Rule target should still be preserved
4449        let targets: Vec<_> = rule.targets().collect();
4450        assert_eq!(targets, vec!["rule"]);
4451    }
4452
4453    #[test]
4454    fn test_clear_commands_empty_rule() {
4455        let mut rule: Rule = "rule:\n".parse().unwrap();
4456
4457        rule.clear_commands();
4458        assert_eq!(rule.recipe_count(), 0);
4459
4460        let targets: Vec<_> = rule.targets().collect();
4461        assert_eq!(targets, vec!["rule"]);
4462    }
4463
4464    #[test]
4465    fn test_rule_manipulation_preserves_structure() {
4466        // Test that makefile structure (comments, variables, etc.) is preserved during rule manipulation
4467        let input = r#"# Comment
4468VAR = value
4469
4470rule1:
4471	command1
4472
4473# Another comment
4474rule2:
4475	command2
4476
4477VAR2 = value2
4478"#;
4479
4480        let mut makefile: Makefile = input.parse().unwrap();
4481        let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4482
4483        // Insert rule in the middle
4484        makefile.insert_rule(1, new_rule).unwrap();
4485
4486        // Check that rules are correct
4487        let targets: Vec<_> = makefile
4488            .rules()
4489            .flat_map(|r| r.targets().collect::<Vec<_>>())
4490            .collect();
4491        assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
4492
4493        // Check that variables are preserved
4494        let vars: Vec<_> = makefile.variable_definitions().collect();
4495        assert_eq!(vars.len(), 2);
4496
4497        // The structure should be preserved in the output
4498        let output = makefile.code();
4499        assert!(output.contains("# Comment"));
4500        assert!(output.contains("VAR = value"));
4501        assert!(output.contains("# Another comment"));
4502        assert!(output.contains("VAR2 = value2"));
4503    }
4504
4505    #[test]
4506    fn test_replace_rule_with_multiple_targets() {
4507        let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
4508        let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
4509
4510        makefile.replace_rule(0, new_rule).unwrap();
4511
4512        let targets: Vec<_> = makefile
4513            .rules()
4514            .flat_map(|r| r.targets().collect::<Vec<_>>())
4515            .collect();
4516        assert_eq!(targets, vec!["new_target"]);
4517    }
4518
4519    #[test]
4520    fn test_empty_makefile_operations() {
4521        let mut makefile = Makefile::new();
4522
4523        // Test operations on empty makefile
4524        assert!(makefile
4525            .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
4526            .is_err());
4527        assert!(makefile.remove_rule(0).is_err());
4528
4529        // Insert into empty makefile should work
4530        let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
4531        makefile.insert_rule(0, new_rule).unwrap();
4532        assert_eq!(makefile.rules().count(), 1);
4533    }
4534
4535    #[test]
4536    fn test_command_operations_preserve_indentation() {
4537        let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
4538            .parse()
4539            .unwrap();
4540
4541        rule.insert_command(1, "middle_command");
4542        let recipes: Vec<_> = rule.recipes().collect();
4543        assert_eq!(
4544            recipes,
4545            vec!["\tdeep_indent", "middle_command", "shallow_indent"]
4546        );
4547    }
4548
4549    #[test]
4550    fn test_rule_operations_with_variables_and_includes() {
4551        let input = r#"VAR1 = value1
4552include common.mk
4553
4554rule1:
4555	command1
4556
4557VAR2 = value2
4558include other.mk
4559
4560rule2:
4561	command2
4562"#;
4563
4564        let mut makefile: Makefile = input.parse().unwrap();
4565
4566        // Remove middle rule
4567        makefile.remove_rule(0).unwrap();
4568
4569        // Verify structure is preserved
4570        let output = makefile.code();
4571        assert!(output.contains("VAR1 = value1"));
4572        assert!(output.contains("include common.mk"));
4573        assert!(output.contains("VAR2 = value2"));
4574        assert!(output.contains("include other.mk"));
4575
4576        // Only rule2 should remain
4577        assert_eq!(makefile.rules().count(), 1);
4578        let remaining_targets: Vec<_> = makefile
4579            .rules()
4580            .flat_map(|r| r.targets().collect::<Vec<_>>())
4581            .collect();
4582        assert_eq!(remaining_targets, vec!["rule2"]);
4583    }
4584
4585    #[test]
4586    fn test_command_manipulation_edge_cases() {
4587        // Test with rule that has no commands
4588        let mut empty_rule: Rule = "empty:\n".parse().unwrap();
4589        assert_eq!(empty_rule.recipe_count(), 0);
4590
4591        empty_rule.insert_command(0, "first_command");
4592        assert_eq!(empty_rule.recipe_count(), 1);
4593
4594        // Test clearing already empty rule
4595        let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
4596        empty_rule2.clear_commands();
4597        assert_eq!(empty_rule2.recipe_count(), 0);
4598    }
4599
4600    #[test]
4601    fn test_large_makefile_performance() {
4602        // Create a makefile with many rules to test performance doesn't degrade
4603        let mut makefile = Makefile::new();
4604
4605        // Add 100 rules
4606        for i in 0..100 {
4607            let rule_name = format!("rule{}", i);
4608            makefile
4609                .add_rule(&rule_name)
4610                .push_command(&format!("command{}", i));
4611        }
4612
4613        assert_eq!(makefile.rules().count(), 100);
4614
4615        // Replace rule in the middle - should be efficient
4616        let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
4617        makefile.replace_rule(50, new_rule).unwrap();
4618
4619        // Verify the change
4620        let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
4621        assert_eq!(rule_50_targets, vec!["middle_rule"]);
4622
4623        assert_eq!(makefile.rules().count(), 100); // Count unchanged
4624    }
4625
4626    #[test]
4627    fn test_complex_recipe_manipulation() {
4628        let mut complex_rule: Rule = r#"complex:
4629	@echo "Starting build"
4630	$(CC) $(CFLAGS) -o $@ $<
4631	@echo "Build complete"
4632	chmod +x $@
4633"#
4634        .parse()
4635        .unwrap();
4636
4637        assert_eq!(complex_rule.recipe_count(), 4);
4638
4639        // Remove the echo statements, keep the actual build commands
4640        complex_rule.remove_command(0); // Remove first echo
4641        complex_rule.remove_command(1); // Remove second echo (now at index 1, not 2)
4642
4643        let final_recipes: Vec<_> = complex_rule.recipes().collect();
4644        assert_eq!(final_recipes.len(), 2);
4645        assert!(final_recipes[0].contains("$(CC)"));
4646        assert!(final_recipes[1].contains("chmod"));
4647    }
4648
4649    #[test]
4650    fn test_variable_definition_remove() {
4651        let makefile: Makefile = r#"VAR1 = value1
4652VAR2 = value2
4653VAR3 = value3
4654"#
4655        .parse()
4656        .unwrap();
4657
4658        // Verify we have 3 variables
4659        assert_eq!(makefile.variable_definitions().count(), 3);
4660
4661        // Remove the second variable
4662        let mut var2 = makefile
4663            .variable_definitions()
4664            .nth(1)
4665            .expect("Should have second variable");
4666        assert_eq!(var2.name(), Some("VAR2".to_string()));
4667        var2.remove();
4668
4669        // Verify we now have 2 variables and VAR2 is gone
4670        assert_eq!(makefile.variable_definitions().count(), 2);
4671        let var_names: Vec<_> = makefile
4672            .variable_definitions()
4673            .filter_map(|v| v.name())
4674            .collect();
4675        assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4676    }
4677
4678    #[test]
4679    fn test_variable_definition_set_value() {
4680        let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4681
4682        let mut var = makefile
4683            .variable_definitions()
4684            .next()
4685            .expect("Should have variable");
4686        assert_eq!(var.raw_value(), Some("old_value".to_string()));
4687
4688        // Change the value
4689        var.set_value("new_value");
4690
4691        // Verify the value changed
4692        assert_eq!(var.raw_value(), Some("new_value".to_string()));
4693        assert!(makefile.code().contains("VAR = new_value"));
4694    }
4695
4696    #[test]
4697    fn test_variable_definition_set_value_preserves_format() {
4698        let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4699
4700        let mut var = makefile
4701            .variable_definitions()
4702            .next()
4703            .expect("Should have variable");
4704        assert_eq!(var.raw_value(), Some("old_value".to_string()));
4705
4706        // Change the value
4707        var.set_value("new_value");
4708
4709        // Verify the value changed but format preserved
4710        assert_eq!(var.raw_value(), Some("new_value".to_string()));
4711        let code = makefile.code();
4712        assert!(code.contains("export"), "Should preserve export prefix");
4713        assert!(code.contains(":="), "Should preserve := operator");
4714        assert!(code.contains("new_value"), "Should have new value");
4715    }
4716
4717    #[test]
4718    fn test_makefile_find_variable() {
4719        let makefile: Makefile = r#"VAR1 = value1
4720VAR2 = value2
4721VAR3 = value3
4722"#
4723        .parse()
4724        .unwrap();
4725
4726        // Find existing variable
4727        let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4728        assert_eq!(vars.len(), 1);
4729        assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4730        assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4731
4732        // Try to find non-existent variable
4733        assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4734    }
4735
4736    #[test]
4737    fn test_makefile_find_variable_with_export() {
4738        let makefile: Makefile = r#"VAR1 = value1
4739export VAR2 := value2
4740VAR3 = value3
4741"#
4742        .parse()
4743        .unwrap();
4744
4745        // Find exported variable
4746        let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4747        assert_eq!(vars.len(), 1);
4748        assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4749        assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4750    }
4751
4752    #[test]
4753    fn test_variable_definition_is_export() {
4754        let makefile: Makefile = r#"VAR1 = value1
4755export VAR2 := value2
4756export VAR3 = value3
4757VAR4 := value4
4758"#
4759        .parse()
4760        .unwrap();
4761
4762        let vars: Vec<_> = makefile.variable_definitions().collect();
4763        assert_eq!(vars.len(), 4);
4764
4765        assert!(!vars[0].is_export());
4766        assert!(vars[1].is_export());
4767        assert!(vars[2].is_export());
4768        assert!(!vars[3].is_export());
4769    }
4770
4771    #[test]
4772    fn test_makefile_find_variable_multiple() {
4773        let makefile: Makefile = r#"VAR1 = value1
4774VAR1 = value2
4775VAR2 = other
4776VAR1 = value3
4777"#
4778        .parse()
4779        .unwrap();
4780
4781        // Find all VAR1 definitions
4782        let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4783        assert_eq!(vars.len(), 3);
4784        assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4785        assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4786        assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4787
4788        // Find VAR2
4789        let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4790        assert_eq!(var2s.len(), 1);
4791        assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4792    }
4793
4794    #[test]
4795    fn test_variable_remove_and_find() {
4796        let makefile: Makefile = r#"VAR1 = value1
4797VAR2 = value2
4798VAR3 = value3
4799"#
4800        .parse()
4801        .unwrap();
4802
4803        // Find and remove VAR2
4804        let mut var2 = makefile
4805            .find_variable("VAR2")
4806            .next()
4807            .expect("Should find VAR2");
4808        var2.remove();
4809
4810        // Verify VAR2 is gone
4811        assert_eq!(makefile.find_variable("VAR2").count(), 0);
4812
4813        // Verify other variables still exist
4814        assert_eq!(makefile.find_variable("VAR1").count(), 1);
4815        assert_eq!(makefile.find_variable("VAR3").count(), 1);
4816    }
4817
4818    #[test]
4819    fn test_variable_remove_with_comment() {
4820        let makefile: Makefile = r#"VAR1 = value1
4821# This is a comment about VAR2
4822VAR2 = value2
4823VAR3 = value3
4824"#
4825        .parse()
4826        .unwrap();
4827
4828        // Remove VAR2
4829        let mut var2 = makefile
4830            .variable_definitions()
4831            .nth(1)
4832            .expect("Should have second variable");
4833        assert_eq!(var2.name(), Some("VAR2".to_string()));
4834        var2.remove();
4835
4836        // Verify the comment is also removed
4837        assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4838    }
4839
4840    #[test]
4841    fn test_variable_remove_with_multiple_comments() {
4842        let makefile: Makefile = r#"VAR1 = value1
4843# Comment line 1
4844# Comment line 2
4845# Comment line 3
4846VAR2 = value2
4847VAR3 = value3
4848"#
4849        .parse()
4850        .unwrap();
4851
4852        // Remove VAR2
4853        let mut var2 = makefile
4854            .variable_definitions()
4855            .nth(1)
4856            .expect("Should have second variable");
4857        var2.remove();
4858
4859        // Verify all comments are removed
4860        assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4861    }
4862
4863    #[test]
4864    fn test_variable_remove_with_empty_line() {
4865        let makefile: Makefile = r#"VAR1 = value1
4866
4867# Comment about VAR2
4868VAR2 = value2
4869VAR3 = value3
4870"#
4871        .parse()
4872        .unwrap();
4873
4874        // Remove VAR2
4875        let mut var2 = makefile
4876            .variable_definitions()
4877            .nth(1)
4878            .expect("Should have second variable");
4879        var2.remove();
4880
4881        // Verify comment and up to 1 empty line are removed
4882        // Should have VAR1, then newline, then VAR3 (empty line removed)
4883        assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4884    }
4885
4886    #[test]
4887    fn test_variable_remove_with_multiple_empty_lines() {
4888        let makefile: Makefile = r#"VAR1 = value1
4889
4890
4891# Comment about VAR2
4892VAR2 = value2
4893VAR3 = value3
4894"#
4895        .parse()
4896        .unwrap();
4897
4898        // Remove VAR2
4899        let mut var2 = makefile
4900            .variable_definitions()
4901            .nth(1)
4902            .expect("Should have second variable");
4903        var2.remove();
4904
4905        // Verify comment and only 1 empty line are removed (one empty line preserved)
4906        // Should preserve one empty line before where VAR2 was
4907        assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4908    }
4909
4910    #[test]
4911    fn test_rule_remove_with_comment() {
4912        let makefile: Makefile = r#"rule1:
4913	command1
4914
4915# Comment about rule2
4916rule2:
4917	command2
4918rule3:
4919	command3
4920"#
4921        .parse()
4922        .unwrap();
4923
4924        // Remove rule2
4925        let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4926        rule2.remove().unwrap();
4927
4928        // Verify the comment is removed
4929        // Note: The empty line after rule1 is part of rule1's text, not a sibling, so it's preserved
4930        assert_eq!(
4931            makefile.code(),
4932            "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4933        );
4934    }
4935
4936    #[test]
4937    fn test_variable_remove_preserves_shebang() {
4938        let makefile: Makefile = r#"#!/usr/bin/make -f
4939# This is a regular comment
4940VAR1 = value1
4941VAR2 = value2
4942"#
4943        .parse()
4944        .unwrap();
4945
4946        // Remove VAR1
4947        let mut var1 = makefile.variable_definitions().next().unwrap();
4948        var1.remove();
4949
4950        // Verify the shebang is preserved but regular comment is removed
4951        let code = makefile.code();
4952        assert!(code.starts_with("#!/usr/bin/make -f"));
4953        assert!(!code.contains("regular comment"));
4954        assert!(!code.contains("VAR1"));
4955        assert!(code.contains("VAR2"));
4956    }
4957
4958    #[test]
4959    fn test_variable_remove_preserves_subsequent_comments() {
4960        let makefile: Makefile = r#"VAR1 = value1
4961# Comment about VAR2
4962VAR2 = value2
4963
4964# Comment about VAR3
4965VAR3 = value3
4966"#
4967        .parse()
4968        .unwrap();
4969
4970        // Remove VAR2
4971        let mut var2 = makefile
4972            .variable_definitions()
4973            .nth(1)
4974            .expect("Should have second variable");
4975        var2.remove();
4976
4977        // Verify preceding comment is removed but subsequent comment/empty line are preserved
4978        let code = makefile.code();
4979        assert_eq!(
4980            code,
4981            "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4982        );
4983    }
4984
4985    #[test]
4986    fn test_variable_remove_after_shebang_preserves_empty_line() {
4987        let makefile: Makefile = r#"#!/usr/bin/make -f
4988export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4989
4990%:
4991	dh $@
4992"#
4993        .parse()
4994        .unwrap();
4995
4996        // Remove the variable
4997        let mut var = makefile.variable_definitions().next().unwrap();
4998        var.remove();
4999
5000        // Verify shebang is preserved and empty line after variable is preserved
5001        assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
5002    }
5003
5004    #[test]
5005    fn test_rule_add_prerequisite() {
5006        let mut rule: Rule = "target: dep1\n".parse().unwrap();
5007        rule.add_prerequisite("dep2").unwrap();
5008        assert_eq!(
5009            rule.prerequisites().collect::<Vec<_>>(),
5010            vec!["dep1", "dep2"]
5011        );
5012        // Verify proper spacing
5013        assert_eq!(rule.to_string(), "target: dep1 dep2\n");
5014    }
5015
5016    #[test]
5017    fn test_rule_add_prerequisite_to_rule_without_prereqs() {
5018        // Regression test for missing space after colon when adding first prerequisite
5019        let mut rule: Rule = "target:\n".parse().unwrap();
5020        rule.add_prerequisite("dep1").unwrap();
5021        assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
5022        // Should have space after colon
5023        assert_eq!(rule.to_string(), "target: dep1\n");
5024    }
5025
5026    #[test]
5027    fn test_rule_remove_prerequisite() {
5028        let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
5029        assert!(rule.remove_prerequisite("dep2").unwrap());
5030        assert_eq!(
5031            rule.prerequisites().collect::<Vec<_>>(),
5032            vec!["dep1", "dep3"]
5033        );
5034        assert!(!rule.remove_prerequisite("nonexistent").unwrap());
5035    }
5036
5037    #[test]
5038    fn test_rule_set_prerequisites() {
5039        let mut rule: Rule = "target: old_dep\n".parse().unwrap();
5040        rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
5041            .unwrap();
5042        assert_eq!(
5043            rule.prerequisites().collect::<Vec<_>>(),
5044            vec!["new_dep1", "new_dep2"]
5045        );
5046    }
5047
5048    #[test]
5049    fn test_rule_set_prerequisites_empty() {
5050        let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
5051        rule.set_prerequisites(vec![]).unwrap();
5052        assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
5053    }
5054
5055    #[test]
5056    fn test_rule_add_target() {
5057        let mut rule: Rule = "target1: dep1\n".parse().unwrap();
5058        rule.add_target("target2").unwrap();
5059        assert_eq!(
5060            rule.targets().collect::<Vec<_>>(),
5061            vec!["target1", "target2"]
5062        );
5063    }
5064
5065    #[test]
5066    fn test_rule_set_targets() {
5067        let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5068        rule.set_targets(vec!["new_target1", "new_target2"])
5069            .unwrap();
5070        assert_eq!(
5071            rule.targets().collect::<Vec<_>>(),
5072            vec!["new_target1", "new_target2"]
5073        );
5074    }
5075
5076    #[test]
5077    fn test_rule_set_targets_empty() {
5078        let mut rule: Rule = "target: dep1\n".parse().unwrap();
5079        let result = rule.set_targets(vec![]);
5080        assert!(result.is_err());
5081        // Verify target wasn't changed
5082        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
5083    }
5084
5085    #[test]
5086    fn test_rule_has_target() {
5087        let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
5088        assert!(rule.has_target("target1"));
5089        assert!(rule.has_target("target2"));
5090        assert!(!rule.has_target("target3"));
5091        assert!(!rule.has_target("nonexistent"));
5092    }
5093
5094    #[test]
5095    fn test_rule_rename_target() {
5096        let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5097        assert!(rule.rename_target("old_target", "new_target").unwrap());
5098        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
5099        // Try renaming non-existent target
5100        assert!(!rule.rename_target("nonexistent", "something").unwrap());
5101    }
5102
5103    #[test]
5104    fn test_rule_rename_target_multiple() {
5105        let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5106        assert!(rule.rename_target("target2", "renamed_target").unwrap());
5107        assert_eq!(
5108            rule.targets().collect::<Vec<_>>(),
5109            vec!["target1", "renamed_target", "target3"]
5110        );
5111    }
5112
5113    #[test]
5114    fn test_rule_remove_target() {
5115        let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5116        assert!(rule.remove_target("target2").unwrap());
5117        assert_eq!(
5118            rule.targets().collect::<Vec<_>>(),
5119            vec!["target1", "target3"]
5120        );
5121        // Try removing non-existent target
5122        assert!(!rule.remove_target("nonexistent").unwrap());
5123    }
5124
5125    #[test]
5126    fn test_rule_remove_target_last() {
5127        let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
5128        let result = rule.remove_target("single_target");
5129        assert!(result.is_err());
5130        // Verify target wasn't removed
5131        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
5132    }
5133
5134    #[test]
5135    fn test_rule_target_manipulation_preserves_prerequisites() {
5136        let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
5137
5138        // Remove a target
5139        rule.remove_target("target1").unwrap();
5140        assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
5141        assert_eq!(
5142            rule.prerequisites().collect::<Vec<_>>(),
5143            vec!["dep1", "dep2"]
5144        );
5145        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5146
5147        // Add a target
5148        rule.add_target("target3").unwrap();
5149        assert_eq!(
5150            rule.targets().collect::<Vec<_>>(),
5151            vec!["target2", "target3"]
5152        );
5153        assert_eq!(
5154            rule.prerequisites().collect::<Vec<_>>(),
5155            vec!["dep1", "dep2"]
5156        );
5157        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5158
5159        // Rename a target
5160        rule.rename_target("target2", "renamed").unwrap();
5161        assert_eq!(
5162            rule.targets().collect::<Vec<_>>(),
5163            vec!["renamed", "target3"]
5164        );
5165        assert_eq!(
5166            rule.prerequisites().collect::<Vec<_>>(),
5167            vec!["dep1", "dep2"]
5168        );
5169        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5170    }
5171
5172    #[test]
5173    fn test_rule_remove() {
5174        let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5175        let rule = makefile.find_rule_by_target("rule1").unwrap();
5176        rule.remove().unwrap();
5177        assert_eq!(makefile.rules().count(), 1);
5178        assert!(makefile.find_rule_by_target("rule1").is_none());
5179        assert!(makefile.find_rule_by_target("rule2").is_some());
5180    }
5181
5182    #[test]
5183    fn test_rule_remove_last_trims_blank_lines() {
5184        // Regression test for bug where removing the last rule left trailing blank lines
5185        let makefile: Makefile =
5186            "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
5187                .parse()
5188                .unwrap();
5189
5190        // Remove the last rule (override_dh_missing)
5191        let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
5192        rule.remove().unwrap();
5193
5194        // Should not have trailing blank line
5195        assert_eq!(makefile.code(), "%:\n\tdh $@\n");
5196        assert_eq!(makefile.rules().count(), 1);
5197    }
5198
5199    #[test]
5200    fn test_makefile_find_rule_by_target() {
5201        let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5202        let rule = makefile.find_rule_by_target("rule2");
5203        assert!(rule.is_some());
5204        assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
5205        assert!(makefile.find_rule_by_target("nonexistent").is_none());
5206    }
5207
5208    #[test]
5209    fn test_makefile_find_rules_by_target() {
5210        let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
5211            .parse()
5212            .unwrap();
5213        assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
5214        assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
5215        assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
5216    }
5217
5218    #[test]
5219    fn test_makefile_find_rule_by_target_pattern_simple() {
5220        let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5221        let rule = makefile.find_rule_by_target_pattern("foo.o");
5222        assert!(rule.is_some());
5223        assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
5224    }
5225
5226    #[test]
5227    fn test_makefile_find_rule_by_target_pattern_no_match() {
5228        let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5229        let rule = makefile.find_rule_by_target_pattern("foo.c");
5230        assert!(rule.is_none());
5231    }
5232
5233    #[test]
5234    fn test_makefile_find_rule_by_target_pattern_exact() {
5235        let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5236        let rule = makefile.find_rule_by_target_pattern("foo.o");
5237        assert!(rule.is_some());
5238        assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
5239    }
5240
5241    #[test]
5242    fn test_makefile_find_rule_by_target_pattern_prefix() {
5243        let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5244        let rule = makefile.find_rule_by_target_pattern("libfoo.a");
5245        assert!(rule.is_some());
5246        assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
5247    }
5248
5249    #[test]
5250    fn test_makefile_find_rule_by_target_pattern_suffix() {
5251        let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5252        let rule = makefile.find_rule_by_target_pattern("foo_test.o");
5253        assert!(rule.is_some());
5254        assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
5255    }
5256
5257    #[test]
5258    fn test_makefile_find_rule_by_target_pattern_middle() {
5259        let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5260        let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
5261        assert!(rule.is_some());
5262        assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
5263    }
5264
5265    #[test]
5266    fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
5267        let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
5268        let rule = makefile.find_rule_by_target_pattern("anything");
5269        assert!(rule.is_some());
5270        assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
5271    }
5272
5273    #[test]
5274    fn test_makefile_find_rules_by_target_pattern_multiple() {
5275        let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
5276            .parse()
5277            .unwrap();
5278        let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5279        assert_eq!(rules.len(), 2);
5280    }
5281
5282    #[test]
5283    fn test_makefile_find_rules_by_target_pattern_mixed() {
5284        let makefile: Makefile =
5285            "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
5286                .parse()
5287                .unwrap();
5288        let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5289        assert_eq!(rules.len(), 2); // Matches both %.o and foo.o
5290        let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
5291        assert_eq!(rules.len(), 1); // Only exact match
5292    }
5293
5294    #[test]
5295    fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
5296        let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5297        let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5298        assert_eq!(rules.len(), 1);
5299        let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
5300        assert_eq!(rules.len(), 0);
5301    }
5302
5303    #[test]
5304    fn test_matches_pattern_exact() {
5305        assert!(matches_pattern("foo.o", "foo.o"));
5306        assert!(!matches_pattern("foo.o", "bar.o"));
5307    }
5308
5309    #[test]
5310    fn test_matches_pattern_suffix() {
5311        assert!(matches_pattern("%.o", "foo.o"));
5312        assert!(matches_pattern("%.o", "bar.o"));
5313        assert!(matches_pattern("%.o", "baz/qux.o"));
5314        assert!(!matches_pattern("%.o", "foo.c"));
5315    }
5316
5317    #[test]
5318    fn test_matches_pattern_prefix() {
5319        assert!(matches_pattern("lib%.a", "libfoo.a"));
5320        assert!(matches_pattern("lib%.a", "libbar.a"));
5321        assert!(!matches_pattern("lib%.a", "foo.a"));
5322        assert!(!matches_pattern("lib%.a", "lib.a"));
5323    }
5324
5325    #[test]
5326    fn test_matches_pattern_middle() {
5327        assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
5328        assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
5329        assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
5330        assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
5331    }
5332
5333    #[test]
5334    fn test_matches_pattern_wildcard_only() {
5335        assert!(matches_pattern("%", "anything"));
5336        assert!(matches_pattern("%", "foo.o"));
5337        // GNU make: stem must be non-empty, so "%" does NOT match ""
5338        assert!(!matches_pattern("%", ""));
5339    }
5340
5341    #[test]
5342    fn test_matches_pattern_empty_stem() {
5343        // GNU make: stem must be non-empty
5344        assert!(!matches_pattern("%.o", ".o")); // stem would be empty
5345        assert!(!matches_pattern("lib%", "lib")); // stem would be empty
5346        assert!(!matches_pattern("lib%.a", "lib.a")); // stem would be empty
5347    }
5348
5349    #[test]
5350    fn test_matches_pattern_multiple_wildcards_not_supported() {
5351        // GNU make does NOT support multiple % in pattern rules
5352        // These should not match (fall back to exact match)
5353        assert!(!matches_pattern("%foo%bar", "xfooybarz"));
5354        assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
5355    }
5356
5357    #[test]
5358    fn test_makefile_add_phony_target() {
5359        let mut makefile = Makefile::new();
5360        makefile.add_phony_target("clean").unwrap();
5361        assert!(makefile.is_phony("clean"));
5362        assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
5363    }
5364
5365    #[test]
5366    fn test_makefile_add_phony_target_existing() {
5367        let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
5368        makefile.add_phony_target("clean").unwrap();
5369        assert!(makefile.is_phony("test"));
5370        assert!(makefile.is_phony("clean"));
5371        let targets: Vec<_> = makefile.phony_targets().collect();
5372        assert!(targets.contains(&"test".to_string()));
5373        assert!(targets.contains(&"clean".to_string()));
5374    }
5375
5376    #[test]
5377    fn test_makefile_remove_phony_target() {
5378        let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5379        assert!(makefile.remove_phony_target("clean").unwrap());
5380        assert!(!makefile.is_phony("clean"));
5381        assert!(makefile.is_phony("test"));
5382        assert!(!makefile.remove_phony_target("nonexistent").unwrap());
5383    }
5384
5385    #[test]
5386    fn test_makefile_remove_phony_target_last() {
5387        let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
5388        assert!(makefile.remove_phony_target("clean").unwrap());
5389        assert!(!makefile.is_phony("clean"));
5390        // .PHONY rule should be removed entirely
5391        assert!(makefile.find_rule_by_target(".PHONY").is_none());
5392    }
5393
5394    #[test]
5395    fn test_makefile_is_phony() {
5396        let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5397        assert!(makefile.is_phony("clean"));
5398        assert!(makefile.is_phony("test"));
5399        assert!(!makefile.is_phony("build"));
5400    }
5401
5402    #[test]
5403    fn test_makefile_phony_targets() {
5404        let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5405        let phony_targets: Vec<_> = makefile.phony_targets().collect();
5406        assert_eq!(phony_targets, vec!["clean", "test", "build"]);
5407    }
5408
5409    #[test]
5410    fn test_makefile_phony_targets_empty() {
5411        let makefile = Makefile::new();
5412        assert_eq!(makefile.phony_targets().count(), 0);
5413    }
5414
5415    #[test]
5416    fn test_makefile_remove_first_phony_target_no_extra_space() {
5417        let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5418        assert!(makefile.remove_phony_target("clean").unwrap());
5419        let result = makefile.to_string();
5420        assert_eq!(result, ".PHONY: test build\n");
5421    }
5422
5423    #[test]
5424    fn test_recipe_with_leading_comments_and_blank_lines() {
5425        // Regression test for bug where recipes with leading comments and blank lines
5426        // were not parsed correctly. The parser would stop parsing recipes when it
5427        // encountered a newline, missing subsequent recipe lines.
5428        let makefile_text = r#"#!/usr/bin/make
5429
5430%:
5431	dh $@
5432
5433override_dh_build:
5434	# The next line is empty
5435
5436	dh_python3
5437"#;
5438        let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
5439
5440        let rules: Vec<_> = makefile.rules().collect();
5441        assert_eq!(rules.len(), 2, "Expected 2 rules");
5442
5443        // First rule: %
5444        let rule0 = &rules[0];
5445        assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
5446        assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
5447
5448        // Second rule: override_dh_build
5449        let rule1 = &rules[1];
5450        assert_eq!(
5451            rule1.targets().collect::<Vec<_>>(),
5452            vec!["override_dh_build"]
5453        );
5454
5455        // The key assertion: we should have at least the actual command recipe
5456        let recipes: Vec<_> = rule1.recipes().collect();
5457        assert!(
5458            !recipes.is_empty(),
5459            "Expected at least one recipe for override_dh_build, got none"
5460        );
5461        assert!(
5462            recipes.contains(&"dh_python3".to_string()),
5463            "Expected 'dh_python3' in recipes, got: {:?}",
5464            recipes
5465        );
5466    }
5467
5468    #[test]
5469    fn test_rule_parse_preserves_trailing_blank_lines() {
5470        // Regression test: ensure that trailing blank lines are preserved
5471        // when parsing a rule and using it with replace_rule()
5472        let input = r#"override_dh_systemd_enable:
5473	dh_systemd_enable -pracoon
5474
5475override_dh_install:
5476	dh_install
5477"#;
5478
5479        let mut mf: Makefile = input.parse().unwrap();
5480
5481        // Get first rule and convert to string
5482        let rule = mf.rules().next().unwrap();
5483        let rule_text = rule.to_string();
5484
5485        // Should include trailing blank line
5486        assert_eq!(
5487            rule_text,
5488            "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
5489        );
5490
5491        // Modify the text
5492        let modified =
5493            rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
5494
5495        // Parse back - should preserve trailing blank line
5496        let new_rule: Rule = modified.parse().unwrap();
5497        assert_eq!(
5498            new_rule.to_string(),
5499            "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
5500        );
5501
5502        // Replace in makefile
5503        mf.replace_rule(0, new_rule).unwrap();
5504
5505        // Verify blank line is still present in output
5506        let output = mf.to_string();
5507        assert!(
5508            output.contains(
5509                "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
5510            ),
5511            "Blank line between rules should be preserved. Got: {:?}",
5512            output
5513        );
5514    }
5515
5516    #[test]
5517    fn test_rule_parse_round_trip_with_trailing_newlines() {
5518        // Test that parsing and stringifying a rule preserves exact trailing newlines
5519        let test_cases = vec![
5520            "rule:\n\tcommand\n",     // One newline
5521            "rule:\n\tcommand\n\n",   // Two newlines (blank line)
5522            "rule:\n\tcommand\n\n\n", // Three newlines (two blank lines)
5523        ];
5524
5525        for rule_text in test_cases {
5526            let rule: Rule = rule_text.parse().unwrap();
5527            let result = rule.to_string();
5528            assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
5529        }
5530    }
5531
5532    #[test]
5533    fn test_rule_clone() {
5534        // Test that Rule can be cloned and produces an identical copy
5535        let rule_text = "rule:\n\tcommand\n\n";
5536        let rule: Rule = rule_text.parse().unwrap();
5537        let cloned = rule.clone();
5538
5539        // Both should produce the same string representation
5540        assert_eq!(rule.to_string(), cloned.to_string());
5541        assert_eq!(rule.to_string(), rule_text);
5542        assert_eq!(cloned.to_string(), rule_text);
5543
5544        // Verify targets and recipes are the same
5545        assert_eq!(
5546            rule.targets().collect::<Vec<_>>(),
5547            cloned.targets().collect::<Vec<_>>()
5548        );
5549        assert_eq!(
5550            rule.recipes().collect::<Vec<_>>(),
5551            cloned.recipes().collect::<Vec<_>>()
5552        );
5553    }
5554
5555    #[test]
5556    fn test_makefile_clone() {
5557        // Test that Makefile and other AST nodes can be cloned
5558        let input = "VAR = value\n\nrule:\n\tcommand\n";
5559        let makefile: Makefile = input.parse().unwrap();
5560        let cloned = makefile.clone();
5561
5562        // Both should produce the same string representation
5563        assert_eq!(makefile.to_string(), cloned.to_string());
5564        assert_eq!(makefile.to_string(), input);
5565
5566        // Verify rule count is the same
5567        assert_eq!(makefile.rules().count(), cloned.rules().count());
5568
5569        // Verify variable definitions are the same
5570        assert_eq!(
5571            makefile.variable_definitions().count(),
5572            cloned.variable_definitions().count()
5573        );
5574    }
5575
5576    #[test]
5577    fn test_conditional_with_recipe_line() {
5578        // Test that conditionals with recipe lines (tab-indented) work correctly
5579        let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
5580        let parsed = parse(input, None);
5581
5582        // Should parse without errors
5583        assert!(
5584            parsed.errors.is_empty(),
5585            "Expected no parse errors, but got: {:?}",
5586            parsed.errors
5587        );
5588
5589        // Should preserve the code
5590        let mf = parsed.root();
5591        assert_eq!(mf.code(), input);
5592    }
5593
5594    #[test]
5595    fn test_conditional_in_rule_recipe() {
5596        // Test conditional inside a rule's recipe section
5597        let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
5598        let parsed = parse(input, None);
5599
5600        // Should parse without errors
5601        assert!(
5602            parsed.errors.is_empty(),
5603            "Expected no parse errors, but got: {:?}",
5604            parsed.errors
5605        );
5606
5607        // Should preserve the code
5608        let mf = parsed.root();
5609        assert_eq!(mf.code(), input);
5610
5611        // Should have exactly one rule
5612        assert_eq!(mf.rules().count(), 1);
5613    }
5614
5615    #[test]
5616    fn test_rule_items() {
5617        use crate::RuleItem;
5618
5619        // Test rule with both recipes and conditionals
5620        let input = r#"test:
5621	echo "before"
5622ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
5623	./run-tests
5624endif
5625	echo "after"
5626"#;
5627        let rule: Rule = input.parse().unwrap();
5628
5629        let items: Vec<_> = rule.items().collect();
5630        assert_eq!(
5631            items.len(),
5632            3,
5633            "Expected 3 items: recipe, conditional, recipe"
5634        );
5635
5636        // Check first item is a recipe
5637        match &items[0] {
5638            RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
5639            RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5640        }
5641
5642        // Check second item is a conditional
5643        match &items[1] {
5644            RuleItem::Conditional(c) => {
5645                assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5646            }
5647            RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
5648        }
5649
5650        // Check third item is a recipe
5651        match &items[2] {
5652            RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5653            RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5654        }
5655
5656        // Test rule with only recipes (no conditionals)
5657        let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5658        let simple_items: Vec<_> = simple_rule.items().collect();
5659        assert_eq!(simple_items.len(), 2);
5660
5661        match &simple_items[0] {
5662            RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5663            _ => panic!("Expected recipe"),
5664        }
5665
5666        match &simple_items[1] {
5667            RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5668            _ => panic!("Expected recipe"),
5669        }
5670
5671        // Test rule with only conditional (no plain recipes)
5672        let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5673            .parse()
5674            .unwrap();
5675        let cond_items: Vec<_> = cond_only.items().collect();
5676        assert_eq!(cond_items.len(), 1);
5677
5678        match &cond_items[0] {
5679            RuleItem::Conditional(c) => {
5680                assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5681            }
5682            _ => panic!("Expected conditional"),
5683        }
5684    }
5685
5686    #[test]
5687    fn test_conditionals_iterator() {
5688        let makefile: Makefile = r#"ifdef DEBUG
5689VAR = debug
5690endif
5691
5692ifndef RELEASE
5693OTHER = dev
5694endif
5695"#
5696        .parse()
5697        .unwrap();
5698
5699        let conditionals: Vec<_> = makefile.conditionals().collect();
5700        assert_eq!(conditionals.len(), 2);
5701
5702        assert_eq!(
5703            conditionals[0].conditional_type(),
5704            Some("ifdef".to_string())
5705        );
5706        assert_eq!(
5707            conditionals[1].conditional_type(),
5708            Some("ifndef".to_string())
5709        );
5710    }
5711
5712    #[test]
5713    fn test_conditional_type_and_condition() {
5714        let makefile: Makefile = r#"ifdef DEBUG
5715VAR = debug
5716endif
5717"#
5718        .parse()
5719        .unwrap();
5720
5721        let conditional = makefile.conditionals().next().unwrap();
5722        assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5723        assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5724    }
5725
5726    #[test]
5727    fn test_conditional_has_else() {
5728        let makefile_with_else: Makefile = r#"ifdef DEBUG
5729VAR = debug
5730else
5731VAR = release
5732endif
5733"#
5734        .parse()
5735        .unwrap();
5736
5737        let conditional = makefile_with_else.conditionals().next().unwrap();
5738        assert!(conditional.has_else());
5739
5740        let makefile_without_else: Makefile = r#"ifdef DEBUG
5741VAR = debug
5742endif
5743"#
5744        .parse()
5745        .unwrap();
5746
5747        let conditional = makefile_without_else.conditionals().next().unwrap();
5748        assert!(!conditional.has_else());
5749    }
5750
5751    #[test]
5752    fn test_conditional_if_body() {
5753        let makefile: Makefile = r#"ifdef DEBUG
5754VAR = debug
5755endif
5756"#
5757        .parse()
5758        .unwrap();
5759
5760        let conditional = makefile.conditionals().next().unwrap();
5761        let if_body = conditional.if_body();
5762        assert!(if_body.is_some());
5763        assert!(if_body.unwrap().contains("VAR = debug"));
5764    }
5765
5766    #[test]
5767    fn test_conditional_else_body() {
5768        let makefile: Makefile = r#"ifdef DEBUG
5769VAR = debug
5770else
5771VAR = release
5772endif
5773"#
5774        .parse()
5775        .unwrap();
5776
5777        let conditional = makefile.conditionals().next().unwrap();
5778        let else_body = conditional.else_body();
5779        assert!(else_body.is_some());
5780        assert!(else_body.unwrap().contains("VAR = release"));
5781    }
5782
5783    #[test]
5784    fn test_add_conditional_ifdef() {
5785        let mut makefile = Makefile::new();
5786        let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5787        assert!(result.is_ok());
5788
5789        let code = makefile.to_string();
5790        assert!(code.contains("ifdef DEBUG"));
5791        assert!(code.contains("VAR = debug"));
5792        assert!(code.contains("endif"));
5793    }
5794
5795    #[test]
5796    fn test_add_conditional_with_else() {
5797        let mut makefile = Makefile::new();
5798        let result =
5799            makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5800        assert!(result.is_ok());
5801
5802        let code = makefile.to_string();
5803        assert!(code.contains("ifdef DEBUG"));
5804        assert!(code.contains("VAR = debug"));
5805        assert!(code.contains("else"));
5806        assert!(code.contains("VAR = release"));
5807        assert!(code.contains("endif"));
5808    }
5809
5810    #[test]
5811    fn test_add_conditional_invalid_type() {
5812        let mut makefile = Makefile::new();
5813        let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5814        assert!(result.is_err());
5815    }
5816
5817    #[test]
5818    fn test_add_conditional_formatting() {
5819        let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5820        let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5821        assert!(result.is_ok());
5822
5823        let code = makefile.to_string();
5824        // Should have a blank line before the conditional
5825        assert!(code.contains("\n\nifdef DEBUG"));
5826    }
5827
5828    #[test]
5829    fn test_conditional_remove() {
5830        let makefile: Makefile = r#"ifdef DEBUG
5831VAR = debug
5832endif
5833
5834VAR2 = value2
5835"#
5836        .parse()
5837        .unwrap();
5838
5839        let mut conditional = makefile.conditionals().next().unwrap();
5840        let result = conditional.remove();
5841        assert!(result.is_ok());
5842
5843        let code = makefile.to_string();
5844        assert!(!code.contains("ifdef DEBUG"));
5845        assert!(!code.contains("VAR = debug"));
5846        assert!(code.contains("VAR2 = value2"));
5847    }
5848
5849    #[test]
5850    fn test_add_conditional_ifndef() {
5851        let mut makefile = Makefile::new();
5852        let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5853        assert!(result.is_ok());
5854
5855        let code = makefile.to_string();
5856        assert!(code.contains("ifndef NDEBUG"));
5857        assert!(code.contains("VAR = enabled"));
5858        assert!(code.contains("endif"));
5859    }
5860
5861    #[test]
5862    fn test_add_conditional_ifeq() {
5863        let mut makefile = Makefile::new();
5864        let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5865        assert!(result.is_ok());
5866
5867        let code = makefile.to_string();
5868        assert!(code.contains("ifeq ($(OS),Linux)"));
5869        assert!(code.contains("VAR = linux"));
5870        assert!(code.contains("endif"));
5871    }
5872
5873    #[test]
5874    fn test_add_conditional_ifneq() {
5875        let mut makefile = Makefile::new();
5876        let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5877        assert!(result.is_ok());
5878
5879        let code = makefile.to_string();
5880        assert!(code.contains("ifneq ($(OS),Windows)"));
5881        assert!(code.contains("VAR = unix"));
5882        assert!(code.contains("endif"));
5883    }
5884
5885    #[test]
5886    fn test_conditional_api_integration() {
5887        // Create a makefile with a rule and a variable
5888        let mut makefile: Makefile = r#"VAR1 = value1
5889
5890rule1:
5891	command1
5892"#
5893        .parse()
5894        .unwrap();
5895
5896        // Add a conditional
5897        makefile
5898            .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5899            .unwrap();
5900
5901        // Verify the conditional was added
5902        assert_eq!(makefile.conditionals().count(), 1);
5903        let conditional = makefile.conditionals().next().unwrap();
5904        assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5905        assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5906        assert!(conditional.has_else());
5907
5908        // Verify the original content is preserved
5909        assert_eq!(makefile.variable_definitions().count(), 1);
5910        assert_eq!(makefile.rules().count(), 1);
5911    }
5912
5913    #[test]
5914    fn test_conditional_if_items() {
5915        let makefile: Makefile = r#"ifdef DEBUG
5916VAR = debug
5917rule:
5918	command
5919endif
5920"#
5921        .parse()
5922        .unwrap();
5923
5924        let cond = makefile.conditionals().next().unwrap();
5925        let items: Vec<_> = cond.if_items().collect();
5926        assert_eq!(items.len(), 2); // One variable, one rule
5927
5928        match &items[0] {
5929            MakefileItem::Variable(v) => {
5930                assert_eq!(v.name(), Some("VAR".to_string()));
5931            }
5932            _ => panic!("Expected variable"),
5933        }
5934
5935        match &items[1] {
5936            MakefileItem::Rule(r) => {
5937                assert!(r.targets().any(|t| t == "rule"));
5938            }
5939            _ => panic!("Expected rule"),
5940        }
5941    }
5942
5943    #[test]
5944    fn test_conditional_else_items() {
5945        let makefile: Makefile = r#"ifdef DEBUG
5946VAR = debug
5947else
5948VAR2 = release
5949rule2:
5950	command
5951endif
5952"#
5953        .parse()
5954        .unwrap();
5955
5956        let cond = makefile.conditionals().next().unwrap();
5957        let items: Vec<_> = cond.else_items().collect();
5958        assert_eq!(items.len(), 2); // One variable, one rule
5959
5960        match &items[0] {
5961            MakefileItem::Variable(v) => {
5962                assert_eq!(v.name(), Some("VAR2".to_string()));
5963            }
5964            _ => panic!("Expected variable"),
5965        }
5966
5967        match &items[1] {
5968            MakefileItem::Rule(r) => {
5969                assert!(r.targets().any(|t| t == "rule2"));
5970            }
5971            _ => panic!("Expected rule"),
5972        }
5973    }
5974
5975    #[test]
5976    fn test_conditional_add_if_item() {
5977        let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5978        let mut cond = makefile.conditionals().next().unwrap();
5979
5980        // Parse a variable from a temporary makefile
5981        let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5982        let var = temp.variable_definitions().next().unwrap();
5983        cond.add_if_item(MakefileItem::Variable(var));
5984
5985        let code = makefile.to_string();
5986        assert!(code.contains("CFLAGS = -g"));
5987
5988        // Verify it's in the if branch
5989        let cond = makefile.conditionals().next().unwrap();
5990        assert_eq!(cond.if_items().count(), 1);
5991    }
5992
5993    #[test]
5994    fn test_conditional_add_else_item() {
5995        let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
5996        let mut cond = makefile.conditionals().next().unwrap();
5997
5998        // Parse a variable from a temporary makefile
5999        let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
6000        let var = temp.variable_definitions().next().unwrap();
6001        cond.add_else_item(MakefileItem::Variable(var));
6002
6003        let code = makefile.to_string();
6004        assert!(code.contains("else"));
6005        assert!(code.contains("CFLAGS = -O2"));
6006
6007        // Verify it's in the else branch
6008        let cond = makefile.conditionals().next().unwrap();
6009        assert_eq!(cond.else_items().count(), 1);
6010    }
6011
6012    #[test]
6013    fn test_add_conditional_with_items() {
6014        let mut makefile = Makefile::new();
6015
6016        // Parse items from temporary makefiles
6017        let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
6018        let var1 = temp1.variable_definitions().next().unwrap();
6019
6020        let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
6021        let var2 = temp2.variable_definitions().next().unwrap();
6022
6023        let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
6024        let rule1 = temp3.rules().next().unwrap();
6025
6026        let result = makefile.add_conditional_with_items(
6027            "ifdef",
6028            "DEBUG",
6029            vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
6030            Some(vec![MakefileItem::Variable(var2)]),
6031        );
6032
6033        assert!(result.is_ok());
6034
6035        let code = makefile.to_string();
6036        assert!(code.contains("ifdef DEBUG"));
6037        assert!(code.contains("CFLAGS = -g"));
6038        assert!(code.contains("debug:"));
6039        assert!(code.contains("else"));
6040        assert!(code.contains("CFLAGS = -O2"));
6041    }
6042
6043    #[test]
6044    fn test_conditional_items_with_nested_conditional() {
6045        let makefile: Makefile = r#"ifdef DEBUG
6046VAR = debug
6047ifdef VERBOSE
6048	VAR2 = verbose
6049endif
6050endif
6051"#
6052        .parse()
6053        .unwrap();
6054
6055        let cond = makefile.conditionals().next().unwrap();
6056        let items: Vec<_> = cond.if_items().collect();
6057        assert_eq!(items.len(), 2); // One variable, one nested conditional
6058
6059        match &items[0] {
6060            MakefileItem::Variable(v) => {
6061                assert_eq!(v.name(), Some("VAR".to_string()));
6062            }
6063            _ => panic!("Expected variable"),
6064        }
6065
6066        match &items[1] {
6067            MakefileItem::Conditional(c) => {
6068                assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6069            }
6070            _ => panic!("Expected conditional"),
6071        }
6072    }
6073
6074    #[test]
6075    fn test_conditional_items_with_include() {
6076        let makefile: Makefile = r#"ifdef DEBUG
6077include debug.mk
6078VAR = debug
6079endif
6080"#
6081        .parse()
6082        .unwrap();
6083
6084        let cond = makefile.conditionals().next().unwrap();
6085        let items: Vec<_> = cond.if_items().collect();
6086        assert_eq!(items.len(), 2); // One include, one variable
6087
6088        match &items[0] {
6089            MakefileItem::Include(i) => {
6090                assert_eq!(i.path(), Some("debug.mk".to_string()));
6091            }
6092            _ => panic!("Expected include"),
6093        }
6094
6095        match &items[1] {
6096            MakefileItem::Variable(v) => {
6097                assert_eq!(v.name(), Some("VAR".to_string()));
6098            }
6099            _ => panic!("Expected variable"),
6100        }
6101    }
6102
6103    #[test]
6104    fn test_makefile_items_iterator() {
6105        let makefile: Makefile = r#"VAR = value
6106ifdef DEBUG
6107CFLAGS = -g
6108endif
6109rule:
6110	command
6111include common.mk
6112"#
6113        .parse()
6114        .unwrap();
6115
6116        // First verify we can find each type individually
6117        // variable_definitions() is recursive, so it finds VAR and CFLAGS (inside conditional)
6118        assert_eq!(makefile.variable_definitions().count(), 2);
6119        assert_eq!(makefile.conditionals().count(), 1);
6120        assert_eq!(makefile.rules().count(), 1);
6121
6122        let items: Vec<_> = makefile.items().collect();
6123        // Note: include directives might not be at top level, need to check
6124        assert!(
6125            items.len() >= 3,
6126            "Expected at least 3 items, got {}",
6127            items.len()
6128        );
6129
6130        match &items[0] {
6131            MakefileItem::Variable(v) => {
6132                assert_eq!(v.name(), Some("VAR".to_string()));
6133            }
6134            _ => panic!("Expected variable at position 0"),
6135        }
6136
6137        match &items[1] {
6138            MakefileItem::Conditional(c) => {
6139                assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6140            }
6141            _ => panic!("Expected conditional at position 1"),
6142        }
6143
6144        match &items[2] {
6145            MakefileItem::Rule(r) => {
6146                let targets: Vec<_> = r.targets().collect();
6147                assert_eq!(targets, vec!["rule"]);
6148            }
6149            _ => panic!("Expected rule at position 2"),
6150        }
6151    }
6152
6153    #[test]
6154    fn test_conditional_unwrap() {
6155        let makefile: Makefile = r#"ifdef DEBUG
6156VAR = debug
6157rule:
6158	command
6159endif
6160"#
6161        .parse()
6162        .unwrap();
6163
6164        let mut cond = makefile.conditionals().next().unwrap();
6165        cond.unwrap().unwrap();
6166
6167        let code = makefile.to_string();
6168        let expected = "VAR = debug\nrule:\n\tcommand\n";
6169        assert_eq!(code, expected);
6170
6171        // Should have no conditionals now
6172        assert_eq!(makefile.conditionals().count(), 0);
6173
6174        // Should still have the variable and rule
6175        assert_eq!(makefile.variable_definitions().count(), 1);
6176        assert_eq!(makefile.rules().count(), 1);
6177    }
6178
6179    #[test]
6180    fn test_conditional_unwrap_with_else_fails() {
6181        let makefile: Makefile = r#"ifdef DEBUG
6182VAR = debug
6183else
6184VAR = release
6185endif
6186"#
6187        .parse()
6188        .unwrap();
6189
6190        let mut cond = makefile.conditionals().next().unwrap();
6191        let result = cond.unwrap();
6192
6193        assert!(result.is_err());
6194        assert!(result
6195            .unwrap_err()
6196            .to_string()
6197            .contains("Cannot unwrap conditional with else clause"));
6198    }
6199
6200    #[test]
6201    fn test_conditional_unwrap_nested() {
6202        let makefile: Makefile = r#"ifdef OUTER
6203VAR = outer
6204ifdef INNER
6205VAR2 = inner
6206endif
6207endif
6208"#
6209        .parse()
6210        .unwrap();
6211
6212        // Unwrap the outer conditional
6213        let mut outer_cond = makefile.conditionals().next().unwrap();
6214        outer_cond.unwrap().unwrap();
6215
6216        let code = makefile.to_string();
6217        let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
6218        assert_eq!(code, expected);
6219    }
6220
6221    #[test]
6222    fn test_conditional_unwrap_empty() {
6223        let makefile: Makefile = r#"ifdef DEBUG
6224endif
6225"#
6226        .parse()
6227        .unwrap();
6228
6229        let mut cond = makefile.conditionals().next().unwrap();
6230        cond.unwrap().unwrap();
6231
6232        let code = makefile.to_string();
6233        assert_eq!(code, "");
6234    }
6235
6236    #[test]
6237    fn test_rule_parent() {
6238        let makefile: Makefile = r#"all:
6239	echo "test"
6240"#
6241        .parse()
6242        .unwrap();
6243
6244        let rule = makefile.rules().next().unwrap();
6245        let parent = rule.parent();
6246        // Parent is ROOT node which doesn't cast to MakefileItem
6247        assert!(parent.is_none());
6248    }
6249
6250    #[test]
6251    fn test_item_parent_in_conditional() {
6252        let makefile: Makefile = r#"ifdef DEBUG
6253VAR = debug
6254rule:
6255	command
6256endif
6257"#
6258        .parse()
6259        .unwrap();
6260
6261        let cond = makefile.conditionals().next().unwrap();
6262
6263        // Get items from the conditional
6264        let items: Vec<_> = cond.if_items().collect();
6265        assert_eq!(items.len(), 2);
6266
6267        // Check variable parent is the conditional
6268        if let MakefileItem::Variable(var) = &items[0] {
6269            let parent = var.parent();
6270            assert!(parent.is_some());
6271            if let Some(MakefileItem::Conditional(_)) = parent {
6272                // Expected - parent is a conditional
6273            } else {
6274                panic!("Expected variable parent to be a Conditional");
6275            }
6276        } else {
6277            panic!("Expected first item to be a Variable");
6278        }
6279
6280        // Check rule parent is the conditional
6281        if let MakefileItem::Rule(rule) = &items[1] {
6282            let parent = rule.parent();
6283            assert!(parent.is_some());
6284            if let Some(MakefileItem::Conditional(_)) = parent {
6285                // Expected - parent is a conditional
6286            } else {
6287                panic!("Expected rule parent to be a Conditional");
6288            }
6289        } else {
6290            panic!("Expected second item to be a Rule");
6291        }
6292    }
6293
6294    #[test]
6295    fn test_nested_conditional_parent() {
6296        let makefile: Makefile = r#"ifdef OUTER
6297VAR = outer
6298ifdef INNER
6299VAR2 = inner
6300endif
6301endif
6302"#
6303        .parse()
6304        .unwrap();
6305
6306        let outer_cond = makefile.conditionals().next().unwrap();
6307
6308        // Get inner conditional from outer conditional's items
6309        let items: Vec<_> = outer_cond.if_items().collect();
6310
6311        // Find the nested conditional
6312        let inner_cond = items
6313            .iter()
6314            .find_map(|item| {
6315                if let MakefileItem::Conditional(c) = item {
6316                    Some(c)
6317                } else {
6318                    None
6319                }
6320            })
6321            .unwrap();
6322
6323        // Inner conditional's parent should be the outer conditional
6324        let parent = inner_cond.parent();
6325        assert!(parent.is_some());
6326        if let Some(MakefileItem::Conditional(_)) = parent {
6327            // Expected - parent is a conditional
6328        } else {
6329            panic!("Expected inner conditional's parent to be a Conditional");
6330        }
6331    }
6332
6333    #[test]
6334    fn test_line_col() {
6335        let text = r#"# Comment at line 0
6336VAR1 = value1
6337VAR2 = value2
6338
6339rule1: dep1 dep2
6340	command1
6341	command2
6342
6343rule2:
6344	command3
6345
6346ifdef DEBUG
6347CFLAGS = -g
6348endif
6349"#;
6350        let makefile: Makefile = text.parse().unwrap();
6351
6352        // Test variable definition line numbers
6353        // variable_definitions() is recursive, so it finds VAR1, VAR2, and CFLAGS (inside conditional)
6354        let vars: Vec<_> = makefile.variable_definitions().collect();
6355        assert_eq!(vars.len(), 3);
6356
6357        // VAR1 starts at line 1
6358        assert_eq!(vars[0].line(), 1);
6359        assert_eq!(vars[0].column(), 0);
6360        assert_eq!(vars[0].line_col(), (1, 0));
6361
6362        // VAR2 starts at line 2
6363        assert_eq!(vars[1].line(), 2);
6364        assert_eq!(vars[1].column(), 0);
6365
6366        // CFLAGS starts at line 12 (inside ifdef DEBUG)
6367        assert_eq!(vars[2].line(), 12);
6368        assert_eq!(vars[2].column(), 0);
6369
6370        // Test rule line numbers
6371        let rules: Vec<_> = makefile.rules().collect();
6372        assert_eq!(rules.len(), 2);
6373
6374        // rule1 starts at line 4
6375        assert_eq!(rules[0].line(), 4);
6376        assert_eq!(rules[0].column(), 0);
6377        assert_eq!(rules[0].line_col(), (4, 0));
6378
6379        // rule2 starts at line 8
6380        assert_eq!(rules[1].line(), 8);
6381        assert_eq!(rules[1].column(), 0);
6382
6383        // Test conditional line numbers
6384        let conditionals: Vec<_> = makefile.conditionals().collect();
6385        assert_eq!(conditionals.len(), 1);
6386
6387        // ifdef DEBUG starts at line 11
6388        assert_eq!(conditionals[0].line(), 11);
6389        assert_eq!(conditionals[0].column(), 0);
6390        assert_eq!(conditionals[0].line_col(), (11, 0));
6391    }
6392
6393    #[test]
6394    fn test_line_col_multiline() {
6395        let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
6396        let makefile: Makefile = text.parse().unwrap();
6397
6398        // Variable definition starts at line 0
6399        let vars: Vec<_> = makefile.variable_definitions().collect();
6400        assert_eq!(vars.len(), 1);
6401        assert_eq!(vars[0].line(), 0);
6402        assert_eq!(vars[0].column(), 0);
6403
6404        // Rule starts at line 4
6405        let rules: Vec<_> = makefile.rules().collect();
6406        assert_eq!(rules.len(), 1);
6407        assert_eq!(rules[0].line(), 4);
6408        assert_eq!(rules[0].column(), 0);
6409    }
6410
6411    #[test]
6412    fn test_line_col_includes() {
6413        let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
6414        let makefile: Makefile = text.parse().unwrap();
6415
6416        // Variable at line 0
6417        let vars: Vec<_> = makefile.variable_definitions().collect();
6418        assert_eq!(vars[0].line(), 0);
6419
6420        // Includes at lines 2 and 3
6421        let includes: Vec<_> = makefile.includes().collect();
6422        assert_eq!(includes.len(), 2);
6423        assert_eq!(includes[0].line(), 2);
6424        assert_eq!(includes[0].column(), 0);
6425        assert_eq!(includes[1].line(), 3);
6426        assert_eq!(includes[1].column(), 0);
6427    }
6428
6429    #[test]
6430    fn test_conditional_in_rule_vs_toplevel() {
6431        // Conditional immediately after rule (no blank line) - part of rule
6432        let text1 = r#"rule:
6433	command
6434ifeq (,$(X))
6435	test
6436endif
6437"#;
6438        let makefile: Makefile = text1.parse().unwrap();
6439        let rules: Vec<_> = makefile.rules().collect();
6440        let conditionals: Vec<_> = makefile.conditionals().collect();
6441
6442        assert_eq!(rules.len(), 1);
6443        assert_eq!(
6444            conditionals.len(),
6445            0,
6446            "Conditional should be part of rule, not top-level"
6447        );
6448
6449        // Conditional after blank line - top-level
6450        let text2 = r#"rule:
6451	command
6452
6453ifeq (,$(X))
6454	test
6455endif
6456"#;
6457        let makefile: Makefile = text2.parse().unwrap();
6458        let rules: Vec<_> = makefile.rules().collect();
6459        let conditionals: Vec<_> = makefile.conditionals().collect();
6460
6461        assert_eq!(rules.len(), 1);
6462        assert_eq!(
6463            conditionals.len(),
6464            1,
6465            "Conditional after blank line should be top-level"
6466        );
6467        assert_eq!(conditionals[0].line(), 3);
6468    }
6469
6470    #[test]
6471    fn test_nested_conditionals_line_tracking() {
6472        let text = r#"ifdef OUTER
6473VAR1 = value1
6474ifdef INNER
6475VAR2 = value2
6476endif
6477VAR3 = value3
6478endif
6479"#;
6480        let makefile: Makefile = text.parse().unwrap();
6481
6482        let conditionals: Vec<_> = makefile.conditionals().collect();
6483        assert_eq!(
6484            conditionals.len(),
6485            1,
6486            "Only outer conditional should be top-level"
6487        );
6488        assert_eq!(conditionals[0].line(), 0);
6489        assert_eq!(conditionals[0].column(), 0);
6490    }
6491
6492    #[test]
6493    fn test_conditional_else_line_tracking() {
6494        let text = r#"VAR1 = before
6495
6496ifdef DEBUG
6497DEBUG_FLAGS = -g
6498else
6499DEBUG_FLAGS = -O2
6500endif
6501
6502VAR2 = after
6503"#;
6504        let makefile: Makefile = text.parse().unwrap();
6505
6506        let conditionals: Vec<_> = makefile.conditionals().collect();
6507        assert_eq!(conditionals.len(), 1);
6508        assert_eq!(conditionals[0].line(), 2);
6509        assert_eq!(conditionals[0].column(), 0);
6510    }
6511
6512    #[test]
6513    fn test_broken_conditional_endif_without_if() {
6514        // endif without matching if - parser should handle gracefully
6515        let text = "VAR = value\nendif\n";
6516        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6517
6518        // Should parse without crashing
6519        let vars: Vec<_> = makefile.variable_definitions().collect();
6520        assert_eq!(vars.len(), 1);
6521        assert_eq!(vars[0].line(), 0);
6522    }
6523
6524    #[test]
6525    fn test_broken_conditional_else_without_if() {
6526        // else without matching if
6527        let text = "VAR = value\nelse\nVAR2 = other\n";
6528        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6529
6530        // Should parse without crashing
6531        let vars: Vec<_> = makefile.variable_definitions().collect();
6532        assert!(!vars.is_empty(), "Should parse at least the first variable");
6533        assert_eq!(vars[0].line(), 0);
6534    }
6535
6536    #[test]
6537    fn test_broken_conditional_missing_endif() {
6538        // ifdef without matching endif
6539        let text = r#"ifdef DEBUG
6540DEBUG_FLAGS = -g
6541VAR = value
6542"#;
6543        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6544
6545        // Should parse without crashing
6546        assert!(makefile.code().contains("ifdef DEBUG"));
6547    }
6548
6549    #[test]
6550    fn test_multiple_conditionals_line_tracking() {
6551        let text = r#"ifdef A
6552VAR_A = a
6553endif
6554
6555ifdef B
6556VAR_B = b
6557endif
6558
6559ifdef C
6560VAR_C = c
6561endif
6562"#;
6563        let makefile: Makefile = text.parse().unwrap();
6564
6565        let conditionals: Vec<_> = makefile.conditionals().collect();
6566        assert_eq!(conditionals.len(), 3);
6567        assert_eq!(conditionals[0].line(), 0);
6568        assert_eq!(conditionals[1].line(), 4);
6569        assert_eq!(conditionals[2].line(), 8);
6570    }
6571
6572    #[test]
6573    fn test_conditional_with_multiple_else_ifeq() {
6574        let text = r#"ifeq ($(OS),Windows)
6575EXT = .exe
6576else ifeq ($(OS),Linux)
6577EXT = .bin
6578else
6579EXT = .out
6580endif
6581"#;
6582        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6583
6584        let conditionals: Vec<_> = makefile.conditionals().collect();
6585        assert_eq!(conditionals.len(), 1);
6586        assert_eq!(conditionals[0].line(), 0);
6587        assert_eq!(conditionals[0].column(), 0);
6588    }
6589
6590    #[test]
6591    fn test_conditional_types_line_tracking() {
6592        let text = r#"ifdef VAR1
6593A = 1
6594endif
6595
6596ifndef VAR2
6597B = 2
6598endif
6599
6600ifeq ($(X),y)
6601C = 3
6602endif
6603
6604ifneq ($(Y),n)
6605D = 4
6606endif
6607"#;
6608        let makefile: Makefile = text.parse().unwrap();
6609
6610        let conditionals: Vec<_> = makefile.conditionals().collect();
6611        assert_eq!(conditionals.len(), 4);
6612
6613        assert_eq!(conditionals[0].line(), 0); // ifdef
6614        assert_eq!(
6615            conditionals[0].conditional_type(),
6616            Some("ifdef".to_string())
6617        );
6618
6619        assert_eq!(conditionals[1].line(), 4); // ifndef
6620        assert_eq!(
6621            conditionals[1].conditional_type(),
6622            Some("ifndef".to_string())
6623        );
6624
6625        assert_eq!(conditionals[2].line(), 8); // ifeq
6626        assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
6627
6628        assert_eq!(conditionals[3].line(), 12); // ifneq
6629        assert_eq!(
6630            conditionals[3].conditional_type(),
6631            Some("ifneq".to_string())
6632        );
6633    }
6634
6635    #[test]
6636    fn test_conditional_in_rule_with_recipes() {
6637        let text = r#"test:
6638	echo "start"
6639ifdef VERBOSE
6640	echo "verbose mode"
6641endif
6642	echo "end"
6643"#;
6644        let makefile: Makefile = text.parse().unwrap();
6645
6646        let rules: Vec<_> = makefile.rules().collect();
6647        let conditionals: Vec<_> = makefile.conditionals().collect();
6648
6649        assert_eq!(rules.len(), 1);
6650        assert_eq!(rules[0].line(), 0);
6651        // Conditional is part of the rule, not top-level
6652        assert_eq!(conditionals.len(), 0);
6653    }
6654
6655    #[test]
6656    fn test_broken_conditional_double_else() {
6657        // Two else clauses in one conditional
6658        let text = r#"ifdef DEBUG
6659A = 1
6660else
6661B = 2
6662else
6663C = 3
6664endif
6665"#;
6666        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6667
6668        // Should parse without crashing, though it's malformed
6669        assert!(makefile.code().contains("ifdef DEBUG"));
6670    }
6671
6672    #[test]
6673    fn test_broken_conditional_mismatched_nesting() {
6674        // Mismatched nesting - more endifs than ifs
6675        let text = r#"ifdef A
6676VAR = value
6677endif
6678endif
6679"#;
6680        let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6681
6682        // Should parse without crashing
6683        // The extra endif will be parsed separately, so we may get more than 1 item
6684        let conditionals: Vec<_> = makefile.conditionals().collect();
6685        assert!(
6686            !conditionals.is_empty(),
6687            "Should parse at least the first conditional"
6688        );
6689    }
6690
6691    #[test]
6692    fn test_conditional_with_comment_line_tracking() {
6693        let text = r#"# This is a comment
6694ifdef DEBUG
6695# Another comment
6696CFLAGS = -g
6697endif
6698# Final comment
6699"#;
6700        let makefile: Makefile = text.parse().unwrap();
6701
6702        let conditionals: Vec<_> = makefile.conditionals().collect();
6703        assert_eq!(conditionals.len(), 1);
6704        assert_eq!(conditionals[0].line(), 1);
6705        assert_eq!(conditionals[0].column(), 0);
6706    }
6707
6708    #[test]
6709    fn test_conditional_after_variable_with_blank_lines() {
6710        let text = r#"VAR1 = value1
6711
6712
6713ifdef DEBUG
6714VAR2 = value2
6715endif
6716"#;
6717        let makefile: Makefile = text.parse().unwrap();
6718
6719        let vars: Vec<_> = makefile.variable_definitions().collect();
6720        let conditionals: Vec<_> = makefile.conditionals().collect();
6721
6722        // variable_definitions() is recursive, so it finds VAR1 and VAR2 (inside conditional)
6723        assert_eq!(vars.len(), 2);
6724        assert_eq!(vars[0].line(), 0); // VAR1
6725        assert_eq!(vars[1].line(), 4); // VAR2
6726
6727        assert_eq!(conditionals.len(), 1);
6728        assert_eq!(conditionals[0].line(), 3);
6729    }
6730
6731    #[test]
6732    fn test_empty_conditional_line_tracking() {
6733        let text = r#"ifdef DEBUG
6734endif
6735
6736ifndef RELEASE
6737endif
6738"#;
6739        let makefile: Makefile = text.parse().unwrap();
6740
6741        let conditionals: Vec<_> = makefile.conditionals().collect();
6742        assert_eq!(conditionals.len(), 2);
6743        assert_eq!(conditionals[0].line(), 0);
6744        assert_eq!(conditionals[1].line(), 3);
6745    }
6746
6747    #[test]
6748    fn test_recipe_line_tracking() {
6749        let text = r#"build:
6750	echo "Building..."
6751	gcc -o app main.c
6752	echo "Done"
6753
6754test:
6755	./run-tests
6756"#;
6757        let makefile: Makefile = text.parse().unwrap();
6758
6759        // Test first rule's recipes
6760        let rule1 = makefile.rules().next().expect("Should have first rule");
6761        let recipes: Vec<_> = rule1.recipe_nodes().collect();
6762        assert_eq!(recipes.len(), 3);
6763
6764        assert_eq!(recipes[0].text(), "echo \"Building...\"");
6765        assert_eq!(recipes[0].line(), 1);
6766        assert_eq!(recipes[0].column(), 0);
6767
6768        assert_eq!(recipes[1].text(), "gcc -o app main.c");
6769        assert_eq!(recipes[1].line(), 2);
6770        assert_eq!(recipes[1].column(), 0);
6771
6772        assert_eq!(recipes[2].text(), "echo \"Done\"");
6773        assert_eq!(recipes[2].line(), 3);
6774        assert_eq!(recipes[2].column(), 0);
6775
6776        // Test second rule's recipes
6777        let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6778        let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6779        assert_eq!(recipes2.len(), 1);
6780
6781        assert_eq!(recipes2[0].text(), "./run-tests");
6782        assert_eq!(recipes2[0].line(), 6);
6783        assert_eq!(recipes2[0].column(), 0);
6784    }
6785
6786    #[test]
6787    fn test_recipe_with_variables_line_tracking() {
6788        let text = r#"install:
6789	mkdir -p $(DESTDIR)
6790	cp $(BINARY) $(DESTDIR)/
6791"#;
6792        let makefile: Makefile = text.parse().unwrap();
6793        let rule = makefile.rules().next().expect("Should have rule");
6794        let recipes: Vec<_> = rule.recipe_nodes().collect();
6795
6796        assert_eq!(recipes.len(), 2);
6797        assert_eq!(recipes[0].line(), 1);
6798        assert_eq!(recipes[1].line(), 2);
6799    }
6800
6801    #[test]
6802    fn test_recipe_text_no_leading_tab() {
6803        // Test that Recipe::text() does not include the leading tab
6804        let text = "test:\n\techo hello\n\t\techo nested\n\t  echo with spaces\n";
6805        let makefile: Makefile = text.parse().unwrap();
6806        let rule = makefile.rules().next().expect("Should have rule");
6807        let recipes: Vec<_> = rule.recipe_nodes().collect();
6808
6809        assert_eq!(recipes.len(), 3);
6810
6811        // Debug: print syntax tree for the first recipe
6812        eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6813
6814        // First recipe: single tab
6815        assert_eq!(recipes[0].text(), "echo hello");
6816
6817        // Second recipe: double tab (nested)
6818        eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6819        assert_eq!(recipes[1].text(), "\techo nested");
6820
6821        // Third recipe: tab followed by spaces
6822        eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6823        assert_eq!(recipes[2].text(), "  echo with spaces");
6824    }
6825
6826    #[test]
6827    fn test_recipe_parent() {
6828        let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6829        let rule = makefile.rules().next().unwrap();
6830        let recipe = rule.recipe_nodes().next().unwrap();
6831
6832        let parent = recipe.parent().expect("Recipe should have parent");
6833        assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6834        assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6835    }
6836
6837    #[test]
6838    fn test_recipe_is_silent_various_prefixes() {
6839        let makefile: Makefile = r#"test:
6840	@echo silent
6841	-echo ignore
6842	+echo always
6843	@-echo silent_ignore
6844	-@echo ignore_silent
6845	+@echo always_silent
6846	echo normal
6847"#
6848        .parse()
6849        .unwrap();
6850
6851        let rule = makefile.rules().next().unwrap();
6852        let recipes: Vec<_> = rule.recipe_nodes().collect();
6853
6854        assert_eq!(recipes.len(), 7);
6855        assert!(recipes[0].is_silent(), "@echo should be silent");
6856        assert!(!recipes[1].is_silent(), "-echo should not be silent");
6857        assert!(!recipes[2].is_silent(), "+echo should not be silent");
6858        assert!(recipes[3].is_silent(), "@-echo should be silent");
6859        assert!(recipes[4].is_silent(), "-@echo should be silent");
6860        assert!(recipes[5].is_silent(), "+@echo should be silent");
6861        assert!(!recipes[6].is_silent(), "echo should not be silent");
6862    }
6863
6864    #[test]
6865    fn test_recipe_is_ignore_errors_various_prefixes() {
6866        let makefile: Makefile = r#"test:
6867	@echo silent
6868	-echo ignore
6869	+echo always
6870	@-echo silent_ignore
6871	-@echo ignore_silent
6872	+-echo always_ignore
6873	echo normal
6874"#
6875        .parse()
6876        .unwrap();
6877
6878        let rule = makefile.rules().next().unwrap();
6879        let recipes: Vec<_> = rule.recipe_nodes().collect();
6880
6881        assert_eq!(recipes.len(), 7);
6882        assert!(
6883            !recipes[0].is_ignore_errors(),
6884            "@echo should not ignore errors"
6885        );
6886        assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6887        assert!(
6888            !recipes[2].is_ignore_errors(),
6889            "+echo should not ignore errors"
6890        );
6891        assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6892        assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6893        assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6894        assert!(
6895            !recipes[6].is_ignore_errors(),
6896            "echo should not ignore errors"
6897        );
6898    }
6899
6900    #[test]
6901    fn test_recipe_set_prefix_add() {
6902        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6903        let rule = makefile.rules().next().unwrap();
6904        let mut recipe = rule.recipe_nodes().next().unwrap();
6905
6906        recipe.set_prefix("@");
6907        assert_eq!(recipe.text(), "@echo hello");
6908        assert!(recipe.is_silent());
6909    }
6910
6911    #[test]
6912    fn test_recipe_set_prefix_change() {
6913        let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6914        let rule = makefile.rules().next().unwrap();
6915        let mut recipe = rule.recipe_nodes().next().unwrap();
6916
6917        recipe.set_prefix("-");
6918        assert_eq!(recipe.text(), "-echo hello");
6919        assert!(!recipe.is_silent());
6920        assert!(recipe.is_ignore_errors());
6921    }
6922
6923    #[test]
6924    fn test_recipe_set_prefix_remove() {
6925        let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
6926        let rule = makefile.rules().next().unwrap();
6927        let mut recipe = rule.recipe_nodes().next().unwrap();
6928
6929        recipe.set_prefix("");
6930        assert_eq!(recipe.text(), "echo hello");
6931        assert!(!recipe.is_silent());
6932        assert!(!recipe.is_ignore_errors());
6933    }
6934
6935    #[test]
6936    fn test_recipe_set_prefix_combinations() {
6937        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6938        let rule = makefile.rules().next().unwrap();
6939        let mut recipe = rule.recipe_nodes().next().unwrap();
6940
6941        recipe.set_prefix("@-");
6942        assert_eq!(recipe.text(), "@-echo hello");
6943        assert!(recipe.is_silent());
6944        assert!(recipe.is_ignore_errors());
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
6952    #[test]
6953    fn test_recipe_replace_text_basic() {
6954        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6955        let rule = makefile.rules().next().unwrap();
6956        let mut recipe = rule.recipe_nodes().next().unwrap();
6957
6958        recipe.replace_text("echo world");
6959        assert_eq!(recipe.text(), "echo world");
6960
6961        // Verify it's still accessible from the rule
6962        let rule = makefile.rules().next().unwrap();
6963        assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6964    }
6965
6966    #[test]
6967    fn test_recipe_replace_text_with_prefix() {
6968        let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6969        let rule = makefile.rules().next().unwrap();
6970        let mut recipe = rule.recipe_nodes().next().unwrap();
6971
6972        recipe.replace_text("@echo goodbye");
6973        assert_eq!(recipe.text(), "@echo goodbye");
6974        assert!(recipe.is_silent());
6975    }
6976
6977    #[test]
6978    fn test_recipe_insert_before_single() {
6979        let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6980        let rule = makefile.rules().next().unwrap();
6981        let recipe = rule.recipe_nodes().next().unwrap();
6982
6983        recipe.insert_before("echo hello");
6984
6985        let rule = makefile.rules().next().unwrap();
6986        let recipes: Vec<_> = rule.recipes().collect();
6987        assert_eq!(recipes, vec!["echo hello", "echo world"]);
6988    }
6989
6990    #[test]
6991    fn test_recipe_insert_before_multiple() {
6992        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6993            .parse()
6994            .unwrap();
6995        let rule = makefile.rules().next().unwrap();
6996        let recipes: Vec<_> = rule.recipe_nodes().collect();
6997
6998        // Insert before the second recipe
6999        recipes[1].insert_before("echo middle");
7000
7001        let rule = makefile.rules().next().unwrap();
7002        let new_recipes: Vec<_> = rule.recipes().collect();
7003        assert_eq!(
7004            new_recipes,
7005            vec!["echo one", "echo middle", "echo two", "echo three"]
7006        );
7007    }
7008
7009    #[test]
7010    fn test_recipe_insert_before_first() {
7011        let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7012        let rule = makefile.rules().next().unwrap();
7013        let recipes: Vec<_> = rule.recipe_nodes().collect();
7014
7015        recipes[0].insert_before("echo zero");
7016
7017        let rule = makefile.rules().next().unwrap();
7018        let new_recipes: Vec<_> = rule.recipes().collect();
7019        assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
7020    }
7021
7022    #[test]
7023    fn test_recipe_insert_after_single() {
7024        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7025        let rule = makefile.rules().next().unwrap();
7026        let recipe = rule.recipe_nodes().next().unwrap();
7027
7028        recipe.insert_after("echo world");
7029
7030        let rule = makefile.rules().next().unwrap();
7031        let recipes: Vec<_> = rule.recipes().collect();
7032        assert_eq!(recipes, vec!["echo hello", "echo world"]);
7033    }
7034
7035    #[test]
7036    fn test_recipe_insert_after_multiple() {
7037        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7038            .parse()
7039            .unwrap();
7040        let rule = makefile.rules().next().unwrap();
7041        let recipes: Vec<_> = rule.recipe_nodes().collect();
7042
7043        // Insert after the second recipe
7044        recipes[1].insert_after("echo middle");
7045
7046        let rule = makefile.rules().next().unwrap();
7047        let new_recipes: Vec<_> = rule.recipes().collect();
7048        assert_eq!(
7049            new_recipes,
7050            vec!["echo one", "echo two", "echo middle", "echo three"]
7051        );
7052    }
7053
7054    #[test]
7055    fn test_recipe_insert_after_last() {
7056        let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7057        let rule = makefile.rules().next().unwrap();
7058        let recipes: Vec<_> = rule.recipe_nodes().collect();
7059
7060        recipes[1].insert_after("echo three");
7061
7062        let rule = makefile.rules().next().unwrap();
7063        let new_recipes: Vec<_> = rule.recipes().collect();
7064        assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
7065    }
7066
7067    #[test]
7068    fn test_recipe_remove_single() {
7069        let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7070        let rule = makefile.rules().next().unwrap();
7071        let recipe = rule.recipe_nodes().next().unwrap();
7072
7073        recipe.remove();
7074
7075        let rule = makefile.rules().next().unwrap();
7076        assert_eq!(rule.recipes().count(), 0);
7077    }
7078
7079    #[test]
7080    fn test_recipe_remove_first() {
7081        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7082            .parse()
7083            .unwrap();
7084        let rule = makefile.rules().next().unwrap();
7085        let recipes: Vec<_> = rule.recipe_nodes().collect();
7086
7087        recipes[0].remove();
7088
7089        let rule = makefile.rules().next().unwrap();
7090        let new_recipes: Vec<_> = rule.recipes().collect();
7091        assert_eq!(new_recipes, vec!["echo two", "echo three"]);
7092    }
7093
7094    #[test]
7095    fn test_recipe_remove_middle() {
7096        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7097            .parse()
7098            .unwrap();
7099        let rule = makefile.rules().next().unwrap();
7100        let recipes: Vec<_> = rule.recipe_nodes().collect();
7101
7102        recipes[1].remove();
7103
7104        let rule = makefile.rules().next().unwrap();
7105        let new_recipes: Vec<_> = rule.recipes().collect();
7106        assert_eq!(new_recipes, vec!["echo one", "echo three"]);
7107    }
7108
7109    #[test]
7110    fn test_recipe_remove_last() {
7111        let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7112            .parse()
7113            .unwrap();
7114        let rule = makefile.rules().next().unwrap();
7115        let recipes: Vec<_> = rule.recipe_nodes().collect();
7116
7117        recipes[2].remove();
7118
7119        let rule = makefile.rules().next().unwrap();
7120        let new_recipes: Vec<_> = rule.recipes().collect();
7121        assert_eq!(new_recipes, vec!["echo one", "echo two"]);
7122    }
7123
7124    #[test]
7125    fn test_recipe_multiple_operations() {
7126        let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7127        let rule = makefile.rules().next().unwrap();
7128        let mut recipe = rule.recipe_nodes().next().unwrap();
7129
7130        // Replace text
7131        recipe.replace_text("echo modified");
7132        assert_eq!(recipe.text(), "echo modified");
7133
7134        // Add prefix
7135        recipe.set_prefix("@");
7136        assert_eq!(recipe.text(), "@echo modified");
7137
7138        // Insert after
7139        recipe.insert_after("echo three");
7140
7141        // Verify all changes
7142        let rule = makefile.rules().next().unwrap();
7143        let recipes: Vec<_> = rule.recipes().collect();
7144        assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
7145    }
7146
7147    #[test]
7148    fn test_from_str_relaxed_valid() {
7149        let input = "all: foo\n\tfoo bar\n";
7150        let (makefile, errors) = Makefile::from_str_relaxed(input);
7151        assert!(errors.is_empty());
7152        assert_eq!(makefile.rules().count(), 1);
7153        assert_eq!(makefile.to_string(), input);
7154    }
7155
7156    #[test]
7157    fn test_from_str_relaxed_with_errors() {
7158        // "rule target\n\tcommand" produces a parse error (missing colon)
7159        let input = "rule target\n\tcommand\n";
7160        let (makefile, errors) = Makefile::from_str_relaxed(input);
7161        assert!(!errors.is_empty());
7162        // Round-trip preserves all text
7163        assert_eq!(makefile.to_string(), input);
7164    }
7165
7166    #[test]
7167    fn test_positioned_errors_have_valid_ranges() {
7168        let input = "rule target\n\tcommand\n";
7169        let parsed = Makefile::parse(input);
7170        assert!(!parsed.ok());
7171
7172        let positioned = parsed.positioned_errors();
7173        assert!(!positioned.is_empty());
7174
7175        for err in positioned {
7176            // Range should be within the input
7177            let start: u32 = err.range.start().into();
7178            let end: u32 = err.range.end().into();
7179            assert!(start <= end);
7180            assert!((end as usize) <= input.len());
7181        }
7182    }
7183
7184    #[test]
7185    fn test_positioned_errors_point_to_error_location() {
7186        let input = "rule target\n\tcommand\n";
7187        let parsed = Makefile::parse(input);
7188        assert!(!parsed.ok());
7189
7190        let positioned = parsed.positioned_errors();
7191        assert!(!positioned.is_empty());
7192
7193        let err = &positioned[0];
7194        let start: usize = err.range.start().into();
7195        let end: usize = err.range.end().into();
7196        // The error should point somewhere in the input
7197        let error_text = &input[start..end];
7198        assert!(!error_text.is_empty());
7199
7200        // Tree should still be accessible
7201        let tree = parsed.tree();
7202        assert_eq!(tree.to_string(), input);
7203    }
7204
7205    #[test]
7206    fn test_tree_with_errors_preserves_text() {
7207        let input = "rule target\n\tcommand\nVAR = value\n";
7208        let parsed = Makefile::parse(input);
7209        assert!(!parsed.ok());
7210
7211        let tree = parsed.tree();
7212        assert_eq!(tree.to_string(), input);
7213
7214        // Valid parts should still be accessible
7215        assert_eq!(tree.variable_definitions().count(), 1);
7216    }
7217}
7218
7219#[cfg(test)]
7220mod test_continuation {
7221    use super::*;
7222
7223    #[test]
7224    fn test_recipe_continuation_lines() {
7225        let makefile_content = r#"override_dh_autoreconf:
7226	set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \
7227	  dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \
7228	  sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs
7229	dh_autoreconf
7230"#;
7231
7232        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7233        let rule = makefile.rules().next().unwrap();
7234
7235        let recipes: Vec<_> = rule.recipe_nodes().collect();
7236
7237        // Should have 2 recipe nodes: one multi-line command and one single-line
7238        assert_eq!(recipes.len(), 2);
7239
7240        // First recipe should contain all three physical lines with newlines preserved,
7241        // and the leading tab stripped from each continuation line
7242        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";
7243        assert_eq!(recipes[0].text(), expected_first);
7244
7245        // Second recipe should be the standalone dh_autoreconf line
7246        assert_eq!(recipes[1].text(), "dh_autoreconf");
7247    }
7248
7249    #[test]
7250    fn test_simple_continuation() {
7251        let makefile_content = "test:\n\techo hello && \\\n\t  echo world\n";
7252
7253        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7254        let rule = makefile.rules().next().unwrap();
7255        let recipes: Vec<_> = rule.recipe_nodes().collect();
7256
7257        assert_eq!(recipes.len(), 1);
7258        assert_eq!(recipes[0].text(), "echo hello && \\\n  echo world");
7259    }
7260
7261    #[test]
7262    fn test_multiple_continuations() {
7263        let makefile_content = "test:\n\techo line1 && \\\n\t  echo line2 && \\\n\t  echo line3 && \\\n\t  echo line4\n";
7264
7265        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7266        let rule = makefile.rules().next().unwrap();
7267        let recipes: Vec<_> = rule.recipe_nodes().collect();
7268
7269        assert_eq!(recipes.len(), 1);
7270        assert_eq!(
7271            recipes[0].text(),
7272            "echo line1 && \\\n  echo line2 && \\\n  echo line3 && \\\n  echo line4"
7273        );
7274    }
7275
7276    #[test]
7277    fn test_continuation_round_trip() {
7278        let makefile_content = "test:\n\techo hello && \\\n\t  echo world\n\techo done\n";
7279
7280        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7281        let output = makefile.to_string();
7282
7283        // Should preserve the exact content
7284        assert_eq!(output, makefile_content);
7285    }
7286
7287    #[test]
7288    fn test_continuation_with_silent_prefix() {
7289        let makefile_content = "test:\n\t@echo hello && \\\n\t  echo world\n";
7290
7291        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7292        let rule = makefile.rules().next().unwrap();
7293        let recipes: Vec<_> = rule.recipe_nodes().collect();
7294
7295        assert_eq!(recipes.len(), 1);
7296        assert_eq!(recipes[0].text(), "@echo hello && \\\n  echo world");
7297        assert!(recipes[0].is_silent());
7298    }
7299
7300    #[test]
7301    fn test_mixed_continued_and_non_continued() {
7302        let makefile_content = r#"test:
7303	echo first
7304	echo second && \
7305	  echo third
7306	echo fourth
7307"#;
7308
7309        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7310        let rule = makefile.rules().next().unwrap();
7311        let recipes: Vec<_> = rule.recipe_nodes().collect();
7312
7313        assert_eq!(recipes.len(), 3);
7314        assert_eq!(recipes[0].text(), "echo first");
7315        assert_eq!(recipes[1].text(), "echo second && \\\n  echo third");
7316        assert_eq!(recipes[2].text(), "echo fourth");
7317    }
7318
7319    #[test]
7320    fn test_continuation_replace_command() {
7321        let makefile_content = "test:\n\techo hello && \\\n\t  echo world\n\techo done\n";
7322
7323        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7324        let mut rule = makefile.rules().next().unwrap();
7325
7326        // Replace the multi-line command
7327        rule.replace_command(0, "echo replaced");
7328
7329        let recipes: Vec<_> = rule.recipe_nodes().collect();
7330        assert_eq!(recipes.len(), 2);
7331        assert_eq!(recipes[0].text(), "echo replaced");
7332        assert_eq!(recipes[1].text(), "echo done");
7333    }
7334
7335    #[test]
7336    fn test_continuation_count() {
7337        let makefile_content = "test:\n\techo hello && \\\n\t  echo world\n\techo done\n";
7338
7339        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7340        let rule = makefile.rules().next().unwrap();
7341
7342        // Even though there are 3 physical lines, there should be 2 logical recipe nodes
7343        assert_eq!(rule.recipe_count(), 2);
7344        assert_eq!(rule.recipe_nodes().count(), 2);
7345
7346        // recipes() should return one string per logical recipe node
7347        let recipes_list: Vec<_> = rule.recipes().collect();
7348        assert_eq!(
7349            recipes_list,
7350            vec!["echo hello && \\\n  echo world", "echo done"]
7351        );
7352    }
7353
7354    #[test]
7355    fn test_backslash_in_middle_of_line() {
7356        // Backslash not at end should not trigger continuation
7357        let makefile_content = "test:\n\techo hello\\nworld\n\techo done\n";
7358
7359        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7360        let rule = makefile.rules().next().unwrap();
7361        let recipes: Vec<_> = rule.recipe_nodes().collect();
7362
7363        assert_eq!(recipes.len(), 2);
7364        assert_eq!(recipes[0].text(), "echo hello\\nworld");
7365        assert_eq!(recipes[1].text(), "echo done");
7366    }
7367
7368    #[test]
7369    fn test_shell_for_loop_with_continuation() {
7370        // Regression test for Debian bug #1128608 / GitHub issue (if any)
7371        // Ensures shell for loops with backslash continuations are treated as
7372        // a single recipe node and preserve the 'done' statement
7373        let makefile_content = r#"override_dh_installman:
7374	for i in foo bar; do \
7375		pod2man --section=1 $$i ; \
7376	done
7377"#;
7378
7379        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7380        let rule = makefile.rules().next().unwrap();
7381
7382        // Should have exactly 1 recipe node containing the entire for loop
7383        let recipes: Vec<_> = rule.recipe_nodes().collect();
7384        assert_eq!(recipes.len(), 1);
7385
7386        // The recipe text should contain the complete for loop including 'done'
7387        let recipe_text = recipes[0].text();
7388        let expected_recipe = "for i in foo bar; do \\\n\tpod2man --section=1 $$i ; \\\ndone";
7389        assert_eq!(recipe_text, expected_recipe);
7390
7391        // Round-trip should preserve the complete structure
7392        let output = makefile.to_string();
7393        assert_eq!(output, makefile_content);
7394    }
7395
7396    #[test]
7397    fn test_shell_for_loop_remove_command() {
7398        // Regression test: removing other commands shouldn't affect 'done'
7399        // This simulates lintian-brush modifying debian/rules files
7400        let makefile_content = r#"override_dh_installman:
7401	for i in foo bar; do \
7402		pod2man --section=1 $$i ; \
7403	done
7404	echo "Done with man pages"
7405"#;
7406
7407        let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7408        let mut rule = makefile.rules().next().unwrap();
7409
7410        // Should have 2 recipe nodes: the for loop and the echo
7411        assert_eq!(rule.recipe_count(), 2);
7412
7413        // Remove the second command (the echo)
7414        rule.remove_command(1);
7415
7416        // Should now have only the for loop
7417        let recipes: Vec<_> = rule.recipe_nodes().collect();
7418        assert_eq!(recipes.len(), 1);
7419
7420        // The for loop should still be complete with 'done'
7421        let output = makefile.to_string();
7422        let expected_output = r#"override_dh_installman:
7423	for i in foo bar; do \
7424		pod2man --section=1 $$i ; \
7425	done
7426"#;
7427        assert_eq!(output, expected_output);
7428    }
7429
7430    #[test]
7431    fn test_variable_reference_paren() {
7432        let makefile: Makefile = "CFLAGS = $(BASE_FLAGS) -Wall\n".parse().unwrap();
7433        let refs: Vec<_> = makefile.variable_references().collect();
7434        assert_eq!(refs.len(), 1);
7435        assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7436        assert_eq!(refs[0].to_string(), "$(BASE_FLAGS)");
7437    }
7438
7439    #[test]
7440    fn test_variable_reference_brace() {
7441        let makefile: Makefile = "CFLAGS = ${BASE_FLAGS} -Wall\n".parse().unwrap();
7442        let refs: Vec<_> = makefile.variable_references().collect();
7443        assert_eq!(refs.len(), 1);
7444        assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7445        assert_eq!(refs[0].to_string(), "${BASE_FLAGS}");
7446    }
7447
7448    #[test]
7449    fn test_variable_reference_in_prerequisites() {
7450        let makefile: Makefile = "all: $(TARGETS)\n".parse().unwrap();
7451        let refs: Vec<_> = makefile.variable_references().collect();
7452        let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7453        assert!(names.contains(&"TARGETS".to_string()));
7454    }
7455
7456    #[test]
7457    fn test_variable_reference_multiple() {
7458        let makefile: Makefile =
7459            "CFLAGS = $(BASE_FLAGS) -Wall\nLDFLAGS = $(BASE_LDFLAGS) -lm\nall: $(TARGETS)\n"
7460                .parse()
7461                .unwrap();
7462        let refs: Vec<_> = makefile.variable_references().collect();
7463        let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7464        assert!(names.contains(&"BASE_FLAGS".to_string()));
7465        assert!(names.contains(&"BASE_LDFLAGS".to_string()));
7466        assert!(names.contains(&"TARGETS".to_string()));
7467    }
7468
7469    #[test]
7470    fn test_variable_reference_nested() {
7471        let makefile: Makefile = "FOO = $($(INNER))\n".parse().unwrap();
7472        let refs: Vec<_> = makefile.variable_references().collect();
7473        let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7474        assert!(names.contains(&"INNER".to_string()));
7475    }
7476
7477    #[test]
7478    fn test_variable_reference_line_col() {
7479        let makefile: Makefile = "A = 1\nB = $(FOO)\n".parse().unwrap();
7480        let refs: Vec<_> = makefile.variable_references().collect();
7481        assert_eq!(refs.len(), 1);
7482        assert_eq!(refs[0].name(), Some("FOO".to_string()));
7483        assert_eq!(refs[0].line(), 1);
7484        assert_eq!(refs[0].column(), 4);
7485        assert_eq!(refs[0].line_col(), (1, 4));
7486    }
7487
7488    #[test]
7489    fn test_variable_reference_no_refs() {
7490        let makefile: Makefile = "A = hello\nall:\n\techo done\n".parse().unwrap();
7491        let refs: Vec<_> = makefile.variable_references().collect();
7492        assert_eq!(refs.len(), 0);
7493    }
7494
7495    #[test]
7496    fn test_variable_reference_mixed_styles() {
7497        let makefile: Makefile = "A = $(FOO) ${BAR}\n".parse().unwrap();
7498        let refs: Vec<_> = makefile.variable_references().collect();
7499        let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7500        assert_eq!(names.len(), 2);
7501        assert!(names.contains(&"FOO".to_string()));
7502        assert!(names.contains(&"BAR".to_string()));
7503    }
7504
7505    #[test]
7506    fn test_brace_variable_in_prerequisites() {
7507        let makefile: Makefile = "all: ${OBJS}\n".parse().unwrap();
7508        let refs: Vec<_> = makefile.variable_references().collect();
7509        assert_eq!(refs.len(), 1);
7510        assert_eq!(refs[0].name(), Some("OBJS".to_string()));
7511    }
7512
7513    #[test]
7514    fn test_parse_brace_variable_roundtrip() {
7515        let input = "CFLAGS = ${BASE_FLAGS} -Wall\n";
7516        let makefile: Makefile = input.parse().unwrap();
7517        assert_eq!(makefile.to_string(), input);
7518    }
7519
7520    #[test]
7521    fn test_parse_nested_variable_in_value_roundtrip() {
7522        let input = "FOO = $(BAR) baz $(QUUX)\n";
7523        let makefile: Makefile = input.parse().unwrap();
7524        assert_eq!(makefile.to_string(), input);
7525    }
7526
7527    #[test]
7528    fn test_is_function_call() {
7529        let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7530        let refs: Vec<_> = makefile.variable_references().collect();
7531        assert_eq!(refs.len(), 1);
7532        assert!(refs[0].is_function_call());
7533    }
7534
7535    #[test]
7536    fn test_is_function_call_simple_variable() {
7537        let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7538        let refs: Vec<_> = makefile.variable_references().collect();
7539        assert_eq!(refs.len(), 1);
7540        assert!(!refs[0].is_function_call());
7541    }
7542
7543    #[test]
7544    fn test_is_function_call_with_commas() {
7545        let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7546        let refs: Vec<_> = makefile.variable_references().collect();
7547        assert_eq!(refs.len(), 1);
7548        assert!(refs[0].is_function_call());
7549    }
7550
7551    #[test]
7552    fn test_is_function_call_braces() {
7553        let makefile: Makefile = "FILES = ${wildcard *.c}\n".parse().unwrap();
7554        let refs: Vec<_> = makefile.variable_references().collect();
7555        assert_eq!(refs.len(), 1);
7556        assert!(refs[0].is_function_call());
7557    }
7558
7559    #[test]
7560    fn test_argument_count_simple_variable() {
7561        let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7562        let refs: Vec<_> = makefile.variable_references().collect();
7563        assert_eq!(refs[0].argument_count(), 0);
7564    }
7565
7566    #[test]
7567    fn test_argument_count_one_arg() {
7568        let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7569        let refs: Vec<_> = makefile.variable_references().collect();
7570        assert_eq!(refs[0].argument_count(), 1);
7571    }
7572
7573    #[test]
7574    fn test_argument_count_three_args() {
7575        let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7576        let refs: Vec<_> = makefile.variable_references().collect();
7577        assert_eq!(refs[0].argument_count(), 3);
7578    }
7579
7580    #[test]
7581    fn test_argument_index_at_offset_subst() {
7582        let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7583        let refs: Vec<_> = makefile.variable_references().collect();
7584        // "X = $(subst a,b,text)"
7585        //  0123456789012345678901
7586        //              ^first arg (offset 12)
7587        //                ^second arg (offset 14)
7588        //                  ^third arg (offset 16)
7589        assert_eq!(refs[0].argument_index_at_offset(12), Some(0));
7590        assert_eq!(refs[0].argument_index_at_offset(14), Some(1));
7591        assert_eq!(refs[0].argument_index_at_offset(16), Some(2));
7592    }
7593
7594    #[test]
7595    fn test_argument_index_at_offset_outside() {
7596        let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7597        let refs: Vec<_> = makefile.variable_references().collect();
7598        // Before the reference
7599        assert_eq!(refs[0].argument_index_at_offset(0), None);
7600        // After the reference
7601        assert_eq!(refs[0].argument_index_at_offset(22), None);
7602    }
7603
7604    #[test]
7605    fn test_argument_index_at_offset_simple_variable() {
7606        let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7607        let refs: Vec<_> = makefile.variable_references().collect();
7608        assert_eq!(refs[0].argument_index_at_offset(11), None);
7609    }
7610
7611    #[test]
7612    fn test_lex_braces() {
7613        use crate::lex::lex;
7614        let tokens = lex("${FOO}");
7615        let kinds: Vec<_> = tokens.iter().map(|(k, _)| *k).collect();
7616        assert!(kinds.contains(&DOLLAR));
7617        assert!(kinds.contains(&LBRACE));
7618        assert!(kinds.contains(&RBRACE));
7619    }
7620
7621    #[test]
7622    fn test_parse_quoted_string_inside_function_call() {
7623        // The lexer emits a balanced quoted string as one QUOTE token, so a
7624        // quoted argument with embedded parentheses must not break paren
7625        // balance tracking inside a $(...) expression. Lone or asymmetric
7626        // quotes (it's, foo'bar) must not swallow the rest of the line.
7627        let cases = [
7628            "X = $(if a,'foo')\n",
7629            "X = $(if a,'foo (bar)')\n",
7630            "X = $(if a,'(')\n",
7631            "X = $(if a,')')\n",
7632            "X = $(if $(SKIP),-k 'not ($(call f,$(s),$(SKIP)))')\n",
7633            "X = foo'bar\nY = baz\n",
7634            "X = it's fine\n",
7635            "X = $(if a,it's)\n",
7636            "X = '\nY = bar\n",
7637        ];
7638        for src in cases {
7639            let parsed: Makefile = src.parse().unwrap_or_else(|e| {
7640                panic!("failed to parse {src:?}: {e:?}");
7641            });
7642            assert_eq!(parsed.to_string(), src, "round-trip mismatch for {src:?}");
7643        }
7644    }
7645}