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)]
9pub enum Error {
11 Io(std::io::Error),
13
14 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)]
36pub struct ParseError {
38 pub errors: Vec<ErrorInfo>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct ErrorInfo {
45 pub message: String,
47 pub line: usize,
49 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
74pub enum Lang {}
75impl rowan::Language for Lang {
76 type Kind = SyntaxKind;
77 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
78 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
79 }
80 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
81 kind.into()
82 }
83}
84
85use rowan::GreenNode;
88
89use rowan::GreenNodeBuilder;
93
94#[derive(Debug)]
97pub(crate) struct Parse {
98 pub(crate) green_node: GreenNode,
99 #[allow(unused)]
100 pub(crate) errors: Vec<ErrorInfo>,
101}
102
103pub(crate) fn parse(text: &str, variant: Option<MakefileVariant>) -> Parse {
104 struct Parser {
105 tokens: Vec<(SyntaxKind, String)>,
108 builder: GreenNodeBuilder<'static>,
110 errors: Vec<ErrorInfo>,
113 original_text: String,
115 variant: Option<MakefileVariant>,
117 }
118
119 impl Parser {
120 fn error(&mut self, msg: String) {
121 self.builder.start_node(ERROR.into());
122
123 let (line, context) = if self.current() == Some(INDENT) {
124 let lines: Vec<&str> = self.original_text.lines().collect();
126 let tab_line = lines
127 .iter()
128 .enumerate()
129 .find(|(_, line)| line.starts_with('\t'))
130 .map(|(i, _)| i + 1)
131 .unwrap_or(1);
132
133 let next_line = tab_line + 1;
135 if next_line <= lines.len() {
136 (next_line, lines[next_line - 1].to_string())
137 } else {
138 (tab_line, lines[tab_line - 1].to_string())
139 }
140 } else {
141 let line = self.get_line_number_for_position(self.tokens.len());
142 (line, self.get_context_for_line(line))
143 };
144
145 let message = if self.current() == Some(INDENT) && !msg.contains("indented") {
146 if !self.tokens.is_empty() && self.tokens[self.tokens.len() - 1].0 == IDENTIFIER {
147 "expected ':'".to_string()
148 } else {
149 "indented line not part of a rule".to_string()
150 }
151 } else {
152 msg
153 };
154
155 self.errors.push(ErrorInfo {
156 message,
157 line,
158 context,
159 });
160
161 if self.current().is_some() {
162 self.bump();
163 }
164 self.builder.finish_node();
165 }
166
167 fn get_line_number_for_position(&self, position: usize) -> usize {
168 if position >= self.tokens.len() {
169 return self.original_text.matches('\n').count() + 1;
170 }
171
172 self.tokens[0..position]
174 .iter()
175 .filter(|(kind, _)| *kind == NEWLINE)
176 .count()
177 + 1
178 }
179
180 fn get_context_for_line(&self, line_number: usize) -> String {
181 self.original_text
182 .lines()
183 .nth(line_number - 1)
184 .unwrap_or("")
185 .to_string()
186 }
187
188 fn parse_recipe_line(&mut self) {
189 self.builder.start_node(RECIPE.into());
190
191 if self.current() != Some(INDENT) {
193 self.error("recipe line must start with a tab".to_string());
194 self.builder.finish_node();
195 return;
196 }
197 self.bump();
198
199 while self.current().is_some() && self.current() != Some(NEWLINE) {
202 self.bump();
203 }
204
205 if self.current() == Some(NEWLINE) {
207 self.bump();
208 }
209
210 self.builder.finish_node();
211 }
212
213 fn parse_rule_target(&mut self) -> bool {
214 match self.current() {
215 Some(IDENTIFIER) => {
216 if self.is_archive_member() {
218 self.parse_archive_member();
219 } else {
220 self.bump();
221 }
222 true
223 }
224 Some(DOLLAR) => {
225 self.parse_variable_reference();
226 true
227 }
228 _ => {
229 self.error("expected rule target".to_string());
230 false
231 }
232 }
233 }
234
235 fn is_archive_member(&self) -> bool {
236 if self.tokens.len() < 2 {
239 return false;
240 }
241
242 let current_is_identifier = self.current() == Some(IDENTIFIER);
244 let next_is_lparen =
245 self.tokens.len() > 1 && self.tokens[self.tokens.len() - 2].0 == LPAREN;
246
247 current_is_identifier && next_is_lparen
248 }
249
250 fn parse_archive_member(&mut self) {
251 if self.current() == Some(IDENTIFIER) {
262 self.bump();
263 }
264
265 if self.current() == Some(LPAREN) {
267 self.bump();
268
269 self.builder.start_node(ARCHIVE_MEMBERS.into());
271
272 while self.current().is_some() && self.current() != Some(RPAREN) {
274 match self.current() {
275 Some(IDENTIFIER) | Some(TEXT) => {
276 self.builder.start_node(ARCHIVE_MEMBER.into());
278 self.bump();
279 self.builder.finish_node();
280 }
281 Some(WHITESPACE) => self.bump(),
282 Some(DOLLAR) => {
283 self.builder.start_node(ARCHIVE_MEMBER.into());
285 self.parse_variable_reference();
286 self.builder.finish_node();
287 }
288 _ => break,
289 }
290 }
291
292 self.builder.finish_node();
294
295 if self.current() == Some(RPAREN) {
297 self.bump();
298 } else {
299 self.error("expected ')' to close archive member".to_string());
300 }
301 }
302 }
303
304 fn parse_rule_dependencies(&mut self) {
305 self.builder.start_node(PREREQUISITES.into());
306
307 while self.current().is_some() && self.current() != Some(NEWLINE) {
308 match self.current() {
309 Some(WHITESPACE) => {
310 self.bump(); }
312 Some(IDENTIFIER) => {
313 self.builder.start_node(PREREQUISITE.into());
315
316 if self.is_archive_member() {
317 self.parse_archive_member();
318 } else {
319 self.bump(); }
321
322 self.builder.finish_node(); }
324 Some(DOLLAR) => {
325 self.builder.start_node(PREREQUISITE.into());
327
328 self.bump(); if self.current() == Some(LPAREN) {
332 self.bump(); let mut paren_count = 1;
334
335 while self.current().is_some() && paren_count > 0 {
336 if self.current() == Some(LPAREN) {
337 paren_count += 1;
338 } else if self.current() == Some(RPAREN) {
339 paren_count -= 1;
340 }
341 self.bump();
342 }
343 } else {
344 if self.current().is_some() {
346 self.bump();
347 }
348 }
349
350 self.builder.finish_node(); }
352 _ => {
353 self.bump();
355 }
356 }
357 }
358
359 self.builder.finish_node(); }
361
362 fn parse_rule_recipes(&mut self) {
363 let mut conditional_depth = 0;
365 let mut newline_count = 0;
367
368 loop {
369 match self.current() {
370 Some(INDENT) => {
371 newline_count = 0;
372 self.parse_recipe_line();
373 }
374 Some(NEWLINE) => {
375 newline_count += 1;
376 self.bump();
377 }
378 Some(COMMENT) => {
379 if conditional_depth == 0 && newline_count >= 1 {
381 break;
382 }
383 newline_count = 0;
384 self.parse_comment();
385 }
386 Some(IDENTIFIER) => {
387 let token = &self.tokens.last().unwrap().1.clone();
388 if (token == "ifdef"
390 || token == "ifndef"
391 || token == "ifeq"
392 || token == "ifneq")
393 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
394 {
395 if conditional_depth == 0 && newline_count >= 1 {
398 break;
399 }
400 newline_count = 0;
401 conditional_depth += 1;
402 self.parse_conditional();
403 conditional_depth -= 1;
406 } else if token == "include" || token == "-include" || token == "sinclude" {
407 if conditional_depth == 0 && newline_count >= 1 {
409 break;
410 }
411 newline_count = 0;
412 self.parse_include();
413 } else if token == "else" || token == "endif" {
414 break;
417 } else {
418 if conditional_depth == 0 {
420 break;
421 }
422 break;
425 }
426 }
427 _ => break,
428 }
429 }
430 }
431
432 fn find_and_consume_colon(&mut self) -> bool {
433 self.skip_ws();
435
436 if self.current() == Some(OPERATOR) && self.tokens.last().unwrap().1 == ":" {
438 self.bump();
439 return true;
440 }
441
442 let has_colon = self
444 .tokens
445 .iter()
446 .rev()
447 .any(|(kind, text)| *kind == OPERATOR && text == ":");
448
449 if has_colon {
450 while self.current().is_some() {
452 if self.current() == Some(OPERATOR)
453 && self.tokens.last().map(|(_, text)| text.as_str()) == Some(":")
454 {
455 self.bump();
456 return true;
457 }
458 self.bump();
459 }
460 }
461
462 self.error("expected ':'".to_string());
463 false
464 }
465
466 fn parse_rule(&mut self) {
467 self.builder.start_node(RULE.into());
468
469 self.skip_ws();
471 self.builder.start_node(TARGETS.into());
472 let has_target = self.parse_rule_targets();
473 self.builder.finish_node();
474
475 let has_colon = if has_target {
477 self.find_and_consume_colon()
478 } else {
479 false
480 };
481
482 if has_target && has_colon {
484 self.skip_ws();
485 self.parse_rule_dependencies();
486 self.expect_eol();
487
488 self.parse_rule_recipes();
490 }
491
492 self.builder.finish_node();
493 }
494
495 fn parse_rule_targets(&mut self) -> bool {
496 let has_first_target = self.parse_rule_target();
498
499 if !has_first_target {
500 return false;
501 }
502
503 loop {
505 self.skip_ws();
506
507 if self.current() == Some(OPERATOR) && self.tokens.last().unwrap().1 == ":" {
509 break;
510 }
511
512 match self.current() {
514 Some(IDENTIFIER) | Some(DOLLAR) => {
515 if !self.parse_rule_target() {
516 break;
517 }
518 }
519 _ => break,
520 }
521 }
522
523 true
524 }
525
526 fn parse_comment(&mut self) {
527 if self.current() == Some(COMMENT) {
528 self.bump(); if self.current() == Some(NEWLINE) {
532 self.bump(); } else if self.current() == Some(WHITESPACE) {
534 self.skip_ws();
536 if self.current() == Some(NEWLINE) {
537 self.bump();
538 }
539 }
540 } else {
542 self.error("expected comment".to_string());
543 }
544 }
545
546 fn parse_assignment(&mut self) {
547 self.builder.start_node(VARIABLE.into());
548
549 self.skip_ws();
551 if self.current() == Some(IDENTIFIER) && self.tokens.last().unwrap().1 == "export" {
552 self.bump();
553 self.skip_ws();
554 }
555
556 match self.current() {
558 Some(IDENTIFIER) => self.bump(),
559 Some(DOLLAR) => self.parse_variable_reference(),
560 _ => {
561 self.error("expected variable name".to_string());
562 self.builder.finish_node();
563 return;
564 }
565 }
566
567 self.skip_ws();
569 match self.current() {
570 Some(OPERATOR) => {
571 let op = &self.tokens.last().unwrap().1;
572 if ["=", ":=", "::=", ":::=", "+=", "?=", "!="].contains(&op.as_str()) {
573 self.bump();
574 self.skip_ws();
575
576 self.builder.start_node(EXPR.into());
578 while self.current().is_some() && self.current() != Some(NEWLINE) {
579 self.bump();
580 }
581 self.builder.finish_node();
582
583 if self.current() == Some(NEWLINE) {
585 self.bump();
586 } else {
587 self.error("expected newline after variable value".to_string());
588 }
589 } else {
590 self.error(format!("invalid assignment operator: {}", op));
591 }
592 }
593 _ => self.error("expected assignment operator".to_string()),
594 }
595
596 self.builder.finish_node();
597 }
598
599 fn parse_variable_reference(&mut self) {
600 self.builder.start_node(EXPR.into());
601 self.bump(); if self.current() == Some(LPAREN) {
604 self.bump(); let mut is_function = false;
608
609 if self.current() == Some(IDENTIFIER) {
610 let function_name = &self.tokens.last().unwrap().1;
611 let known_functions = [
613 "shell", "wildcard", "call", "eval", "file", "abspath", "dir",
614 ];
615 if known_functions.contains(&function_name.as_str()) {
616 is_function = true;
617 }
618 }
619
620 if is_function {
621 self.bump();
623
624 self.consume_balanced_parens(1);
626 } else {
627 self.parse_parenthesized_expr_internal(true);
629 }
630 } else {
631 self.error("expected ( after $ in variable reference".to_string());
632 }
633
634 self.builder.finish_node();
635 }
636
637 fn parse_parenthesized_expr(&mut self) {
640 self.builder.start_node(EXPR.into());
641
642 if self.current() == Some(LPAREN) {
644 self.bump(); self.parse_parenthesized_expr_internal(false);
647 } else if self.current() == Some(QUOTE) {
648 self.parse_quoted_comparison();
650 } else {
651 self.error("expected opening parenthesis or quote".to_string());
652 }
653
654 self.builder.finish_node();
655 }
656
657 fn parse_parenthesized_expr_internal(&mut self, is_variable_ref: bool) {
659 let mut paren_count = 1;
660
661 while paren_count > 0 && self.current().is_some() {
662 match self.current() {
663 Some(LPAREN) => {
664 paren_count += 1;
665 self.bump();
666 self.builder.start_node(EXPR.into());
668 }
669 Some(RPAREN) => {
670 paren_count -= 1;
671 self.bump();
672 if paren_count > 0 {
673 self.builder.finish_node();
674 }
675 }
676 Some(QUOTE) => {
677 self.parse_quoted_string();
679 }
680 Some(DOLLAR) => {
681 self.parse_variable_reference();
683 }
684 Some(_) => self.bump(),
685 None => {
686 self.error(if is_variable_ref {
687 "unclosed variable reference".to_string()
688 } else {
689 "unclosed parenthesis".to_string()
690 });
691 break;
692 }
693 }
694 }
695
696 if !is_variable_ref {
697 self.skip_ws();
698 self.expect_eol();
699 }
700 }
701
702 fn parse_quoted_comparison(&mut self) {
705 if self.current() == Some(QUOTE) {
707 self.bump(); } else {
709 self.error("expected first quoted argument".to_string());
710 }
711
712 self.skip_ws();
714
715 if self.current() == Some(QUOTE) {
717 self.bump(); } else {
719 self.error("expected second quoted argument".to_string());
720 }
721
722 self.skip_ws();
724 self.expect_eol();
725 }
726
727 fn parse_quoted_string(&mut self) {
729 self.bump(); while !self.is_at_eof() && self.current() != Some(QUOTE) {
731 self.bump();
732 }
733 if self.current() == Some(QUOTE) {
734 self.bump();
735 }
736 }
737
738 fn parse_conditional_keyword(&mut self) -> Option<String> {
739 if self.current() != Some(IDENTIFIER) {
740 self.error(
741 "expected conditional keyword (ifdef, ifndef, ifeq, or ifneq)".to_string(),
742 );
743 return None;
744 }
745
746 let token = self.tokens.last().unwrap().1.clone();
747 if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&token.as_str()) {
748 self.error(format!("unknown conditional directive: {}", token));
749 return None;
750 }
751
752 self.bump();
753 Some(token)
754 }
755
756 fn parse_simple_condition(&mut self) {
757 self.builder.start_node(EXPR.into());
758
759 self.skip_ws();
761
762 let mut found_var = false;
764
765 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
766 match self.current() {
767 Some(WHITESPACE) => self.skip_ws(),
768 Some(DOLLAR) => {
769 found_var = true;
770 self.parse_variable_reference();
771 }
772 Some(_) => {
773 found_var = true;
775 self.bump();
776 }
777 None => break,
778 }
779 }
780
781 if !found_var {
782 self.error("expected condition after conditional directive".to_string());
784 }
785
786 self.builder.finish_node();
787
788 if self.current() == Some(NEWLINE) {
790 self.bump();
791 } else if !self.is_at_eof() {
792 self.skip_until_newline();
793 }
794 }
795
796 fn is_conditional_directive(&self, token: &str) -> bool {
798 token == "ifdef"
799 || token == "ifndef"
800 || token == "ifeq"
801 || token == "ifneq"
802 || token == "else"
803 || token == "endif"
804 }
805
806 fn handle_conditional_token(&mut self, token: &str, depth: &mut usize) -> bool {
808 match token {
809 "ifdef" | "ifndef" | "ifeq" | "ifneq"
810 if matches!(self.variant, None | Some(MakefileVariant::GNUMake)) =>
811 {
812 self.parse_conditional();
815 true
816 }
817 "else" => {
818 if *depth == 0 {
820 self.error("else without matching if".to_string());
821 self.bump();
823 false
824 } else {
825 self.builder.start_node(CONDITIONAL_ELSE.into());
827
828 self.bump();
830 self.skip_ws();
831
832 if self.current() == Some(IDENTIFIER) {
834 let next_token = &self.tokens.last().unwrap().1;
835 if next_token == "ifdef"
836 || next_token == "ifndef"
837 || next_token == "ifeq"
838 || next_token == "ifneq"
839 {
840 match next_token.as_str() {
843 "ifdef" | "ifndef" => {
844 self.bump(); self.skip_ws();
846 self.parse_simple_condition();
847 }
848 "ifeq" | "ifneq" => {
849 self.bump(); self.skip_ws();
851 self.parse_parenthesized_expr();
852 }
853 _ => unreachable!(),
854 }
855 } else {
857 }
860 } else {
861 }
863
864 self.builder.finish_node(); true
866 }
867 }
868 "endif" => {
869 if *depth == 0 {
871 self.error("endif without matching if".to_string());
872 self.bump();
874 false
875 } else {
876 *depth -= 1;
877
878 self.builder.start_node(CONDITIONAL_ENDIF.into());
880
881 self.bump();
883
884 self.skip_ws();
886
887 if self.current() == Some(COMMENT) {
892 self.parse_comment();
893 } else if self.current() == Some(NEWLINE) {
894 self.bump();
895 } else if self.current() == Some(WHITESPACE) {
896 self.skip_ws();
898 if self.current() == Some(NEWLINE) {
899 self.bump();
900 }
901 } else if !self.is_at_eof() {
903 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
906 self.bump();
907 }
908 if self.current() == Some(NEWLINE) {
909 self.bump();
910 }
911 }
912 self.builder.finish_node(); true
916 }
917 }
918 _ => false,
919 }
920 }
921
922 fn parse_conditional(&mut self) {
923 self.builder.start_node(CONDITIONAL.into());
924
925 self.builder.start_node(CONDITIONAL_IF.into());
927
928 let Some(token) = self.parse_conditional_keyword() else {
930 self.skip_until_newline();
931 self.builder.finish_node(); self.builder.finish_node(); return;
934 };
935
936 self.skip_ws();
938
939 match token.as_str() {
941 "ifdef" | "ifndef" => {
942 self.parse_simple_condition();
943 }
944 "ifeq" | "ifneq" => {
945 self.parse_parenthesized_expr();
946 }
947 _ => unreachable!("Invalid conditional token"),
948 }
949
950 self.skip_ws();
952 if self.current() == Some(COMMENT) {
953 self.parse_comment();
954 }
955 self.builder.finish_node(); let mut depth = 1;
961
962 let mut position_count = std::collections::HashMap::<usize, usize>::new();
964 let max_repetitions = 15; while depth > 0 && !self.is_at_eof() {
967 let current_pos = self.tokens.len();
969 *position_count.entry(current_pos).or_insert(0) += 1;
970
971 if position_count.get(¤t_pos).unwrap() > &max_repetitions {
974 break;
977 }
978
979 match self.current() {
980 None => {
981 self.error("unterminated conditional (missing endif)".to_string());
982 break;
983 }
984 Some(IDENTIFIER) => {
985 let token = self.tokens.last().unwrap().1.clone();
986 if !self.handle_conditional_token(&token, &mut depth) {
987 if token == "include" || token == "-include" || token == "sinclude" {
988 self.parse_include();
989 } else {
990 self.parse_normal_content();
991 }
992 }
993 }
994 Some(INDENT) => self.parse_recipe_line(),
995 Some(WHITESPACE) => self.bump(),
996 Some(COMMENT) => self.parse_comment(),
997 Some(NEWLINE) => self.bump(),
998 Some(DOLLAR) => self.parse_normal_content(),
999 Some(QUOTE) => self.parse_quoted_string(),
1000 Some(_) => {
1001 self.bump();
1003 }
1004 }
1005 }
1006
1007 self.builder.finish_node();
1008 }
1009
1010 fn parse_normal_content(&mut self) {
1012 self.skip_ws();
1014
1015 if self.is_assignment_line() {
1017 self.parse_assignment();
1018 } else {
1019 self.parse_rule();
1021 }
1022 }
1023
1024 fn parse_include(&mut self) {
1025 self.builder.start_node(INCLUDE.into());
1026
1027 if self.current() != Some(IDENTIFIER)
1029 || (!["include", "-include", "sinclude"]
1030 .contains(&self.tokens.last().unwrap().1.as_str()))
1031 {
1032 self.error("expected include directive".to_string());
1033 self.builder.finish_node();
1034 return;
1035 }
1036 self.bump();
1037 self.skip_ws();
1038
1039 self.builder.start_node(EXPR.into());
1041 let mut found_path = false;
1042
1043 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1044 match self.current() {
1045 Some(WHITESPACE) => self.skip_ws(),
1046 Some(DOLLAR) => {
1047 found_path = true;
1048 self.parse_variable_reference();
1049 }
1050 Some(_) => {
1051 found_path = true;
1053 self.bump();
1054 }
1055 None => break,
1056 }
1057 }
1058
1059 if !found_path {
1060 self.error("expected file path after include".to_string());
1061 }
1062
1063 self.builder.finish_node();
1064
1065 if self.current() == Some(NEWLINE) {
1067 self.bump();
1068 } else if !self.is_at_eof() {
1069 self.error("expected newline after include".to_string());
1070 self.skip_until_newline();
1071 }
1072
1073 self.builder.finish_node();
1074 }
1075
1076 fn parse_identifier_token(&mut self) -> bool {
1077 let token = &self.tokens.last().unwrap().1;
1078
1079 if token.starts_with("%") {
1081 self.parse_rule();
1082 return true;
1083 }
1084
1085 if token.starts_with("if")
1086 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1087 {
1088 self.parse_conditional();
1089 return true;
1090 }
1091
1092 if token == "include" || token == "-include" || token == "sinclude" {
1093 self.parse_include();
1094 return true;
1095 }
1096
1097 self.parse_normal_content();
1099 true
1100 }
1101
1102 fn parse_token(&mut self) -> bool {
1103 match self.current() {
1104 None => false,
1105 Some(IDENTIFIER) => {
1106 let token = &self.tokens.last().unwrap().1;
1107 if self.is_conditional_directive(token)
1108 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1109 {
1110 self.parse_conditional();
1111 true
1112 } else {
1113 self.parse_identifier_token()
1114 }
1115 }
1116 Some(DOLLAR) => {
1117 self.parse_normal_content();
1118 true
1119 }
1120 Some(NEWLINE) => {
1121 self.builder.start_node(BLANK_LINE.into());
1122 self.bump();
1123 self.builder.finish_node();
1124 true
1125 }
1126 Some(COMMENT) => {
1127 self.parse_comment();
1128 true
1129 }
1130 Some(WHITESPACE) => {
1131 if self.is_end_of_file_or_newline_after_whitespace() {
1133 self.skip_ws();
1136 return true;
1137 }
1138
1139 let look_ahead_pos = self.tokens.len().saturating_sub(1);
1142 let mut is_documentation_or_help = false;
1143
1144 if look_ahead_pos > 0 {
1145 let next_token = &self.tokens[look_ahead_pos - 1];
1146 if next_token.0 == IDENTIFIER
1149 || next_token.0 == COMMENT
1150 || next_token.0 == TEXT
1151 {
1152 is_documentation_or_help = true;
1153 }
1154 }
1155
1156 if is_documentation_or_help {
1157 self.skip_ws();
1160 while self.current().is_some() && self.current() != Some(NEWLINE) {
1161 self.bump();
1162 }
1163 if self.current() == Some(NEWLINE) {
1164 self.bump();
1165 }
1166 } else {
1167 self.skip_ws();
1168 }
1169 true
1170 }
1171 Some(INDENT) => {
1172 self.bump();
1174
1175 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1177 self.bump();
1178 }
1179 if self.current() == Some(NEWLINE) {
1180 self.bump();
1181 }
1182 true
1183 }
1184 Some(kind) => {
1185 self.error(format!("unexpected token {:?}", kind));
1186 self.bump();
1187 true
1188 }
1189 }
1190 }
1191
1192 fn parse(mut self) -> Parse {
1193 self.builder.start_node(ROOT.into());
1194
1195 while self.parse_token() {}
1196
1197 self.builder.finish_node();
1198
1199 Parse {
1200 green_node: self.builder.finish(),
1201 errors: self.errors,
1202 }
1203 }
1204
1205 fn is_assignment_line(&mut self) -> bool {
1207 let assignment_ops = ["=", ":=", "::=", ":::=", "+=", "?=", "!="];
1208 let mut pos = self.tokens.len().saturating_sub(1);
1209 let mut seen_identifier = false;
1210 let mut seen_export = false;
1211
1212 while pos > 0 {
1213 let (kind, text) = &self.tokens[pos];
1214
1215 match kind {
1216 NEWLINE => break,
1217 IDENTIFIER if text == "export" => seen_export = true,
1218 IDENTIFIER if !seen_identifier => seen_identifier = true,
1219 OPERATOR if assignment_ops.contains(&text.as_str()) => {
1220 return seen_identifier || seen_export
1221 }
1222 OPERATOR if text == ":" => return false, WHITESPACE => (),
1224 _ if seen_export => return true, _ => return false,
1226 }
1227 pos = pos.saturating_sub(1);
1228 }
1229 false
1230 }
1231
1232 fn bump(&mut self) {
1234 let (kind, text) = self.tokens.pop().unwrap();
1235 self.builder.token(kind.into(), text.as_str());
1236 }
1237 fn current(&self) -> Option<SyntaxKind> {
1239 self.tokens.last().map(|(kind, _)| *kind)
1240 }
1241
1242 fn expect_eol(&mut self) {
1243 self.skip_ws();
1245
1246 match self.current() {
1247 Some(NEWLINE) => {
1248 self.bump();
1249 }
1250 None => {
1251 }
1253 n => {
1254 self.error(format!("expected newline, got {:?}", n));
1255 self.skip_until_newline();
1257 }
1258 }
1259 }
1260
1261 fn is_at_eof(&self) -> bool {
1263 self.current().is_none()
1264 }
1265
1266 fn is_at_eof_or_only_whitespace(&self) -> bool {
1268 if self.is_at_eof() {
1269 return true;
1270 }
1271
1272 self.tokens
1274 .iter()
1275 .rev()
1276 .all(|(kind, _)| matches!(*kind, WHITESPACE | NEWLINE))
1277 }
1278
1279 fn skip_ws(&mut self) {
1280 while self.current() == Some(WHITESPACE) {
1281 self.bump()
1282 }
1283 }
1284
1285 fn skip_until_newline(&mut self) {
1286 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1287 self.bump();
1288 }
1289 if self.current() == Some(NEWLINE) {
1290 self.bump();
1291 }
1292 }
1293
1294 fn consume_balanced_parens(&mut self, start_paren_count: usize) -> usize {
1296 let mut paren_count = start_paren_count;
1297
1298 while paren_count > 0 && self.current().is_some() {
1299 match self.current() {
1300 Some(LPAREN) => {
1301 paren_count += 1;
1302 self.bump();
1303 }
1304 Some(RPAREN) => {
1305 paren_count -= 1;
1306 self.bump();
1307 if paren_count == 0 {
1308 break;
1309 }
1310 }
1311 Some(DOLLAR) => {
1312 self.parse_variable_reference();
1314 }
1315 Some(_) => self.bump(),
1316 None => {
1317 self.error("unclosed parenthesis".to_string());
1318 break;
1319 }
1320 }
1321 }
1322
1323 paren_count
1324 }
1325
1326 fn is_end_of_file_or_newline_after_whitespace(&self) -> bool {
1328 if self.is_at_eof_or_only_whitespace() {
1330 return true;
1331 }
1332
1333 if self.tokens.len() <= 1 {
1335 return true;
1336 }
1337
1338 false
1339 }
1340 }
1341
1342 let mut tokens = lex(text);
1343 tokens.reverse();
1344 Parser {
1345 tokens,
1346 builder: GreenNodeBuilder::new(),
1347 errors: Vec::new(),
1348 original_text: text.to_string(),
1349 variant,
1350 }
1351 .parse()
1352}
1353
1354pub(crate) type SyntaxNode = rowan::SyntaxNode<Lang>;
1360#[allow(unused)]
1361type SyntaxToken = rowan::SyntaxToken<Lang>;
1362#[allow(unused)]
1363pub(crate) type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
1364
1365impl Parse {
1366 fn syntax(&self) -> SyntaxNode {
1367 SyntaxNode::new_root_mut(self.green_node.clone())
1368 }
1369
1370 pub(crate) fn root(&self) -> Makefile {
1371 Makefile::cast(self.syntax()).unwrap()
1372 }
1373}
1374
1375fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
1378 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
1379 let mut line = 0;
1380 let mut last_newline_offset = rowan::TextSize::from(0);
1381
1382 for element in root.preorder_with_tokens() {
1383 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
1384 if token.text_range().start() >= offset {
1385 break;
1386 }
1387
1388 for (idx, _) in token.text().match_indices('\n') {
1390 line += 1;
1391 last_newline_offset =
1392 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
1393 }
1394 }
1395 }
1396
1397 let column: usize = (offset - last_newline_offset).into();
1398 (line, column)
1399}
1400
1401macro_rules! ast_node {
1402 ($ast:ident, $kind:ident) => {
1403 #[derive(Clone, PartialEq, Eq, Hash)]
1404 #[repr(transparent)]
1405 pub struct $ast(SyntaxNode);
1407
1408 impl AstNode for $ast {
1409 type Language = Lang;
1410
1411 fn can_cast(kind: SyntaxKind) -> bool {
1412 kind == $kind
1413 }
1414
1415 fn cast(syntax: SyntaxNode) -> Option<Self> {
1416 if Self::can_cast(syntax.kind()) {
1417 Some(Self(syntax))
1418 } else {
1419 None
1420 }
1421 }
1422
1423 fn syntax(&self) -> &SyntaxNode {
1424 &self.0
1425 }
1426 }
1427
1428 impl $ast {
1429 pub fn line(&self) -> usize {
1431 line_col_at_offset(&self.0, self.0.text_range().start()).0
1432 }
1433
1434 pub fn column(&self) -> usize {
1436 line_col_at_offset(&self.0, self.0.text_range().start()).1
1437 }
1438
1439 pub fn line_col(&self) -> (usize, usize) {
1442 line_col_at_offset(&self.0, self.0.text_range().start())
1443 }
1444 }
1445
1446 impl core::fmt::Display for $ast {
1447 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1448 write!(f, "{}", self.0.text())
1449 }
1450 }
1451 };
1452}
1453
1454ast_node!(Makefile, ROOT);
1455ast_node!(Rule, RULE);
1456ast_node!(Recipe, RECIPE);
1457ast_node!(Identifier, IDENTIFIER);
1458ast_node!(VariableDefinition, VARIABLE);
1459ast_node!(Include, INCLUDE);
1460ast_node!(ArchiveMembers, ARCHIVE_MEMBERS);
1461ast_node!(ArchiveMember, ARCHIVE_MEMBER);
1462ast_node!(Conditional, CONDITIONAL);
1463
1464impl Recipe {
1465 pub fn text(&self) -> String {
1467 self.syntax()
1468 .children_with_tokens()
1469 .filter_map(|it| {
1470 if let Some(token) = it.as_token() {
1471 if token.kind() == TEXT {
1472 return Some(token.text().to_string());
1473 }
1474 }
1475 None
1476 })
1477 .collect::<Vec<_>>()
1478 .join("")
1479 }
1480
1481 pub fn parent(&self) -> Option<Rule> {
1494 self.syntax().parent().and_then(Rule::cast)
1495 }
1496
1497 pub fn is_silent(&self) -> bool {
1510 let text = self.text();
1511 text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1512 }
1513
1514 pub fn is_ignore_errors(&self) -> bool {
1527 let text = self.text();
1528 text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
1529 }
1530
1531 pub fn set_prefix(&mut self, prefix: &str) {
1548 let text = self.text();
1549
1550 let stripped = text.trim_start_matches(['@', '-', '+']);
1552
1553 let new_text = format!("{}{}", prefix, stripped);
1555
1556 self.replace_text(&new_text);
1557 }
1558
1559 pub fn replace_text(&mut self, new_text: &str) {
1572 let node = self.syntax();
1573 let parent = node.parent().expect("Recipe node must have a parent");
1574 let node_index = node.index();
1575
1576 let mut builder = GreenNodeBuilder::new();
1578 builder.start_node(RECIPE.into());
1579
1580 if let Some(indent_token) = node
1582 .children_with_tokens()
1583 .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
1584 {
1585 builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
1586 } else {
1587 builder.token(INDENT.into(), "\t");
1588 }
1589
1590 builder.token(TEXT.into(), new_text);
1591
1592 if let Some(newline_token) = node
1594 .children_with_tokens()
1595 .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
1596 {
1597 builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
1598 } else {
1599 builder.token(NEWLINE.into(), "\n");
1600 }
1601
1602 builder.finish_node();
1603 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1604
1605 parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
1607
1608 *self = parent
1612 .children_with_tokens()
1613 .nth(node_index)
1614 .and_then(|element| element.into_node())
1615 .and_then(Recipe::cast)
1616 .expect("New recipe node should exist at the same index");
1617 }
1618
1619 pub fn insert_before(&self, text: &str) {
1632 let node = self.syntax();
1633 let parent = node.parent().expect("Recipe node must have a parent");
1634 let node_index = node.index();
1635
1636 let mut builder = GreenNodeBuilder::new();
1638 builder.start_node(RECIPE.into());
1639 builder.token(INDENT.into(), "\t");
1640 builder.token(TEXT.into(), text);
1641 builder.token(NEWLINE.into(), "\n");
1642 builder.finish_node();
1643 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1644
1645 parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
1647 }
1648
1649 pub fn insert_after(&self, text: &str) {
1662 let node = self.syntax();
1663 let parent = node.parent().expect("Recipe node must have a parent");
1664 let node_index = node.index();
1665
1666 let mut builder = GreenNodeBuilder::new();
1668 builder.start_node(RECIPE.into());
1669 builder.token(INDENT.into(), "\t");
1670 builder.token(TEXT.into(), text);
1671 builder.token(NEWLINE.into(), "\n");
1672 builder.finish_node();
1673 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1674
1675 parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
1677 }
1678
1679 pub fn remove(&self) {
1692 let node = self.syntax();
1693 let parent = node.parent().expect("Recipe node must have a parent");
1694 let node_index = node.index();
1695
1696 parent.splice_children(node_index..node_index + 1, vec![]);
1698 }
1699}
1700
1701pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
1705 let mut newlines_to_remove = vec![];
1707 let mut current = node.last_child_or_token();
1708
1709 while let Some(element) = current {
1710 match &element {
1711 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
1712 newlines_to_remove.push(token.clone());
1713 current = token.prev_sibling_or_token();
1714 }
1715 rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
1716 let mut recipe_current = n.last_child_or_token();
1718 while let Some(recipe_element) = recipe_current {
1719 match &recipe_element {
1720 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
1721 newlines_to_remove.push(token.clone());
1722 recipe_current = token.prev_sibling_or_token();
1723 }
1724 _ => break,
1725 }
1726 }
1727 break; }
1729 _ => break,
1730 }
1731 }
1732
1733 if newlines_to_remove.len() > 1 {
1736 newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
1738
1739 for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
1740 let parent = token.parent().unwrap();
1741 let idx = token.index();
1742 parent.splice_children(idx..idx + 1, vec![]);
1743 }
1744 }
1745}
1746
1747pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
1755 let mut collected_elements = vec![];
1756 let mut found_comment = false;
1757
1758 let mut current = node.prev_sibling_or_token();
1760 while let Some(element) = current {
1761 match &element {
1762 rowan::NodeOrToken::Token(token) => match token.kind() {
1763 COMMENT => {
1764 if token.text().starts_with("#!") {
1765 break; }
1767 found_comment = true;
1768 collected_elements.push(element.clone());
1769 }
1770 NEWLINE | WHITESPACE => {
1771 collected_elements.push(element.clone());
1772 }
1773 _ => break, },
1775 rowan::NodeOrToken::Node(n) => {
1776 if n.kind() == BLANK_LINE {
1778 collected_elements.push(element.clone());
1779 } else {
1780 break; }
1782 }
1783 }
1784 current = element.prev_sibling_or_token();
1785 }
1786
1787 let mut elements_to_remove = vec![];
1790 let mut consecutive_newlines = 0;
1791 for element in collected_elements.iter().rev() {
1792 let should_remove = match element {
1793 rowan::NodeOrToken::Token(token) => match token.kind() {
1794 COMMENT => {
1795 consecutive_newlines = 0;
1796 found_comment
1797 }
1798 NEWLINE => {
1799 consecutive_newlines += 1;
1800 found_comment && consecutive_newlines <= 1
1801 }
1802 WHITESPACE => found_comment,
1803 _ => false,
1804 },
1805 rowan::NodeOrToken::Node(n) => {
1806 if n.kind() == BLANK_LINE {
1808 consecutive_newlines += 1;
1809 found_comment && consecutive_newlines <= 1
1810 } else {
1811 false
1812 }
1813 }
1814 };
1815
1816 if should_remove {
1817 elements_to_remove.push(element.clone());
1818 }
1819 }
1820
1821 let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
1824 all_to_remove.extend(elements_to_remove.into_iter().rev());
1825
1826 all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
1828
1829 for element in all_to_remove {
1830 let idx = element.index();
1831 parent.splice_children(idx..idx + 1, vec![]);
1832 }
1833}
1834
1835impl FromStr for Rule {
1836 type Err = crate::Error;
1837
1838 fn from_str(s: &str) -> Result<Self, Self::Err> {
1839 Rule::parse(s).to_rule_result()
1840 }
1841}
1842
1843impl FromStr for Makefile {
1844 type Err = crate::Error;
1845
1846 fn from_str(s: &str) -> Result<Self, Self::Err> {
1847 Makefile::parse(s).to_result()
1848 }
1849}
1850
1851#[cfg(test)]
1852mod tests {
1853 use super::*;
1854 use crate::ast::makefile::MakefileItem;
1855 use crate::pattern::matches_pattern;
1856
1857 #[test]
1858 fn test_conditionals() {
1859 let code = "ifdef DEBUG\n DEBUG_FLAG := 1\nendif\n";
1863 let mut buf = code.as_bytes();
1864 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
1865 assert!(makefile.code().contains("DEBUG_FLAG"));
1866
1867 let code =
1869 "ifeq ($(OS),Windows_NT)\n RESULT := windows\nelse\n RESULT := unix\nendif\n";
1870 let mut buf = code.as_bytes();
1871 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
1872 assert!(makefile.code().contains("RESULT"));
1873 assert!(makefile.code().contains("windows"));
1874
1875 let code = "ifdef DEBUG\n CFLAGS += -g\n ifdef VERBOSE\n CFLAGS += -v\n endif\nelse\n CFLAGS += -O2\nendif\n";
1877 let mut buf = code.as_bytes();
1878 let makefile = Makefile::read_relaxed(&mut buf)
1879 .expect("Failed to parse nested conditionals with else");
1880 assert!(makefile.code().contains("CFLAGS"));
1881 assert!(makefile.code().contains("VERBOSE"));
1882
1883 let code = "ifdef DEBUG\nendif\n";
1885 let mut buf = code.as_bytes();
1886 let makefile =
1887 Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
1888 assert!(makefile.code().contains("ifdef DEBUG"));
1889
1890 let code = "ifeq ($(OS),Windows)\n EXT := .exe\nelse ifeq ($(OS),Linux)\n EXT := .bin\nelse\n EXT := .out\nendif\n";
1892 let mut buf = code.as_bytes();
1893 let makefile =
1894 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
1895 assert!(makefile.code().contains("EXT"));
1896
1897 let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
1899 let mut buf = code.as_bytes();
1900 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
1901 assert!(makefile.code().contains("DEBUG"));
1902
1903 let code = "ifdef \nDEBUG := 1\nendif\n";
1905 let mut buf = code.as_bytes();
1906 let makefile = Makefile::read_relaxed(&mut buf)
1907 .expect("Failed to parse with recovery - missing condition");
1908 assert!(makefile.code().contains("DEBUG"));
1909 }
1910
1911 #[test]
1912 fn test_parse_simple() {
1913 const SIMPLE: &str = r#"VARIABLE = value
1914
1915rule: dependency
1916 command
1917"#;
1918 let parsed = parse(SIMPLE, None);
1919 assert!(parsed.errors.is_empty());
1920 let node = parsed.syntax();
1921 assert_eq!(
1922 format!("{:#?}", node),
1923 r#"ROOT@0..44
1924 VARIABLE@0..17
1925 IDENTIFIER@0..8 "VARIABLE"
1926 WHITESPACE@8..9 " "
1927 OPERATOR@9..10 "="
1928 WHITESPACE@10..11 " "
1929 EXPR@11..16
1930 IDENTIFIER@11..16 "value"
1931 NEWLINE@16..17 "\n"
1932 BLANK_LINE@17..18
1933 NEWLINE@17..18 "\n"
1934 RULE@18..44
1935 TARGETS@18..22
1936 IDENTIFIER@18..22 "rule"
1937 OPERATOR@22..23 ":"
1938 WHITESPACE@23..24 " "
1939 PREREQUISITES@24..34
1940 PREREQUISITE@24..34
1941 IDENTIFIER@24..34 "dependency"
1942 NEWLINE@34..35 "\n"
1943 RECIPE@35..44
1944 INDENT@35..36 "\t"
1945 TEXT@36..43 "command"
1946 NEWLINE@43..44 "\n"
1947"#
1948 );
1949
1950 let root = parsed.root();
1951
1952 let mut rules = root.rules().collect::<Vec<_>>();
1953 assert_eq!(rules.len(), 1);
1954 let rule = rules.pop().unwrap();
1955 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
1956 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
1957 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
1958
1959 let mut variables = root.variable_definitions().collect::<Vec<_>>();
1960 assert_eq!(variables.len(), 1);
1961 let variable = variables.pop().unwrap();
1962 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
1963 assert_eq!(variable.raw_value(), Some("value".to_string()));
1964 }
1965
1966 #[test]
1967 fn test_parse_export_assign() {
1968 const EXPORT: &str = r#"export VARIABLE := value
1969"#;
1970 let parsed = parse(EXPORT, None);
1971 assert!(parsed.errors.is_empty());
1972 let node = parsed.syntax();
1973 assert_eq!(
1974 format!("{:#?}", node),
1975 r#"ROOT@0..25
1976 VARIABLE@0..25
1977 IDENTIFIER@0..6 "export"
1978 WHITESPACE@6..7 " "
1979 IDENTIFIER@7..15 "VARIABLE"
1980 WHITESPACE@15..16 " "
1981 OPERATOR@16..18 ":="
1982 WHITESPACE@18..19 " "
1983 EXPR@19..24
1984 IDENTIFIER@19..24 "value"
1985 NEWLINE@24..25 "\n"
1986"#
1987 );
1988
1989 let root = parsed.root();
1990
1991 let mut variables = root.variable_definitions().collect::<Vec<_>>();
1992 assert_eq!(variables.len(), 1);
1993 let variable = variables.pop().unwrap();
1994 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
1995 assert_eq!(variable.raw_value(), Some("value".to_string()));
1996 }
1997
1998 #[test]
1999 fn test_parse_multiple_prerequisites() {
2000 const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2001 command
2002
2003"#;
2004 let parsed = parse(MULTIPLE_PREREQUISITES, None);
2005 assert!(parsed.errors.is_empty());
2006 let node = parsed.syntax();
2007 assert_eq!(
2008 format!("{:#?}", node),
2009 r#"ROOT@0..40
2010 RULE@0..40
2011 TARGETS@0..4
2012 IDENTIFIER@0..4 "rule"
2013 OPERATOR@4..5 ":"
2014 WHITESPACE@5..6 " "
2015 PREREQUISITES@6..29
2016 PREREQUISITE@6..17
2017 IDENTIFIER@6..17 "dependency1"
2018 WHITESPACE@17..18 " "
2019 PREREQUISITE@18..29
2020 IDENTIFIER@18..29 "dependency2"
2021 NEWLINE@29..30 "\n"
2022 RECIPE@30..39
2023 INDENT@30..31 "\t"
2024 TEXT@31..38 "command"
2025 NEWLINE@38..39 "\n"
2026 NEWLINE@39..40 "\n"
2027"#
2028 );
2029 let root = parsed.root();
2030
2031 let rule = root.rules().next().unwrap();
2032 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2033 assert_eq!(
2034 rule.prerequisites().collect::<Vec<_>>(),
2035 vec!["dependency1", "dependency2"]
2036 );
2037 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2038 }
2039
2040 #[test]
2041 fn test_add_rule() {
2042 let mut makefile = Makefile::new();
2043 let rule = makefile.add_rule("rule");
2044 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2045 assert_eq!(
2046 rule.prerequisites().collect::<Vec<_>>(),
2047 Vec::<String>::new()
2048 );
2049
2050 assert_eq!(makefile.to_string(), "rule:\n");
2051 }
2052
2053 #[test]
2054 fn test_add_rule_with_shebang() {
2055 let content = r#"#!/usr/bin/make -f
2057
2058build: blah
2059 $(MAKE) install
2060
2061clean:
2062 dh_clean
2063"#;
2064
2065 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2066 let initial_count = makefile.rules().count();
2067 assert_eq!(initial_count, 2);
2068
2069 let rule = makefile.add_rule("build-indep");
2071 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2072
2073 assert_eq!(makefile.rules().count(), initial_count + 1);
2075 }
2076
2077 #[test]
2078 fn test_add_rule_formatting() {
2079 let content = r#"build: blah
2081 $(MAKE) install
2082
2083clean:
2084 dh_clean
2085"#;
2086
2087 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2088 let mut rule = makefile.add_rule("build-indep");
2089 rule.add_prerequisite("build").unwrap();
2090
2091 let expected = r#"build: blah
2092 $(MAKE) install
2093
2094clean:
2095 dh_clean
2096
2097build-indep: build
2098"#;
2099
2100 assert_eq!(makefile.to_string(), expected);
2101 }
2102
2103 #[test]
2104 fn test_push_command() {
2105 let mut makefile = Makefile::new();
2106 let mut rule = makefile.add_rule("rule");
2107
2108 rule.push_command("command");
2110 rule.push_command("command2");
2111
2112 assert_eq!(
2114 rule.recipes().collect::<Vec<_>>(),
2115 vec!["command", "command2"]
2116 );
2117
2118 rule.push_command("command3");
2120 assert_eq!(
2121 rule.recipes().collect::<Vec<_>>(),
2122 vec!["command", "command2", "command3"]
2123 );
2124
2125 assert_eq!(
2127 makefile.to_string(),
2128 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2129 );
2130
2131 assert_eq!(
2133 rule.to_string(),
2134 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2135 );
2136 }
2137
2138 #[test]
2139 fn test_replace_command() {
2140 let mut makefile = Makefile::new();
2141 let mut rule = makefile.add_rule("rule");
2142
2143 rule.push_command("command");
2145 rule.push_command("command2");
2146
2147 assert_eq!(
2149 rule.recipes().collect::<Vec<_>>(),
2150 vec!["command", "command2"]
2151 );
2152
2153 rule.replace_command(0, "new command");
2155 assert_eq!(
2156 rule.recipes().collect::<Vec<_>>(),
2157 vec!["new command", "command2"]
2158 );
2159
2160 assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2162
2163 assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2165 }
2166
2167 #[test]
2168 fn test_replace_command_with_comments() {
2169 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";
2172
2173 let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2174
2175 let mut rule = makefile.rules().next().unwrap();
2176
2177 assert_eq!(rule.recipes().count(), 1);
2179 assert_eq!(
2180 rule.recipes().collect::<Vec<_>>(),
2181 vec!["dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"]
2182 );
2183
2184 assert!(rule.replace_command(0, "dh_strip"));
2186
2187 assert_eq!(rule.recipes().count(), 1);
2189 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["dh_strip"]);
2190 }
2191
2192 #[test]
2193 fn test_parse_rule_without_newline() {
2194 let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2195 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2196 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2197 let rule = "rule: dependency".parse::<Rule>().unwrap();
2198 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2199 assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2200 }
2201
2202 #[test]
2203 fn test_parse_makefile_without_newline() {
2204 let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2205 assert_eq!(makefile.rules().count(), 1);
2206 }
2207
2208 #[test]
2209 fn test_from_reader() {
2210 let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2211 assert_eq!(makefile.rules().count(), 1);
2212 }
2213
2214 #[test]
2215 fn test_parse_with_tab_after_last_newline() {
2216 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2217 assert_eq!(makefile.rules().count(), 1);
2218 }
2219
2220 #[test]
2221 fn test_parse_with_space_after_last_newline() {
2222 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2223 assert_eq!(makefile.rules().count(), 1);
2224 }
2225
2226 #[test]
2227 fn test_parse_with_comment_after_last_newline() {
2228 let makefile =
2229 Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2230 assert_eq!(makefile.rules().count(), 1);
2231 }
2232
2233 #[test]
2234 fn test_parse_with_variable_rule() {
2235 let makefile =
2236 Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2237 .unwrap();
2238
2239 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2241 assert_eq!(vars.len(), 1);
2242 assert_eq!(vars[0].name(), Some("RULE".to_string()));
2243 assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2244
2245 let rules = makefile.rules().collect::<Vec<_>>();
2247 assert_eq!(rules.len(), 1);
2248 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2249 assert_eq!(
2250 rules[0].prerequisites().collect::<Vec<_>>(),
2251 vec!["dependency"]
2252 );
2253 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2254 }
2255
2256 #[test]
2257 fn test_parse_with_variable_dependency() {
2258 let makefile =
2259 Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2260
2261 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2263 assert_eq!(vars.len(), 1);
2264 assert_eq!(vars[0].name(), Some("DEP".to_string()));
2265 assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2266
2267 let rules = makefile.rules().collect::<Vec<_>>();
2269 assert_eq!(rules.len(), 1);
2270 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2271 assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2272 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2273 }
2274
2275 #[test]
2276 fn test_parse_with_variable_command() {
2277 let makefile =
2278 Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2279
2280 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2282 assert_eq!(vars.len(), 1);
2283 assert_eq!(vars[0].name(), Some("COM".to_string()));
2284 assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2285
2286 let rules = makefile.rules().collect::<Vec<_>>();
2288 assert_eq!(rules.len(), 1);
2289 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2290 assert_eq!(
2291 rules[0].prerequisites().collect::<Vec<_>>(),
2292 vec!["dependency"]
2293 );
2294 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2295 }
2296
2297 #[test]
2298 fn test_regular_line_error_reporting() {
2299 let input = "rule target\n\tcommand";
2300
2301 let parsed = parse(input, None);
2303 let direct_error = &parsed.errors[0];
2304
2305 assert_eq!(direct_error.line, 2);
2307 assert!(
2308 direct_error.message.contains("expected"),
2309 "Error message should contain 'expected': {}",
2310 direct_error.message
2311 );
2312 assert_eq!(direct_error.context, "\tcommand");
2313
2314 let reader_result = Makefile::from_reader(input.as_bytes());
2316 let parse_error = match reader_result {
2317 Ok(_) => panic!("Expected Parse error from from_reader"),
2318 Err(err) => match err {
2319 self::Error::Parse(parse_err) => parse_err,
2320 _ => panic!("Expected Parse error"),
2321 },
2322 };
2323
2324 let error_text = parse_error.to_string();
2326 assert!(error_text.contains("Error at line 2:"));
2327 assert!(error_text.contains("2| \tcommand"));
2328 }
2329
2330 #[test]
2331 fn test_parsing_error_context_with_bad_syntax() {
2332 let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2334
2335 match Makefile::from_reader(input.as_bytes()) {
2337 Ok(makefile) => {
2338 assert_eq!(
2340 makefile.rules().count(),
2341 0,
2342 "Should not have found any rules"
2343 );
2344 }
2345 Err(err) => match err {
2346 self::Error::Parse(error) => {
2347 assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2349 assert!(
2350 !error.errors[0].context.is_empty(),
2351 "Error context should not be empty"
2352 );
2353 }
2354 _ => panic!("Unexpected error type"),
2355 },
2356 };
2357 }
2358
2359 #[test]
2360 fn test_error_message_format() {
2361 let parse_error = ParseError {
2363 errors: vec![ErrorInfo {
2364 message: "test error".to_string(),
2365 line: 42,
2366 context: "some problematic code".to_string(),
2367 }],
2368 };
2369
2370 let error_text = parse_error.to_string();
2371 assert!(error_text.contains("Error at line 42: test error"));
2372 assert!(error_text.contains("42| some problematic code"));
2373 }
2374
2375 #[test]
2376 fn test_line_number_calculation() {
2377 let test_cases = [
2379 ("rule dependency\n\tcommand", 2), ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2), ("var = value\n#comment\n\tindented line", 3), ];
2383
2384 for (input, expected_line) in test_cases {
2385 match input.parse::<Makefile>() {
2387 Ok(_) => {
2388 continue;
2391 }
2392 Err(err) => {
2393 if let Error::Parse(parse_err) = err {
2394 assert_eq!(
2396 parse_err.errors[0].line, expected_line,
2397 "Line number should match the expected line"
2398 );
2399
2400 if parse_err.errors[0].message.contains("indented") {
2402 assert!(
2403 parse_err.errors[0].context.starts_with('\t'),
2404 "Context for indentation errors should include the tab character"
2405 );
2406 }
2407 } else {
2408 panic!("Expected parse error, got: {:?}", err);
2409 }
2410 }
2411 }
2412 }
2413 }
2414
2415 #[test]
2416 fn test_conditional_features() {
2417 let code = r#"
2419# Set variables based on DEBUG flag
2420ifdef DEBUG
2421 CFLAGS += -g -DDEBUG
2422else
2423 CFLAGS = -O2
2424endif
2425
2426# Define a build rule
2427all: $(OBJS)
2428 $(CC) $(CFLAGS) -o $@ $^
2429"#;
2430
2431 let mut buf = code.as_bytes();
2432 let makefile =
2433 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2434
2435 assert!(!makefile.code().is_empty(), "Makefile has content");
2438
2439 let rules = makefile.rules().collect::<Vec<_>>();
2441 assert!(!rules.is_empty(), "Should have found rules");
2442
2443 assert!(code.contains("ifdef DEBUG"));
2445 assert!(code.contains("endif"));
2446
2447 let code_with_var = r#"
2449# Define a variable first
2450CC = gcc
2451
2452ifdef DEBUG
2453 CFLAGS += -g -DDEBUG
2454else
2455 CFLAGS = -O2
2456endif
2457
2458all: $(OBJS)
2459 $(CC) $(CFLAGS) -o $@ $^
2460"#;
2461
2462 let mut buf = code_with_var.as_bytes();
2463 let makefile =
2464 Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2465
2466 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2468 assert!(
2469 !vars.is_empty(),
2470 "Should have found at least the CC variable definition"
2471 );
2472 }
2473
2474 #[test]
2475 fn test_include_directive() {
2476 let parsed = parse(
2477 "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2478 None,
2479 );
2480 assert!(parsed.errors.is_empty());
2481 let node = parsed.syntax();
2482 assert!(format!("{:#?}", node).contains("INCLUDE@"));
2483 }
2484
2485 #[test]
2486 fn test_export_variables() {
2487 let parsed = parse("export SHELL := /bin/bash\n", None);
2488 assert!(parsed.errors.is_empty());
2489 let makefile = parsed.root();
2490 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2491 assert_eq!(vars.len(), 1);
2492 let shell_var = vars
2493 .iter()
2494 .find(|v| v.name() == Some("SHELL".to_string()))
2495 .unwrap();
2496 assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2497 }
2498
2499 #[test]
2500 fn test_variable_scopes() {
2501 let parsed = parse(
2502 "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
2503 None,
2504 );
2505 assert!(parsed.errors.is_empty());
2506 let makefile = parsed.root();
2507 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2508 assert_eq!(vars.len(), 4);
2509 let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
2510 assert!(var_names.contains(&"SIMPLE".to_string()));
2511 assert!(var_names.contains(&"IMMEDIATE".to_string()));
2512 assert!(var_names.contains(&"CONDITIONAL".to_string()));
2513 assert!(var_names.contains(&"APPEND".to_string()));
2514 }
2515
2516 #[test]
2517 fn test_pattern_rule_parsing() {
2518 let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
2519 assert!(parsed.errors.is_empty());
2520 let makefile = parsed.root();
2521 let rules = makefile.rules().collect::<Vec<_>>();
2522 assert_eq!(rules.len(), 1);
2523 assert_eq!(rules[0].targets().next().unwrap(), "%.o");
2524 assert!(rules[0].recipes().next().unwrap().contains("$@"));
2525 }
2526
2527 #[test]
2528 fn test_include_variants() {
2529 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
2531 let parsed = parse(makefile_str, None);
2532 assert!(parsed.errors.is_empty());
2533
2534 let node = parsed.syntax();
2536 let debug_str = format!("{:#?}", node);
2537
2538 assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
2540
2541 let makefile = parsed.root();
2543
2544 let include_count = makefile
2546 .syntax()
2547 .children()
2548 .filter(|child| child.kind() == INCLUDE)
2549 .count();
2550 assert_eq!(include_count, 4);
2551
2552 assert!(makefile
2554 .included_files()
2555 .any(|path| path.contains("$(VAR)")));
2556 }
2557
2558 #[test]
2559 fn test_include_api() {
2560 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
2562 let makefile: Makefile = makefile_str.parse().unwrap();
2563
2564 let includes: Vec<_> = makefile.includes().collect();
2566 assert_eq!(includes.len(), 3);
2567
2568 assert!(!includes[0].is_optional()); assert!(includes[1].is_optional()); assert!(includes[2].is_optional()); let files: Vec<_> = makefile.included_files().collect();
2575 assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
2576
2577 assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
2579 assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
2580 assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
2581 }
2582
2583 #[test]
2584 fn test_include_integration() {
2585 let phony_makefile = Makefile::from_reader(
2589 ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
2590 .as_bytes()
2591 ).unwrap();
2592
2593 assert_eq!(phony_makefile.rules().count(), 2);
2595
2596 let normal_rules_count = phony_makefile
2598 .rules()
2599 .filter(|r| !r.targets().any(|t| t.starts_with('.')))
2600 .count();
2601 assert_eq!(normal_rules_count, 1);
2602
2603 assert_eq!(phony_makefile.includes().count(), 1);
2605 assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
2606
2607 let simple_makefile = Makefile::from_reader(
2609 "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
2610 .as_bytes(),
2611 )
2612 .unwrap();
2613 assert_eq!(simple_makefile.rules().count(), 1);
2614 assert_eq!(simple_makefile.includes().count(), 1);
2615 }
2616
2617 #[test]
2618 fn test_real_conditional_directives() {
2619 let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
2621 let mut buf = conditional.as_bytes();
2622 let makefile =
2623 Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
2624 let code = makefile.code();
2625 assert!(code.contains("ifdef DEBUG"));
2626 assert!(code.contains("else"));
2627 assert!(code.contains("endif"));
2628
2629 let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
2631 let mut buf = nested.as_bytes();
2632 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
2633 let code = makefile.code();
2634 assert!(code.contains("ifdef DEBUG"));
2635 assert!(code.contains("ifdef VERBOSE"));
2636
2637 let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
2639 let mut buf = ifeq.as_bytes();
2640 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
2641 let code = makefile.code();
2642 assert!(code.contains("ifeq"));
2643 assert!(code.contains("Windows_NT"));
2644 }
2645
2646 #[test]
2647 fn test_indented_text_outside_rules() {
2648 let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \" help show help\"\n";
2650 let parsed = parse(help_text, None);
2651 assert!(parsed.errors.is_empty());
2652
2653 let root = parsed.root();
2655 let rules = root.rules().collect::<Vec<_>>();
2656 assert_eq!(rules.len(), 1);
2657
2658 let help_rule = &rules[0];
2659 let recipes = help_rule.recipes().collect::<Vec<_>>();
2660 assert_eq!(recipes.len(), 2);
2661 assert!(recipes[0].contains("Available targets"));
2662 assert!(recipes[1].contains("help"));
2663 }
2664
2665 #[test]
2666 fn test_comment_handling_in_recipes() {
2667 let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
2669
2670 let parsed = parse(recipe_comment, None);
2672
2673 assert!(
2675 parsed.errors.is_empty(),
2676 "Should parse recipe with comments without errors"
2677 );
2678
2679 let root = parsed.root();
2681 let rules = root.rules().collect::<Vec<_>>();
2682 assert_eq!(rules.len(), 1, "Should find exactly one rule");
2683
2684 let build_rule = &rules[0];
2686 assert_eq!(
2687 build_rule.targets().collect::<Vec<_>>(),
2688 vec!["build"],
2689 "Rule should have 'build' as target"
2690 );
2691
2692 let recipes = build_rule.recipes().collect::<Vec<_>>();
2696 assert_eq!(
2697 recipes.len(),
2698 1,
2699 "Should find exactly one recipe line (comment lines are filtered)"
2700 );
2701 assert!(
2702 recipes[0].contains("gcc -o app"),
2703 "Recipe should be the command line"
2704 );
2705 assert!(
2706 !recipes[0].contains("This is a comment"),
2707 "Comments should not be included in recipe lines"
2708 );
2709 }
2710
2711 #[test]
2712 fn test_multiline_variables() {
2713 let multiline = "SOURCES = main.c \\\n util.c\n";
2715
2716 let parsed = parse(multiline, None);
2718
2719 let root = parsed.root();
2721 let vars = root.variable_definitions().collect::<Vec<_>>();
2722 assert!(!vars.is_empty(), "Should find at least one variable");
2723
2724 let operators = "CFLAGS := -Wall \\\n -Werror\n";
2728 let parsed_operators = parse(operators, None);
2729
2730 let root = parsed_operators.root();
2732 let vars = root.variable_definitions().collect::<Vec<_>>();
2733 assert!(
2734 !vars.is_empty(),
2735 "Should find at least one variable with := operator"
2736 );
2737
2738 let append = "LDFLAGS += -L/usr/lib \\\n -lm\n";
2740 let parsed_append = parse(append, None);
2741
2742 let root = parsed_append.root();
2744 let vars = root.variable_definitions().collect::<Vec<_>>();
2745 assert!(
2746 !vars.is_empty(),
2747 "Should find at least one variable with += operator"
2748 );
2749 }
2750
2751 #[test]
2752 fn test_whitespace_and_eof_handling() {
2753 let blank_lines = "VAR = value\n\n\n";
2755
2756 let parsed_blank = parse(blank_lines, None);
2757
2758 let root = parsed_blank.root();
2760 let vars = root.variable_definitions().collect::<Vec<_>>();
2761 assert_eq!(
2762 vars.len(),
2763 1,
2764 "Should find one variable in blank lines test"
2765 );
2766
2767 let trailing_space = "VAR = value \n";
2769
2770 let parsed_space = parse(trailing_space, None);
2771
2772 let root = parsed_space.root();
2774 let vars = root.variable_definitions().collect::<Vec<_>>();
2775 assert_eq!(
2776 vars.len(),
2777 1,
2778 "Should find one variable in trailing space test"
2779 );
2780
2781 let no_newline = "VAR = value";
2783
2784 let parsed_no_newline = parse(no_newline, None);
2785
2786 let root = parsed_no_newline.root();
2788 let vars = root.variable_definitions().collect::<Vec<_>>();
2789 assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
2790 assert_eq!(
2791 vars[0].name(),
2792 Some("VAR".to_string()),
2793 "Variable name should be VAR"
2794 );
2795 }
2796
2797 #[test]
2798 fn test_complex_variable_references() {
2799 let wildcard = "SOURCES = $(wildcard *.c)\n";
2801 let parsed = parse(wildcard, None);
2802 assert!(parsed.errors.is_empty());
2803
2804 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
2806 let parsed = parse(nested, None);
2807 assert!(parsed.errors.is_empty());
2808
2809 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
2811 let parsed = parse(patsubst, None);
2812 assert!(parsed.errors.is_empty());
2813 }
2814
2815 #[test]
2816 fn test_complex_variable_references_minimal() {
2817 let wildcard = "SOURCES = $(wildcard *.c)\n";
2819 let parsed = parse(wildcard, None);
2820 assert!(parsed.errors.is_empty());
2821
2822 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
2824 let parsed = parse(nested, None);
2825 assert!(parsed.errors.is_empty());
2826
2827 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
2829 let parsed = parse(patsubst, None);
2830 assert!(parsed.errors.is_empty());
2831 }
2832
2833 #[test]
2834 fn test_multiline_variable_with_backslash() {
2835 let content = r#"
2836LONG_VAR = This is a long variable \
2837 that continues on the next line \
2838 and even one more line
2839"#;
2840
2841 let mut buf = content.as_bytes();
2843 let makefile =
2844 Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
2845
2846 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2848 assert_eq!(
2849 vars.len(),
2850 1,
2851 "Expected 1 variable but found {}",
2852 vars.len()
2853 );
2854 let var_value = vars[0].raw_value();
2855 assert!(var_value.is_some(), "Variable value is None");
2856
2857 let value_str = var_value.unwrap();
2859 assert!(
2860 value_str.contains("long variable"),
2861 "Value doesn't contain expected content"
2862 );
2863 }
2864
2865 #[test]
2866 fn test_multiline_variable_with_mixed_operators() {
2867 let content = r#"
2868PREFIX ?= /usr/local
2869CFLAGS := -Wall -O2 \
2870 -I$(PREFIX)/include \
2871 -DDEBUG
2872"#;
2873 let mut buf = content.as_bytes();
2875 let makefile = Makefile::read_relaxed(&mut buf)
2876 .expect("Failed to parse multiline variable with operators");
2877
2878 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2880 assert!(
2881 !vars.is_empty(),
2882 "Expected at least 1 variable, found {}",
2883 vars.len()
2884 );
2885
2886 let prefix_var = vars
2888 .iter()
2889 .find(|v| v.name().unwrap_or_default() == "PREFIX");
2890 assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
2891 assert!(
2892 prefix_var.unwrap().raw_value().is_some(),
2893 "PREFIX variable has no value"
2894 );
2895
2896 let cflags_var = vars
2898 .iter()
2899 .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
2900 assert!(
2901 cflags_var.is_some(),
2902 "Expected to find CFLAGS variable (or part of it)"
2903 );
2904 }
2905
2906 #[test]
2907 fn test_indented_help_text() {
2908 let content = r#"
2909.PHONY: help
2910help:
2911 @echo "Available targets:"
2912 @echo " build - Build the project"
2913 @echo " test - Run tests"
2914 @echo " clean - Remove build artifacts"
2915"#;
2916 let mut buf = content.as_bytes();
2918 let makefile =
2919 Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
2920
2921 let rules = makefile.rules().collect::<Vec<_>>();
2923 assert!(!rules.is_empty(), "Expected at least one rule");
2924
2925 let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
2927 assert!(help_rule.is_some(), "Expected to find help rule");
2928
2929 let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
2931 assert!(
2932 !recipes.is_empty(),
2933 "Expected at least one recipe line in help rule"
2934 );
2935 assert!(
2936 recipes.iter().any(|r| r.contains("Available targets")),
2937 "Expected to find 'Available targets' in recipes"
2938 );
2939 }
2940
2941 #[test]
2942 fn test_indented_lines_in_conditionals() {
2943 let content = r#"
2944ifdef DEBUG
2945 CFLAGS += -g -DDEBUG
2946 # This is a comment inside conditional
2947 ifdef VERBOSE
2948 CFLAGS += -v
2949 endif
2950endif
2951"#;
2952 let mut buf = content.as_bytes();
2954 let makefile = Makefile::read_relaxed(&mut buf)
2955 .expect("Failed to parse indented lines in conditionals");
2956
2957 let code = makefile.code();
2959 assert!(code.contains("ifdef DEBUG"));
2960 assert!(code.contains("ifdef VERBOSE"));
2961 assert!(code.contains("endif"));
2962 }
2963
2964 #[test]
2965 fn test_recipe_with_colon() {
2966 let content = r#"
2967build:
2968 @echo "Building at: $(shell date)"
2969 gcc -o program main.c
2970"#;
2971 let parsed = parse(content, None);
2972 assert!(
2973 parsed.errors.is_empty(),
2974 "Failed to parse recipe with colon: {:?}",
2975 parsed.errors
2976 );
2977 }
2978
2979 #[test]
2980 #[ignore]
2981 fn test_double_colon_rules() {
2982 let content = r#"
2985%.o :: %.c
2986 $(CC) -c $< -o $@
2987
2988# Double colon allows multiple rules for same target
2989all:: prerequisite1
2990 @echo "First rule for all"
2991
2992all:: prerequisite2
2993 @echo "Second rule for all"
2994"#;
2995 let mut buf = content.as_bytes();
2996 let makefile =
2997 Makefile::read_relaxed(&mut buf).expect("Failed to parse double colon rules");
2998
2999 let rules = makefile.rules().collect::<Vec<_>>();
3001 assert!(!rules.is_empty(), "Expected at least one rule");
3002
3003 let all_rules = rules
3005 .iter()
3006 .filter(|r| r.targets().any(|t| t.contains("all")));
3007 assert!(
3008 all_rules.count() > 0,
3009 "Expected to find at least one rule containing 'all'"
3010 );
3011 }
3012
3013 #[test]
3014 fn test_else_conditional_directives() {
3015 let content = r#"
3017ifeq ($(OS),Windows_NT)
3018 TARGET = windows
3019else ifeq ($(OS),Darwin)
3020 TARGET = macos
3021else ifeq ($(OS),Linux)
3022 TARGET = linux
3023else
3024 TARGET = unknown
3025endif
3026"#;
3027 let mut buf = content.as_bytes();
3028 let makefile =
3029 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3030 assert!(makefile.code().contains("else ifeq"));
3031 assert!(makefile.code().contains("TARGET"));
3032
3033 let content = r#"
3035ifdef WINDOWS
3036 TARGET = windows
3037else ifdef DARWIN
3038 TARGET = macos
3039else ifdef LINUX
3040 TARGET = linux
3041else
3042 TARGET = unknown
3043endif
3044"#;
3045 let mut buf = content.as_bytes();
3046 let makefile =
3047 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3048 assert!(makefile.code().contains("else ifdef"));
3049
3050 let content = r#"
3052ifndef NOWINDOWS
3053 TARGET = windows
3054else ifndef NODARWIN
3055 TARGET = macos
3056else
3057 TARGET = linux
3058endif
3059"#;
3060 let mut buf = content.as_bytes();
3061 let makefile =
3062 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3063 assert!(makefile.code().contains("else ifndef"));
3064
3065 let content = r#"
3067ifneq ($(OS),Windows_NT)
3068 TARGET = not_windows
3069else ifneq ($(OS),Darwin)
3070 TARGET = not_macos
3071else
3072 TARGET = darwin
3073endif
3074"#;
3075 let mut buf = content.as_bytes();
3076 let makefile =
3077 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3078 assert!(makefile.code().contains("else ifneq"));
3079 }
3080
3081 #[test]
3082 fn test_complex_else_conditionals() {
3083 let content = r#"VAR1 := foo
3085VAR2 := bar
3086
3087ifeq ($(VAR1),foo)
3088 RESULT := foo_matched
3089else ifdef VAR2
3090 RESULT := var2_defined
3091else ifndef VAR3
3092 RESULT := var3_not_defined
3093else
3094 RESULT := final_else
3095endif
3096
3097all:
3098 @echo $(RESULT)
3099"#;
3100 let mut buf = content.as_bytes();
3101 let makefile =
3102 Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3103
3104 let code = makefile.code();
3106 assert!(code.contains("ifeq ($(VAR1),foo)"));
3107 assert!(code.contains("else ifdef VAR2"));
3108 assert!(code.contains("else ifndef VAR3"));
3109 assert!(code.contains("else"));
3110 assert!(code.contains("endif"));
3111 assert!(code.contains("RESULT"));
3112
3113 let rules: Vec<_> = makefile.rules().collect();
3115 assert_eq!(rules.len(), 1);
3116 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3117 }
3118
3119 #[test]
3120 fn test_conditional_token_structure() {
3121 let content = r#"ifdef VAR1
3123X := 1
3124else ifdef VAR2
3125X := 2
3126else
3127X := 3
3128endif
3129"#;
3130 let mut buf = content.as_bytes();
3131 let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3132
3133 let syntax = makefile.syntax();
3135
3136 let mut found_conditional = false;
3138 let mut found_conditional_if = false;
3139 let mut found_conditional_else = false;
3140 let mut found_conditional_endif = false;
3141
3142 fn check_node(
3143 node: &SyntaxNode,
3144 found_cond: &mut bool,
3145 found_if: &mut bool,
3146 found_else: &mut bool,
3147 found_endif: &mut bool,
3148 ) {
3149 match node.kind() {
3150 SyntaxKind::CONDITIONAL => *found_cond = true,
3151 SyntaxKind::CONDITIONAL_IF => *found_if = true,
3152 SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3153 SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3154 _ => {}
3155 }
3156
3157 for child in node.children() {
3158 check_node(&child, found_cond, found_if, found_else, found_endif);
3159 }
3160 }
3161
3162 check_node(
3163 syntax,
3164 &mut found_conditional,
3165 &mut found_conditional_if,
3166 &mut found_conditional_else,
3167 &mut found_conditional_endif,
3168 );
3169
3170 assert!(found_conditional, "Should have CONDITIONAL node");
3171 assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3172 assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3173 assert!(
3174 found_conditional_endif,
3175 "Should have CONDITIONAL_ENDIF node"
3176 );
3177 }
3178
3179 #[test]
3180 fn test_ambiguous_assignment_vs_rule() {
3181 const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3183
3184 let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3185 let makefile =
3186 Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3187
3188 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3189 let rules = makefile.rules().collect::<Vec<_>>();
3190
3191 assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3192 assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3193
3194 assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3195
3196 const SIMPLE_RULE: &str = "target: dependency\n";
3198
3199 let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3200 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3201
3202 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3203 let rules = makefile.rules().collect::<Vec<_>>();
3204
3205 assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3206 assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3207
3208 let rule = &rules[0];
3209 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3210 }
3211
3212 #[test]
3213 fn test_nested_conditionals() {
3214 let content = r#"
3215ifdef RELEASE
3216 CFLAGS += -O3
3217 ifndef DEBUG
3218 ifneq ($(ARCH),arm)
3219 CFLAGS += -march=native
3220 else
3221 CFLAGS += -mcpu=cortex-a72
3222 endif
3223 endif
3224endif
3225"#;
3226 let mut buf = content.as_bytes();
3228 let makefile =
3229 Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3230
3231 let code = makefile.code();
3233 assert!(code.contains("ifdef RELEASE"));
3234 assert!(code.contains("ifndef DEBUG"));
3235 assert!(code.contains("ifneq"));
3236 }
3237
3238 #[test]
3239 fn test_space_indented_recipes() {
3240 let content = r#"
3243build:
3244 @echo "Building with spaces instead of tabs"
3245 gcc -o program main.c
3246"#;
3247 let mut buf = content.as_bytes();
3249 let makefile =
3250 Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3251
3252 let rules = makefile.rules().collect::<Vec<_>>();
3254 assert!(!rules.is_empty(), "Expected at least one rule");
3255
3256 let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3258 assert!(build_rule.is_some(), "Expected to find build rule");
3259 }
3260
3261 #[test]
3262 fn test_complex_variable_functions() {
3263 let content = r#"
3264FILES := $(shell find . -name "*.c")
3265OBJS := $(patsubst %.c,%.o,$(FILES))
3266NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3267HEADERS := ${wildcard *.h}
3268"#;
3269 let parsed = parse(content, None);
3270 assert!(
3271 parsed.errors.is_empty(),
3272 "Failed to parse complex variable functions: {:?}",
3273 parsed.errors
3274 );
3275 }
3276
3277 #[test]
3278 fn test_nested_variable_expansions() {
3279 let content = r#"
3280VERSION = 1.0
3281PACKAGE = myapp
3282TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3283INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3284"#;
3285 let parsed = parse(content, None);
3286 assert!(
3287 parsed.errors.is_empty(),
3288 "Failed to parse nested variable expansions: {:?}",
3289 parsed.errors
3290 );
3291 }
3292
3293 #[test]
3294 fn test_special_directives() {
3295 let content = r#"
3296# Special makefile directives
3297.PHONY: all clean
3298.SUFFIXES: .c .o
3299.DEFAULT: all
3300
3301# Variable definition and export directive
3302export PATH := /usr/bin:/bin
3303"#;
3304 let mut buf = content.as_bytes();
3306 let makefile =
3307 Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3308
3309 let rules = makefile.rules().collect::<Vec<_>>();
3311
3312 let phony_rule = rules
3314 .iter()
3315 .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3316 assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3317
3318 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3320 assert!(!vars.is_empty(), "Expected to find at least one variable");
3321 }
3322
3323 #[test]
3326 fn test_comprehensive_real_world_makefile() {
3327 let content = r#"
3329# Basic variable assignment
3330VERSION = 1.0.0
3331
3332# Phony target
3333.PHONY: all clean
3334
3335# Simple rule
3336all:
3337 echo "Building version $(VERSION)"
3338
3339# Another rule with dependencies
3340clean:
3341 rm -f *.o
3342"#;
3343
3344 let parsed = parse(content, None);
3346
3347 assert!(parsed.errors.is_empty(), "Expected no parsing errors");
3349
3350 let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
3352 assert!(!variables.is_empty(), "Expected at least one variable");
3353 assert_eq!(
3354 variables[0].name(),
3355 Some("VERSION".to_string()),
3356 "Expected VERSION variable"
3357 );
3358
3359 let rules = parsed.root().rules().collect::<Vec<_>>();
3361 assert!(!rules.is_empty(), "Expected at least one rule");
3362
3363 let rule_targets: Vec<String> = rules
3365 .iter()
3366 .flat_map(|r| r.targets().collect::<Vec<_>>())
3367 .collect();
3368 assert!(
3369 rule_targets.contains(&".PHONY".to_string()),
3370 "Expected .PHONY rule"
3371 );
3372 assert!(
3373 rule_targets.contains(&"all".to_string()),
3374 "Expected 'all' rule"
3375 );
3376 assert!(
3377 rule_targets.contains(&"clean".to_string()),
3378 "Expected 'clean' rule"
3379 );
3380 }
3381
3382 #[test]
3383 fn test_indented_help_text_outside_rules() {
3384 let content = r#"
3386# Targets with help text
3387help:
3388 @echo "Available targets:"
3389 @echo " build build the project"
3390 @echo " test run tests"
3391 @echo " clean clean build artifacts"
3392
3393# Another target
3394clean:
3395 rm -rf build/
3396"#;
3397
3398 let parsed = parse(content, None);
3400
3401 assert!(
3403 parsed.errors.is_empty(),
3404 "Failed to parse indented help text"
3405 );
3406
3407 let rules = parsed.root().rules().collect::<Vec<_>>();
3409 assert_eq!(rules.len(), 2, "Expected to find two rules");
3410
3411 let help_rule = rules
3413 .iter()
3414 .find(|r| r.targets().any(|t| t == "help"))
3415 .expect("Expected to find help rule");
3416
3417 let clean_rule = rules
3418 .iter()
3419 .find(|r| r.targets().any(|t| t == "clean"))
3420 .expect("Expected to find clean rule");
3421
3422 let help_recipes = help_rule.recipes().collect::<Vec<_>>();
3424 assert!(
3425 !help_recipes.is_empty(),
3426 "Help rule should have recipe lines"
3427 );
3428 assert!(
3429 help_recipes
3430 .iter()
3431 .any(|line| line.contains("Available targets")),
3432 "Help recipes should include 'Available targets' line"
3433 );
3434
3435 let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
3437 assert!(
3438 !clean_recipes.is_empty(),
3439 "Clean rule should have recipe lines"
3440 );
3441 assert!(
3442 clean_recipes.iter().any(|line| line.contains("rm -rf")),
3443 "Clean recipes should include 'rm -rf' command"
3444 );
3445 }
3446
3447 #[test]
3448 fn test_makefile1_phony_pattern() {
3449 let content = "#line 2145\n.PHONY: $(PHONY)\n";
3451
3452 let result = parse(content, None);
3454
3455 assert!(
3457 result.errors.is_empty(),
3458 "Failed to parse .PHONY: $(PHONY) pattern"
3459 );
3460
3461 let rules = result.root().rules().collect::<Vec<_>>();
3463 assert_eq!(rules.len(), 1, "Expected 1 rule");
3464 assert_eq!(
3465 rules[0].targets().next().unwrap(),
3466 ".PHONY",
3467 "Expected .PHONY rule"
3468 );
3469
3470 let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
3472 assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
3473 assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
3474 }
3475
3476 #[test]
3477 fn test_skip_until_newline_behavior() {
3478 let input = "text without newline";
3480 let parsed = parse(input, None);
3481 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
3483
3484 let input_with_newline = "text\nafter newline";
3485 let parsed2 = parse(input_with_newline, None);
3486 assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
3487 }
3488
3489 #[test]
3490 #[ignore] fn test_error_with_indent_token() {
3492 let input = "\tinvalid indented line";
3494 let parsed = parse(input, None);
3495 assert!(!parsed.errors.is_empty());
3497
3498 let error_msg = &parsed.errors[0].message;
3499 assert!(error_msg.contains("recipe commences before first target"));
3500 }
3501
3502 #[test]
3503 fn test_conditional_token_handling() {
3504 let input = r#"
3506ifndef VAR
3507 CFLAGS = -DTEST
3508endif
3509"#;
3510 let parsed = parse(input, None);
3511 let makefile = parsed.root();
3513 let _vars = makefile.variable_definitions().collect::<Vec<_>>();
3514 let nested = r#"
3518ifdef DEBUG
3519 ifndef RELEASE
3520 CFLAGS = -g
3521 endif
3522endif
3523"#;
3524 let parsed_nested = parse(nested, None);
3525 let _makefile = parsed_nested.root();
3527 }
3528
3529 #[test]
3530 fn test_include_vs_conditional_logic() {
3531 let input = r#"
3533include file.mk
3534ifdef VAR
3535 VALUE = 1
3536endif
3537"#;
3538 let parsed = parse(input, None);
3539 let makefile = parsed.root();
3541 let includes = makefile.includes().collect::<Vec<_>>();
3542 assert!(!includes.is_empty() || !parsed.errors.is_empty());
3544
3545 let optional_include = r#"
3547-include optional.mk
3548ifndef VAR
3549 VALUE = default
3550endif
3551"#;
3552 let parsed2 = parse(optional_include, None);
3553 let _makefile = parsed2.root();
3555 }
3556
3557 #[test]
3558 fn test_balanced_parens_counting() {
3559 let input = r#"
3561VAR = $(call func,$(nested,arg),extra)
3562COMPLEX = $(if $(condition),$(then_val),$(else_val))
3563"#;
3564 let parsed = parse(input, None);
3565 assert!(parsed.errors.is_empty());
3566
3567 let makefile = parsed.root();
3568 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3569 assert_eq!(vars.len(), 2);
3570 }
3571
3572 #[test]
3573 fn test_documentation_lookahead() {
3574 let input = r#"
3576# Documentation comment
3577help:
3578 @echo "Usage instructions"
3579 @echo "More help text"
3580"#;
3581 let parsed = parse(input, None);
3582 assert!(parsed.errors.is_empty());
3583
3584 let makefile = parsed.root();
3585 let rules = makefile.rules().collect::<Vec<_>>();
3586 assert_eq!(rules.len(), 1);
3587 assert_eq!(rules[0].targets().next().unwrap(), "help");
3588 }
3589
3590 #[test]
3591 fn test_edge_case_empty_input() {
3592 let parsed = parse("", None);
3594 assert!(parsed.errors.is_empty());
3595
3596 let parsed2 = parse(" \n \n", None);
3598 let _makefile = parsed2.root();
3601 }
3602
3603 #[test]
3604 fn test_malformed_conditional_recovery() {
3605 let input = r#"
3607ifdef
3608 # Missing condition variable
3609endif
3610"#;
3611 let parsed = parse(input, None);
3612 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
3615 }
3616
3617 #[test]
3618 fn test_replace_rule() {
3619 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
3620 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3621
3622 makefile.replace_rule(0, new_rule).unwrap();
3623
3624 let targets: Vec<_> = makefile
3625 .rules()
3626 .flat_map(|r| r.targets().collect::<Vec<_>>())
3627 .collect();
3628 assert_eq!(targets, vec!["new_rule", "rule2"]);
3629
3630 let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
3631 assert_eq!(recipes, vec!["new_command"]);
3632 }
3633
3634 #[test]
3635 fn test_replace_rule_out_of_bounds() {
3636 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3637 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3638
3639 let result = makefile.replace_rule(5, new_rule);
3640 assert!(result.is_err());
3641 }
3642
3643 #[test]
3644 fn test_remove_rule() {
3645 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
3646 .parse()
3647 .unwrap();
3648
3649 let removed = makefile.remove_rule(1).unwrap();
3650 assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
3651
3652 let remaining_targets: Vec<_> = makefile
3653 .rules()
3654 .flat_map(|r| r.targets().collect::<Vec<_>>())
3655 .collect();
3656 assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
3657 assert_eq!(makefile.rules().count(), 2);
3658 }
3659
3660 #[test]
3661 fn test_remove_rule_out_of_bounds() {
3662 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3663
3664 let result = makefile.remove_rule(5);
3665 assert!(result.is_err());
3666 }
3667
3668 #[test]
3669 fn test_insert_rule() {
3670 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
3671 let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
3672
3673 makefile.insert_rule(1, new_rule).unwrap();
3674
3675 let targets: Vec<_> = makefile
3676 .rules()
3677 .flat_map(|r| r.targets().collect::<Vec<_>>())
3678 .collect();
3679 assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
3680 assert_eq!(makefile.rules().count(), 3);
3681 }
3682
3683 #[test]
3684 fn test_insert_rule_at_end() {
3685 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3686 let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
3687
3688 makefile.insert_rule(1, new_rule).unwrap();
3689
3690 let targets: Vec<_> = makefile
3691 .rules()
3692 .flat_map(|r| r.targets().collect::<Vec<_>>())
3693 .collect();
3694 assert_eq!(targets, vec!["rule1", "end_rule"]);
3695 }
3696
3697 #[test]
3698 fn test_insert_rule_out_of_bounds() {
3699 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3700 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3701
3702 let result = makefile.insert_rule(5, new_rule);
3703 assert!(result.is_err());
3704 }
3705
3706 #[test]
3707 fn test_insert_rule_preserves_blank_line_spacing_at_end() {
3708 let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
3710 let mut makefile: Makefile = input.parse().unwrap();
3711 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
3712
3713 makefile.insert_rule(2, new_rule).unwrap();
3714
3715 let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
3716 assert_eq!(makefile.to_string(), expected);
3717 }
3718
3719 #[test]
3720 fn test_insert_rule_adds_blank_lines_when_missing() {
3721 let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
3723 let mut makefile: Makefile = input.parse().unwrap();
3724 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
3725
3726 makefile.insert_rule(2, new_rule).unwrap();
3727
3728 let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
3729 assert_eq!(makefile.to_string(), expected);
3730 }
3731
3732 #[test]
3733 fn test_remove_command() {
3734 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
3735 .parse()
3736 .unwrap();
3737
3738 rule.remove_command(1);
3739 let recipes: Vec<_> = rule.recipes().collect();
3740 assert_eq!(recipes, vec!["command1", "command3"]);
3741 assert_eq!(rule.recipe_count(), 2);
3742 }
3743
3744 #[test]
3745 fn test_remove_command_out_of_bounds() {
3746 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
3747
3748 let result = rule.remove_command(5);
3749 assert!(!result);
3750 }
3751
3752 #[test]
3753 fn test_insert_command() {
3754 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
3755
3756 rule.insert_command(1, "command2");
3757 let recipes: Vec<_> = rule.recipes().collect();
3758 assert_eq!(recipes, vec!["command1", "command2", "command3"]);
3759 }
3760
3761 #[test]
3762 fn test_insert_command_at_end() {
3763 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
3764
3765 rule.insert_command(1, "command2");
3766 let recipes: Vec<_> = rule.recipes().collect();
3767 assert_eq!(recipes, vec!["command1", "command2"]);
3768 }
3769
3770 #[test]
3771 fn test_insert_command_in_empty_rule() {
3772 let mut rule: Rule = "rule:\n".parse().unwrap();
3773
3774 rule.insert_command(0, "new_command");
3775 let recipes: Vec<_> = rule.recipes().collect();
3776 assert_eq!(recipes, vec!["new_command"]);
3777 }
3778
3779 #[test]
3780 fn test_recipe_count() {
3781 let rule1: Rule = "rule:\n".parse().unwrap();
3782 assert_eq!(rule1.recipe_count(), 0);
3783
3784 let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
3785 assert_eq!(rule2.recipe_count(), 2);
3786 }
3787
3788 #[test]
3789 fn test_clear_commands() {
3790 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
3791 .parse()
3792 .unwrap();
3793
3794 rule.clear_commands();
3795 assert_eq!(rule.recipe_count(), 0);
3796
3797 let recipes: Vec<_> = rule.recipes().collect();
3798 assert_eq!(recipes, Vec::<String>::new());
3799
3800 let targets: Vec<_> = rule.targets().collect();
3802 assert_eq!(targets, vec!["rule"]);
3803 }
3804
3805 #[test]
3806 fn test_clear_commands_empty_rule() {
3807 let mut rule: Rule = "rule:\n".parse().unwrap();
3808
3809 rule.clear_commands();
3810 assert_eq!(rule.recipe_count(), 0);
3811
3812 let targets: Vec<_> = rule.targets().collect();
3813 assert_eq!(targets, vec!["rule"]);
3814 }
3815
3816 #[test]
3817 fn test_rule_manipulation_preserves_structure() {
3818 let input = r#"# Comment
3820VAR = value
3821
3822rule1:
3823 command1
3824
3825# Another comment
3826rule2:
3827 command2
3828
3829VAR2 = value2
3830"#;
3831
3832 let mut makefile: Makefile = input.parse().unwrap();
3833 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3834
3835 makefile.insert_rule(1, new_rule).unwrap();
3837
3838 let targets: Vec<_> = makefile
3840 .rules()
3841 .flat_map(|r| r.targets().collect::<Vec<_>>())
3842 .collect();
3843 assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
3844
3845 let vars: Vec<_> = makefile.variable_definitions().collect();
3847 assert_eq!(vars.len(), 2);
3848
3849 let output = makefile.code();
3851 assert!(output.contains("# Comment"));
3852 assert!(output.contains("VAR = value"));
3853 assert!(output.contains("# Another comment"));
3854 assert!(output.contains("VAR2 = value2"));
3855 }
3856
3857 #[test]
3858 fn test_replace_rule_with_multiple_targets() {
3859 let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
3860 let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
3861
3862 makefile.replace_rule(0, new_rule).unwrap();
3863
3864 let targets: Vec<_> = makefile
3865 .rules()
3866 .flat_map(|r| r.targets().collect::<Vec<_>>())
3867 .collect();
3868 assert_eq!(targets, vec!["new_target"]);
3869 }
3870
3871 #[test]
3872 fn test_empty_makefile_operations() {
3873 let mut makefile = Makefile::new();
3874
3875 assert!(makefile
3877 .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
3878 .is_err());
3879 assert!(makefile.remove_rule(0).is_err());
3880
3881 let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
3883 makefile.insert_rule(0, new_rule).unwrap();
3884 assert_eq!(makefile.rules().count(), 1);
3885 }
3886
3887 #[test]
3888 fn test_command_operations_preserve_indentation() {
3889 let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
3890 .parse()
3891 .unwrap();
3892
3893 rule.insert_command(1, "middle_command");
3894 let recipes: Vec<_> = rule.recipes().collect();
3895 assert_eq!(
3896 recipes,
3897 vec!["\tdeep_indent", "middle_command", "shallow_indent"]
3898 );
3899 }
3900
3901 #[test]
3902 fn test_rule_operations_with_variables_and_includes() {
3903 let input = r#"VAR1 = value1
3904include common.mk
3905
3906rule1:
3907 command1
3908
3909VAR2 = value2
3910include other.mk
3911
3912rule2:
3913 command2
3914"#;
3915
3916 let mut makefile: Makefile = input.parse().unwrap();
3917
3918 makefile.remove_rule(0).unwrap();
3920
3921 let output = makefile.code();
3923 assert!(output.contains("VAR1 = value1"));
3924 assert!(output.contains("include common.mk"));
3925 assert!(output.contains("VAR2 = value2"));
3926 assert!(output.contains("include other.mk"));
3927
3928 assert_eq!(makefile.rules().count(), 1);
3930 let remaining_targets: Vec<_> = makefile
3931 .rules()
3932 .flat_map(|r| r.targets().collect::<Vec<_>>())
3933 .collect();
3934 assert_eq!(remaining_targets, vec!["rule2"]);
3935 }
3936
3937 #[test]
3938 fn test_command_manipulation_edge_cases() {
3939 let mut empty_rule: Rule = "empty:\n".parse().unwrap();
3941 assert_eq!(empty_rule.recipe_count(), 0);
3942
3943 empty_rule.insert_command(0, "first_command");
3944 assert_eq!(empty_rule.recipe_count(), 1);
3945
3946 let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
3948 empty_rule2.clear_commands();
3949 assert_eq!(empty_rule2.recipe_count(), 0);
3950 }
3951
3952 #[test]
3953 fn test_large_makefile_performance() {
3954 let mut makefile = Makefile::new();
3956
3957 for i in 0..100 {
3959 let rule_name = format!("rule{}", i);
3960 makefile
3961 .add_rule(&rule_name)
3962 .push_command(&format!("command{}", i));
3963 }
3964
3965 assert_eq!(makefile.rules().count(), 100);
3966
3967 let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
3969 makefile.replace_rule(50, new_rule).unwrap();
3970
3971 let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
3973 assert_eq!(rule_50_targets, vec!["middle_rule"]);
3974
3975 assert_eq!(makefile.rules().count(), 100); }
3977
3978 #[test]
3979 fn test_complex_recipe_manipulation() {
3980 let mut complex_rule: Rule = r#"complex:
3981 @echo "Starting build"
3982 $(CC) $(CFLAGS) -o $@ $<
3983 @echo "Build complete"
3984 chmod +x $@
3985"#
3986 .parse()
3987 .unwrap();
3988
3989 assert_eq!(complex_rule.recipe_count(), 4);
3990
3991 complex_rule.remove_command(0); complex_rule.remove_command(1); let final_recipes: Vec<_> = complex_rule.recipes().collect();
3996 assert_eq!(final_recipes.len(), 2);
3997 assert!(final_recipes[0].contains("$(CC)"));
3998 assert!(final_recipes[1].contains("chmod"));
3999 }
4000
4001 #[test]
4002 fn test_variable_definition_remove() {
4003 let makefile: Makefile = r#"VAR1 = value1
4004VAR2 = value2
4005VAR3 = value3
4006"#
4007 .parse()
4008 .unwrap();
4009
4010 assert_eq!(makefile.variable_definitions().count(), 3);
4012
4013 let mut var2 = makefile
4015 .variable_definitions()
4016 .nth(1)
4017 .expect("Should have second variable");
4018 assert_eq!(var2.name(), Some("VAR2".to_string()));
4019 var2.remove();
4020
4021 assert_eq!(makefile.variable_definitions().count(), 2);
4023 let var_names: Vec<_> = makefile
4024 .variable_definitions()
4025 .filter_map(|v| v.name())
4026 .collect();
4027 assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4028 }
4029
4030 #[test]
4031 fn test_variable_definition_set_value() {
4032 let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4033
4034 let mut var = makefile
4035 .variable_definitions()
4036 .next()
4037 .expect("Should have variable");
4038 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4039
4040 var.set_value("new_value");
4042
4043 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4045 assert!(makefile.code().contains("VAR = new_value"));
4046 }
4047
4048 #[test]
4049 fn test_variable_definition_set_value_preserves_format() {
4050 let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4051
4052 let mut var = makefile
4053 .variable_definitions()
4054 .next()
4055 .expect("Should have variable");
4056 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4057
4058 var.set_value("new_value");
4060
4061 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4063 let code = makefile.code();
4064 assert!(code.contains("export"), "Should preserve export prefix");
4065 assert!(code.contains(":="), "Should preserve := operator");
4066 assert!(code.contains("new_value"), "Should have new value");
4067 }
4068
4069 #[test]
4070 fn test_makefile_find_variable() {
4071 let makefile: Makefile = r#"VAR1 = value1
4072VAR2 = value2
4073VAR3 = value3
4074"#
4075 .parse()
4076 .unwrap();
4077
4078 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4080 assert_eq!(vars.len(), 1);
4081 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4082 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4083
4084 assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4086 }
4087
4088 #[test]
4089 fn test_makefile_find_variable_with_export() {
4090 let makefile: Makefile = r#"VAR1 = value1
4091export VAR2 := value2
4092VAR3 = value3
4093"#
4094 .parse()
4095 .unwrap();
4096
4097 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4099 assert_eq!(vars.len(), 1);
4100 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4101 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4102 }
4103
4104 #[test]
4105 fn test_variable_definition_is_export() {
4106 let makefile: Makefile = r#"VAR1 = value1
4107export VAR2 := value2
4108export VAR3 = value3
4109VAR4 := value4
4110"#
4111 .parse()
4112 .unwrap();
4113
4114 let vars: Vec<_> = makefile.variable_definitions().collect();
4115 assert_eq!(vars.len(), 4);
4116
4117 assert!(!vars[0].is_export());
4118 assert!(vars[1].is_export());
4119 assert!(vars[2].is_export());
4120 assert!(!vars[3].is_export());
4121 }
4122
4123 #[test]
4124 fn test_makefile_find_variable_multiple() {
4125 let makefile: Makefile = r#"VAR1 = value1
4126VAR1 = value2
4127VAR2 = other
4128VAR1 = value3
4129"#
4130 .parse()
4131 .unwrap();
4132
4133 let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4135 assert_eq!(vars.len(), 3);
4136 assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4137 assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4138 assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4139
4140 let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4142 assert_eq!(var2s.len(), 1);
4143 assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4144 }
4145
4146 #[test]
4147 fn test_variable_remove_and_find() {
4148 let makefile: Makefile = r#"VAR1 = value1
4149VAR2 = value2
4150VAR3 = value3
4151"#
4152 .parse()
4153 .unwrap();
4154
4155 let mut var2 = makefile
4157 .find_variable("VAR2")
4158 .next()
4159 .expect("Should find VAR2");
4160 var2.remove();
4161
4162 assert_eq!(makefile.find_variable("VAR2").count(), 0);
4164
4165 assert_eq!(makefile.find_variable("VAR1").count(), 1);
4167 assert_eq!(makefile.find_variable("VAR3").count(), 1);
4168 }
4169
4170 #[test]
4171 fn test_variable_remove_with_comment() {
4172 let makefile: Makefile = r#"VAR1 = value1
4173# This is a comment about VAR2
4174VAR2 = value2
4175VAR3 = value3
4176"#
4177 .parse()
4178 .unwrap();
4179
4180 let mut var2 = makefile
4182 .variable_definitions()
4183 .nth(1)
4184 .expect("Should have second variable");
4185 assert_eq!(var2.name(), Some("VAR2".to_string()));
4186 var2.remove();
4187
4188 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4190 }
4191
4192 #[test]
4193 fn test_variable_remove_with_multiple_comments() {
4194 let makefile: Makefile = r#"VAR1 = value1
4195# Comment line 1
4196# Comment line 2
4197# Comment line 3
4198VAR2 = value2
4199VAR3 = value3
4200"#
4201 .parse()
4202 .unwrap();
4203
4204 let mut var2 = makefile
4206 .variable_definitions()
4207 .nth(1)
4208 .expect("Should have second variable");
4209 var2.remove();
4210
4211 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4213 }
4214
4215 #[test]
4216 fn test_variable_remove_with_empty_line() {
4217 let makefile: Makefile = r#"VAR1 = value1
4218
4219# Comment about VAR2
4220VAR2 = value2
4221VAR3 = value3
4222"#
4223 .parse()
4224 .unwrap();
4225
4226 let mut var2 = makefile
4228 .variable_definitions()
4229 .nth(1)
4230 .expect("Should have second variable");
4231 var2.remove();
4232
4233 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4236 }
4237
4238 #[test]
4239 fn test_variable_remove_with_multiple_empty_lines() {
4240 let makefile: Makefile = r#"VAR1 = value1
4241
4242
4243# Comment about VAR2
4244VAR2 = value2
4245VAR3 = value3
4246"#
4247 .parse()
4248 .unwrap();
4249
4250 let mut var2 = makefile
4252 .variable_definitions()
4253 .nth(1)
4254 .expect("Should have second variable");
4255 var2.remove();
4256
4257 assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4260 }
4261
4262 #[test]
4263 fn test_rule_remove_with_comment() {
4264 let makefile: Makefile = r#"rule1:
4265 command1
4266
4267# Comment about rule2
4268rule2:
4269 command2
4270rule3:
4271 command3
4272"#
4273 .parse()
4274 .unwrap();
4275
4276 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4278 rule2.remove().unwrap();
4279
4280 assert_eq!(
4283 makefile.code(),
4284 "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4285 );
4286 }
4287
4288 #[test]
4289 fn test_variable_remove_preserves_shebang() {
4290 let makefile: Makefile = r#"#!/usr/bin/make -f
4291# This is a regular comment
4292VAR1 = value1
4293VAR2 = value2
4294"#
4295 .parse()
4296 .unwrap();
4297
4298 let mut var1 = makefile.variable_definitions().next().unwrap();
4300 var1.remove();
4301
4302 let code = makefile.code();
4304 assert!(code.starts_with("#!/usr/bin/make -f"));
4305 assert!(!code.contains("regular comment"));
4306 assert!(!code.contains("VAR1"));
4307 assert!(code.contains("VAR2"));
4308 }
4309
4310 #[test]
4311 fn test_variable_remove_preserves_subsequent_comments() {
4312 let makefile: Makefile = r#"VAR1 = value1
4313# Comment about VAR2
4314VAR2 = value2
4315
4316# Comment about VAR3
4317VAR3 = value3
4318"#
4319 .parse()
4320 .unwrap();
4321
4322 let mut var2 = makefile
4324 .variable_definitions()
4325 .nth(1)
4326 .expect("Should have second variable");
4327 var2.remove();
4328
4329 let code = makefile.code();
4331 assert_eq!(
4332 code,
4333 "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4334 );
4335 }
4336
4337 #[test]
4338 fn test_variable_remove_after_shebang_preserves_empty_line() {
4339 let makefile: Makefile = r#"#!/usr/bin/make -f
4340export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4341
4342%:
4343 dh $@
4344"#
4345 .parse()
4346 .unwrap();
4347
4348 let mut var = makefile.variable_definitions().next().unwrap();
4350 var.remove();
4351
4352 assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
4354 }
4355
4356 #[test]
4357 fn test_rule_add_prerequisite() {
4358 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4359 rule.add_prerequisite("dep2").unwrap();
4360 assert_eq!(
4361 rule.prerequisites().collect::<Vec<_>>(),
4362 vec!["dep1", "dep2"]
4363 );
4364 assert_eq!(rule.to_string(), "target: dep1 dep2\n");
4366 }
4367
4368 #[test]
4369 fn test_rule_add_prerequisite_to_rule_without_prereqs() {
4370 let mut rule: Rule = "target:\n".parse().unwrap();
4372 rule.add_prerequisite("dep1").unwrap();
4373 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
4374 assert_eq!(rule.to_string(), "target: dep1\n");
4376 }
4377
4378 #[test]
4379 fn test_rule_remove_prerequisite() {
4380 let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
4381 assert!(rule.remove_prerequisite("dep2").unwrap());
4382 assert_eq!(
4383 rule.prerequisites().collect::<Vec<_>>(),
4384 vec!["dep1", "dep3"]
4385 );
4386 assert!(!rule.remove_prerequisite("nonexistent").unwrap());
4387 }
4388
4389 #[test]
4390 fn test_rule_set_prerequisites() {
4391 let mut rule: Rule = "target: old_dep\n".parse().unwrap();
4392 rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
4393 .unwrap();
4394 assert_eq!(
4395 rule.prerequisites().collect::<Vec<_>>(),
4396 vec!["new_dep1", "new_dep2"]
4397 );
4398 }
4399
4400 #[test]
4401 fn test_rule_set_prerequisites_empty() {
4402 let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
4403 rule.set_prerequisites(vec![]).unwrap();
4404 assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
4405 }
4406
4407 #[test]
4408 fn test_rule_add_target() {
4409 let mut rule: Rule = "target1: dep1\n".parse().unwrap();
4410 rule.add_target("target2").unwrap();
4411 assert_eq!(
4412 rule.targets().collect::<Vec<_>>(),
4413 vec!["target1", "target2"]
4414 );
4415 }
4416
4417 #[test]
4418 fn test_rule_set_targets() {
4419 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
4420 rule.set_targets(vec!["new_target1", "new_target2"])
4421 .unwrap();
4422 assert_eq!(
4423 rule.targets().collect::<Vec<_>>(),
4424 vec!["new_target1", "new_target2"]
4425 );
4426 }
4427
4428 #[test]
4429 fn test_rule_set_targets_empty() {
4430 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4431 let result = rule.set_targets(vec![]);
4432 assert!(result.is_err());
4433 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
4435 }
4436
4437 #[test]
4438 fn test_rule_has_target() {
4439 let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
4440 assert!(rule.has_target("target1"));
4441 assert!(rule.has_target("target2"));
4442 assert!(!rule.has_target("target3"));
4443 assert!(!rule.has_target("nonexistent"));
4444 }
4445
4446 #[test]
4447 fn test_rule_rename_target() {
4448 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
4449 assert!(rule.rename_target("old_target", "new_target").unwrap());
4450 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
4451 assert!(!rule.rename_target("nonexistent", "something").unwrap());
4453 }
4454
4455 #[test]
4456 fn test_rule_rename_target_multiple() {
4457 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
4458 assert!(rule.rename_target("target2", "renamed_target").unwrap());
4459 assert_eq!(
4460 rule.targets().collect::<Vec<_>>(),
4461 vec!["target1", "renamed_target", "target3"]
4462 );
4463 }
4464
4465 #[test]
4466 fn test_rule_remove_target() {
4467 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
4468 assert!(rule.remove_target("target2").unwrap());
4469 assert_eq!(
4470 rule.targets().collect::<Vec<_>>(),
4471 vec!["target1", "target3"]
4472 );
4473 assert!(!rule.remove_target("nonexistent").unwrap());
4475 }
4476
4477 #[test]
4478 fn test_rule_remove_target_last() {
4479 let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
4480 let result = rule.remove_target("single_target");
4481 assert!(result.is_err());
4482 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
4484 }
4485
4486 #[test]
4487 fn test_rule_target_manipulation_preserves_prerequisites() {
4488 let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
4489
4490 rule.remove_target("target1").unwrap();
4492 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
4493 assert_eq!(
4494 rule.prerequisites().collect::<Vec<_>>(),
4495 vec!["dep1", "dep2"]
4496 );
4497 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4498
4499 rule.add_target("target3").unwrap();
4501 assert_eq!(
4502 rule.targets().collect::<Vec<_>>(),
4503 vec!["target2", "target3"]
4504 );
4505 assert_eq!(
4506 rule.prerequisites().collect::<Vec<_>>(),
4507 vec!["dep1", "dep2"]
4508 );
4509 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4510
4511 rule.rename_target("target2", "renamed").unwrap();
4513 assert_eq!(
4514 rule.targets().collect::<Vec<_>>(),
4515 vec!["renamed", "target3"]
4516 );
4517 assert_eq!(
4518 rule.prerequisites().collect::<Vec<_>>(),
4519 vec!["dep1", "dep2"]
4520 );
4521 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4522 }
4523
4524 #[test]
4525 fn test_rule_remove() {
4526 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4527 let rule = makefile.find_rule_by_target("rule1").unwrap();
4528 rule.remove().unwrap();
4529 assert_eq!(makefile.rules().count(), 1);
4530 assert!(makefile.find_rule_by_target("rule1").is_none());
4531 assert!(makefile.find_rule_by_target("rule2").is_some());
4532 }
4533
4534 #[test]
4535 fn test_rule_remove_last_trims_blank_lines() {
4536 let makefile: Makefile =
4538 "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
4539 .parse()
4540 .unwrap();
4541
4542 let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
4544 rule.remove().unwrap();
4545
4546 assert_eq!(makefile.code(), "%:\n\tdh $@\n");
4548 assert_eq!(makefile.rules().count(), 1);
4549 }
4550
4551 #[test]
4552 fn test_makefile_find_rule_by_target() {
4553 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4554 let rule = makefile.find_rule_by_target("rule2");
4555 assert!(rule.is_some());
4556 assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
4557 assert!(makefile.find_rule_by_target("nonexistent").is_none());
4558 }
4559
4560 #[test]
4561 fn test_makefile_find_rules_by_target() {
4562 let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
4563 .parse()
4564 .unwrap();
4565 assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
4566 assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
4567 assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
4568 }
4569
4570 #[test]
4571 fn test_makefile_find_rule_by_target_pattern_simple() {
4572 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4573 let rule = makefile.find_rule_by_target_pattern("foo.o");
4574 assert!(rule.is_some());
4575 assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
4576 }
4577
4578 #[test]
4579 fn test_makefile_find_rule_by_target_pattern_no_match() {
4580 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4581 let rule = makefile.find_rule_by_target_pattern("foo.c");
4582 assert!(rule.is_none());
4583 }
4584
4585 #[test]
4586 fn test_makefile_find_rule_by_target_pattern_exact() {
4587 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
4588 let rule = makefile.find_rule_by_target_pattern("foo.o");
4589 assert!(rule.is_some());
4590 assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
4591 }
4592
4593 #[test]
4594 fn test_makefile_find_rule_by_target_pattern_prefix() {
4595 let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
4596 let rule = makefile.find_rule_by_target_pattern("libfoo.a");
4597 assert!(rule.is_some());
4598 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
4599 }
4600
4601 #[test]
4602 fn test_makefile_find_rule_by_target_pattern_suffix() {
4603 let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4604 let rule = makefile.find_rule_by_target_pattern("foo_test.o");
4605 assert!(rule.is_some());
4606 assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
4607 }
4608
4609 #[test]
4610 fn test_makefile_find_rule_by_target_pattern_middle() {
4611 let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
4612 let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
4613 assert!(rule.is_some());
4614 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
4615 }
4616
4617 #[test]
4618 fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
4619 let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
4620 let rule = makefile.find_rule_by_target_pattern("anything");
4621 assert!(rule.is_some());
4622 assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
4623 }
4624
4625 #[test]
4626 fn test_makefile_find_rules_by_target_pattern_multiple() {
4627 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
4628 .parse()
4629 .unwrap();
4630 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4631 assert_eq!(rules.len(), 2);
4632 }
4633
4634 #[test]
4635 fn test_makefile_find_rules_by_target_pattern_mixed() {
4636 let makefile: Makefile =
4637 "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
4638 .parse()
4639 .unwrap();
4640 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4641 assert_eq!(rules.len(), 2); let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
4643 assert_eq!(rules.len(), 1); }
4645
4646 #[test]
4647 fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
4648 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
4649 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4650 assert_eq!(rules.len(), 1);
4651 let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
4652 assert_eq!(rules.len(), 0);
4653 }
4654
4655 #[test]
4656 fn test_matches_pattern_exact() {
4657 assert!(matches_pattern("foo.o", "foo.o"));
4658 assert!(!matches_pattern("foo.o", "bar.o"));
4659 }
4660
4661 #[test]
4662 fn test_matches_pattern_suffix() {
4663 assert!(matches_pattern("%.o", "foo.o"));
4664 assert!(matches_pattern("%.o", "bar.o"));
4665 assert!(matches_pattern("%.o", "baz/qux.o"));
4666 assert!(!matches_pattern("%.o", "foo.c"));
4667 }
4668
4669 #[test]
4670 fn test_matches_pattern_prefix() {
4671 assert!(matches_pattern("lib%.a", "libfoo.a"));
4672 assert!(matches_pattern("lib%.a", "libbar.a"));
4673 assert!(!matches_pattern("lib%.a", "foo.a"));
4674 assert!(!matches_pattern("lib%.a", "lib.a"));
4675 }
4676
4677 #[test]
4678 fn test_matches_pattern_middle() {
4679 assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
4680 assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
4681 assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
4682 assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
4683 }
4684
4685 #[test]
4686 fn test_matches_pattern_wildcard_only() {
4687 assert!(matches_pattern("%", "anything"));
4688 assert!(matches_pattern("%", "foo.o"));
4689 assert!(!matches_pattern("%", ""));
4691 }
4692
4693 #[test]
4694 fn test_matches_pattern_empty_stem() {
4695 assert!(!matches_pattern("%.o", ".o")); assert!(!matches_pattern("lib%", "lib")); assert!(!matches_pattern("lib%.a", "lib.a")); }
4700
4701 #[test]
4702 fn test_matches_pattern_multiple_wildcards_not_supported() {
4703 assert!(!matches_pattern("%foo%bar", "xfooybarz"));
4706 assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
4707 }
4708
4709 #[test]
4710 fn test_makefile_add_phony_target() {
4711 let mut makefile = Makefile::new();
4712 makefile.add_phony_target("clean").unwrap();
4713 assert!(makefile.is_phony("clean"));
4714 assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
4715 }
4716
4717 #[test]
4718 fn test_makefile_add_phony_target_existing() {
4719 let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
4720 makefile.add_phony_target("clean").unwrap();
4721 assert!(makefile.is_phony("test"));
4722 assert!(makefile.is_phony("clean"));
4723 let targets: Vec<_> = makefile.phony_targets().collect();
4724 assert!(targets.contains(&"test".to_string()));
4725 assert!(targets.contains(&"clean".to_string()));
4726 }
4727
4728 #[test]
4729 fn test_makefile_remove_phony_target() {
4730 let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
4731 assert!(makefile.remove_phony_target("clean").unwrap());
4732 assert!(!makefile.is_phony("clean"));
4733 assert!(makefile.is_phony("test"));
4734 assert!(!makefile.remove_phony_target("nonexistent").unwrap());
4735 }
4736
4737 #[test]
4738 fn test_makefile_remove_phony_target_last() {
4739 let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
4740 assert!(makefile.remove_phony_target("clean").unwrap());
4741 assert!(!makefile.is_phony("clean"));
4742 assert!(makefile.find_rule_by_target(".PHONY").is_none());
4744 }
4745
4746 #[test]
4747 fn test_makefile_is_phony() {
4748 let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
4749 assert!(makefile.is_phony("clean"));
4750 assert!(makefile.is_phony("test"));
4751 assert!(!makefile.is_phony("build"));
4752 }
4753
4754 #[test]
4755 fn test_makefile_phony_targets() {
4756 let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
4757 let phony_targets: Vec<_> = makefile.phony_targets().collect();
4758 assert_eq!(phony_targets, vec!["clean", "test", "build"]);
4759 }
4760
4761 #[test]
4762 fn test_makefile_phony_targets_empty() {
4763 let makefile = Makefile::new();
4764 assert_eq!(makefile.phony_targets().count(), 0);
4765 }
4766
4767 #[test]
4768 fn test_makefile_remove_first_phony_target_no_extra_space() {
4769 let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
4770 assert!(makefile.remove_phony_target("clean").unwrap());
4771 let result = makefile.to_string();
4772 assert_eq!(result, ".PHONY: test build\n");
4773 }
4774
4775 #[test]
4776 fn test_recipe_with_leading_comments_and_blank_lines() {
4777 let makefile_text = r#"#!/usr/bin/make
4781
4782%:
4783 dh $@
4784
4785override_dh_build:
4786 # The next line is empty
4787
4788 dh_python3
4789"#;
4790 let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
4791
4792 let rules: Vec<_> = makefile.rules().collect();
4793 assert_eq!(rules.len(), 2, "Expected 2 rules");
4794
4795 let rule0 = &rules[0];
4797 assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
4798 assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
4799
4800 let rule1 = &rules[1];
4802 assert_eq!(
4803 rule1.targets().collect::<Vec<_>>(),
4804 vec!["override_dh_build"]
4805 );
4806
4807 let recipes: Vec<_> = rule1.recipes().collect();
4809 assert!(
4810 !recipes.is_empty(),
4811 "Expected at least one recipe for override_dh_build, got none"
4812 );
4813 assert!(
4814 recipes.contains(&"dh_python3".to_string()),
4815 "Expected 'dh_python3' in recipes, got: {:?}",
4816 recipes
4817 );
4818 }
4819
4820 #[test]
4821 fn test_rule_parse_preserves_trailing_blank_lines() {
4822 let input = r#"override_dh_systemd_enable:
4825 dh_systemd_enable -pracoon
4826
4827override_dh_install:
4828 dh_install
4829"#;
4830
4831 let mut mf: Makefile = input.parse().unwrap();
4832
4833 let rule = mf.rules().next().unwrap();
4835 let rule_text = rule.to_string();
4836
4837 assert_eq!(
4839 rule_text,
4840 "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
4841 );
4842
4843 let modified =
4845 rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
4846
4847 let new_rule: Rule = modified.parse().unwrap();
4849 assert_eq!(
4850 new_rule.to_string(),
4851 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
4852 );
4853
4854 mf.replace_rule(0, new_rule).unwrap();
4856
4857 let output = mf.to_string();
4859 assert!(
4860 output.contains(
4861 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
4862 ),
4863 "Blank line between rules should be preserved. Got: {:?}",
4864 output
4865 );
4866 }
4867
4868 #[test]
4869 fn test_rule_parse_round_trip_with_trailing_newlines() {
4870 let test_cases = vec![
4872 "rule:\n\tcommand\n", "rule:\n\tcommand\n\n", "rule:\n\tcommand\n\n\n", ];
4876
4877 for rule_text in test_cases {
4878 let rule: Rule = rule_text.parse().unwrap();
4879 let result = rule.to_string();
4880 assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
4881 }
4882 }
4883
4884 #[test]
4885 fn test_rule_clone() {
4886 let rule_text = "rule:\n\tcommand\n\n";
4888 let rule: Rule = rule_text.parse().unwrap();
4889 let cloned = rule.clone();
4890
4891 assert_eq!(rule.to_string(), cloned.to_string());
4893 assert_eq!(rule.to_string(), rule_text);
4894 assert_eq!(cloned.to_string(), rule_text);
4895
4896 assert_eq!(
4898 rule.targets().collect::<Vec<_>>(),
4899 cloned.targets().collect::<Vec<_>>()
4900 );
4901 assert_eq!(
4902 rule.recipes().collect::<Vec<_>>(),
4903 cloned.recipes().collect::<Vec<_>>()
4904 );
4905 }
4906
4907 #[test]
4908 fn test_makefile_clone() {
4909 let input = "VAR = value\n\nrule:\n\tcommand\n";
4911 let makefile: Makefile = input.parse().unwrap();
4912 let cloned = makefile.clone();
4913
4914 assert_eq!(makefile.to_string(), cloned.to_string());
4916 assert_eq!(makefile.to_string(), input);
4917
4918 assert_eq!(makefile.rules().count(), cloned.rules().count());
4920
4921 assert_eq!(
4923 makefile.variable_definitions().count(),
4924 cloned.variable_definitions().count()
4925 );
4926 }
4927
4928 #[test]
4929 fn test_conditional_with_recipe_line() {
4930 let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
4932 let parsed = parse(input, None);
4933
4934 assert!(
4936 parsed.errors.is_empty(),
4937 "Expected no parse errors, but got: {:?}",
4938 parsed.errors
4939 );
4940
4941 let mf = parsed.root();
4943 assert_eq!(mf.code(), input);
4944 }
4945
4946 #[test]
4947 fn test_conditional_in_rule_recipe() {
4948 let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
4950 let parsed = parse(input, None);
4951
4952 assert!(
4954 parsed.errors.is_empty(),
4955 "Expected no parse errors, but got: {:?}",
4956 parsed.errors
4957 );
4958
4959 let mf = parsed.root();
4961 assert_eq!(mf.code(), input);
4962
4963 assert_eq!(mf.rules().count(), 1);
4965 }
4966
4967 #[test]
4968 fn test_rule_items() {
4969 use crate::RuleItem;
4970
4971 let input = r#"test:
4973 echo "before"
4974ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
4975 ./run-tests
4976endif
4977 echo "after"
4978"#;
4979 let rule: Rule = input.parse().unwrap();
4980
4981 let items: Vec<_> = rule.items().collect();
4982 assert_eq!(
4983 items.len(),
4984 3,
4985 "Expected 3 items: recipe, conditional, recipe"
4986 );
4987
4988 match &items[0] {
4990 RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
4991 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
4992 }
4993
4994 match &items[1] {
4996 RuleItem::Conditional(c) => {
4997 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
4998 }
4999 RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
5000 }
5001
5002 match &items[2] {
5004 RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5005 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5006 }
5007
5008 let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5010 let simple_items: Vec<_> = simple_rule.items().collect();
5011 assert_eq!(simple_items.len(), 2);
5012
5013 match &simple_items[0] {
5014 RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5015 _ => panic!("Expected recipe"),
5016 }
5017
5018 match &simple_items[1] {
5019 RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5020 _ => panic!("Expected recipe"),
5021 }
5022
5023 let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5025 .parse()
5026 .unwrap();
5027 let cond_items: Vec<_> = cond_only.items().collect();
5028 assert_eq!(cond_items.len(), 1);
5029
5030 match &cond_items[0] {
5031 RuleItem::Conditional(c) => {
5032 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5033 }
5034 _ => panic!("Expected conditional"),
5035 }
5036 }
5037
5038 #[test]
5039 fn test_conditionals_iterator() {
5040 let makefile: Makefile = r#"ifdef DEBUG
5041VAR = debug
5042endif
5043
5044ifndef RELEASE
5045OTHER = dev
5046endif
5047"#
5048 .parse()
5049 .unwrap();
5050
5051 let conditionals: Vec<_> = makefile.conditionals().collect();
5052 assert_eq!(conditionals.len(), 2);
5053
5054 assert_eq!(
5055 conditionals[0].conditional_type(),
5056 Some("ifdef".to_string())
5057 );
5058 assert_eq!(
5059 conditionals[1].conditional_type(),
5060 Some("ifndef".to_string())
5061 );
5062 }
5063
5064 #[test]
5065 fn test_conditional_type_and_condition() {
5066 let makefile: Makefile = r#"ifdef DEBUG
5067VAR = debug
5068endif
5069"#
5070 .parse()
5071 .unwrap();
5072
5073 let conditional = makefile.conditionals().next().unwrap();
5074 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5075 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5076 }
5077
5078 #[test]
5079 fn test_conditional_has_else() {
5080 let makefile_with_else: Makefile = r#"ifdef DEBUG
5081VAR = debug
5082else
5083VAR = release
5084endif
5085"#
5086 .parse()
5087 .unwrap();
5088
5089 let conditional = makefile_with_else.conditionals().next().unwrap();
5090 assert!(conditional.has_else());
5091
5092 let makefile_without_else: Makefile = r#"ifdef DEBUG
5093VAR = debug
5094endif
5095"#
5096 .parse()
5097 .unwrap();
5098
5099 let conditional = makefile_without_else.conditionals().next().unwrap();
5100 assert!(!conditional.has_else());
5101 }
5102
5103 #[test]
5104 fn test_conditional_if_body() {
5105 let makefile: Makefile = r#"ifdef DEBUG
5106VAR = debug
5107endif
5108"#
5109 .parse()
5110 .unwrap();
5111
5112 let conditional = makefile.conditionals().next().unwrap();
5113 let if_body = conditional.if_body();
5114 assert!(if_body.is_some());
5115 assert!(if_body.unwrap().contains("VAR = debug"));
5116 }
5117
5118 #[test]
5119 fn test_conditional_else_body() {
5120 let makefile: Makefile = r#"ifdef DEBUG
5121VAR = debug
5122else
5123VAR = release
5124endif
5125"#
5126 .parse()
5127 .unwrap();
5128
5129 let conditional = makefile.conditionals().next().unwrap();
5130 let else_body = conditional.else_body();
5131 assert!(else_body.is_some());
5132 assert!(else_body.unwrap().contains("VAR = release"));
5133 }
5134
5135 #[test]
5136 fn test_add_conditional_ifdef() {
5137 let mut makefile = Makefile::new();
5138 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5139 assert!(result.is_ok());
5140
5141 let code = makefile.to_string();
5142 assert!(code.contains("ifdef DEBUG"));
5143 assert!(code.contains("VAR = debug"));
5144 assert!(code.contains("endif"));
5145 }
5146
5147 #[test]
5148 fn test_add_conditional_with_else() {
5149 let mut makefile = Makefile::new();
5150 let result =
5151 makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5152 assert!(result.is_ok());
5153
5154 let code = makefile.to_string();
5155 assert!(code.contains("ifdef DEBUG"));
5156 assert!(code.contains("VAR = debug"));
5157 assert!(code.contains("else"));
5158 assert!(code.contains("VAR = release"));
5159 assert!(code.contains("endif"));
5160 }
5161
5162 #[test]
5163 fn test_add_conditional_invalid_type() {
5164 let mut makefile = Makefile::new();
5165 let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5166 assert!(result.is_err());
5167 }
5168
5169 #[test]
5170 fn test_add_conditional_formatting() {
5171 let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5172 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5173 assert!(result.is_ok());
5174
5175 let code = makefile.to_string();
5176 assert!(code.contains("\n\nifdef DEBUG"));
5178 }
5179
5180 #[test]
5181 fn test_conditional_remove() {
5182 let makefile: Makefile = r#"ifdef DEBUG
5183VAR = debug
5184endif
5185
5186VAR2 = value2
5187"#
5188 .parse()
5189 .unwrap();
5190
5191 let mut conditional = makefile.conditionals().next().unwrap();
5192 let result = conditional.remove();
5193 assert!(result.is_ok());
5194
5195 let code = makefile.to_string();
5196 assert!(!code.contains("ifdef DEBUG"));
5197 assert!(!code.contains("VAR = debug"));
5198 assert!(code.contains("VAR2 = value2"));
5199 }
5200
5201 #[test]
5202 fn test_add_conditional_ifndef() {
5203 let mut makefile = Makefile::new();
5204 let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5205 assert!(result.is_ok());
5206
5207 let code = makefile.to_string();
5208 assert!(code.contains("ifndef NDEBUG"));
5209 assert!(code.contains("VAR = enabled"));
5210 assert!(code.contains("endif"));
5211 }
5212
5213 #[test]
5214 fn test_add_conditional_ifeq() {
5215 let mut makefile = Makefile::new();
5216 let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5217 assert!(result.is_ok());
5218
5219 let code = makefile.to_string();
5220 assert!(code.contains("ifeq ($(OS),Linux)"));
5221 assert!(code.contains("VAR = linux"));
5222 assert!(code.contains("endif"));
5223 }
5224
5225 #[test]
5226 fn test_add_conditional_ifneq() {
5227 let mut makefile = Makefile::new();
5228 let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5229 assert!(result.is_ok());
5230
5231 let code = makefile.to_string();
5232 assert!(code.contains("ifneq ($(OS),Windows)"));
5233 assert!(code.contains("VAR = unix"));
5234 assert!(code.contains("endif"));
5235 }
5236
5237 #[test]
5238 fn test_conditional_api_integration() {
5239 let mut makefile: Makefile = r#"VAR1 = value1
5241
5242rule1:
5243 command1
5244"#
5245 .parse()
5246 .unwrap();
5247
5248 makefile
5250 .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5251 .unwrap();
5252
5253 assert_eq!(makefile.conditionals().count(), 1);
5255 let conditional = makefile.conditionals().next().unwrap();
5256 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5257 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5258 assert!(conditional.has_else());
5259
5260 assert_eq!(makefile.variable_definitions().count(), 1);
5262 assert_eq!(makefile.rules().count(), 1);
5263 }
5264
5265 #[test]
5266 fn test_conditional_if_items() {
5267 let makefile: Makefile = r#"ifdef DEBUG
5268VAR = debug
5269rule:
5270 command
5271endif
5272"#
5273 .parse()
5274 .unwrap();
5275
5276 let cond = makefile.conditionals().next().unwrap();
5277 let items: Vec<_> = cond.if_items().collect();
5278 assert_eq!(items.len(), 2); match &items[0] {
5281 MakefileItem::Variable(v) => {
5282 assert_eq!(v.name(), Some("VAR".to_string()));
5283 }
5284 _ => panic!("Expected variable"),
5285 }
5286
5287 match &items[1] {
5288 MakefileItem::Rule(r) => {
5289 assert!(r.targets().any(|t| t == "rule"));
5290 }
5291 _ => panic!("Expected rule"),
5292 }
5293 }
5294
5295 #[test]
5296 fn test_conditional_else_items() {
5297 let makefile: Makefile = r#"ifdef DEBUG
5298VAR = debug
5299else
5300VAR2 = release
5301rule2:
5302 command
5303endif
5304"#
5305 .parse()
5306 .unwrap();
5307
5308 let cond = makefile.conditionals().next().unwrap();
5309 let items: Vec<_> = cond.else_items().collect();
5310 assert_eq!(items.len(), 2); match &items[0] {
5313 MakefileItem::Variable(v) => {
5314 assert_eq!(v.name(), Some("VAR2".to_string()));
5315 }
5316 _ => panic!("Expected variable"),
5317 }
5318
5319 match &items[1] {
5320 MakefileItem::Rule(r) => {
5321 assert!(r.targets().any(|t| t == "rule2"));
5322 }
5323 _ => panic!("Expected rule"),
5324 }
5325 }
5326
5327 #[test]
5328 fn test_conditional_add_if_item() {
5329 let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5330 let mut cond = makefile.conditionals().next().unwrap();
5331
5332 let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5334 let var = temp.variable_definitions().next().unwrap();
5335 cond.add_if_item(MakefileItem::Variable(var));
5336
5337 let code = makefile.to_string();
5338 assert!(code.contains("CFLAGS = -g"));
5339
5340 let cond = makefile.conditionals().next().unwrap();
5342 assert_eq!(cond.if_items().count(), 1);
5343 }
5344
5345 #[test]
5346 fn test_conditional_add_else_item() {
5347 let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
5348 let mut cond = makefile.conditionals().next().unwrap();
5349
5350 let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5352 let var = temp.variable_definitions().next().unwrap();
5353 cond.add_else_item(MakefileItem::Variable(var));
5354
5355 let code = makefile.to_string();
5356 assert!(code.contains("else"));
5357 assert!(code.contains("CFLAGS = -O2"));
5358
5359 let cond = makefile.conditionals().next().unwrap();
5361 assert_eq!(cond.else_items().count(), 1);
5362 }
5363
5364 #[test]
5365 fn test_add_conditional_with_items() {
5366 let mut makefile = Makefile::new();
5367
5368 let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
5370 let var1 = temp1.variable_definitions().next().unwrap();
5371
5372 let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5373 let var2 = temp2.variable_definitions().next().unwrap();
5374
5375 let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
5376 let rule1 = temp3.rules().next().unwrap();
5377
5378 let result = makefile.add_conditional_with_items(
5379 "ifdef",
5380 "DEBUG",
5381 vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
5382 Some(vec![MakefileItem::Variable(var2)]),
5383 );
5384
5385 assert!(result.is_ok());
5386
5387 let code = makefile.to_string();
5388 assert!(code.contains("ifdef DEBUG"));
5389 assert!(code.contains("CFLAGS = -g"));
5390 assert!(code.contains("debug:"));
5391 assert!(code.contains("else"));
5392 assert!(code.contains("CFLAGS = -O2"));
5393 }
5394
5395 #[test]
5396 fn test_conditional_items_with_nested_conditional() {
5397 let makefile: Makefile = r#"ifdef DEBUG
5398VAR = debug
5399ifdef VERBOSE
5400 VAR2 = verbose
5401endif
5402endif
5403"#
5404 .parse()
5405 .unwrap();
5406
5407 let cond = makefile.conditionals().next().unwrap();
5408 let items: Vec<_> = cond.if_items().collect();
5409 assert_eq!(items.len(), 2); match &items[0] {
5412 MakefileItem::Variable(v) => {
5413 assert_eq!(v.name(), Some("VAR".to_string()));
5414 }
5415 _ => panic!("Expected variable"),
5416 }
5417
5418 match &items[1] {
5419 MakefileItem::Conditional(c) => {
5420 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
5421 }
5422 _ => panic!("Expected conditional"),
5423 }
5424 }
5425
5426 #[test]
5427 fn test_conditional_items_with_include() {
5428 let makefile: Makefile = r#"ifdef DEBUG
5429include debug.mk
5430VAR = debug
5431endif
5432"#
5433 .parse()
5434 .unwrap();
5435
5436 let cond = makefile.conditionals().next().unwrap();
5437 let items: Vec<_> = cond.if_items().collect();
5438 assert_eq!(items.len(), 2); match &items[0] {
5441 MakefileItem::Include(i) => {
5442 assert_eq!(i.path(), Some("debug.mk".to_string()));
5443 }
5444 _ => panic!("Expected include"),
5445 }
5446
5447 match &items[1] {
5448 MakefileItem::Variable(v) => {
5449 assert_eq!(v.name(), Some("VAR".to_string()));
5450 }
5451 _ => panic!("Expected variable"),
5452 }
5453 }
5454
5455 #[test]
5456 fn test_makefile_items_iterator() {
5457 let makefile: Makefile = r#"VAR = value
5458ifdef DEBUG
5459CFLAGS = -g
5460endif
5461rule:
5462 command
5463include common.mk
5464"#
5465 .parse()
5466 .unwrap();
5467
5468 assert_eq!(makefile.variable_definitions().count(), 2);
5471 assert_eq!(makefile.conditionals().count(), 1);
5472 assert_eq!(makefile.rules().count(), 1);
5473
5474 let items: Vec<_> = makefile.items().collect();
5475 assert!(
5477 items.len() >= 3,
5478 "Expected at least 3 items, got {}",
5479 items.len()
5480 );
5481
5482 match &items[0] {
5483 MakefileItem::Variable(v) => {
5484 assert_eq!(v.name(), Some("VAR".to_string()));
5485 }
5486 _ => panic!("Expected variable at position 0"),
5487 }
5488
5489 match &items[1] {
5490 MakefileItem::Conditional(c) => {
5491 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
5492 }
5493 _ => panic!("Expected conditional at position 1"),
5494 }
5495
5496 match &items[2] {
5497 MakefileItem::Rule(r) => {
5498 let targets: Vec<_> = r.targets().collect();
5499 assert_eq!(targets, vec!["rule"]);
5500 }
5501 _ => panic!("Expected rule at position 2"),
5502 }
5503 }
5504
5505 #[test]
5506 fn test_conditional_unwrap() {
5507 let makefile: Makefile = r#"ifdef DEBUG
5508VAR = debug
5509rule:
5510 command
5511endif
5512"#
5513 .parse()
5514 .unwrap();
5515
5516 let mut cond = makefile.conditionals().next().unwrap();
5517 cond.unwrap().unwrap();
5518
5519 let code = makefile.to_string();
5520 let expected = "VAR = debug\nrule:\n\tcommand\n";
5521 assert_eq!(code, expected);
5522
5523 assert_eq!(makefile.conditionals().count(), 0);
5525
5526 assert_eq!(makefile.variable_definitions().count(), 1);
5528 assert_eq!(makefile.rules().count(), 1);
5529 }
5530
5531 #[test]
5532 fn test_conditional_unwrap_with_else_fails() {
5533 let makefile: Makefile = r#"ifdef DEBUG
5534VAR = debug
5535else
5536VAR = release
5537endif
5538"#
5539 .parse()
5540 .unwrap();
5541
5542 let mut cond = makefile.conditionals().next().unwrap();
5543 let result = cond.unwrap();
5544
5545 assert!(result.is_err());
5546 assert!(result
5547 .unwrap_err()
5548 .to_string()
5549 .contains("Cannot unwrap conditional with else clause"));
5550 }
5551
5552 #[test]
5553 fn test_conditional_unwrap_nested() {
5554 let makefile: Makefile = r#"ifdef OUTER
5555VAR = outer
5556ifdef INNER
5557VAR2 = inner
5558endif
5559endif
5560"#
5561 .parse()
5562 .unwrap();
5563
5564 let mut outer_cond = makefile.conditionals().next().unwrap();
5566 outer_cond.unwrap().unwrap();
5567
5568 let code = makefile.to_string();
5569 let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
5570 assert_eq!(code, expected);
5571 }
5572
5573 #[test]
5574 fn test_conditional_unwrap_empty() {
5575 let makefile: Makefile = r#"ifdef DEBUG
5576endif
5577"#
5578 .parse()
5579 .unwrap();
5580
5581 let mut cond = makefile.conditionals().next().unwrap();
5582 cond.unwrap().unwrap();
5583
5584 let code = makefile.to_string();
5585 assert_eq!(code, "");
5586 }
5587
5588 #[test]
5589 fn test_rule_parent() {
5590 let makefile: Makefile = r#"all:
5591 echo "test"
5592"#
5593 .parse()
5594 .unwrap();
5595
5596 let rule = makefile.rules().next().unwrap();
5597 let parent = rule.parent();
5598 assert!(parent.is_none());
5600 }
5601
5602 #[test]
5603 fn test_item_parent_in_conditional() {
5604 let makefile: Makefile = r#"ifdef DEBUG
5605VAR = debug
5606rule:
5607 command
5608endif
5609"#
5610 .parse()
5611 .unwrap();
5612
5613 let cond = makefile.conditionals().next().unwrap();
5614
5615 let items: Vec<_> = cond.if_items().collect();
5617 assert_eq!(items.len(), 2);
5618
5619 if let MakefileItem::Variable(var) = &items[0] {
5621 let parent = var.parent();
5622 assert!(parent.is_some());
5623 if let Some(MakefileItem::Conditional(_)) = parent {
5624 } else {
5626 panic!("Expected variable parent to be a Conditional");
5627 }
5628 } else {
5629 panic!("Expected first item to be a Variable");
5630 }
5631
5632 if let MakefileItem::Rule(rule) = &items[1] {
5634 let parent = rule.parent();
5635 assert!(parent.is_some());
5636 if let Some(MakefileItem::Conditional(_)) = parent {
5637 } else {
5639 panic!("Expected rule parent to be a Conditional");
5640 }
5641 } else {
5642 panic!("Expected second item to be a Rule");
5643 }
5644 }
5645
5646 #[test]
5647 fn test_nested_conditional_parent() {
5648 let makefile: Makefile = r#"ifdef OUTER
5649VAR = outer
5650ifdef INNER
5651VAR2 = inner
5652endif
5653endif
5654"#
5655 .parse()
5656 .unwrap();
5657
5658 let outer_cond = makefile.conditionals().next().unwrap();
5659
5660 let items: Vec<_> = outer_cond.if_items().collect();
5662
5663 let inner_cond = items
5665 .iter()
5666 .find_map(|item| {
5667 if let MakefileItem::Conditional(c) = item {
5668 Some(c)
5669 } else {
5670 None
5671 }
5672 })
5673 .unwrap();
5674
5675 let parent = inner_cond.parent();
5677 assert!(parent.is_some());
5678 if let Some(MakefileItem::Conditional(_)) = parent {
5679 } else {
5681 panic!("Expected inner conditional's parent to be a Conditional");
5682 }
5683 }
5684
5685 #[test]
5686 fn test_line_col() {
5687 let text = r#"# Comment at line 0
5688VAR1 = value1
5689VAR2 = value2
5690
5691rule1: dep1 dep2
5692 command1
5693 command2
5694
5695rule2:
5696 command3
5697
5698ifdef DEBUG
5699CFLAGS = -g
5700endif
5701"#;
5702 let makefile: Makefile = text.parse().unwrap();
5703
5704 let vars: Vec<_> = makefile.variable_definitions().collect();
5707 assert_eq!(vars.len(), 3);
5708
5709 assert_eq!(vars[0].line(), 1);
5711 assert_eq!(vars[0].column(), 0);
5712 assert_eq!(vars[0].line_col(), (1, 0));
5713
5714 assert_eq!(vars[1].line(), 2);
5716 assert_eq!(vars[1].column(), 0);
5717
5718 assert_eq!(vars[2].line(), 12);
5720 assert_eq!(vars[2].column(), 0);
5721
5722 let rules: Vec<_> = makefile.rules().collect();
5724 assert_eq!(rules.len(), 2);
5725
5726 assert_eq!(rules[0].line(), 4);
5728 assert_eq!(rules[0].column(), 0);
5729 assert_eq!(rules[0].line_col(), (4, 0));
5730
5731 assert_eq!(rules[1].line(), 8);
5733 assert_eq!(rules[1].column(), 0);
5734
5735 let conditionals: Vec<_> = makefile.conditionals().collect();
5737 assert_eq!(conditionals.len(), 1);
5738
5739 assert_eq!(conditionals[0].line(), 11);
5741 assert_eq!(conditionals[0].column(), 0);
5742 assert_eq!(conditionals[0].line_col(), (11, 0));
5743 }
5744
5745 #[test]
5746 fn test_line_col_multiline() {
5747 let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
5748 let makefile: Makefile = text.parse().unwrap();
5749
5750 let vars: Vec<_> = makefile.variable_definitions().collect();
5752 assert_eq!(vars.len(), 1);
5753 assert_eq!(vars[0].line(), 0);
5754 assert_eq!(vars[0].column(), 0);
5755
5756 let rules: Vec<_> = makefile.rules().collect();
5758 assert_eq!(rules.len(), 1);
5759 assert_eq!(rules[0].line(), 4);
5760 assert_eq!(rules[0].column(), 0);
5761 }
5762
5763 #[test]
5764 fn test_line_col_includes() {
5765 let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
5766 let makefile: Makefile = text.parse().unwrap();
5767
5768 let vars: Vec<_> = makefile.variable_definitions().collect();
5770 assert_eq!(vars[0].line(), 0);
5771
5772 let includes: Vec<_> = makefile.includes().collect();
5774 assert_eq!(includes.len(), 2);
5775 assert_eq!(includes[0].line(), 2);
5776 assert_eq!(includes[0].column(), 0);
5777 assert_eq!(includes[1].line(), 3);
5778 assert_eq!(includes[1].column(), 0);
5779 }
5780
5781 #[test]
5782 fn test_conditional_in_rule_vs_toplevel() {
5783 let text1 = r#"rule:
5785 command
5786ifeq (,$(X))
5787 test
5788endif
5789"#;
5790 let makefile: Makefile = text1.parse().unwrap();
5791 let rules: Vec<_> = makefile.rules().collect();
5792 let conditionals: Vec<_> = makefile.conditionals().collect();
5793
5794 assert_eq!(rules.len(), 1);
5795 assert_eq!(
5796 conditionals.len(),
5797 0,
5798 "Conditional should be part of rule, not top-level"
5799 );
5800
5801 let text2 = r#"rule:
5803 command
5804
5805ifeq (,$(X))
5806 test
5807endif
5808"#;
5809 let makefile: Makefile = text2.parse().unwrap();
5810 let rules: Vec<_> = makefile.rules().collect();
5811 let conditionals: Vec<_> = makefile.conditionals().collect();
5812
5813 assert_eq!(rules.len(), 1);
5814 assert_eq!(
5815 conditionals.len(),
5816 1,
5817 "Conditional after blank line should be top-level"
5818 );
5819 assert_eq!(conditionals[0].line(), 3);
5820 }
5821
5822 #[test]
5823 fn test_nested_conditionals_line_tracking() {
5824 let text = r#"ifdef OUTER
5825VAR1 = value1
5826ifdef INNER
5827VAR2 = value2
5828endif
5829VAR3 = value3
5830endif
5831"#;
5832 let makefile: Makefile = text.parse().unwrap();
5833
5834 let conditionals: Vec<_> = makefile.conditionals().collect();
5835 assert_eq!(
5836 conditionals.len(),
5837 1,
5838 "Only outer conditional should be top-level"
5839 );
5840 assert_eq!(conditionals[0].line(), 0);
5841 assert_eq!(conditionals[0].column(), 0);
5842 }
5843
5844 #[test]
5845 fn test_conditional_else_line_tracking() {
5846 let text = r#"VAR1 = before
5847
5848ifdef DEBUG
5849DEBUG_FLAGS = -g
5850else
5851DEBUG_FLAGS = -O2
5852endif
5853
5854VAR2 = after
5855"#;
5856 let makefile: Makefile = text.parse().unwrap();
5857
5858 let conditionals: Vec<_> = makefile.conditionals().collect();
5859 assert_eq!(conditionals.len(), 1);
5860 assert_eq!(conditionals[0].line(), 2);
5861 assert_eq!(conditionals[0].column(), 0);
5862 }
5863
5864 #[test]
5865 fn test_broken_conditional_endif_without_if() {
5866 let text = "VAR = value\nendif\n";
5868 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5869
5870 let vars: Vec<_> = makefile.variable_definitions().collect();
5872 assert_eq!(vars.len(), 1);
5873 assert_eq!(vars[0].line(), 0);
5874 }
5875
5876 #[test]
5877 fn test_broken_conditional_else_without_if() {
5878 let text = "VAR = value\nelse\nVAR2 = other\n";
5880 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5881
5882 let vars: Vec<_> = makefile.variable_definitions().collect();
5884 assert!(!vars.is_empty(), "Should parse at least the first variable");
5885 assert_eq!(vars[0].line(), 0);
5886 }
5887
5888 #[test]
5889 fn test_broken_conditional_missing_endif() {
5890 let text = r#"ifdef DEBUG
5892DEBUG_FLAGS = -g
5893VAR = value
5894"#;
5895 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5896
5897 assert!(makefile.code().contains("ifdef DEBUG"));
5899 }
5900
5901 #[test]
5902 fn test_multiple_conditionals_line_tracking() {
5903 let text = r#"ifdef A
5904VAR_A = a
5905endif
5906
5907ifdef B
5908VAR_B = b
5909endif
5910
5911ifdef C
5912VAR_C = c
5913endif
5914"#;
5915 let makefile: Makefile = text.parse().unwrap();
5916
5917 let conditionals: Vec<_> = makefile.conditionals().collect();
5918 assert_eq!(conditionals.len(), 3);
5919 assert_eq!(conditionals[0].line(), 0);
5920 assert_eq!(conditionals[1].line(), 4);
5921 assert_eq!(conditionals[2].line(), 8);
5922 }
5923
5924 #[test]
5925 fn test_conditional_with_multiple_else_ifeq() {
5926 let text = r#"ifeq ($(OS),Windows)
5927EXT = .exe
5928else ifeq ($(OS),Linux)
5929EXT = .bin
5930else
5931EXT = .out
5932endif
5933"#;
5934 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5935
5936 let conditionals: Vec<_> = makefile.conditionals().collect();
5937 assert_eq!(conditionals.len(), 1);
5938 assert_eq!(conditionals[0].line(), 0);
5939 assert_eq!(conditionals[0].column(), 0);
5940 }
5941
5942 #[test]
5943 fn test_conditional_types_line_tracking() {
5944 let text = r#"ifdef VAR1
5945A = 1
5946endif
5947
5948ifndef VAR2
5949B = 2
5950endif
5951
5952ifeq ($(X),y)
5953C = 3
5954endif
5955
5956ifneq ($(Y),n)
5957D = 4
5958endif
5959"#;
5960 let makefile: Makefile = text.parse().unwrap();
5961
5962 let conditionals: Vec<_> = makefile.conditionals().collect();
5963 assert_eq!(conditionals.len(), 4);
5964
5965 assert_eq!(conditionals[0].line(), 0); assert_eq!(
5967 conditionals[0].conditional_type(),
5968 Some("ifdef".to_string())
5969 );
5970
5971 assert_eq!(conditionals[1].line(), 4); assert_eq!(
5973 conditionals[1].conditional_type(),
5974 Some("ifndef".to_string())
5975 );
5976
5977 assert_eq!(conditionals[2].line(), 8); assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
5979
5980 assert_eq!(conditionals[3].line(), 12); assert_eq!(
5982 conditionals[3].conditional_type(),
5983 Some("ifneq".to_string())
5984 );
5985 }
5986
5987 #[test]
5988 fn test_conditional_in_rule_with_recipes() {
5989 let text = r#"test:
5990 echo "start"
5991ifdef VERBOSE
5992 echo "verbose mode"
5993endif
5994 echo "end"
5995"#;
5996 let makefile: Makefile = text.parse().unwrap();
5997
5998 let rules: Vec<_> = makefile.rules().collect();
5999 let conditionals: Vec<_> = makefile.conditionals().collect();
6000
6001 assert_eq!(rules.len(), 1);
6002 assert_eq!(rules[0].line(), 0);
6003 assert_eq!(conditionals.len(), 0);
6005 }
6006
6007 #[test]
6008 fn test_broken_conditional_double_else() {
6009 let text = r#"ifdef DEBUG
6011A = 1
6012else
6013B = 2
6014else
6015C = 3
6016endif
6017"#;
6018 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6019
6020 assert!(makefile.code().contains("ifdef DEBUG"));
6022 }
6023
6024 #[test]
6025 fn test_broken_conditional_mismatched_nesting() {
6026 let text = r#"ifdef A
6028VAR = value
6029endif
6030endif
6031"#;
6032 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6033
6034 let conditionals: Vec<_> = makefile.conditionals().collect();
6037 assert!(
6038 !conditionals.is_empty(),
6039 "Should parse at least the first conditional"
6040 );
6041 }
6042
6043 #[test]
6044 fn test_conditional_with_comment_line_tracking() {
6045 let text = r#"# This is a comment
6046ifdef DEBUG
6047# Another comment
6048CFLAGS = -g
6049endif
6050# Final comment
6051"#;
6052 let makefile: Makefile = text.parse().unwrap();
6053
6054 let conditionals: Vec<_> = makefile.conditionals().collect();
6055 assert_eq!(conditionals.len(), 1);
6056 assert_eq!(conditionals[0].line(), 1);
6057 assert_eq!(conditionals[0].column(), 0);
6058 }
6059
6060 #[test]
6061 fn test_conditional_after_variable_with_blank_lines() {
6062 let text = r#"VAR1 = value1
6063
6064
6065ifdef DEBUG
6066VAR2 = value2
6067endif
6068"#;
6069 let makefile: Makefile = text.parse().unwrap();
6070
6071 let vars: Vec<_> = makefile.variable_definitions().collect();
6072 let conditionals: Vec<_> = makefile.conditionals().collect();
6073
6074 assert_eq!(vars.len(), 2);
6076 assert_eq!(vars[0].line(), 0); assert_eq!(vars[1].line(), 4); assert_eq!(conditionals.len(), 1);
6080 assert_eq!(conditionals[0].line(), 3);
6081 }
6082
6083 #[test]
6084 fn test_empty_conditional_line_tracking() {
6085 let text = r#"ifdef DEBUG
6086endif
6087
6088ifndef RELEASE
6089endif
6090"#;
6091 let makefile: Makefile = text.parse().unwrap();
6092
6093 let conditionals: Vec<_> = makefile.conditionals().collect();
6094 assert_eq!(conditionals.len(), 2);
6095 assert_eq!(conditionals[0].line(), 0);
6096 assert_eq!(conditionals[1].line(), 3);
6097 }
6098
6099 #[test]
6100 fn test_recipe_line_tracking() {
6101 let text = r#"build:
6102 echo "Building..."
6103 gcc -o app main.c
6104 echo "Done"
6105
6106test:
6107 ./run-tests
6108"#;
6109 let makefile: Makefile = text.parse().unwrap();
6110
6111 let rule1 = makefile.rules().next().expect("Should have first rule");
6113 let recipes: Vec<_> = rule1.recipe_nodes().collect();
6114 assert_eq!(recipes.len(), 3);
6115
6116 assert_eq!(recipes[0].text(), "echo \"Building...\"");
6117 assert_eq!(recipes[0].line(), 1);
6118 assert_eq!(recipes[0].column(), 0);
6119
6120 assert_eq!(recipes[1].text(), "gcc -o app main.c");
6121 assert_eq!(recipes[1].line(), 2);
6122 assert_eq!(recipes[1].column(), 0);
6123
6124 assert_eq!(recipes[2].text(), "echo \"Done\"");
6125 assert_eq!(recipes[2].line(), 3);
6126 assert_eq!(recipes[2].column(), 0);
6127
6128 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6130 let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6131 assert_eq!(recipes2.len(), 1);
6132
6133 assert_eq!(recipes2[0].text(), "./run-tests");
6134 assert_eq!(recipes2[0].line(), 6);
6135 assert_eq!(recipes2[0].column(), 0);
6136 }
6137
6138 #[test]
6139 fn test_recipe_with_variables_line_tracking() {
6140 let text = r#"install:
6141 mkdir -p $(DESTDIR)
6142 cp $(BINARY) $(DESTDIR)/
6143"#;
6144 let makefile: Makefile = text.parse().unwrap();
6145 let rule = makefile.rules().next().expect("Should have rule");
6146 let recipes: Vec<_> = rule.recipe_nodes().collect();
6147
6148 assert_eq!(recipes.len(), 2);
6149 assert_eq!(recipes[0].line(), 1);
6150 assert_eq!(recipes[1].line(), 2);
6151 }
6152
6153 #[test]
6154 fn test_recipe_text_no_leading_tab() {
6155 let text = "test:\n\techo hello\n\t\techo nested\n\t echo with spaces\n";
6157 let makefile: Makefile = text.parse().unwrap();
6158 let rule = makefile.rules().next().expect("Should have rule");
6159 let recipes: Vec<_> = rule.recipe_nodes().collect();
6160
6161 assert_eq!(recipes.len(), 3);
6162
6163 eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6165
6166 assert_eq!(recipes[0].text(), "echo hello");
6168
6169 eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6171 assert_eq!(recipes[1].text(), "\techo nested");
6172
6173 eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6175 assert_eq!(recipes[2].text(), " echo with spaces");
6176 }
6177
6178 #[test]
6179 fn test_recipe_parent() {
6180 let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6181 let rule = makefile.rules().next().unwrap();
6182 let recipe = rule.recipe_nodes().next().unwrap();
6183
6184 let parent = recipe.parent().expect("Recipe should have parent");
6185 assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6186 assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6187 }
6188
6189 #[test]
6190 fn test_recipe_is_silent_various_prefixes() {
6191 let makefile: Makefile = r#"test:
6192 @echo silent
6193 -echo ignore
6194 +echo always
6195 @-echo silent_ignore
6196 -@echo ignore_silent
6197 +@echo always_silent
6198 echo normal
6199"#
6200 .parse()
6201 .unwrap();
6202
6203 let rule = makefile.rules().next().unwrap();
6204 let recipes: Vec<_> = rule.recipe_nodes().collect();
6205
6206 assert_eq!(recipes.len(), 7);
6207 assert!(recipes[0].is_silent(), "@echo should be silent");
6208 assert!(!recipes[1].is_silent(), "-echo should not be silent");
6209 assert!(!recipes[2].is_silent(), "+echo should not be silent");
6210 assert!(recipes[3].is_silent(), "@-echo should be silent");
6211 assert!(recipes[4].is_silent(), "-@echo should be silent");
6212 assert!(recipes[5].is_silent(), "+@echo should be silent");
6213 assert!(!recipes[6].is_silent(), "echo should not be silent");
6214 }
6215
6216 #[test]
6217 fn test_recipe_is_ignore_errors_various_prefixes() {
6218 let makefile: Makefile = r#"test:
6219 @echo silent
6220 -echo ignore
6221 +echo always
6222 @-echo silent_ignore
6223 -@echo ignore_silent
6224 +-echo always_ignore
6225 echo normal
6226"#
6227 .parse()
6228 .unwrap();
6229
6230 let rule = makefile.rules().next().unwrap();
6231 let recipes: Vec<_> = rule.recipe_nodes().collect();
6232
6233 assert_eq!(recipes.len(), 7);
6234 assert!(
6235 !recipes[0].is_ignore_errors(),
6236 "@echo should not ignore errors"
6237 );
6238 assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6239 assert!(
6240 !recipes[2].is_ignore_errors(),
6241 "+echo should not ignore errors"
6242 );
6243 assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6244 assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6245 assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6246 assert!(
6247 !recipes[6].is_ignore_errors(),
6248 "echo should not ignore errors"
6249 );
6250 }
6251
6252 #[test]
6253 fn test_recipe_set_prefix_add() {
6254 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6255 let rule = makefile.rules().next().unwrap();
6256 let mut recipe = rule.recipe_nodes().next().unwrap();
6257
6258 recipe.set_prefix("@");
6259 assert_eq!(recipe.text(), "@echo hello");
6260 assert!(recipe.is_silent());
6261 }
6262
6263 #[test]
6264 fn test_recipe_set_prefix_change() {
6265 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6266 let rule = makefile.rules().next().unwrap();
6267 let mut recipe = rule.recipe_nodes().next().unwrap();
6268
6269 recipe.set_prefix("-");
6270 assert_eq!(recipe.text(), "-echo hello");
6271 assert!(!recipe.is_silent());
6272 assert!(recipe.is_ignore_errors());
6273 }
6274
6275 #[test]
6276 fn test_recipe_set_prefix_remove() {
6277 let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
6278 let rule = makefile.rules().next().unwrap();
6279 let mut recipe = rule.recipe_nodes().next().unwrap();
6280
6281 recipe.set_prefix("");
6282 assert_eq!(recipe.text(), "echo hello");
6283 assert!(!recipe.is_silent());
6284 assert!(!recipe.is_ignore_errors());
6285 }
6286
6287 #[test]
6288 fn test_recipe_set_prefix_combinations() {
6289 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6290 let rule = makefile.rules().next().unwrap();
6291 let mut recipe = rule.recipe_nodes().next().unwrap();
6292
6293 recipe.set_prefix("@-");
6294 assert_eq!(recipe.text(), "@-echo hello");
6295 assert!(recipe.is_silent());
6296 assert!(recipe.is_ignore_errors());
6297
6298 recipe.set_prefix("-@");
6299 assert_eq!(recipe.text(), "-@echo hello");
6300 assert!(recipe.is_silent());
6301 assert!(recipe.is_ignore_errors());
6302 }
6303
6304 #[test]
6305 fn test_recipe_replace_text_basic() {
6306 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6307 let rule = makefile.rules().next().unwrap();
6308 let mut recipe = rule.recipe_nodes().next().unwrap();
6309
6310 recipe.replace_text("echo world");
6311 assert_eq!(recipe.text(), "echo world");
6312
6313 let rule = makefile.rules().next().unwrap();
6315 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6316 }
6317
6318 #[test]
6319 fn test_recipe_replace_text_with_prefix() {
6320 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6321 let rule = makefile.rules().next().unwrap();
6322 let mut recipe = rule.recipe_nodes().next().unwrap();
6323
6324 recipe.replace_text("@echo goodbye");
6325 assert_eq!(recipe.text(), "@echo goodbye");
6326 assert!(recipe.is_silent());
6327 }
6328
6329 #[test]
6330 fn test_recipe_insert_before_single() {
6331 let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6332 let rule = makefile.rules().next().unwrap();
6333 let recipe = rule.recipe_nodes().next().unwrap();
6334
6335 recipe.insert_before("echo hello");
6336
6337 let rule = makefile.rules().next().unwrap();
6338 let recipes: Vec<_> = rule.recipes().collect();
6339 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6340 }
6341
6342 #[test]
6343 fn test_recipe_insert_before_multiple() {
6344 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6345 .parse()
6346 .unwrap();
6347 let rule = makefile.rules().next().unwrap();
6348 let recipes: Vec<_> = rule.recipe_nodes().collect();
6349
6350 recipes[1].insert_before("echo middle");
6352
6353 let rule = makefile.rules().next().unwrap();
6354 let new_recipes: Vec<_> = rule.recipes().collect();
6355 assert_eq!(
6356 new_recipes,
6357 vec!["echo one", "echo middle", "echo two", "echo three"]
6358 );
6359 }
6360
6361 #[test]
6362 fn test_recipe_insert_before_first() {
6363 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6364 let rule = makefile.rules().next().unwrap();
6365 let recipes: Vec<_> = rule.recipe_nodes().collect();
6366
6367 recipes[0].insert_before("echo zero");
6368
6369 let rule = makefile.rules().next().unwrap();
6370 let new_recipes: Vec<_> = rule.recipes().collect();
6371 assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
6372 }
6373
6374 #[test]
6375 fn test_recipe_insert_after_single() {
6376 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6377 let rule = makefile.rules().next().unwrap();
6378 let recipe = rule.recipe_nodes().next().unwrap();
6379
6380 recipe.insert_after("echo world");
6381
6382 let rule = makefile.rules().next().unwrap();
6383 let recipes: Vec<_> = rule.recipes().collect();
6384 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6385 }
6386
6387 #[test]
6388 fn test_recipe_insert_after_multiple() {
6389 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6390 .parse()
6391 .unwrap();
6392 let rule = makefile.rules().next().unwrap();
6393 let recipes: Vec<_> = rule.recipe_nodes().collect();
6394
6395 recipes[1].insert_after("echo middle");
6397
6398 let rule = makefile.rules().next().unwrap();
6399 let new_recipes: Vec<_> = rule.recipes().collect();
6400 assert_eq!(
6401 new_recipes,
6402 vec!["echo one", "echo two", "echo middle", "echo three"]
6403 );
6404 }
6405
6406 #[test]
6407 fn test_recipe_insert_after_last() {
6408 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6409 let rule = makefile.rules().next().unwrap();
6410 let recipes: Vec<_> = rule.recipe_nodes().collect();
6411
6412 recipes[1].insert_after("echo three");
6413
6414 let rule = makefile.rules().next().unwrap();
6415 let new_recipes: Vec<_> = rule.recipes().collect();
6416 assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
6417 }
6418
6419 #[test]
6420 fn test_recipe_remove_single() {
6421 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6422 let rule = makefile.rules().next().unwrap();
6423 let recipe = rule.recipe_nodes().next().unwrap();
6424
6425 recipe.remove();
6426
6427 let rule = makefile.rules().next().unwrap();
6428 assert_eq!(rule.recipes().count(), 0);
6429 }
6430
6431 #[test]
6432 fn test_recipe_remove_first() {
6433 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6434 .parse()
6435 .unwrap();
6436 let rule = makefile.rules().next().unwrap();
6437 let recipes: Vec<_> = rule.recipe_nodes().collect();
6438
6439 recipes[0].remove();
6440
6441 let rule = makefile.rules().next().unwrap();
6442 let new_recipes: Vec<_> = rule.recipes().collect();
6443 assert_eq!(new_recipes, vec!["echo two", "echo three"]);
6444 }
6445
6446 #[test]
6447 fn test_recipe_remove_middle() {
6448 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6449 .parse()
6450 .unwrap();
6451 let rule = makefile.rules().next().unwrap();
6452 let recipes: Vec<_> = rule.recipe_nodes().collect();
6453
6454 recipes[1].remove();
6455
6456 let rule = makefile.rules().next().unwrap();
6457 let new_recipes: Vec<_> = rule.recipes().collect();
6458 assert_eq!(new_recipes, vec!["echo one", "echo three"]);
6459 }
6460
6461 #[test]
6462 fn test_recipe_remove_last() {
6463 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6464 .parse()
6465 .unwrap();
6466 let rule = makefile.rules().next().unwrap();
6467 let recipes: Vec<_> = rule.recipe_nodes().collect();
6468
6469 recipes[2].remove();
6470
6471 let rule = makefile.rules().next().unwrap();
6472 let new_recipes: Vec<_> = rule.recipes().collect();
6473 assert_eq!(new_recipes, vec!["echo one", "echo two"]);
6474 }
6475
6476 #[test]
6477 fn test_recipe_multiple_operations() {
6478 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6479 let rule = makefile.rules().next().unwrap();
6480 let mut recipe = rule.recipe_nodes().next().unwrap();
6481
6482 recipe.replace_text("echo modified");
6484 assert_eq!(recipe.text(), "echo modified");
6485
6486 recipe.set_prefix("@");
6488 assert_eq!(recipe.text(), "@echo modified");
6489
6490 recipe.insert_after("echo three");
6492
6493 let rule = makefile.rules().next().unwrap();
6495 let recipes: Vec<_> = rule.recipes().collect();
6496 assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
6497 }
6498}