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 *depth += 1;
813 self.parse_conditional();
814 true
815 }
816 "else" => {
817 if *depth == 0 {
819 self.error("else without matching if".to_string());
820 self.bump();
822 false
823 } else {
824 self.builder.start_node(CONDITIONAL_ELSE.into());
826
827 self.bump();
829 self.skip_ws();
830
831 if self.current() == Some(IDENTIFIER) {
833 let next_token = &self.tokens.last().unwrap().1;
834 if next_token == "ifdef"
835 || next_token == "ifndef"
836 || next_token == "ifeq"
837 || next_token == "ifneq"
838 {
839 match next_token.as_str() {
842 "ifdef" | "ifndef" => {
843 self.bump(); self.skip_ws();
845 self.parse_simple_condition();
846 }
847 "ifeq" | "ifneq" => {
848 self.bump(); self.skip_ws();
850 self.parse_parenthesized_expr();
851 }
852 _ => unreachable!(),
853 }
854 } else {
856 }
859 } else {
860 }
862
863 self.builder.finish_node(); true
865 }
866 }
867 "endif" => {
868 if *depth == 0 {
870 self.error("endif without matching if".to_string());
871 self.bump();
873 false
874 } else {
875 *depth -= 1;
876
877 self.builder.start_node(CONDITIONAL_ENDIF.into());
879
880 self.bump();
882
883 self.skip_ws();
885
886 if self.current() == Some(COMMENT) {
891 self.parse_comment();
892 } else if self.current() == Some(NEWLINE) {
893 self.bump();
894 } else if self.current() == Some(WHITESPACE) {
895 self.skip_ws();
897 if self.current() == Some(NEWLINE) {
898 self.bump();
899 }
900 } else if !self.is_at_eof() {
902 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
905 self.bump();
906 }
907 if self.current() == Some(NEWLINE) {
908 self.bump();
909 }
910 }
911 self.builder.finish_node(); true
915 }
916 }
917 _ => false,
918 }
919 }
920
921 fn parse_conditional(&mut self) {
922 self.builder.start_node(CONDITIONAL.into());
923
924 self.builder.start_node(CONDITIONAL_IF.into());
926
927 let Some(token) = self.parse_conditional_keyword() else {
929 self.skip_until_newline();
930 self.builder.finish_node(); self.builder.finish_node(); return;
933 };
934
935 self.skip_ws();
937
938 match token.as_str() {
940 "ifdef" | "ifndef" => {
941 self.parse_simple_condition();
942 }
943 "ifeq" | "ifneq" => {
944 self.parse_parenthesized_expr();
945 }
946 _ => unreachable!("Invalid conditional token"),
947 }
948
949 self.skip_ws();
951 if self.current() == Some(COMMENT) {
952 self.parse_comment();
953 }
954 self.builder.finish_node(); let mut depth = 1;
960
961 let mut position_count = std::collections::HashMap::<usize, usize>::new();
963 let max_repetitions = 15; while depth > 0 && !self.is_at_eof() {
966 let current_pos = self.tokens.len();
968 *position_count.entry(current_pos).or_insert(0) += 1;
969
970 if position_count.get(¤t_pos).unwrap() > &max_repetitions {
973 break;
976 }
977
978 match self.current() {
979 None => {
980 self.error("unterminated conditional (missing endif)".to_string());
981 break;
982 }
983 Some(IDENTIFIER) => {
984 let token = self.tokens.last().unwrap().1.clone();
985 if !self.handle_conditional_token(&token, &mut depth) {
986 if token == "include" || token == "-include" || token == "sinclude" {
987 self.parse_include();
988 } else {
989 self.parse_normal_content();
990 }
991 }
992 }
993 Some(INDENT) => self.parse_recipe_line(),
994 Some(WHITESPACE) => self.bump(),
995 Some(COMMENT) => self.parse_comment(),
996 Some(NEWLINE) => self.bump(),
997 Some(DOLLAR) => self.parse_normal_content(),
998 Some(QUOTE) => self.parse_quoted_string(),
999 Some(_) => {
1000 self.bump();
1002 }
1003 }
1004 }
1005
1006 self.builder.finish_node();
1007 }
1008
1009 fn parse_normal_content(&mut self) {
1011 self.skip_ws();
1013
1014 if self.is_assignment_line() {
1016 self.parse_assignment();
1017 } else {
1018 self.parse_rule();
1020 }
1021 }
1022
1023 fn parse_include(&mut self) {
1024 self.builder.start_node(INCLUDE.into());
1025
1026 if self.current() != Some(IDENTIFIER)
1028 || (!["include", "-include", "sinclude"]
1029 .contains(&self.tokens.last().unwrap().1.as_str()))
1030 {
1031 self.error("expected include directive".to_string());
1032 self.builder.finish_node();
1033 return;
1034 }
1035 self.bump();
1036 self.skip_ws();
1037
1038 self.builder.start_node(EXPR.into());
1040 let mut found_path = false;
1041
1042 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1043 match self.current() {
1044 Some(WHITESPACE) => self.skip_ws(),
1045 Some(DOLLAR) => {
1046 found_path = true;
1047 self.parse_variable_reference();
1048 }
1049 Some(_) => {
1050 found_path = true;
1052 self.bump();
1053 }
1054 None => break,
1055 }
1056 }
1057
1058 if !found_path {
1059 self.error("expected file path after include".to_string());
1060 }
1061
1062 self.builder.finish_node();
1063
1064 if self.current() == Some(NEWLINE) {
1066 self.bump();
1067 } else if !self.is_at_eof() {
1068 self.error("expected newline after include".to_string());
1069 self.skip_until_newline();
1070 }
1071
1072 self.builder.finish_node();
1073 }
1074
1075 fn parse_identifier_token(&mut self) -> bool {
1076 let token = &self.tokens.last().unwrap().1;
1077
1078 if token.starts_with("%") {
1080 self.parse_rule();
1081 return true;
1082 }
1083
1084 if token.starts_with("if")
1085 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1086 {
1087 self.parse_conditional();
1088 return true;
1089 }
1090
1091 if token == "include" || token == "-include" || token == "sinclude" {
1092 self.parse_include();
1093 return true;
1094 }
1095
1096 self.parse_normal_content();
1098 true
1099 }
1100
1101 fn parse_token(&mut self) -> bool {
1102 match self.current() {
1103 None => false,
1104 Some(IDENTIFIER) => {
1105 let token = &self.tokens.last().unwrap().1;
1106 if self.is_conditional_directive(token)
1107 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1108 {
1109 self.parse_conditional();
1110 true
1111 } else {
1112 self.parse_identifier_token()
1113 }
1114 }
1115 Some(DOLLAR) => {
1116 self.parse_normal_content();
1117 true
1118 }
1119 Some(NEWLINE) => {
1120 self.builder.start_node(BLANK_LINE.into());
1121 self.bump();
1122 self.builder.finish_node();
1123 true
1124 }
1125 Some(COMMENT) => {
1126 self.parse_comment();
1127 true
1128 }
1129 Some(WHITESPACE) => {
1130 if self.is_end_of_file_or_newline_after_whitespace() {
1132 self.skip_ws();
1135 return true;
1136 }
1137
1138 let look_ahead_pos = self.tokens.len().saturating_sub(1);
1141 let mut is_documentation_or_help = false;
1142
1143 if look_ahead_pos > 0 {
1144 let next_token = &self.tokens[look_ahead_pos - 1];
1145 if next_token.0 == IDENTIFIER
1148 || next_token.0 == COMMENT
1149 || next_token.0 == TEXT
1150 {
1151 is_documentation_or_help = true;
1152 }
1153 }
1154
1155 if is_documentation_or_help {
1156 self.skip_ws();
1159 while self.current().is_some() && self.current() != Some(NEWLINE) {
1160 self.bump();
1161 }
1162 if self.current() == Some(NEWLINE) {
1163 self.bump();
1164 }
1165 } else {
1166 self.skip_ws();
1167 }
1168 true
1169 }
1170 Some(INDENT) => {
1171 self.bump();
1173
1174 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1176 self.bump();
1177 }
1178 if self.current() == Some(NEWLINE) {
1179 self.bump();
1180 }
1181 true
1182 }
1183 Some(kind) => {
1184 self.error(format!("unexpected token {:?}", kind));
1185 self.bump();
1186 true
1187 }
1188 }
1189 }
1190
1191 fn parse(mut self) -> Parse {
1192 self.builder.start_node(ROOT.into());
1193
1194 while self.parse_token() {}
1195
1196 self.builder.finish_node();
1197
1198 Parse {
1199 green_node: self.builder.finish(),
1200 errors: self.errors,
1201 }
1202 }
1203
1204 fn is_assignment_line(&mut self) -> bool {
1206 let assignment_ops = ["=", ":=", "::=", ":::=", "+=", "?=", "!="];
1207 let mut pos = self.tokens.len().saturating_sub(1);
1208 let mut seen_identifier = false;
1209 let mut seen_export = false;
1210
1211 while pos > 0 {
1212 let (kind, text) = &self.tokens[pos];
1213
1214 match kind {
1215 NEWLINE => break,
1216 IDENTIFIER if text == "export" => seen_export = true,
1217 IDENTIFIER if !seen_identifier => seen_identifier = true,
1218 OPERATOR if assignment_ops.contains(&text.as_str()) => {
1219 return seen_identifier || seen_export
1220 }
1221 OPERATOR if text == ":" => return false, WHITESPACE => (),
1223 _ if seen_export => return true, _ => return false,
1225 }
1226 pos = pos.saturating_sub(1);
1227 }
1228 false
1229 }
1230
1231 fn bump(&mut self) {
1233 let (kind, text) = self.tokens.pop().unwrap();
1234 self.builder.token(kind.into(), text.as_str());
1235 }
1236 fn current(&self) -> Option<SyntaxKind> {
1238 self.tokens.last().map(|(kind, _)| *kind)
1239 }
1240
1241 fn expect_eol(&mut self) {
1242 self.skip_ws();
1244
1245 match self.current() {
1246 Some(NEWLINE) => {
1247 self.bump();
1248 }
1249 None => {
1250 }
1252 n => {
1253 self.error(format!("expected newline, got {:?}", n));
1254 self.skip_until_newline();
1256 }
1257 }
1258 }
1259
1260 fn is_at_eof(&self) -> bool {
1262 self.current().is_none()
1263 }
1264
1265 fn is_at_eof_or_only_whitespace(&self) -> bool {
1267 if self.is_at_eof() {
1268 return true;
1269 }
1270
1271 self.tokens
1273 .iter()
1274 .rev()
1275 .all(|(kind, _)| matches!(*kind, WHITESPACE | NEWLINE))
1276 }
1277
1278 fn skip_ws(&mut self) {
1279 while self.current() == Some(WHITESPACE) {
1280 self.bump()
1281 }
1282 }
1283
1284 fn skip_until_newline(&mut self) {
1285 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1286 self.bump();
1287 }
1288 if self.current() == Some(NEWLINE) {
1289 self.bump();
1290 }
1291 }
1292
1293 fn consume_balanced_parens(&mut self, start_paren_count: usize) -> usize {
1295 let mut paren_count = start_paren_count;
1296
1297 while paren_count > 0 && self.current().is_some() {
1298 match self.current() {
1299 Some(LPAREN) => {
1300 paren_count += 1;
1301 self.bump();
1302 }
1303 Some(RPAREN) => {
1304 paren_count -= 1;
1305 self.bump();
1306 if paren_count == 0 {
1307 break;
1308 }
1309 }
1310 Some(DOLLAR) => {
1311 self.parse_variable_reference();
1313 }
1314 Some(_) => self.bump(),
1315 None => {
1316 self.error("unclosed parenthesis".to_string());
1317 break;
1318 }
1319 }
1320 }
1321
1322 paren_count
1323 }
1324
1325 fn is_end_of_file_or_newline_after_whitespace(&self) -> bool {
1327 if self.is_at_eof_or_only_whitespace() {
1329 return true;
1330 }
1331
1332 if self.tokens.len() <= 1 {
1334 return true;
1335 }
1336
1337 false
1338 }
1339 }
1340
1341 let mut tokens = lex(text);
1342 tokens.reverse();
1343 Parser {
1344 tokens,
1345 builder: GreenNodeBuilder::new(),
1346 errors: Vec::new(),
1347 original_text: text.to_string(),
1348 variant,
1349 }
1350 .parse()
1351}
1352
1353pub(crate) type SyntaxNode = rowan::SyntaxNode<Lang>;
1359#[allow(unused)]
1360type SyntaxToken = rowan::SyntaxToken<Lang>;
1361#[allow(unused)]
1362pub(crate) type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
1363
1364impl Parse {
1365 fn syntax(&self) -> SyntaxNode {
1366 SyntaxNode::new_root_mut(self.green_node.clone())
1367 }
1368
1369 pub(crate) fn root(&self) -> Makefile {
1370 Makefile::cast(self.syntax()).unwrap()
1371 }
1372}
1373
1374fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
1377 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
1378 let mut line = 0;
1379 let mut last_newline_offset = rowan::TextSize::from(0);
1380
1381 for element in root.preorder_with_tokens() {
1382 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
1383 if token.text_range().start() >= offset {
1384 break;
1385 }
1386
1387 for (idx, _) in token.text().match_indices('\n') {
1389 line += 1;
1390 last_newline_offset =
1391 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
1392 }
1393 }
1394 }
1395
1396 let column: usize = (offset - last_newline_offset).into();
1397 (line, column)
1398}
1399
1400macro_rules! ast_node {
1401 ($ast:ident, $kind:ident) => {
1402 #[derive(Clone, PartialEq, Eq, Hash)]
1403 #[repr(transparent)]
1404 pub struct $ast(SyntaxNode);
1406
1407 impl AstNode for $ast {
1408 type Language = Lang;
1409
1410 fn can_cast(kind: SyntaxKind) -> bool {
1411 kind == $kind
1412 }
1413
1414 fn cast(syntax: SyntaxNode) -> Option<Self> {
1415 if Self::can_cast(syntax.kind()) {
1416 Some(Self(syntax))
1417 } else {
1418 None
1419 }
1420 }
1421
1422 fn syntax(&self) -> &SyntaxNode {
1423 &self.0
1424 }
1425 }
1426
1427 impl $ast {
1428 pub fn line(&self) -> usize {
1430 line_col_at_offset(&self.0, self.0.text_range().start()).0
1431 }
1432
1433 pub fn column(&self) -> usize {
1435 line_col_at_offset(&self.0, self.0.text_range().start()).1
1436 }
1437
1438 pub fn line_col(&self) -> (usize, usize) {
1441 line_col_at_offset(&self.0, self.0.text_range().start())
1442 }
1443 }
1444
1445 impl core::fmt::Display for $ast {
1446 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1447 write!(f, "{}", self.0.text())
1448 }
1449 }
1450 };
1451}
1452
1453ast_node!(Makefile, ROOT);
1454ast_node!(Rule, RULE);
1455ast_node!(Recipe, RECIPE);
1456ast_node!(Identifier, IDENTIFIER);
1457ast_node!(VariableDefinition, VARIABLE);
1458ast_node!(Include, INCLUDE);
1459ast_node!(ArchiveMembers, ARCHIVE_MEMBERS);
1460ast_node!(ArchiveMember, ARCHIVE_MEMBER);
1461ast_node!(Conditional, CONDITIONAL);
1462
1463impl Recipe {
1464 pub fn text(&self) -> String {
1466 self.syntax()
1467 .children_with_tokens()
1468 .filter_map(|it| {
1469 if let Some(token) = it.as_token() {
1470 if token.kind() == TEXT {
1471 return Some(token.text().to_string());
1472 }
1473 }
1474 None
1475 })
1476 .collect::<Vec<_>>()
1477 .join("")
1478 }
1479
1480 pub fn parent(&self) -> Option<Rule> {
1493 self.syntax().parent().and_then(Rule::cast)
1494 }
1495
1496 pub fn is_silent(&self) -> bool {
1509 let text = self.text();
1510 text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1511 }
1512
1513 pub fn is_ignore_errors(&self) -> bool {
1526 let text = self.text();
1527 text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
1528 }
1529
1530 pub fn set_prefix(&mut self, prefix: &str) {
1547 let text = self.text();
1548
1549 let stripped = text.trim_start_matches(['@', '-', '+']);
1551
1552 let new_text = format!("{}{}", prefix, stripped);
1554
1555 self.replace_text(&new_text);
1556 }
1557
1558 pub fn replace_text(&mut self, new_text: &str) {
1571 let node = self.syntax();
1572 let parent = node.parent().expect("Recipe node must have a parent");
1573 let node_index = node.index();
1574
1575 let mut builder = GreenNodeBuilder::new();
1577 builder.start_node(RECIPE.into());
1578
1579 if let Some(indent_token) = node
1581 .children_with_tokens()
1582 .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
1583 {
1584 builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
1585 } else {
1586 builder.token(INDENT.into(), "\t");
1587 }
1588
1589 builder.token(TEXT.into(), new_text);
1590
1591 if let Some(newline_token) = node
1593 .children_with_tokens()
1594 .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
1595 {
1596 builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
1597 } else {
1598 builder.token(NEWLINE.into(), "\n");
1599 }
1600
1601 builder.finish_node();
1602 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1603
1604 parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
1606
1607 *self = parent
1611 .children_with_tokens()
1612 .nth(node_index)
1613 .and_then(|element| element.into_node())
1614 .and_then(Recipe::cast)
1615 .expect("New recipe node should exist at the same index");
1616 }
1617
1618 pub fn insert_before(&self, text: &str) {
1631 let node = self.syntax();
1632 let parent = node.parent().expect("Recipe node must have a parent");
1633 let node_index = node.index();
1634
1635 let mut builder = GreenNodeBuilder::new();
1637 builder.start_node(RECIPE.into());
1638 builder.token(INDENT.into(), "\t");
1639 builder.token(TEXT.into(), text);
1640 builder.token(NEWLINE.into(), "\n");
1641 builder.finish_node();
1642 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1643
1644 parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
1646 }
1647
1648 pub fn insert_after(&self, text: &str) {
1661 let node = self.syntax();
1662 let parent = node.parent().expect("Recipe node must have a parent");
1663 let node_index = node.index();
1664
1665 let mut builder = GreenNodeBuilder::new();
1667 builder.start_node(RECIPE.into());
1668 builder.token(INDENT.into(), "\t");
1669 builder.token(TEXT.into(), text);
1670 builder.token(NEWLINE.into(), "\n");
1671 builder.finish_node();
1672 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1673
1674 parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
1676 }
1677
1678 pub fn remove(&self) {
1691 let node = self.syntax();
1692 let parent = node.parent().expect("Recipe node must have a parent");
1693 let node_index = node.index();
1694
1695 parent.splice_children(node_index..node_index + 1, vec![]);
1697 }
1698}
1699
1700pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
1704 let mut newlines_to_remove = vec![];
1706 let mut current = node.last_child_or_token();
1707
1708 while let Some(element) = current {
1709 match &element {
1710 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
1711 newlines_to_remove.push(token.clone());
1712 current = token.prev_sibling_or_token();
1713 }
1714 rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
1715 let mut recipe_current = n.last_child_or_token();
1717 while let Some(recipe_element) = recipe_current {
1718 match &recipe_element {
1719 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
1720 newlines_to_remove.push(token.clone());
1721 recipe_current = token.prev_sibling_or_token();
1722 }
1723 _ => break,
1724 }
1725 }
1726 break; }
1728 _ => break,
1729 }
1730 }
1731
1732 if newlines_to_remove.len() > 1 {
1735 newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
1737
1738 for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
1739 let parent = token.parent().unwrap();
1740 let idx = token.index();
1741 parent.splice_children(idx..idx + 1, vec![]);
1742 }
1743 }
1744}
1745
1746pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
1754 let mut collected_elements = vec![];
1755 let mut found_comment = false;
1756
1757 let mut current = node.prev_sibling_or_token();
1759 while let Some(element) = current {
1760 match &element {
1761 rowan::NodeOrToken::Token(token) => match token.kind() {
1762 COMMENT => {
1763 if token.text().starts_with("#!") {
1764 break; }
1766 found_comment = true;
1767 collected_elements.push(element.clone());
1768 }
1769 NEWLINE | WHITESPACE => {
1770 collected_elements.push(element.clone());
1771 }
1772 _ => break, },
1774 rowan::NodeOrToken::Node(n) => {
1775 if n.kind() == BLANK_LINE {
1777 collected_elements.push(element.clone());
1778 } else {
1779 break; }
1781 }
1782 }
1783 current = element.prev_sibling_or_token();
1784 }
1785
1786 let mut elements_to_remove = vec![];
1789 let mut consecutive_newlines = 0;
1790 for element in collected_elements.iter().rev() {
1791 let should_remove = match element {
1792 rowan::NodeOrToken::Token(token) => match token.kind() {
1793 COMMENT => {
1794 consecutive_newlines = 0;
1795 found_comment
1796 }
1797 NEWLINE => {
1798 consecutive_newlines += 1;
1799 found_comment && consecutive_newlines <= 1
1800 }
1801 WHITESPACE => found_comment,
1802 _ => false,
1803 },
1804 rowan::NodeOrToken::Node(n) => {
1805 if n.kind() == BLANK_LINE {
1807 consecutive_newlines += 1;
1808 found_comment && consecutive_newlines <= 1
1809 } else {
1810 false
1811 }
1812 }
1813 };
1814
1815 if should_remove {
1816 elements_to_remove.push(element.clone());
1817 }
1818 }
1819
1820 let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
1823 all_to_remove.extend(elements_to_remove.into_iter().rev());
1824
1825 all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
1827
1828 for element in all_to_remove {
1829 let idx = element.index();
1830 parent.splice_children(idx..idx + 1, vec![]);
1831 }
1832}
1833
1834impl FromStr for Rule {
1835 type Err = crate::Error;
1836
1837 fn from_str(s: &str) -> Result<Self, Self::Err> {
1838 Rule::parse(s).to_rule_result()
1839 }
1840}
1841
1842impl FromStr for Makefile {
1843 type Err = crate::Error;
1844
1845 fn from_str(s: &str) -> Result<Self, Self::Err> {
1846 Makefile::parse(s).to_result()
1847 }
1848}
1849
1850#[cfg(test)]
1851mod tests {
1852 use super::*;
1853 use crate::ast::makefile::MakefileItem;
1854 use crate::pattern::matches_pattern;
1855
1856 #[test]
1857 fn test_conditionals() {
1858 let code = "ifdef DEBUG\n DEBUG_FLAG := 1\nendif\n";
1862 let mut buf = code.as_bytes();
1863 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
1864 assert!(makefile.code().contains("DEBUG_FLAG"));
1865
1866 let code =
1868 "ifeq ($(OS),Windows_NT)\n RESULT := windows\nelse\n RESULT := unix\nendif\n";
1869 let mut buf = code.as_bytes();
1870 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
1871 assert!(makefile.code().contains("RESULT"));
1872 assert!(makefile.code().contains("windows"));
1873
1874 let code = "ifdef DEBUG\n CFLAGS += -g\n ifdef VERBOSE\n CFLAGS += -v\n endif\nelse\n CFLAGS += -O2\nendif\n";
1876 let mut buf = code.as_bytes();
1877 let makefile = Makefile::read_relaxed(&mut buf)
1878 .expect("Failed to parse nested conditionals with else");
1879 assert!(makefile.code().contains("CFLAGS"));
1880 assert!(makefile.code().contains("VERBOSE"));
1881
1882 let code = "ifdef DEBUG\nendif\n";
1884 let mut buf = code.as_bytes();
1885 let makefile =
1886 Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
1887 assert!(makefile.code().contains("ifdef DEBUG"));
1888
1889 let code = "ifeq ($(OS),Windows)\n EXT := .exe\nelse ifeq ($(OS),Linux)\n EXT := .bin\nelse\n EXT := .out\nendif\n";
1891 let mut buf = code.as_bytes();
1892 let makefile =
1893 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
1894 assert!(makefile.code().contains("EXT"));
1895
1896 let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
1898 let mut buf = code.as_bytes();
1899 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
1900 assert!(makefile.code().contains("DEBUG"));
1901
1902 let code = "ifdef \nDEBUG := 1\nendif\n";
1904 let mut buf = code.as_bytes();
1905 let makefile = Makefile::read_relaxed(&mut buf)
1906 .expect("Failed to parse with recovery - missing condition");
1907 assert!(makefile.code().contains("DEBUG"));
1908 }
1909
1910 #[test]
1911 fn test_parse_simple() {
1912 const SIMPLE: &str = r#"VARIABLE = value
1913
1914rule: dependency
1915 command
1916"#;
1917 let parsed = parse(SIMPLE, None);
1918 assert!(parsed.errors.is_empty());
1919 let node = parsed.syntax();
1920 assert_eq!(
1921 format!("{:#?}", node),
1922 r#"ROOT@0..44
1923 VARIABLE@0..17
1924 IDENTIFIER@0..8 "VARIABLE"
1925 WHITESPACE@8..9 " "
1926 OPERATOR@9..10 "="
1927 WHITESPACE@10..11 " "
1928 EXPR@11..16
1929 IDENTIFIER@11..16 "value"
1930 NEWLINE@16..17 "\n"
1931 BLANK_LINE@17..18
1932 NEWLINE@17..18 "\n"
1933 RULE@18..44
1934 TARGETS@18..22
1935 IDENTIFIER@18..22 "rule"
1936 OPERATOR@22..23 ":"
1937 WHITESPACE@23..24 " "
1938 PREREQUISITES@24..34
1939 PREREQUISITE@24..34
1940 IDENTIFIER@24..34 "dependency"
1941 NEWLINE@34..35 "\n"
1942 RECIPE@35..44
1943 INDENT@35..36 "\t"
1944 TEXT@36..43 "command"
1945 NEWLINE@43..44 "\n"
1946"#
1947 );
1948
1949 let root = parsed.root();
1950
1951 let mut rules = root.rules().collect::<Vec<_>>();
1952 assert_eq!(rules.len(), 1);
1953 let rule = rules.pop().unwrap();
1954 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
1955 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
1956 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
1957
1958 let mut variables = root.variable_definitions().collect::<Vec<_>>();
1959 assert_eq!(variables.len(), 1);
1960 let variable = variables.pop().unwrap();
1961 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
1962 assert_eq!(variable.raw_value(), Some("value".to_string()));
1963 }
1964
1965 #[test]
1966 fn test_parse_export_assign() {
1967 const EXPORT: &str = r#"export VARIABLE := value
1968"#;
1969 let parsed = parse(EXPORT, None);
1970 assert!(parsed.errors.is_empty());
1971 let node = parsed.syntax();
1972 assert_eq!(
1973 format!("{:#?}", node),
1974 r#"ROOT@0..25
1975 VARIABLE@0..25
1976 IDENTIFIER@0..6 "export"
1977 WHITESPACE@6..7 " "
1978 IDENTIFIER@7..15 "VARIABLE"
1979 WHITESPACE@15..16 " "
1980 OPERATOR@16..18 ":="
1981 WHITESPACE@18..19 " "
1982 EXPR@19..24
1983 IDENTIFIER@19..24 "value"
1984 NEWLINE@24..25 "\n"
1985"#
1986 );
1987
1988 let root = parsed.root();
1989
1990 let mut variables = root.variable_definitions().collect::<Vec<_>>();
1991 assert_eq!(variables.len(), 1);
1992 let variable = variables.pop().unwrap();
1993 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
1994 assert_eq!(variable.raw_value(), Some("value".to_string()));
1995 }
1996
1997 #[test]
1998 fn test_parse_multiple_prerequisites() {
1999 const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2000 command
2001
2002"#;
2003 let parsed = parse(MULTIPLE_PREREQUISITES, None);
2004 assert!(parsed.errors.is_empty());
2005 let node = parsed.syntax();
2006 assert_eq!(
2007 format!("{:#?}", node),
2008 r#"ROOT@0..40
2009 RULE@0..40
2010 TARGETS@0..4
2011 IDENTIFIER@0..4 "rule"
2012 OPERATOR@4..5 ":"
2013 WHITESPACE@5..6 " "
2014 PREREQUISITES@6..29
2015 PREREQUISITE@6..17
2016 IDENTIFIER@6..17 "dependency1"
2017 WHITESPACE@17..18 " "
2018 PREREQUISITE@18..29
2019 IDENTIFIER@18..29 "dependency2"
2020 NEWLINE@29..30 "\n"
2021 RECIPE@30..39
2022 INDENT@30..31 "\t"
2023 TEXT@31..38 "command"
2024 NEWLINE@38..39 "\n"
2025 NEWLINE@39..40 "\n"
2026"#
2027 );
2028 let root = parsed.root();
2029
2030 let rule = root.rules().next().unwrap();
2031 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2032 assert_eq!(
2033 rule.prerequisites().collect::<Vec<_>>(),
2034 vec!["dependency1", "dependency2"]
2035 );
2036 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2037 }
2038
2039 #[test]
2040 fn test_add_rule() {
2041 let mut makefile = Makefile::new();
2042 let rule = makefile.add_rule("rule");
2043 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2044 assert_eq!(
2045 rule.prerequisites().collect::<Vec<_>>(),
2046 Vec::<String>::new()
2047 );
2048
2049 assert_eq!(makefile.to_string(), "rule:\n");
2050 }
2051
2052 #[test]
2053 fn test_add_rule_with_shebang() {
2054 let content = r#"#!/usr/bin/make -f
2056
2057build: blah
2058 $(MAKE) install
2059
2060clean:
2061 dh_clean
2062"#;
2063
2064 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2065 let initial_count = makefile.rules().count();
2066 assert_eq!(initial_count, 2);
2067
2068 let rule = makefile.add_rule("build-indep");
2070 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2071
2072 assert_eq!(makefile.rules().count(), initial_count + 1);
2074 }
2075
2076 #[test]
2077 fn test_add_rule_formatting() {
2078 let content = r#"build: blah
2080 $(MAKE) install
2081
2082clean:
2083 dh_clean
2084"#;
2085
2086 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2087 let mut rule = makefile.add_rule("build-indep");
2088 rule.add_prerequisite("build").unwrap();
2089
2090 let expected = r#"build: blah
2091 $(MAKE) install
2092
2093clean:
2094 dh_clean
2095
2096build-indep: build
2097"#;
2098
2099 assert_eq!(makefile.to_string(), expected);
2100 }
2101
2102 #[test]
2103 fn test_push_command() {
2104 let mut makefile = Makefile::new();
2105 let mut rule = makefile.add_rule("rule");
2106
2107 rule.push_command("command");
2109 rule.push_command("command2");
2110
2111 assert_eq!(
2113 rule.recipes().collect::<Vec<_>>(),
2114 vec!["command", "command2"]
2115 );
2116
2117 rule.push_command("command3");
2119 assert_eq!(
2120 rule.recipes().collect::<Vec<_>>(),
2121 vec!["command", "command2", "command3"]
2122 );
2123
2124 assert_eq!(
2126 makefile.to_string(),
2127 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2128 );
2129
2130 assert_eq!(
2132 rule.to_string(),
2133 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2134 );
2135 }
2136
2137 #[test]
2138 fn test_replace_command() {
2139 let mut makefile = Makefile::new();
2140 let mut rule = makefile.add_rule("rule");
2141
2142 rule.push_command("command");
2144 rule.push_command("command2");
2145
2146 assert_eq!(
2148 rule.recipes().collect::<Vec<_>>(),
2149 vec!["command", "command2"]
2150 );
2151
2152 rule.replace_command(0, "new command");
2154 assert_eq!(
2155 rule.recipes().collect::<Vec<_>>(),
2156 vec!["new command", "command2"]
2157 );
2158
2159 assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2161
2162 assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2164 }
2165
2166 #[test]
2167 fn test_replace_command_with_comments() {
2168 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";
2171
2172 let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2173
2174 let mut rule = makefile.rules().next().unwrap();
2175
2176 assert_eq!(rule.recipes().count(), 1);
2178 assert_eq!(
2179 rule.recipes().collect::<Vec<_>>(),
2180 vec!["dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"]
2181 );
2182
2183 assert!(rule.replace_command(0, "dh_strip"));
2185
2186 assert_eq!(rule.recipes().count(), 1);
2188 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["dh_strip"]);
2189 }
2190
2191 #[test]
2192 fn test_parse_rule_without_newline() {
2193 let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2194 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2195 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2196 let rule = "rule: dependency".parse::<Rule>().unwrap();
2197 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2198 assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2199 }
2200
2201 #[test]
2202 fn test_parse_makefile_without_newline() {
2203 let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2204 assert_eq!(makefile.rules().count(), 1);
2205 }
2206
2207 #[test]
2208 fn test_from_reader() {
2209 let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2210 assert_eq!(makefile.rules().count(), 1);
2211 }
2212
2213 #[test]
2214 fn test_parse_with_tab_after_last_newline() {
2215 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2216 assert_eq!(makefile.rules().count(), 1);
2217 }
2218
2219 #[test]
2220 fn test_parse_with_space_after_last_newline() {
2221 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2222 assert_eq!(makefile.rules().count(), 1);
2223 }
2224
2225 #[test]
2226 fn test_parse_with_comment_after_last_newline() {
2227 let makefile =
2228 Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2229 assert_eq!(makefile.rules().count(), 1);
2230 }
2231
2232 #[test]
2233 fn test_parse_with_variable_rule() {
2234 let makefile =
2235 Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2236 .unwrap();
2237
2238 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2240 assert_eq!(vars.len(), 1);
2241 assert_eq!(vars[0].name(), Some("RULE".to_string()));
2242 assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2243
2244 let rules = makefile.rules().collect::<Vec<_>>();
2246 assert_eq!(rules.len(), 1);
2247 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2248 assert_eq!(
2249 rules[0].prerequisites().collect::<Vec<_>>(),
2250 vec!["dependency"]
2251 );
2252 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2253 }
2254
2255 #[test]
2256 fn test_parse_with_variable_dependency() {
2257 let makefile =
2258 Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2259
2260 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2262 assert_eq!(vars.len(), 1);
2263 assert_eq!(vars[0].name(), Some("DEP".to_string()));
2264 assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2265
2266 let rules = makefile.rules().collect::<Vec<_>>();
2268 assert_eq!(rules.len(), 1);
2269 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2270 assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2271 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2272 }
2273
2274 #[test]
2275 fn test_parse_with_variable_command() {
2276 let makefile =
2277 Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2278
2279 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2281 assert_eq!(vars.len(), 1);
2282 assert_eq!(vars[0].name(), Some("COM".to_string()));
2283 assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2284
2285 let rules = makefile.rules().collect::<Vec<_>>();
2287 assert_eq!(rules.len(), 1);
2288 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2289 assert_eq!(
2290 rules[0].prerequisites().collect::<Vec<_>>(),
2291 vec!["dependency"]
2292 );
2293 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2294 }
2295
2296 #[test]
2297 fn test_regular_line_error_reporting() {
2298 let input = "rule target\n\tcommand";
2299
2300 let parsed = parse(input, None);
2302 let direct_error = &parsed.errors[0];
2303
2304 assert_eq!(direct_error.line, 2);
2306 assert!(
2307 direct_error.message.contains("expected"),
2308 "Error message should contain 'expected': {}",
2309 direct_error.message
2310 );
2311 assert_eq!(direct_error.context, "\tcommand");
2312
2313 let reader_result = Makefile::from_reader(input.as_bytes());
2315 let parse_error = match reader_result {
2316 Ok(_) => panic!("Expected Parse error from from_reader"),
2317 Err(err) => match err {
2318 self::Error::Parse(parse_err) => parse_err,
2319 _ => panic!("Expected Parse error"),
2320 },
2321 };
2322
2323 let error_text = parse_error.to_string();
2325 assert!(error_text.contains("Error at line 2:"));
2326 assert!(error_text.contains("2| \tcommand"));
2327 }
2328
2329 #[test]
2330 fn test_parsing_error_context_with_bad_syntax() {
2331 let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2333
2334 match Makefile::from_reader(input.as_bytes()) {
2336 Ok(makefile) => {
2337 assert_eq!(
2339 makefile.rules().count(),
2340 0,
2341 "Should not have found any rules"
2342 );
2343 }
2344 Err(err) => match err {
2345 self::Error::Parse(error) => {
2346 assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2348 assert!(
2349 !error.errors[0].context.is_empty(),
2350 "Error context should not be empty"
2351 );
2352 }
2353 _ => panic!("Unexpected error type"),
2354 },
2355 };
2356 }
2357
2358 #[test]
2359 fn test_error_message_format() {
2360 let parse_error = ParseError {
2362 errors: vec![ErrorInfo {
2363 message: "test error".to_string(),
2364 line: 42,
2365 context: "some problematic code".to_string(),
2366 }],
2367 };
2368
2369 let error_text = parse_error.to_string();
2370 assert!(error_text.contains("Error at line 42: test error"));
2371 assert!(error_text.contains("42| some problematic code"));
2372 }
2373
2374 #[test]
2375 fn test_line_number_calculation() {
2376 let test_cases = [
2378 ("rule dependency\n\tcommand", 2), ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2), ("var = value\n#comment\n\tindented line", 3), ];
2382
2383 for (input, expected_line) in test_cases {
2384 match input.parse::<Makefile>() {
2386 Ok(_) => {
2387 continue;
2390 }
2391 Err(err) => {
2392 if let Error::Parse(parse_err) = err {
2393 assert_eq!(
2395 parse_err.errors[0].line, expected_line,
2396 "Line number should match the expected line"
2397 );
2398
2399 if parse_err.errors[0].message.contains("indented") {
2401 assert!(
2402 parse_err.errors[0].context.starts_with('\t'),
2403 "Context for indentation errors should include the tab character"
2404 );
2405 }
2406 } else {
2407 panic!("Expected parse error, got: {:?}", err);
2408 }
2409 }
2410 }
2411 }
2412 }
2413
2414 #[test]
2415 fn test_conditional_features() {
2416 let code = r#"
2418# Set variables based on DEBUG flag
2419ifdef DEBUG
2420 CFLAGS += -g -DDEBUG
2421else
2422 CFLAGS = -O2
2423endif
2424
2425# Define a build rule
2426all: $(OBJS)
2427 $(CC) $(CFLAGS) -o $@ $^
2428"#;
2429
2430 let mut buf = code.as_bytes();
2431 let makefile =
2432 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2433
2434 assert!(!makefile.code().is_empty(), "Makefile has content");
2437
2438 let rules = makefile.rules().collect::<Vec<_>>();
2440 assert!(!rules.is_empty(), "Should have found rules");
2441
2442 assert!(code.contains("ifdef DEBUG"));
2444 assert!(code.contains("endif"));
2445
2446 let code_with_var = r#"
2448# Define a variable first
2449CC = gcc
2450
2451ifdef DEBUG
2452 CFLAGS += -g -DDEBUG
2453else
2454 CFLAGS = -O2
2455endif
2456
2457all: $(OBJS)
2458 $(CC) $(CFLAGS) -o $@ $^
2459"#;
2460
2461 let mut buf = code_with_var.as_bytes();
2462 let makefile =
2463 Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2464
2465 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2467 assert!(
2468 !vars.is_empty(),
2469 "Should have found at least the CC variable definition"
2470 );
2471 }
2472
2473 #[test]
2474 fn test_include_directive() {
2475 let parsed = parse(
2476 "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2477 None,
2478 );
2479 assert!(parsed.errors.is_empty());
2480 let node = parsed.syntax();
2481 assert!(format!("{:#?}", node).contains("INCLUDE@"));
2482 }
2483
2484 #[test]
2485 fn test_export_variables() {
2486 let parsed = parse("export SHELL := /bin/bash\n", None);
2487 assert!(parsed.errors.is_empty());
2488 let makefile = parsed.root();
2489 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2490 assert_eq!(vars.len(), 1);
2491 let shell_var = vars
2492 .iter()
2493 .find(|v| v.name() == Some("SHELL".to_string()))
2494 .unwrap();
2495 assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2496 }
2497
2498 #[test]
2499 fn test_variable_scopes() {
2500 let parsed = parse(
2501 "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
2502 None,
2503 );
2504 assert!(parsed.errors.is_empty());
2505 let makefile = parsed.root();
2506 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2507 assert_eq!(vars.len(), 4);
2508 let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
2509 assert!(var_names.contains(&"SIMPLE".to_string()));
2510 assert!(var_names.contains(&"IMMEDIATE".to_string()));
2511 assert!(var_names.contains(&"CONDITIONAL".to_string()));
2512 assert!(var_names.contains(&"APPEND".to_string()));
2513 }
2514
2515 #[test]
2516 fn test_pattern_rule_parsing() {
2517 let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
2518 assert!(parsed.errors.is_empty());
2519 let makefile = parsed.root();
2520 let rules = makefile.rules().collect::<Vec<_>>();
2521 assert_eq!(rules.len(), 1);
2522 assert_eq!(rules[0].targets().next().unwrap(), "%.o");
2523 assert!(rules[0].recipes().next().unwrap().contains("$@"));
2524 }
2525
2526 #[test]
2527 fn test_include_variants() {
2528 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
2530 let parsed = parse(makefile_str, None);
2531 assert!(parsed.errors.is_empty());
2532
2533 let node = parsed.syntax();
2535 let debug_str = format!("{:#?}", node);
2536
2537 assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
2539
2540 let makefile = parsed.root();
2542
2543 let include_count = makefile
2545 .syntax()
2546 .children()
2547 .filter(|child| child.kind() == INCLUDE)
2548 .count();
2549 assert_eq!(include_count, 4);
2550
2551 assert!(makefile
2553 .included_files()
2554 .any(|path| path.contains("$(VAR)")));
2555 }
2556
2557 #[test]
2558 fn test_include_api() {
2559 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
2561 let makefile: Makefile = makefile_str.parse().unwrap();
2562
2563 let includes: Vec<_> = makefile.includes().collect();
2565 assert_eq!(includes.len(), 3);
2566
2567 assert!(!includes[0].is_optional()); assert!(includes[1].is_optional()); assert!(includes[2].is_optional()); let files: Vec<_> = makefile.included_files().collect();
2574 assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
2575
2576 assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
2578 assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
2579 assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
2580 }
2581
2582 #[test]
2583 fn test_include_integration() {
2584 let phony_makefile = Makefile::from_reader(
2588 ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
2589 .as_bytes()
2590 ).unwrap();
2591
2592 assert_eq!(phony_makefile.rules().count(), 2);
2594
2595 let normal_rules_count = phony_makefile
2597 .rules()
2598 .filter(|r| !r.targets().any(|t| t.starts_with('.')))
2599 .count();
2600 assert_eq!(normal_rules_count, 1);
2601
2602 assert_eq!(phony_makefile.includes().count(), 1);
2604 assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
2605
2606 let simple_makefile = Makefile::from_reader(
2608 "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
2609 .as_bytes(),
2610 )
2611 .unwrap();
2612 assert_eq!(simple_makefile.rules().count(), 1);
2613 assert_eq!(simple_makefile.includes().count(), 1);
2614 }
2615
2616 #[test]
2617 fn test_real_conditional_directives() {
2618 let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
2620 let mut buf = conditional.as_bytes();
2621 let makefile =
2622 Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
2623 let code = makefile.code();
2624 assert!(code.contains("ifdef DEBUG"));
2625 assert!(code.contains("else"));
2626 assert!(code.contains("endif"));
2627
2628 let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
2630 let mut buf = nested.as_bytes();
2631 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
2632 let code = makefile.code();
2633 assert!(code.contains("ifdef DEBUG"));
2634 assert!(code.contains("ifdef VERBOSE"));
2635
2636 let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
2638 let mut buf = ifeq.as_bytes();
2639 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
2640 let code = makefile.code();
2641 assert!(code.contains("ifeq"));
2642 assert!(code.contains("Windows_NT"));
2643 }
2644
2645 #[test]
2646 fn test_indented_text_outside_rules() {
2647 let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \" help show help\"\n";
2649 let parsed = parse(help_text, None);
2650 assert!(parsed.errors.is_empty());
2651
2652 let root = parsed.root();
2654 let rules = root.rules().collect::<Vec<_>>();
2655 assert_eq!(rules.len(), 1);
2656
2657 let help_rule = &rules[0];
2658 let recipes = help_rule.recipes().collect::<Vec<_>>();
2659 assert_eq!(recipes.len(), 2);
2660 assert!(recipes[0].contains("Available targets"));
2661 assert!(recipes[1].contains("help"));
2662 }
2663
2664 #[test]
2665 fn test_comment_handling_in_recipes() {
2666 let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
2668
2669 let parsed = parse(recipe_comment, None);
2671
2672 assert!(
2674 parsed.errors.is_empty(),
2675 "Should parse recipe with comments without errors"
2676 );
2677
2678 let root = parsed.root();
2680 let rules = root.rules().collect::<Vec<_>>();
2681 assert_eq!(rules.len(), 1, "Should find exactly one rule");
2682
2683 let build_rule = &rules[0];
2685 assert_eq!(
2686 build_rule.targets().collect::<Vec<_>>(),
2687 vec!["build"],
2688 "Rule should have 'build' as target"
2689 );
2690
2691 let recipes = build_rule.recipes().collect::<Vec<_>>();
2695 assert_eq!(
2696 recipes.len(),
2697 1,
2698 "Should find exactly one recipe line (comment lines are filtered)"
2699 );
2700 assert!(
2701 recipes[0].contains("gcc -o app"),
2702 "Recipe should be the command line"
2703 );
2704 assert!(
2705 !recipes[0].contains("This is a comment"),
2706 "Comments should not be included in recipe lines"
2707 );
2708 }
2709
2710 #[test]
2711 fn test_multiline_variables() {
2712 let multiline = "SOURCES = main.c \\\n util.c\n";
2714
2715 let parsed = parse(multiline, None);
2717
2718 let root = parsed.root();
2720 let vars = root.variable_definitions().collect::<Vec<_>>();
2721 assert!(!vars.is_empty(), "Should find at least one variable");
2722
2723 let operators = "CFLAGS := -Wall \\\n -Werror\n";
2727 let parsed_operators = parse(operators, None);
2728
2729 let root = parsed_operators.root();
2731 let vars = root.variable_definitions().collect::<Vec<_>>();
2732 assert!(
2733 !vars.is_empty(),
2734 "Should find at least one variable with := operator"
2735 );
2736
2737 let append = "LDFLAGS += -L/usr/lib \\\n -lm\n";
2739 let parsed_append = parse(append, None);
2740
2741 let root = parsed_append.root();
2743 let vars = root.variable_definitions().collect::<Vec<_>>();
2744 assert!(
2745 !vars.is_empty(),
2746 "Should find at least one variable with += operator"
2747 );
2748 }
2749
2750 #[test]
2751 fn test_whitespace_and_eof_handling() {
2752 let blank_lines = "VAR = value\n\n\n";
2754
2755 let parsed_blank = parse(blank_lines, None);
2756
2757 let root = parsed_blank.root();
2759 let vars = root.variable_definitions().collect::<Vec<_>>();
2760 assert_eq!(
2761 vars.len(),
2762 1,
2763 "Should find one variable in blank lines test"
2764 );
2765
2766 let trailing_space = "VAR = value \n";
2768
2769 let parsed_space = parse(trailing_space, None);
2770
2771 let root = parsed_space.root();
2773 let vars = root.variable_definitions().collect::<Vec<_>>();
2774 assert_eq!(
2775 vars.len(),
2776 1,
2777 "Should find one variable in trailing space test"
2778 );
2779
2780 let no_newline = "VAR = value";
2782
2783 let parsed_no_newline = parse(no_newline, None);
2784
2785 let root = parsed_no_newline.root();
2787 let vars = root.variable_definitions().collect::<Vec<_>>();
2788 assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
2789 assert_eq!(
2790 vars[0].name(),
2791 Some("VAR".to_string()),
2792 "Variable name should be VAR"
2793 );
2794 }
2795
2796 #[test]
2797 fn test_complex_variable_references() {
2798 let wildcard = "SOURCES = $(wildcard *.c)\n";
2800 let parsed = parse(wildcard, None);
2801 assert!(parsed.errors.is_empty());
2802
2803 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
2805 let parsed = parse(nested, None);
2806 assert!(parsed.errors.is_empty());
2807
2808 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
2810 let parsed = parse(patsubst, None);
2811 assert!(parsed.errors.is_empty());
2812 }
2813
2814 #[test]
2815 fn test_complex_variable_references_minimal() {
2816 let wildcard = "SOURCES = $(wildcard *.c)\n";
2818 let parsed = parse(wildcard, None);
2819 assert!(parsed.errors.is_empty());
2820
2821 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
2823 let parsed = parse(nested, None);
2824 assert!(parsed.errors.is_empty());
2825
2826 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
2828 let parsed = parse(patsubst, None);
2829 assert!(parsed.errors.is_empty());
2830 }
2831
2832 #[test]
2833 fn test_multiline_variable_with_backslash() {
2834 let content = r#"
2835LONG_VAR = This is a long variable \
2836 that continues on the next line \
2837 and even one more line
2838"#;
2839
2840 let mut buf = content.as_bytes();
2842 let makefile =
2843 Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
2844
2845 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2847 assert_eq!(
2848 vars.len(),
2849 1,
2850 "Expected 1 variable but found {}",
2851 vars.len()
2852 );
2853 let var_value = vars[0].raw_value();
2854 assert!(var_value.is_some(), "Variable value is None");
2855
2856 let value_str = var_value.unwrap();
2858 assert!(
2859 value_str.contains("long variable"),
2860 "Value doesn't contain expected content"
2861 );
2862 }
2863
2864 #[test]
2865 fn test_multiline_variable_with_mixed_operators() {
2866 let content = r#"
2867PREFIX ?= /usr/local
2868CFLAGS := -Wall -O2 \
2869 -I$(PREFIX)/include \
2870 -DDEBUG
2871"#;
2872 let mut buf = content.as_bytes();
2874 let makefile = Makefile::read_relaxed(&mut buf)
2875 .expect("Failed to parse multiline variable with operators");
2876
2877 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2879 assert!(
2880 !vars.is_empty(),
2881 "Expected at least 1 variable, found {}",
2882 vars.len()
2883 );
2884
2885 let prefix_var = vars
2887 .iter()
2888 .find(|v| v.name().unwrap_or_default() == "PREFIX");
2889 assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
2890 assert!(
2891 prefix_var.unwrap().raw_value().is_some(),
2892 "PREFIX variable has no value"
2893 );
2894
2895 let cflags_var = vars
2897 .iter()
2898 .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
2899 assert!(
2900 cflags_var.is_some(),
2901 "Expected to find CFLAGS variable (or part of it)"
2902 );
2903 }
2904
2905 #[test]
2906 fn test_indented_help_text() {
2907 let content = r#"
2908.PHONY: help
2909help:
2910 @echo "Available targets:"
2911 @echo " build - Build the project"
2912 @echo " test - Run tests"
2913 @echo " clean - Remove build artifacts"
2914"#;
2915 let mut buf = content.as_bytes();
2917 let makefile =
2918 Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
2919
2920 let rules = makefile.rules().collect::<Vec<_>>();
2922 assert!(!rules.is_empty(), "Expected at least one rule");
2923
2924 let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
2926 assert!(help_rule.is_some(), "Expected to find help rule");
2927
2928 let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
2930 assert!(
2931 !recipes.is_empty(),
2932 "Expected at least one recipe line in help rule"
2933 );
2934 assert!(
2935 recipes.iter().any(|r| r.contains("Available targets")),
2936 "Expected to find 'Available targets' in recipes"
2937 );
2938 }
2939
2940 #[test]
2941 fn test_indented_lines_in_conditionals() {
2942 let content = r#"
2943ifdef DEBUG
2944 CFLAGS += -g -DDEBUG
2945 # This is a comment inside conditional
2946 ifdef VERBOSE
2947 CFLAGS += -v
2948 endif
2949endif
2950"#;
2951 let mut buf = content.as_bytes();
2953 let makefile = Makefile::read_relaxed(&mut buf)
2954 .expect("Failed to parse indented lines in conditionals");
2955
2956 let code = makefile.code();
2958 assert!(code.contains("ifdef DEBUG"));
2959 assert!(code.contains("ifdef VERBOSE"));
2960 assert!(code.contains("endif"));
2961 }
2962
2963 #[test]
2964 fn test_recipe_with_colon() {
2965 let content = r#"
2966build:
2967 @echo "Building at: $(shell date)"
2968 gcc -o program main.c
2969"#;
2970 let parsed = parse(content, None);
2971 assert!(
2972 parsed.errors.is_empty(),
2973 "Failed to parse recipe with colon: {:?}",
2974 parsed.errors
2975 );
2976 }
2977
2978 #[test]
2979 #[ignore]
2980 fn test_double_colon_rules() {
2981 let content = r#"
2984%.o :: %.c
2985 $(CC) -c $< -o $@
2986
2987# Double colon allows multiple rules for same target
2988all:: prerequisite1
2989 @echo "First rule for all"
2990
2991all:: prerequisite2
2992 @echo "Second rule for all"
2993"#;
2994 let mut buf = content.as_bytes();
2995 let makefile =
2996 Makefile::read_relaxed(&mut buf).expect("Failed to parse double colon rules");
2997
2998 let rules = makefile.rules().collect::<Vec<_>>();
3000 assert!(!rules.is_empty(), "Expected at least one rule");
3001
3002 let all_rules = rules
3004 .iter()
3005 .filter(|r| r.targets().any(|t| t.contains("all")));
3006 assert!(
3007 all_rules.count() > 0,
3008 "Expected to find at least one rule containing 'all'"
3009 );
3010 }
3011
3012 #[test]
3013 fn test_else_conditional_directives() {
3014 let content = r#"
3016ifeq ($(OS),Windows_NT)
3017 TARGET = windows
3018else ifeq ($(OS),Darwin)
3019 TARGET = macos
3020else ifeq ($(OS),Linux)
3021 TARGET = linux
3022else
3023 TARGET = unknown
3024endif
3025"#;
3026 let mut buf = content.as_bytes();
3027 let makefile =
3028 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3029 assert!(makefile.code().contains("else ifeq"));
3030 assert!(makefile.code().contains("TARGET"));
3031
3032 let content = r#"
3034ifdef WINDOWS
3035 TARGET = windows
3036else ifdef DARWIN
3037 TARGET = macos
3038else ifdef LINUX
3039 TARGET = linux
3040else
3041 TARGET = unknown
3042endif
3043"#;
3044 let mut buf = content.as_bytes();
3045 let makefile =
3046 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3047 assert!(makefile.code().contains("else ifdef"));
3048
3049 let content = r#"
3051ifndef NOWINDOWS
3052 TARGET = windows
3053else ifndef NODARWIN
3054 TARGET = macos
3055else
3056 TARGET = linux
3057endif
3058"#;
3059 let mut buf = content.as_bytes();
3060 let makefile =
3061 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3062 assert!(makefile.code().contains("else ifndef"));
3063
3064 let content = r#"
3066ifneq ($(OS),Windows_NT)
3067 TARGET = not_windows
3068else ifneq ($(OS),Darwin)
3069 TARGET = not_macos
3070else
3071 TARGET = darwin
3072endif
3073"#;
3074 let mut buf = content.as_bytes();
3075 let makefile =
3076 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3077 assert!(makefile.code().contains("else ifneq"));
3078 }
3079
3080 #[test]
3081 fn test_complex_else_conditionals() {
3082 let content = r#"VAR1 := foo
3084VAR2 := bar
3085
3086ifeq ($(VAR1),foo)
3087 RESULT := foo_matched
3088else ifdef VAR2
3089 RESULT := var2_defined
3090else ifndef VAR3
3091 RESULT := var3_not_defined
3092else
3093 RESULT := final_else
3094endif
3095
3096all:
3097 @echo $(RESULT)
3098"#;
3099 let mut buf = content.as_bytes();
3100 let makefile =
3101 Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3102
3103 let code = makefile.code();
3105 assert!(code.contains("ifeq ($(VAR1),foo)"));
3106 assert!(code.contains("else ifdef VAR2"));
3107 assert!(code.contains("else ifndef VAR3"));
3108 assert!(code.contains("else"));
3109 assert!(code.contains("endif"));
3110 assert!(code.contains("RESULT"));
3111
3112 let rules: Vec<_> = makefile.rules().collect();
3114 assert_eq!(rules.len(), 1);
3115 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3116 }
3117
3118 #[test]
3119 fn test_conditional_token_structure() {
3120 let content = r#"ifdef VAR1
3122X := 1
3123else ifdef VAR2
3124X := 2
3125else
3126X := 3
3127endif
3128"#;
3129 let mut buf = content.as_bytes();
3130 let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3131
3132 let syntax = makefile.syntax();
3134
3135 let mut found_conditional = false;
3137 let mut found_conditional_if = false;
3138 let mut found_conditional_else = false;
3139 let mut found_conditional_endif = false;
3140
3141 fn check_node(
3142 node: &SyntaxNode,
3143 found_cond: &mut bool,
3144 found_if: &mut bool,
3145 found_else: &mut bool,
3146 found_endif: &mut bool,
3147 ) {
3148 match node.kind() {
3149 SyntaxKind::CONDITIONAL => *found_cond = true,
3150 SyntaxKind::CONDITIONAL_IF => *found_if = true,
3151 SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3152 SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3153 _ => {}
3154 }
3155
3156 for child in node.children() {
3157 check_node(&child, found_cond, found_if, found_else, found_endif);
3158 }
3159 }
3160
3161 check_node(
3162 syntax,
3163 &mut found_conditional,
3164 &mut found_conditional_if,
3165 &mut found_conditional_else,
3166 &mut found_conditional_endif,
3167 );
3168
3169 assert!(found_conditional, "Should have CONDITIONAL node");
3170 assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3171 assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3172 assert!(
3173 found_conditional_endif,
3174 "Should have CONDITIONAL_ENDIF node"
3175 );
3176 }
3177
3178 #[test]
3179 fn test_ambiguous_assignment_vs_rule() {
3180 const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3182
3183 let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3184 let makefile =
3185 Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3186
3187 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3188 let rules = makefile.rules().collect::<Vec<_>>();
3189
3190 assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3191 assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3192
3193 assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3194
3195 const SIMPLE_RULE: &str = "target: dependency\n";
3197
3198 let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3199 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3200
3201 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3202 let rules = makefile.rules().collect::<Vec<_>>();
3203
3204 assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3205 assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3206
3207 let rule = &rules[0];
3208 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3209 }
3210
3211 #[test]
3212 fn test_nested_conditionals() {
3213 let content = r#"
3214ifdef RELEASE
3215 CFLAGS += -O3
3216 ifndef DEBUG
3217 ifneq ($(ARCH),arm)
3218 CFLAGS += -march=native
3219 else
3220 CFLAGS += -mcpu=cortex-a72
3221 endif
3222 endif
3223endif
3224"#;
3225 let mut buf = content.as_bytes();
3227 let makefile =
3228 Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3229
3230 let code = makefile.code();
3232 assert!(code.contains("ifdef RELEASE"));
3233 assert!(code.contains("ifndef DEBUG"));
3234 assert!(code.contains("ifneq"));
3235 }
3236
3237 #[test]
3238 fn test_space_indented_recipes() {
3239 let content = r#"
3242build:
3243 @echo "Building with spaces instead of tabs"
3244 gcc -o program main.c
3245"#;
3246 let mut buf = content.as_bytes();
3248 let makefile =
3249 Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3250
3251 let rules = makefile.rules().collect::<Vec<_>>();
3253 assert!(!rules.is_empty(), "Expected at least one rule");
3254
3255 let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3257 assert!(build_rule.is_some(), "Expected to find build rule");
3258 }
3259
3260 #[test]
3261 fn test_complex_variable_functions() {
3262 let content = r#"
3263FILES := $(shell find . -name "*.c")
3264OBJS := $(patsubst %.c,%.o,$(FILES))
3265NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3266HEADERS := ${wildcard *.h}
3267"#;
3268 let parsed = parse(content, None);
3269 assert!(
3270 parsed.errors.is_empty(),
3271 "Failed to parse complex variable functions: {:?}",
3272 parsed.errors
3273 );
3274 }
3275
3276 #[test]
3277 fn test_nested_variable_expansions() {
3278 let content = r#"
3279VERSION = 1.0
3280PACKAGE = myapp
3281TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3282INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3283"#;
3284 let parsed = parse(content, None);
3285 assert!(
3286 parsed.errors.is_empty(),
3287 "Failed to parse nested variable expansions: {:?}",
3288 parsed.errors
3289 );
3290 }
3291
3292 #[test]
3293 fn test_special_directives() {
3294 let content = r#"
3295# Special makefile directives
3296.PHONY: all clean
3297.SUFFIXES: .c .o
3298.DEFAULT: all
3299
3300# Variable definition and export directive
3301export PATH := /usr/bin:/bin
3302"#;
3303 let mut buf = content.as_bytes();
3305 let makefile =
3306 Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3307
3308 let rules = makefile.rules().collect::<Vec<_>>();
3310
3311 let phony_rule = rules
3313 .iter()
3314 .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3315 assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3316
3317 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3319 assert!(!vars.is_empty(), "Expected to find at least one variable");
3320 }
3321
3322 #[test]
3325 fn test_comprehensive_real_world_makefile() {
3326 let content = r#"
3328# Basic variable assignment
3329VERSION = 1.0.0
3330
3331# Phony target
3332.PHONY: all clean
3333
3334# Simple rule
3335all:
3336 echo "Building version $(VERSION)"
3337
3338# Another rule with dependencies
3339clean:
3340 rm -f *.o
3341"#;
3342
3343 let parsed = parse(content, None);
3345
3346 assert!(parsed.errors.is_empty(), "Expected no parsing errors");
3348
3349 let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
3351 assert!(!variables.is_empty(), "Expected at least one variable");
3352 assert_eq!(
3353 variables[0].name(),
3354 Some("VERSION".to_string()),
3355 "Expected VERSION variable"
3356 );
3357
3358 let rules = parsed.root().rules().collect::<Vec<_>>();
3360 assert!(!rules.is_empty(), "Expected at least one rule");
3361
3362 let rule_targets: Vec<String> = rules
3364 .iter()
3365 .flat_map(|r| r.targets().collect::<Vec<_>>())
3366 .collect();
3367 assert!(
3368 rule_targets.contains(&".PHONY".to_string()),
3369 "Expected .PHONY rule"
3370 );
3371 assert!(
3372 rule_targets.contains(&"all".to_string()),
3373 "Expected 'all' rule"
3374 );
3375 assert!(
3376 rule_targets.contains(&"clean".to_string()),
3377 "Expected 'clean' rule"
3378 );
3379 }
3380
3381 #[test]
3382 fn test_indented_help_text_outside_rules() {
3383 let content = r#"
3385# Targets with help text
3386help:
3387 @echo "Available targets:"
3388 @echo " build build the project"
3389 @echo " test run tests"
3390 @echo " clean clean build artifacts"
3391
3392# Another target
3393clean:
3394 rm -rf build/
3395"#;
3396
3397 let parsed = parse(content, None);
3399
3400 assert!(
3402 parsed.errors.is_empty(),
3403 "Failed to parse indented help text"
3404 );
3405
3406 let rules = parsed.root().rules().collect::<Vec<_>>();
3408 assert_eq!(rules.len(), 2, "Expected to find two rules");
3409
3410 let help_rule = rules
3412 .iter()
3413 .find(|r| r.targets().any(|t| t == "help"))
3414 .expect("Expected to find help rule");
3415
3416 let clean_rule = rules
3417 .iter()
3418 .find(|r| r.targets().any(|t| t == "clean"))
3419 .expect("Expected to find clean rule");
3420
3421 let help_recipes = help_rule.recipes().collect::<Vec<_>>();
3423 assert!(
3424 !help_recipes.is_empty(),
3425 "Help rule should have recipe lines"
3426 );
3427 assert!(
3428 help_recipes
3429 .iter()
3430 .any(|line| line.contains("Available targets")),
3431 "Help recipes should include 'Available targets' line"
3432 );
3433
3434 let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
3436 assert!(
3437 !clean_recipes.is_empty(),
3438 "Clean rule should have recipe lines"
3439 );
3440 assert!(
3441 clean_recipes.iter().any(|line| line.contains("rm -rf")),
3442 "Clean recipes should include 'rm -rf' command"
3443 );
3444 }
3445
3446 #[test]
3447 fn test_makefile1_phony_pattern() {
3448 let content = "#line 2145\n.PHONY: $(PHONY)\n";
3450
3451 let result = parse(content, None);
3453
3454 assert!(
3456 result.errors.is_empty(),
3457 "Failed to parse .PHONY: $(PHONY) pattern"
3458 );
3459
3460 let rules = result.root().rules().collect::<Vec<_>>();
3462 assert_eq!(rules.len(), 1, "Expected 1 rule");
3463 assert_eq!(
3464 rules[0].targets().next().unwrap(),
3465 ".PHONY",
3466 "Expected .PHONY rule"
3467 );
3468
3469 let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
3471 assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
3472 assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
3473 }
3474
3475 #[test]
3476 fn test_skip_until_newline_behavior() {
3477 let input = "text without newline";
3479 let parsed = parse(input, None);
3480 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
3482
3483 let input_with_newline = "text\nafter newline";
3484 let parsed2 = parse(input_with_newline, None);
3485 assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
3486 }
3487
3488 #[test]
3489 #[ignore] fn test_error_with_indent_token() {
3491 let input = "\tinvalid indented line";
3493 let parsed = parse(input, None);
3494 assert!(!parsed.errors.is_empty());
3496
3497 let error_msg = &parsed.errors[0].message;
3498 assert!(error_msg.contains("recipe commences before first target"));
3499 }
3500
3501 #[test]
3502 fn test_conditional_token_handling() {
3503 let input = r#"
3505ifndef VAR
3506 CFLAGS = -DTEST
3507endif
3508"#;
3509 let parsed = parse(input, None);
3510 let makefile = parsed.root();
3512 let _vars = makefile.variable_definitions().collect::<Vec<_>>();
3513 let nested = r#"
3517ifdef DEBUG
3518 ifndef RELEASE
3519 CFLAGS = -g
3520 endif
3521endif
3522"#;
3523 let parsed_nested = parse(nested, None);
3524 let _makefile = parsed_nested.root();
3526 }
3527
3528 #[test]
3529 fn test_include_vs_conditional_logic() {
3530 let input = r#"
3532include file.mk
3533ifdef VAR
3534 VALUE = 1
3535endif
3536"#;
3537 let parsed = parse(input, None);
3538 let makefile = parsed.root();
3540 let includes = makefile.includes().collect::<Vec<_>>();
3541 assert!(!includes.is_empty() || !parsed.errors.is_empty());
3543
3544 let optional_include = r#"
3546-include optional.mk
3547ifndef VAR
3548 VALUE = default
3549endif
3550"#;
3551 let parsed2 = parse(optional_include, None);
3552 let _makefile = parsed2.root();
3554 }
3555
3556 #[test]
3557 fn test_balanced_parens_counting() {
3558 let input = r#"
3560VAR = $(call func,$(nested,arg),extra)
3561COMPLEX = $(if $(condition),$(then_val),$(else_val))
3562"#;
3563 let parsed = parse(input, None);
3564 assert!(parsed.errors.is_empty());
3565
3566 let makefile = parsed.root();
3567 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3568 assert_eq!(vars.len(), 2);
3569 }
3570
3571 #[test]
3572 fn test_documentation_lookahead() {
3573 let input = r#"
3575# Documentation comment
3576help:
3577 @echo "Usage instructions"
3578 @echo "More help text"
3579"#;
3580 let parsed = parse(input, None);
3581 assert!(parsed.errors.is_empty());
3582
3583 let makefile = parsed.root();
3584 let rules = makefile.rules().collect::<Vec<_>>();
3585 assert_eq!(rules.len(), 1);
3586 assert_eq!(rules[0].targets().next().unwrap(), "help");
3587 }
3588
3589 #[test]
3590 fn test_edge_case_empty_input() {
3591 let parsed = parse("", None);
3593 assert!(parsed.errors.is_empty());
3594
3595 let parsed2 = parse(" \n \n", None);
3597 let _makefile = parsed2.root();
3600 }
3601
3602 #[test]
3603 fn test_malformed_conditional_recovery() {
3604 let input = r#"
3606ifdef
3607 # Missing condition variable
3608endif
3609"#;
3610 let parsed = parse(input, None);
3611 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
3614 }
3615
3616 #[test]
3617 fn test_replace_rule() {
3618 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
3619 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3620
3621 makefile.replace_rule(0, new_rule).unwrap();
3622
3623 let targets: Vec<_> = makefile
3624 .rules()
3625 .flat_map(|r| r.targets().collect::<Vec<_>>())
3626 .collect();
3627 assert_eq!(targets, vec!["new_rule", "rule2"]);
3628
3629 let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
3630 assert_eq!(recipes, vec!["new_command"]);
3631 }
3632
3633 #[test]
3634 fn test_replace_rule_out_of_bounds() {
3635 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3636 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3637
3638 let result = makefile.replace_rule(5, new_rule);
3639 assert!(result.is_err());
3640 }
3641
3642 #[test]
3643 fn test_remove_rule() {
3644 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
3645 .parse()
3646 .unwrap();
3647
3648 let removed = makefile.remove_rule(1).unwrap();
3649 assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
3650
3651 let remaining_targets: Vec<_> = makefile
3652 .rules()
3653 .flat_map(|r| r.targets().collect::<Vec<_>>())
3654 .collect();
3655 assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
3656 assert_eq!(makefile.rules().count(), 2);
3657 }
3658
3659 #[test]
3660 fn test_remove_rule_out_of_bounds() {
3661 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3662
3663 let result = makefile.remove_rule(5);
3664 assert!(result.is_err());
3665 }
3666
3667 #[test]
3668 fn test_insert_rule() {
3669 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
3670 let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
3671
3672 makefile.insert_rule(1, new_rule).unwrap();
3673
3674 let targets: Vec<_> = makefile
3675 .rules()
3676 .flat_map(|r| r.targets().collect::<Vec<_>>())
3677 .collect();
3678 assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
3679 assert_eq!(makefile.rules().count(), 3);
3680 }
3681
3682 #[test]
3683 fn test_insert_rule_at_end() {
3684 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3685 let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
3686
3687 makefile.insert_rule(1, new_rule).unwrap();
3688
3689 let targets: Vec<_> = makefile
3690 .rules()
3691 .flat_map(|r| r.targets().collect::<Vec<_>>())
3692 .collect();
3693 assert_eq!(targets, vec!["rule1", "end_rule"]);
3694 }
3695
3696 #[test]
3697 fn test_insert_rule_out_of_bounds() {
3698 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3699 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3700
3701 let result = makefile.insert_rule(5, new_rule);
3702 assert!(result.is_err());
3703 }
3704
3705 #[test]
3706 fn test_insert_rule_preserves_blank_line_spacing_at_end() {
3707 let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
3709 let mut makefile: Makefile = input.parse().unwrap();
3710 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
3711
3712 makefile.insert_rule(2, new_rule).unwrap();
3713
3714 let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
3715 assert_eq!(makefile.to_string(), expected);
3716 }
3717
3718 #[test]
3719 fn test_insert_rule_adds_blank_lines_when_missing() {
3720 let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
3722 let mut makefile: Makefile = input.parse().unwrap();
3723 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
3724
3725 makefile.insert_rule(2, new_rule).unwrap();
3726
3727 let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
3728 assert_eq!(makefile.to_string(), expected);
3729 }
3730
3731 #[test]
3732 fn test_remove_command() {
3733 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
3734 .parse()
3735 .unwrap();
3736
3737 rule.remove_command(1);
3738 let recipes: Vec<_> = rule.recipes().collect();
3739 assert_eq!(recipes, vec!["command1", "command3"]);
3740 assert_eq!(rule.recipe_count(), 2);
3741 }
3742
3743 #[test]
3744 fn test_remove_command_out_of_bounds() {
3745 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
3746
3747 let result = rule.remove_command(5);
3748 assert!(!result);
3749 }
3750
3751 #[test]
3752 fn test_insert_command() {
3753 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
3754
3755 rule.insert_command(1, "command2");
3756 let recipes: Vec<_> = rule.recipes().collect();
3757 assert_eq!(recipes, vec!["command1", "command2", "command3"]);
3758 }
3759
3760 #[test]
3761 fn test_insert_command_at_end() {
3762 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
3763
3764 rule.insert_command(1, "command2");
3765 let recipes: Vec<_> = rule.recipes().collect();
3766 assert_eq!(recipes, vec!["command1", "command2"]);
3767 }
3768
3769 #[test]
3770 fn test_insert_command_in_empty_rule() {
3771 let mut rule: Rule = "rule:\n".parse().unwrap();
3772
3773 rule.insert_command(0, "new_command");
3774 let recipes: Vec<_> = rule.recipes().collect();
3775 assert_eq!(recipes, vec!["new_command"]);
3776 }
3777
3778 #[test]
3779 fn test_recipe_count() {
3780 let rule1: Rule = "rule:\n".parse().unwrap();
3781 assert_eq!(rule1.recipe_count(), 0);
3782
3783 let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
3784 assert_eq!(rule2.recipe_count(), 2);
3785 }
3786
3787 #[test]
3788 fn test_clear_commands() {
3789 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
3790 .parse()
3791 .unwrap();
3792
3793 rule.clear_commands();
3794 assert_eq!(rule.recipe_count(), 0);
3795
3796 let recipes: Vec<_> = rule.recipes().collect();
3797 assert_eq!(recipes, Vec::<String>::new());
3798
3799 let targets: Vec<_> = rule.targets().collect();
3801 assert_eq!(targets, vec!["rule"]);
3802 }
3803
3804 #[test]
3805 fn test_clear_commands_empty_rule() {
3806 let mut rule: Rule = "rule:\n".parse().unwrap();
3807
3808 rule.clear_commands();
3809 assert_eq!(rule.recipe_count(), 0);
3810
3811 let targets: Vec<_> = rule.targets().collect();
3812 assert_eq!(targets, vec!["rule"]);
3813 }
3814
3815 #[test]
3816 fn test_rule_manipulation_preserves_structure() {
3817 let input = r#"# Comment
3819VAR = value
3820
3821rule1:
3822 command1
3823
3824# Another comment
3825rule2:
3826 command2
3827
3828VAR2 = value2
3829"#;
3830
3831 let mut makefile: Makefile = input.parse().unwrap();
3832 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3833
3834 makefile.insert_rule(1, new_rule).unwrap();
3836
3837 let targets: Vec<_> = makefile
3839 .rules()
3840 .flat_map(|r| r.targets().collect::<Vec<_>>())
3841 .collect();
3842 assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
3843
3844 let vars: Vec<_> = makefile.variable_definitions().collect();
3846 assert_eq!(vars.len(), 2);
3847
3848 let output = makefile.code();
3850 assert!(output.contains("# Comment"));
3851 assert!(output.contains("VAR = value"));
3852 assert!(output.contains("# Another comment"));
3853 assert!(output.contains("VAR2 = value2"));
3854 }
3855
3856 #[test]
3857 fn test_replace_rule_with_multiple_targets() {
3858 let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
3859 let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
3860
3861 makefile.replace_rule(0, new_rule).unwrap();
3862
3863 let targets: Vec<_> = makefile
3864 .rules()
3865 .flat_map(|r| r.targets().collect::<Vec<_>>())
3866 .collect();
3867 assert_eq!(targets, vec!["new_target"]);
3868 }
3869
3870 #[test]
3871 fn test_empty_makefile_operations() {
3872 let mut makefile = Makefile::new();
3873
3874 assert!(makefile
3876 .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
3877 .is_err());
3878 assert!(makefile.remove_rule(0).is_err());
3879
3880 let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
3882 makefile.insert_rule(0, new_rule).unwrap();
3883 assert_eq!(makefile.rules().count(), 1);
3884 }
3885
3886 #[test]
3887 fn test_command_operations_preserve_indentation() {
3888 let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
3889 .parse()
3890 .unwrap();
3891
3892 rule.insert_command(1, "middle_command");
3893 let recipes: Vec<_> = rule.recipes().collect();
3894 assert_eq!(
3895 recipes,
3896 vec!["\tdeep_indent", "middle_command", "shallow_indent"]
3897 );
3898 }
3899
3900 #[test]
3901 fn test_rule_operations_with_variables_and_includes() {
3902 let input = r#"VAR1 = value1
3903include common.mk
3904
3905rule1:
3906 command1
3907
3908VAR2 = value2
3909include other.mk
3910
3911rule2:
3912 command2
3913"#;
3914
3915 let mut makefile: Makefile = input.parse().unwrap();
3916
3917 makefile.remove_rule(0).unwrap();
3919
3920 let output = makefile.code();
3922 assert!(output.contains("VAR1 = value1"));
3923 assert!(output.contains("include common.mk"));
3924 assert!(output.contains("VAR2 = value2"));
3925 assert!(output.contains("include other.mk"));
3926
3927 assert_eq!(makefile.rules().count(), 1);
3929 let remaining_targets: Vec<_> = makefile
3930 .rules()
3931 .flat_map(|r| r.targets().collect::<Vec<_>>())
3932 .collect();
3933 assert_eq!(remaining_targets, vec!["rule2"]);
3934 }
3935
3936 #[test]
3937 fn test_command_manipulation_edge_cases() {
3938 let mut empty_rule: Rule = "empty:\n".parse().unwrap();
3940 assert_eq!(empty_rule.recipe_count(), 0);
3941
3942 empty_rule.insert_command(0, "first_command");
3943 assert_eq!(empty_rule.recipe_count(), 1);
3944
3945 let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
3947 empty_rule2.clear_commands();
3948 assert_eq!(empty_rule2.recipe_count(), 0);
3949 }
3950
3951 #[test]
3952 fn test_large_makefile_performance() {
3953 let mut makefile = Makefile::new();
3955
3956 for i in 0..100 {
3958 let rule_name = format!("rule{}", i);
3959 makefile
3960 .add_rule(&rule_name)
3961 .push_command(&format!("command{}", i));
3962 }
3963
3964 assert_eq!(makefile.rules().count(), 100);
3965
3966 let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
3968 makefile.replace_rule(50, new_rule).unwrap();
3969
3970 let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
3972 assert_eq!(rule_50_targets, vec!["middle_rule"]);
3973
3974 assert_eq!(makefile.rules().count(), 100); }
3976
3977 #[test]
3978 fn test_complex_recipe_manipulation() {
3979 let mut complex_rule: Rule = r#"complex:
3980 @echo "Starting build"
3981 $(CC) $(CFLAGS) -o $@ $<
3982 @echo "Build complete"
3983 chmod +x $@
3984"#
3985 .parse()
3986 .unwrap();
3987
3988 assert_eq!(complex_rule.recipe_count(), 4);
3989
3990 complex_rule.remove_command(0); complex_rule.remove_command(1); let final_recipes: Vec<_> = complex_rule.recipes().collect();
3995 assert_eq!(final_recipes.len(), 2);
3996 assert!(final_recipes[0].contains("$(CC)"));
3997 assert!(final_recipes[1].contains("chmod"));
3998 }
3999
4000 #[test]
4001 fn test_variable_definition_remove() {
4002 let makefile: Makefile = r#"VAR1 = value1
4003VAR2 = value2
4004VAR3 = value3
4005"#
4006 .parse()
4007 .unwrap();
4008
4009 assert_eq!(makefile.variable_definitions().count(), 3);
4011
4012 let mut var2 = makefile
4014 .variable_definitions()
4015 .nth(1)
4016 .expect("Should have second variable");
4017 assert_eq!(var2.name(), Some("VAR2".to_string()));
4018 var2.remove();
4019
4020 assert_eq!(makefile.variable_definitions().count(), 2);
4022 let var_names: Vec<_> = makefile
4023 .variable_definitions()
4024 .filter_map(|v| v.name())
4025 .collect();
4026 assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4027 }
4028
4029 #[test]
4030 fn test_variable_definition_set_value() {
4031 let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4032
4033 let mut var = makefile
4034 .variable_definitions()
4035 .next()
4036 .expect("Should have variable");
4037 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4038
4039 var.set_value("new_value");
4041
4042 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4044 assert!(makefile.code().contains("VAR = new_value"));
4045 }
4046
4047 #[test]
4048 fn test_variable_definition_set_value_preserves_format() {
4049 let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4050
4051 let mut var = makefile
4052 .variable_definitions()
4053 .next()
4054 .expect("Should have variable");
4055 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4056
4057 var.set_value("new_value");
4059
4060 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4062 let code = makefile.code();
4063 assert!(code.contains("export"), "Should preserve export prefix");
4064 assert!(code.contains(":="), "Should preserve := operator");
4065 assert!(code.contains("new_value"), "Should have new value");
4066 }
4067
4068 #[test]
4069 fn test_makefile_find_variable() {
4070 let makefile: Makefile = r#"VAR1 = value1
4071VAR2 = value2
4072VAR3 = value3
4073"#
4074 .parse()
4075 .unwrap();
4076
4077 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4079 assert_eq!(vars.len(), 1);
4080 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4081 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4082
4083 assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4085 }
4086
4087 #[test]
4088 fn test_makefile_find_variable_with_export() {
4089 let makefile: Makefile = r#"VAR1 = value1
4090export VAR2 := value2
4091VAR3 = value3
4092"#
4093 .parse()
4094 .unwrap();
4095
4096 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4098 assert_eq!(vars.len(), 1);
4099 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4100 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4101 }
4102
4103 #[test]
4104 fn test_variable_definition_is_export() {
4105 let makefile: Makefile = r#"VAR1 = value1
4106export VAR2 := value2
4107export VAR3 = value3
4108VAR4 := value4
4109"#
4110 .parse()
4111 .unwrap();
4112
4113 let vars: Vec<_> = makefile.variable_definitions().collect();
4114 assert_eq!(vars.len(), 4);
4115
4116 assert!(!vars[0].is_export());
4117 assert!(vars[1].is_export());
4118 assert!(vars[2].is_export());
4119 assert!(!vars[3].is_export());
4120 }
4121
4122 #[test]
4123 fn test_makefile_find_variable_multiple() {
4124 let makefile: Makefile = r#"VAR1 = value1
4125VAR1 = value2
4126VAR2 = other
4127VAR1 = value3
4128"#
4129 .parse()
4130 .unwrap();
4131
4132 let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4134 assert_eq!(vars.len(), 3);
4135 assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4136 assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4137 assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4138
4139 let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4141 assert_eq!(var2s.len(), 1);
4142 assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4143 }
4144
4145 #[test]
4146 fn test_variable_remove_and_find() {
4147 let makefile: Makefile = r#"VAR1 = value1
4148VAR2 = value2
4149VAR3 = value3
4150"#
4151 .parse()
4152 .unwrap();
4153
4154 let mut var2 = makefile
4156 .find_variable("VAR2")
4157 .next()
4158 .expect("Should find VAR2");
4159 var2.remove();
4160
4161 assert_eq!(makefile.find_variable("VAR2").count(), 0);
4163
4164 assert_eq!(makefile.find_variable("VAR1").count(), 1);
4166 assert_eq!(makefile.find_variable("VAR3").count(), 1);
4167 }
4168
4169 #[test]
4170 fn test_variable_remove_with_comment() {
4171 let makefile: Makefile = r#"VAR1 = value1
4172# This is a comment about VAR2
4173VAR2 = value2
4174VAR3 = value3
4175"#
4176 .parse()
4177 .unwrap();
4178
4179 let mut var2 = makefile
4181 .variable_definitions()
4182 .nth(1)
4183 .expect("Should have second variable");
4184 assert_eq!(var2.name(), Some("VAR2".to_string()));
4185 var2.remove();
4186
4187 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4189 }
4190
4191 #[test]
4192 fn test_variable_remove_with_multiple_comments() {
4193 let makefile: Makefile = r#"VAR1 = value1
4194# Comment line 1
4195# Comment line 2
4196# Comment line 3
4197VAR2 = value2
4198VAR3 = value3
4199"#
4200 .parse()
4201 .unwrap();
4202
4203 let mut var2 = makefile
4205 .variable_definitions()
4206 .nth(1)
4207 .expect("Should have second variable");
4208 var2.remove();
4209
4210 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4212 }
4213
4214 #[test]
4215 fn test_variable_remove_with_empty_line() {
4216 let makefile: Makefile = r#"VAR1 = value1
4217
4218# Comment about VAR2
4219VAR2 = value2
4220VAR3 = value3
4221"#
4222 .parse()
4223 .unwrap();
4224
4225 let mut var2 = makefile
4227 .variable_definitions()
4228 .nth(1)
4229 .expect("Should have second variable");
4230 var2.remove();
4231
4232 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4235 }
4236
4237 #[test]
4238 fn test_variable_remove_with_multiple_empty_lines() {
4239 let makefile: Makefile = r#"VAR1 = value1
4240
4241
4242# Comment about VAR2
4243VAR2 = value2
4244VAR3 = value3
4245"#
4246 .parse()
4247 .unwrap();
4248
4249 let mut var2 = makefile
4251 .variable_definitions()
4252 .nth(1)
4253 .expect("Should have second variable");
4254 var2.remove();
4255
4256 assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4259 }
4260
4261 #[test]
4262 fn test_rule_remove_with_comment() {
4263 let makefile: Makefile = r#"rule1:
4264 command1
4265
4266# Comment about rule2
4267rule2:
4268 command2
4269rule3:
4270 command3
4271"#
4272 .parse()
4273 .unwrap();
4274
4275 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4277 rule2.remove().unwrap();
4278
4279 assert_eq!(
4282 makefile.code(),
4283 "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4284 );
4285 }
4286
4287 #[test]
4288 fn test_variable_remove_preserves_shebang() {
4289 let makefile: Makefile = r#"#!/usr/bin/make -f
4290# This is a regular comment
4291VAR1 = value1
4292VAR2 = value2
4293"#
4294 .parse()
4295 .unwrap();
4296
4297 let mut var1 = makefile.variable_definitions().next().unwrap();
4299 var1.remove();
4300
4301 let code = makefile.code();
4303 assert!(code.starts_with("#!/usr/bin/make -f"));
4304 assert!(!code.contains("regular comment"));
4305 assert!(!code.contains("VAR1"));
4306 assert!(code.contains("VAR2"));
4307 }
4308
4309 #[test]
4310 fn test_variable_remove_preserves_subsequent_comments() {
4311 let makefile: Makefile = r#"VAR1 = value1
4312# Comment about VAR2
4313VAR2 = value2
4314
4315# Comment about VAR3
4316VAR3 = value3
4317"#
4318 .parse()
4319 .unwrap();
4320
4321 let mut var2 = makefile
4323 .variable_definitions()
4324 .nth(1)
4325 .expect("Should have second variable");
4326 var2.remove();
4327
4328 let code = makefile.code();
4330 assert_eq!(
4331 code,
4332 "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4333 );
4334 }
4335
4336 #[test]
4337 fn test_variable_remove_after_shebang_preserves_empty_line() {
4338 let makefile: Makefile = r#"#!/usr/bin/make -f
4339export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4340
4341%:
4342 dh $@
4343"#
4344 .parse()
4345 .unwrap();
4346
4347 let mut var = makefile.variable_definitions().next().unwrap();
4349 var.remove();
4350
4351 assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
4353 }
4354
4355 #[test]
4356 fn test_rule_add_prerequisite() {
4357 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4358 rule.add_prerequisite("dep2").unwrap();
4359 assert_eq!(
4360 rule.prerequisites().collect::<Vec<_>>(),
4361 vec!["dep1", "dep2"]
4362 );
4363 assert_eq!(rule.to_string(), "target: dep1 dep2\n");
4365 }
4366
4367 #[test]
4368 fn test_rule_add_prerequisite_to_rule_without_prereqs() {
4369 let mut rule: Rule = "target:\n".parse().unwrap();
4371 rule.add_prerequisite("dep1").unwrap();
4372 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
4373 assert_eq!(rule.to_string(), "target: dep1\n");
4375 }
4376
4377 #[test]
4378 fn test_rule_remove_prerequisite() {
4379 let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
4380 assert!(rule.remove_prerequisite("dep2").unwrap());
4381 assert_eq!(
4382 rule.prerequisites().collect::<Vec<_>>(),
4383 vec!["dep1", "dep3"]
4384 );
4385 assert!(!rule.remove_prerequisite("nonexistent").unwrap());
4386 }
4387
4388 #[test]
4389 fn test_rule_set_prerequisites() {
4390 let mut rule: Rule = "target: old_dep\n".parse().unwrap();
4391 rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
4392 .unwrap();
4393 assert_eq!(
4394 rule.prerequisites().collect::<Vec<_>>(),
4395 vec!["new_dep1", "new_dep2"]
4396 );
4397 }
4398
4399 #[test]
4400 fn test_rule_set_prerequisites_empty() {
4401 let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
4402 rule.set_prerequisites(vec![]).unwrap();
4403 assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
4404 }
4405
4406 #[test]
4407 fn test_rule_add_target() {
4408 let mut rule: Rule = "target1: dep1\n".parse().unwrap();
4409 rule.add_target("target2").unwrap();
4410 assert_eq!(
4411 rule.targets().collect::<Vec<_>>(),
4412 vec!["target1", "target2"]
4413 );
4414 }
4415
4416 #[test]
4417 fn test_rule_set_targets() {
4418 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
4419 rule.set_targets(vec!["new_target1", "new_target2"])
4420 .unwrap();
4421 assert_eq!(
4422 rule.targets().collect::<Vec<_>>(),
4423 vec!["new_target1", "new_target2"]
4424 );
4425 }
4426
4427 #[test]
4428 fn test_rule_set_targets_empty() {
4429 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4430 let result = rule.set_targets(vec![]);
4431 assert!(result.is_err());
4432 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
4434 }
4435
4436 #[test]
4437 fn test_rule_has_target() {
4438 let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
4439 assert!(rule.has_target("target1"));
4440 assert!(rule.has_target("target2"));
4441 assert!(!rule.has_target("target3"));
4442 assert!(!rule.has_target("nonexistent"));
4443 }
4444
4445 #[test]
4446 fn test_rule_rename_target() {
4447 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
4448 assert!(rule.rename_target("old_target", "new_target").unwrap());
4449 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
4450 assert!(!rule.rename_target("nonexistent", "something").unwrap());
4452 }
4453
4454 #[test]
4455 fn test_rule_rename_target_multiple() {
4456 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
4457 assert!(rule.rename_target("target2", "renamed_target").unwrap());
4458 assert_eq!(
4459 rule.targets().collect::<Vec<_>>(),
4460 vec!["target1", "renamed_target", "target3"]
4461 );
4462 }
4463
4464 #[test]
4465 fn test_rule_remove_target() {
4466 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
4467 assert!(rule.remove_target("target2").unwrap());
4468 assert_eq!(
4469 rule.targets().collect::<Vec<_>>(),
4470 vec!["target1", "target3"]
4471 );
4472 assert!(!rule.remove_target("nonexistent").unwrap());
4474 }
4475
4476 #[test]
4477 fn test_rule_remove_target_last() {
4478 let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
4479 let result = rule.remove_target("single_target");
4480 assert!(result.is_err());
4481 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
4483 }
4484
4485 #[test]
4486 fn test_rule_target_manipulation_preserves_prerequisites() {
4487 let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
4488
4489 rule.remove_target("target1").unwrap();
4491 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
4492 assert_eq!(
4493 rule.prerequisites().collect::<Vec<_>>(),
4494 vec!["dep1", "dep2"]
4495 );
4496 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4497
4498 rule.add_target("target3").unwrap();
4500 assert_eq!(
4501 rule.targets().collect::<Vec<_>>(),
4502 vec!["target2", "target3"]
4503 );
4504 assert_eq!(
4505 rule.prerequisites().collect::<Vec<_>>(),
4506 vec!["dep1", "dep2"]
4507 );
4508 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4509
4510 rule.rename_target("target2", "renamed").unwrap();
4512 assert_eq!(
4513 rule.targets().collect::<Vec<_>>(),
4514 vec!["renamed", "target3"]
4515 );
4516 assert_eq!(
4517 rule.prerequisites().collect::<Vec<_>>(),
4518 vec!["dep1", "dep2"]
4519 );
4520 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4521 }
4522
4523 #[test]
4524 fn test_rule_remove() {
4525 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4526 let rule = makefile.find_rule_by_target("rule1").unwrap();
4527 rule.remove().unwrap();
4528 assert_eq!(makefile.rules().count(), 1);
4529 assert!(makefile.find_rule_by_target("rule1").is_none());
4530 assert!(makefile.find_rule_by_target("rule2").is_some());
4531 }
4532
4533 #[test]
4534 fn test_rule_remove_last_trims_blank_lines() {
4535 let makefile: Makefile =
4537 "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
4538 .parse()
4539 .unwrap();
4540
4541 let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
4543 rule.remove().unwrap();
4544
4545 assert_eq!(makefile.code(), "%:\n\tdh $@\n");
4547 assert_eq!(makefile.rules().count(), 1);
4548 }
4549
4550 #[test]
4551 fn test_makefile_find_rule_by_target() {
4552 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4553 let rule = makefile.find_rule_by_target("rule2");
4554 assert!(rule.is_some());
4555 assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
4556 assert!(makefile.find_rule_by_target("nonexistent").is_none());
4557 }
4558
4559 #[test]
4560 fn test_makefile_find_rules_by_target() {
4561 let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
4562 .parse()
4563 .unwrap();
4564 assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
4565 assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
4566 assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
4567 }
4568
4569 #[test]
4570 fn test_makefile_find_rule_by_target_pattern_simple() {
4571 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4572 let rule = makefile.find_rule_by_target_pattern("foo.o");
4573 assert!(rule.is_some());
4574 assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
4575 }
4576
4577 #[test]
4578 fn test_makefile_find_rule_by_target_pattern_no_match() {
4579 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4580 let rule = makefile.find_rule_by_target_pattern("foo.c");
4581 assert!(rule.is_none());
4582 }
4583
4584 #[test]
4585 fn test_makefile_find_rule_by_target_pattern_exact() {
4586 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
4587 let rule = makefile.find_rule_by_target_pattern("foo.o");
4588 assert!(rule.is_some());
4589 assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
4590 }
4591
4592 #[test]
4593 fn test_makefile_find_rule_by_target_pattern_prefix() {
4594 let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
4595 let rule = makefile.find_rule_by_target_pattern("libfoo.a");
4596 assert!(rule.is_some());
4597 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
4598 }
4599
4600 #[test]
4601 fn test_makefile_find_rule_by_target_pattern_suffix() {
4602 let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4603 let rule = makefile.find_rule_by_target_pattern("foo_test.o");
4604 assert!(rule.is_some());
4605 assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
4606 }
4607
4608 #[test]
4609 fn test_makefile_find_rule_by_target_pattern_middle() {
4610 let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
4611 let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
4612 assert!(rule.is_some());
4613 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
4614 }
4615
4616 #[test]
4617 fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
4618 let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
4619 let rule = makefile.find_rule_by_target_pattern("anything");
4620 assert!(rule.is_some());
4621 assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
4622 }
4623
4624 #[test]
4625 fn test_makefile_find_rules_by_target_pattern_multiple() {
4626 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
4627 .parse()
4628 .unwrap();
4629 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4630 assert_eq!(rules.len(), 2);
4631 }
4632
4633 #[test]
4634 fn test_makefile_find_rules_by_target_pattern_mixed() {
4635 let makefile: Makefile =
4636 "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
4637 .parse()
4638 .unwrap();
4639 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4640 assert_eq!(rules.len(), 2); let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
4642 assert_eq!(rules.len(), 1); }
4644
4645 #[test]
4646 fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
4647 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
4648 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4649 assert_eq!(rules.len(), 1);
4650 let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
4651 assert_eq!(rules.len(), 0);
4652 }
4653
4654 #[test]
4655 fn test_matches_pattern_exact() {
4656 assert!(matches_pattern("foo.o", "foo.o"));
4657 assert!(!matches_pattern("foo.o", "bar.o"));
4658 }
4659
4660 #[test]
4661 fn test_matches_pattern_suffix() {
4662 assert!(matches_pattern("%.o", "foo.o"));
4663 assert!(matches_pattern("%.o", "bar.o"));
4664 assert!(matches_pattern("%.o", "baz/qux.o"));
4665 assert!(!matches_pattern("%.o", "foo.c"));
4666 }
4667
4668 #[test]
4669 fn test_matches_pattern_prefix() {
4670 assert!(matches_pattern("lib%.a", "libfoo.a"));
4671 assert!(matches_pattern("lib%.a", "libbar.a"));
4672 assert!(!matches_pattern("lib%.a", "foo.a"));
4673 assert!(!matches_pattern("lib%.a", "lib.a"));
4674 }
4675
4676 #[test]
4677 fn test_matches_pattern_middle() {
4678 assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
4679 assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
4680 assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
4681 assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
4682 }
4683
4684 #[test]
4685 fn test_matches_pattern_wildcard_only() {
4686 assert!(matches_pattern("%", "anything"));
4687 assert!(matches_pattern("%", "foo.o"));
4688 assert!(!matches_pattern("%", ""));
4690 }
4691
4692 #[test]
4693 fn test_matches_pattern_empty_stem() {
4694 assert!(!matches_pattern("%.o", ".o")); assert!(!matches_pattern("lib%", "lib")); assert!(!matches_pattern("lib%.a", "lib.a")); }
4699
4700 #[test]
4701 fn test_matches_pattern_multiple_wildcards_not_supported() {
4702 assert!(!matches_pattern("%foo%bar", "xfooybarz"));
4705 assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
4706 }
4707
4708 #[test]
4709 fn test_makefile_add_phony_target() {
4710 let mut makefile = Makefile::new();
4711 makefile.add_phony_target("clean").unwrap();
4712 assert!(makefile.is_phony("clean"));
4713 assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
4714 }
4715
4716 #[test]
4717 fn test_makefile_add_phony_target_existing() {
4718 let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
4719 makefile.add_phony_target("clean").unwrap();
4720 assert!(makefile.is_phony("test"));
4721 assert!(makefile.is_phony("clean"));
4722 let targets: Vec<_> = makefile.phony_targets().collect();
4723 assert!(targets.contains(&"test".to_string()));
4724 assert!(targets.contains(&"clean".to_string()));
4725 }
4726
4727 #[test]
4728 fn test_makefile_remove_phony_target() {
4729 let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
4730 assert!(makefile.remove_phony_target("clean").unwrap());
4731 assert!(!makefile.is_phony("clean"));
4732 assert!(makefile.is_phony("test"));
4733 assert!(!makefile.remove_phony_target("nonexistent").unwrap());
4734 }
4735
4736 #[test]
4737 fn test_makefile_remove_phony_target_last() {
4738 let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
4739 assert!(makefile.remove_phony_target("clean").unwrap());
4740 assert!(!makefile.is_phony("clean"));
4741 assert!(makefile.find_rule_by_target(".PHONY").is_none());
4743 }
4744
4745 #[test]
4746 fn test_makefile_is_phony() {
4747 let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
4748 assert!(makefile.is_phony("clean"));
4749 assert!(makefile.is_phony("test"));
4750 assert!(!makefile.is_phony("build"));
4751 }
4752
4753 #[test]
4754 fn test_makefile_phony_targets() {
4755 let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
4756 let phony_targets: Vec<_> = makefile.phony_targets().collect();
4757 assert_eq!(phony_targets, vec!["clean", "test", "build"]);
4758 }
4759
4760 #[test]
4761 fn test_makefile_phony_targets_empty() {
4762 let makefile = Makefile::new();
4763 assert_eq!(makefile.phony_targets().count(), 0);
4764 }
4765
4766 #[test]
4767 fn test_makefile_remove_first_phony_target_no_extra_space() {
4768 let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
4769 assert!(makefile.remove_phony_target("clean").unwrap());
4770 let result = makefile.to_string();
4771 assert_eq!(result, ".PHONY: test build\n");
4772 }
4773
4774 #[test]
4775 fn test_recipe_with_leading_comments_and_blank_lines() {
4776 let makefile_text = r#"#!/usr/bin/make
4780
4781%:
4782 dh $@
4783
4784override_dh_build:
4785 # The next line is empty
4786
4787 dh_python3
4788"#;
4789 let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
4790
4791 let rules: Vec<_> = makefile.rules().collect();
4792 assert_eq!(rules.len(), 2, "Expected 2 rules");
4793
4794 let rule0 = &rules[0];
4796 assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
4797 assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
4798
4799 let rule1 = &rules[1];
4801 assert_eq!(
4802 rule1.targets().collect::<Vec<_>>(),
4803 vec!["override_dh_build"]
4804 );
4805
4806 let recipes: Vec<_> = rule1.recipes().collect();
4808 assert!(
4809 !recipes.is_empty(),
4810 "Expected at least one recipe for override_dh_build, got none"
4811 );
4812 assert!(
4813 recipes.contains(&"dh_python3".to_string()),
4814 "Expected 'dh_python3' in recipes, got: {:?}",
4815 recipes
4816 );
4817 }
4818
4819 #[test]
4820 fn test_rule_parse_preserves_trailing_blank_lines() {
4821 let input = r#"override_dh_systemd_enable:
4824 dh_systemd_enable -pracoon
4825
4826override_dh_install:
4827 dh_install
4828"#;
4829
4830 let mut mf: Makefile = input.parse().unwrap();
4831
4832 let rule = mf.rules().next().unwrap();
4834 let rule_text = rule.to_string();
4835
4836 assert_eq!(
4838 rule_text,
4839 "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
4840 );
4841
4842 let modified =
4844 rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
4845
4846 let new_rule: Rule = modified.parse().unwrap();
4848 assert_eq!(
4849 new_rule.to_string(),
4850 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
4851 );
4852
4853 mf.replace_rule(0, new_rule).unwrap();
4855
4856 let output = mf.to_string();
4858 assert!(
4859 output.contains(
4860 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
4861 ),
4862 "Blank line between rules should be preserved. Got: {:?}",
4863 output
4864 );
4865 }
4866
4867 #[test]
4868 fn test_rule_parse_round_trip_with_trailing_newlines() {
4869 let test_cases = vec![
4871 "rule:\n\tcommand\n", "rule:\n\tcommand\n\n", "rule:\n\tcommand\n\n\n", ];
4875
4876 for rule_text in test_cases {
4877 let rule: Rule = rule_text.parse().unwrap();
4878 let result = rule.to_string();
4879 assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
4880 }
4881 }
4882
4883 #[test]
4884 fn test_rule_clone() {
4885 let rule_text = "rule:\n\tcommand\n\n";
4887 let rule: Rule = rule_text.parse().unwrap();
4888 let cloned = rule.clone();
4889
4890 assert_eq!(rule.to_string(), cloned.to_string());
4892 assert_eq!(rule.to_string(), rule_text);
4893 assert_eq!(cloned.to_string(), rule_text);
4894
4895 assert_eq!(
4897 rule.targets().collect::<Vec<_>>(),
4898 cloned.targets().collect::<Vec<_>>()
4899 );
4900 assert_eq!(
4901 rule.recipes().collect::<Vec<_>>(),
4902 cloned.recipes().collect::<Vec<_>>()
4903 );
4904 }
4905
4906 #[test]
4907 fn test_makefile_clone() {
4908 let input = "VAR = value\n\nrule:\n\tcommand\n";
4910 let makefile: Makefile = input.parse().unwrap();
4911 let cloned = makefile.clone();
4912
4913 assert_eq!(makefile.to_string(), cloned.to_string());
4915 assert_eq!(makefile.to_string(), input);
4916
4917 assert_eq!(makefile.rules().count(), cloned.rules().count());
4919
4920 assert_eq!(
4922 makefile.variable_definitions().count(),
4923 cloned.variable_definitions().count()
4924 );
4925 }
4926
4927 #[test]
4928 fn test_conditional_with_recipe_line() {
4929 let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
4931 let parsed = parse(input, None);
4932
4933 assert!(
4935 parsed.errors.is_empty(),
4936 "Expected no parse errors, but got: {:?}",
4937 parsed.errors
4938 );
4939
4940 let mf = parsed.root();
4942 assert_eq!(mf.code(), input);
4943 }
4944
4945 #[test]
4946 fn test_conditional_in_rule_recipe() {
4947 let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
4949 let parsed = parse(input, None);
4950
4951 assert!(
4953 parsed.errors.is_empty(),
4954 "Expected no parse errors, but got: {:?}",
4955 parsed.errors
4956 );
4957
4958 let mf = parsed.root();
4960 assert_eq!(mf.code(), input);
4961
4962 assert_eq!(mf.rules().count(), 1);
4964 }
4965
4966 #[test]
4967 fn test_rule_items() {
4968 use crate::RuleItem;
4969
4970 let input = r#"test:
4972 echo "before"
4973ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
4974 ./run-tests
4975endif
4976 echo "after"
4977"#;
4978 let rule: Rule = input.parse().unwrap();
4979
4980 let items: Vec<_> = rule.items().collect();
4981 assert_eq!(
4982 items.len(),
4983 3,
4984 "Expected 3 items: recipe, conditional, recipe"
4985 );
4986
4987 match &items[0] {
4989 RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
4990 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
4991 }
4992
4993 match &items[1] {
4995 RuleItem::Conditional(c) => {
4996 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
4997 }
4998 RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
4999 }
5000
5001 match &items[2] {
5003 RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5004 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5005 }
5006
5007 let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5009 let simple_items: Vec<_> = simple_rule.items().collect();
5010 assert_eq!(simple_items.len(), 2);
5011
5012 match &simple_items[0] {
5013 RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5014 _ => panic!("Expected recipe"),
5015 }
5016
5017 match &simple_items[1] {
5018 RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5019 _ => panic!("Expected recipe"),
5020 }
5021
5022 let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5024 .parse()
5025 .unwrap();
5026 let cond_items: Vec<_> = cond_only.items().collect();
5027 assert_eq!(cond_items.len(), 1);
5028
5029 match &cond_items[0] {
5030 RuleItem::Conditional(c) => {
5031 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5032 }
5033 _ => panic!("Expected conditional"),
5034 }
5035 }
5036
5037 #[test]
5038 fn test_conditionals_iterator() {
5039 let makefile: Makefile = r#"ifdef DEBUG
5040VAR = debug
5041endif
5042
5043ifndef RELEASE
5044OTHER = dev
5045endif
5046"#
5047 .parse()
5048 .unwrap();
5049
5050 let conditionals: Vec<_> = makefile.conditionals().collect();
5051 assert_eq!(conditionals.len(), 2);
5052
5053 assert_eq!(
5054 conditionals[0].conditional_type(),
5055 Some("ifdef".to_string())
5056 );
5057 assert_eq!(
5058 conditionals[1].conditional_type(),
5059 Some("ifndef".to_string())
5060 );
5061 }
5062
5063 #[test]
5064 fn test_conditional_type_and_condition() {
5065 let makefile: Makefile = r#"ifdef DEBUG
5066VAR = debug
5067endif
5068"#
5069 .parse()
5070 .unwrap();
5071
5072 let conditional = makefile.conditionals().next().unwrap();
5073 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5074 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5075 }
5076
5077 #[test]
5078 fn test_conditional_has_else() {
5079 let makefile_with_else: Makefile = r#"ifdef DEBUG
5080VAR = debug
5081else
5082VAR = release
5083endif
5084"#
5085 .parse()
5086 .unwrap();
5087
5088 let conditional = makefile_with_else.conditionals().next().unwrap();
5089 assert!(conditional.has_else());
5090
5091 let makefile_without_else: Makefile = r#"ifdef DEBUG
5092VAR = debug
5093endif
5094"#
5095 .parse()
5096 .unwrap();
5097
5098 let conditional = makefile_without_else.conditionals().next().unwrap();
5099 assert!(!conditional.has_else());
5100 }
5101
5102 #[test]
5103 fn test_conditional_if_body() {
5104 let makefile: Makefile = r#"ifdef DEBUG
5105VAR = debug
5106endif
5107"#
5108 .parse()
5109 .unwrap();
5110
5111 let conditional = makefile.conditionals().next().unwrap();
5112 let if_body = conditional.if_body();
5113 assert!(if_body.is_some());
5114 assert!(if_body.unwrap().contains("VAR = debug"));
5115 }
5116
5117 #[test]
5118 fn test_conditional_else_body() {
5119 let makefile: Makefile = r#"ifdef DEBUG
5120VAR = debug
5121else
5122VAR = release
5123endif
5124"#
5125 .parse()
5126 .unwrap();
5127
5128 let conditional = makefile.conditionals().next().unwrap();
5129 let else_body = conditional.else_body();
5130 assert!(else_body.is_some());
5131 assert!(else_body.unwrap().contains("VAR = release"));
5132 }
5133
5134 #[test]
5135 fn test_add_conditional_ifdef() {
5136 let mut makefile = Makefile::new();
5137 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5138 assert!(result.is_ok());
5139
5140 let code = makefile.to_string();
5141 assert!(code.contains("ifdef DEBUG"));
5142 assert!(code.contains("VAR = debug"));
5143 assert!(code.contains("endif"));
5144 }
5145
5146 #[test]
5147 fn test_add_conditional_with_else() {
5148 let mut makefile = Makefile::new();
5149 let result =
5150 makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5151 assert!(result.is_ok());
5152
5153 let code = makefile.to_string();
5154 assert!(code.contains("ifdef DEBUG"));
5155 assert!(code.contains("VAR = debug"));
5156 assert!(code.contains("else"));
5157 assert!(code.contains("VAR = release"));
5158 assert!(code.contains("endif"));
5159 }
5160
5161 #[test]
5162 fn test_add_conditional_invalid_type() {
5163 let mut makefile = Makefile::new();
5164 let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5165 assert!(result.is_err());
5166 }
5167
5168 #[test]
5169 fn test_add_conditional_formatting() {
5170 let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5171 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5172 assert!(result.is_ok());
5173
5174 let code = makefile.to_string();
5175 assert!(code.contains("\n\nifdef DEBUG"));
5177 }
5178
5179 #[test]
5180 fn test_conditional_remove() {
5181 let makefile: Makefile = r#"ifdef DEBUG
5182VAR = debug
5183endif
5184
5185VAR2 = value2
5186"#
5187 .parse()
5188 .unwrap();
5189
5190 let mut conditional = makefile.conditionals().next().unwrap();
5191 let result = conditional.remove();
5192 assert!(result.is_ok());
5193
5194 let code = makefile.to_string();
5195 assert!(!code.contains("ifdef DEBUG"));
5196 assert!(!code.contains("VAR = debug"));
5197 assert!(code.contains("VAR2 = value2"));
5198 }
5199
5200 #[test]
5201 fn test_add_conditional_ifndef() {
5202 let mut makefile = Makefile::new();
5203 let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5204 assert!(result.is_ok());
5205
5206 let code = makefile.to_string();
5207 assert!(code.contains("ifndef NDEBUG"));
5208 assert!(code.contains("VAR = enabled"));
5209 assert!(code.contains("endif"));
5210 }
5211
5212 #[test]
5213 fn test_add_conditional_ifeq() {
5214 let mut makefile = Makefile::new();
5215 let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5216 assert!(result.is_ok());
5217
5218 let code = makefile.to_string();
5219 assert!(code.contains("ifeq ($(OS),Linux)"));
5220 assert!(code.contains("VAR = linux"));
5221 assert!(code.contains("endif"));
5222 }
5223
5224 #[test]
5225 fn test_add_conditional_ifneq() {
5226 let mut makefile = Makefile::new();
5227 let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5228 assert!(result.is_ok());
5229
5230 let code = makefile.to_string();
5231 assert!(code.contains("ifneq ($(OS),Windows)"));
5232 assert!(code.contains("VAR = unix"));
5233 assert!(code.contains("endif"));
5234 }
5235
5236 #[test]
5237 fn test_conditional_api_integration() {
5238 let mut makefile: Makefile = r#"VAR1 = value1
5240
5241rule1:
5242 command1
5243"#
5244 .parse()
5245 .unwrap();
5246
5247 makefile
5249 .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5250 .unwrap();
5251
5252 assert_eq!(makefile.conditionals().count(), 1);
5254 let conditional = makefile.conditionals().next().unwrap();
5255 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5256 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5257 assert!(conditional.has_else());
5258
5259 assert_eq!(makefile.variable_definitions().count(), 1);
5261 assert_eq!(makefile.rules().count(), 1);
5262 }
5263
5264 #[test]
5265 fn test_conditional_if_items() {
5266 let makefile: Makefile = r#"ifdef DEBUG
5267VAR = debug
5268rule:
5269 command
5270endif
5271"#
5272 .parse()
5273 .unwrap();
5274
5275 let cond = makefile.conditionals().next().unwrap();
5276 let items: Vec<_> = cond.if_items().collect();
5277 assert_eq!(items.len(), 2); match &items[0] {
5280 MakefileItem::Variable(v) => {
5281 assert_eq!(v.name(), Some("VAR".to_string()));
5282 }
5283 _ => panic!("Expected variable"),
5284 }
5285
5286 match &items[1] {
5287 MakefileItem::Rule(r) => {
5288 assert!(r.targets().any(|t| t == "rule"));
5289 }
5290 _ => panic!("Expected rule"),
5291 }
5292 }
5293
5294 #[test]
5295 fn test_conditional_else_items() {
5296 let makefile: Makefile = r#"ifdef DEBUG
5297VAR = debug
5298else
5299VAR2 = release
5300rule2:
5301 command
5302endif
5303"#
5304 .parse()
5305 .unwrap();
5306
5307 let cond = makefile.conditionals().next().unwrap();
5308 let items: Vec<_> = cond.else_items().collect();
5309 assert_eq!(items.len(), 2); match &items[0] {
5312 MakefileItem::Variable(v) => {
5313 assert_eq!(v.name(), Some("VAR2".to_string()));
5314 }
5315 _ => panic!("Expected variable"),
5316 }
5317
5318 match &items[1] {
5319 MakefileItem::Rule(r) => {
5320 assert!(r.targets().any(|t| t == "rule2"));
5321 }
5322 _ => panic!("Expected rule"),
5323 }
5324 }
5325
5326 #[test]
5327 fn test_conditional_add_if_item() {
5328 let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5329 let mut cond = makefile.conditionals().next().unwrap();
5330
5331 let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5333 let var = temp.variable_definitions().next().unwrap();
5334 cond.add_if_item(MakefileItem::Variable(var));
5335
5336 let code = makefile.to_string();
5337 assert!(code.contains("CFLAGS = -g"));
5338
5339 let cond = makefile.conditionals().next().unwrap();
5341 assert_eq!(cond.if_items().count(), 1);
5342 }
5343
5344 #[test]
5345 fn test_conditional_add_else_item() {
5346 let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
5347 let mut cond = makefile.conditionals().next().unwrap();
5348
5349 let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5351 let var = temp.variable_definitions().next().unwrap();
5352 cond.add_else_item(MakefileItem::Variable(var));
5353
5354 let code = makefile.to_string();
5355 assert!(code.contains("else"));
5356 assert!(code.contains("CFLAGS = -O2"));
5357
5358 let cond = makefile.conditionals().next().unwrap();
5360 assert_eq!(cond.else_items().count(), 1);
5361 }
5362
5363 #[test]
5364 fn test_add_conditional_with_items() {
5365 let mut makefile = Makefile::new();
5366
5367 let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
5369 let var1 = temp1.variable_definitions().next().unwrap();
5370
5371 let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5372 let var2 = temp2.variable_definitions().next().unwrap();
5373
5374 let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
5375 let rule1 = temp3.rules().next().unwrap();
5376
5377 let result = makefile.add_conditional_with_items(
5378 "ifdef",
5379 "DEBUG",
5380 vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
5381 Some(vec![MakefileItem::Variable(var2)]),
5382 );
5383
5384 assert!(result.is_ok());
5385
5386 let code = makefile.to_string();
5387 assert!(code.contains("ifdef DEBUG"));
5388 assert!(code.contains("CFLAGS = -g"));
5389 assert!(code.contains("debug:"));
5390 assert!(code.contains("else"));
5391 assert!(code.contains("CFLAGS = -O2"));
5392 }
5393
5394 #[test]
5395 fn test_conditional_items_with_nested_conditional() {
5396 let makefile: Makefile = r#"ifdef DEBUG
5397VAR = debug
5398ifdef VERBOSE
5399 VAR2 = verbose
5400endif
5401endif
5402"#
5403 .parse()
5404 .unwrap();
5405
5406 let cond = makefile.conditionals().next().unwrap();
5407 let items: Vec<_> = cond.if_items().collect();
5408 assert_eq!(items.len(), 2); match &items[0] {
5411 MakefileItem::Variable(v) => {
5412 assert_eq!(v.name(), Some("VAR".to_string()));
5413 }
5414 _ => panic!("Expected variable"),
5415 }
5416
5417 match &items[1] {
5418 MakefileItem::Conditional(c) => {
5419 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
5420 }
5421 _ => panic!("Expected conditional"),
5422 }
5423 }
5424
5425 #[test]
5426 fn test_conditional_items_with_include() {
5427 let makefile: Makefile = r#"ifdef DEBUG
5428include debug.mk
5429VAR = debug
5430endif
5431"#
5432 .parse()
5433 .unwrap();
5434
5435 let cond = makefile.conditionals().next().unwrap();
5436 let items: Vec<_> = cond.if_items().collect();
5437 assert_eq!(items.len(), 2); match &items[0] {
5440 MakefileItem::Include(i) => {
5441 assert_eq!(i.path(), Some("debug.mk".to_string()));
5442 }
5443 _ => panic!("Expected include"),
5444 }
5445
5446 match &items[1] {
5447 MakefileItem::Variable(v) => {
5448 assert_eq!(v.name(), Some("VAR".to_string()));
5449 }
5450 _ => panic!("Expected variable"),
5451 }
5452 }
5453
5454 #[test]
5455 fn test_makefile_items_iterator() {
5456 let makefile: Makefile = r#"VAR = value
5457ifdef DEBUG
5458CFLAGS = -g
5459endif
5460rule:
5461 command
5462include common.mk
5463"#
5464 .parse()
5465 .unwrap();
5466
5467 assert_eq!(makefile.variable_definitions().count(), 1);
5469 assert_eq!(makefile.conditionals().count(), 1);
5470 assert_eq!(makefile.rules().count(), 1);
5471
5472 let items: Vec<_> = makefile.items().collect();
5473 assert!(
5475 items.len() >= 3,
5476 "Expected at least 3 items, got {}",
5477 items.len()
5478 );
5479
5480 match &items[0] {
5481 MakefileItem::Variable(v) => {
5482 assert_eq!(v.name(), Some("VAR".to_string()));
5483 }
5484 _ => panic!("Expected variable at position 0"),
5485 }
5486
5487 match &items[1] {
5488 MakefileItem::Conditional(c) => {
5489 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
5490 }
5491 _ => panic!("Expected conditional at position 1"),
5492 }
5493
5494 match &items[2] {
5495 MakefileItem::Rule(r) => {
5496 let targets: Vec<_> = r.targets().collect();
5497 assert_eq!(targets, vec!["rule"]);
5498 }
5499 _ => panic!("Expected rule at position 2"),
5500 }
5501 }
5502
5503 #[test]
5504 fn test_conditional_unwrap() {
5505 let makefile: Makefile = r#"ifdef DEBUG
5506VAR = debug
5507rule:
5508 command
5509endif
5510"#
5511 .parse()
5512 .unwrap();
5513
5514 let mut cond = makefile.conditionals().next().unwrap();
5515 cond.unwrap().unwrap();
5516
5517 let code = makefile.to_string();
5518 let expected = "VAR = debug\nrule:\n\tcommand\n";
5519 assert_eq!(code, expected);
5520
5521 assert_eq!(makefile.conditionals().count(), 0);
5523
5524 assert_eq!(makefile.variable_definitions().count(), 1);
5526 assert_eq!(makefile.rules().count(), 1);
5527 }
5528
5529 #[test]
5530 fn test_conditional_unwrap_with_else_fails() {
5531 let makefile: Makefile = r#"ifdef DEBUG
5532VAR = debug
5533else
5534VAR = release
5535endif
5536"#
5537 .parse()
5538 .unwrap();
5539
5540 let mut cond = makefile.conditionals().next().unwrap();
5541 let result = cond.unwrap();
5542
5543 assert!(result.is_err());
5544 assert!(result
5545 .unwrap_err()
5546 .to_string()
5547 .contains("Cannot unwrap conditional with else clause"));
5548 }
5549
5550 #[test]
5551 fn test_conditional_unwrap_nested() {
5552 let makefile: Makefile = r#"ifdef OUTER
5553VAR = outer
5554ifdef INNER
5555VAR2 = inner
5556endif
5557endif
5558"#
5559 .parse()
5560 .unwrap();
5561
5562 let mut outer_cond = makefile.conditionals().next().unwrap();
5564 outer_cond.unwrap().unwrap();
5565
5566 let code = makefile.to_string();
5567 let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
5568 assert_eq!(code, expected);
5569 }
5570
5571 #[test]
5572 fn test_conditional_unwrap_empty() {
5573 let makefile: Makefile = r#"ifdef DEBUG
5574endif
5575"#
5576 .parse()
5577 .unwrap();
5578
5579 let mut cond = makefile.conditionals().next().unwrap();
5580 cond.unwrap().unwrap();
5581
5582 let code = makefile.to_string();
5583 assert_eq!(code, "");
5584 }
5585
5586 #[test]
5587 fn test_rule_parent() {
5588 let makefile: Makefile = r#"all:
5589 echo "test"
5590"#
5591 .parse()
5592 .unwrap();
5593
5594 let rule = makefile.rules().next().unwrap();
5595 let parent = rule.parent();
5596 assert!(parent.is_none());
5598 }
5599
5600 #[test]
5601 fn test_item_parent_in_conditional() {
5602 let makefile: Makefile = r#"ifdef DEBUG
5603VAR = debug
5604rule:
5605 command
5606endif
5607"#
5608 .parse()
5609 .unwrap();
5610
5611 let cond = makefile.conditionals().next().unwrap();
5612
5613 let items: Vec<_> = cond.if_items().collect();
5615 assert_eq!(items.len(), 2);
5616
5617 if let MakefileItem::Variable(var) = &items[0] {
5619 let parent = var.parent();
5620 assert!(parent.is_some());
5621 if let Some(MakefileItem::Conditional(_)) = parent {
5622 } else {
5624 panic!("Expected variable parent to be a Conditional");
5625 }
5626 } else {
5627 panic!("Expected first item to be a Variable");
5628 }
5629
5630 if let MakefileItem::Rule(rule) = &items[1] {
5632 let parent = rule.parent();
5633 assert!(parent.is_some());
5634 if let Some(MakefileItem::Conditional(_)) = parent {
5635 } else {
5637 panic!("Expected rule parent to be a Conditional");
5638 }
5639 } else {
5640 panic!("Expected second item to be a Rule");
5641 }
5642 }
5643
5644 #[test]
5645 fn test_nested_conditional_parent() {
5646 let makefile: Makefile = r#"ifdef OUTER
5647VAR = outer
5648ifdef INNER
5649VAR2 = inner
5650endif
5651endif
5652"#
5653 .parse()
5654 .unwrap();
5655
5656 let outer_cond = makefile.conditionals().next().unwrap();
5657
5658 let items: Vec<_> = outer_cond.if_items().collect();
5660
5661 let inner_cond = items
5663 .iter()
5664 .find_map(|item| {
5665 if let MakefileItem::Conditional(c) = item {
5666 Some(c)
5667 } else {
5668 None
5669 }
5670 })
5671 .unwrap();
5672
5673 let parent = inner_cond.parent();
5675 assert!(parent.is_some());
5676 if let Some(MakefileItem::Conditional(_)) = parent {
5677 } else {
5679 panic!("Expected inner conditional's parent to be a Conditional");
5680 }
5681 }
5682
5683 #[test]
5684 fn test_line_col() {
5685 let text = r#"# Comment at line 0
5686VAR1 = value1
5687VAR2 = value2
5688
5689rule1: dep1 dep2
5690 command1
5691 command2
5692
5693rule2:
5694 command3
5695
5696ifdef DEBUG
5697CFLAGS = -g
5698endif
5699"#;
5700 let makefile: Makefile = text.parse().unwrap();
5701
5702 let vars: Vec<_> = makefile.variable_definitions().collect();
5704 assert_eq!(vars.len(), 2);
5705
5706 assert_eq!(vars[0].line(), 1);
5708 assert_eq!(vars[0].column(), 0);
5709 assert_eq!(vars[0].line_col(), (1, 0));
5710
5711 assert_eq!(vars[1].line(), 2);
5713 assert_eq!(vars[1].column(), 0);
5714
5715 let rules: Vec<_> = makefile.rules().collect();
5717 assert_eq!(rules.len(), 2);
5718
5719 assert_eq!(rules[0].line(), 4);
5721 assert_eq!(rules[0].column(), 0);
5722 assert_eq!(rules[0].line_col(), (4, 0));
5723
5724 assert_eq!(rules[1].line(), 8);
5726 assert_eq!(rules[1].column(), 0);
5727
5728 let conditionals: Vec<_> = makefile.conditionals().collect();
5730 assert_eq!(conditionals.len(), 1);
5731
5732 assert_eq!(conditionals[0].line(), 11);
5734 assert_eq!(conditionals[0].column(), 0);
5735 assert_eq!(conditionals[0].line_col(), (11, 0));
5736 }
5737
5738 #[test]
5739 fn test_line_col_multiline() {
5740 let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
5741 let makefile: Makefile = text.parse().unwrap();
5742
5743 let vars: Vec<_> = makefile.variable_definitions().collect();
5745 assert_eq!(vars.len(), 1);
5746 assert_eq!(vars[0].line(), 0);
5747 assert_eq!(vars[0].column(), 0);
5748
5749 let rules: Vec<_> = makefile.rules().collect();
5751 assert_eq!(rules.len(), 1);
5752 assert_eq!(rules[0].line(), 4);
5753 assert_eq!(rules[0].column(), 0);
5754 }
5755
5756 #[test]
5757 fn test_line_col_includes() {
5758 let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
5759 let makefile: Makefile = text.parse().unwrap();
5760
5761 let vars: Vec<_> = makefile.variable_definitions().collect();
5763 assert_eq!(vars[0].line(), 0);
5764
5765 let includes: Vec<_> = makefile.includes().collect();
5767 assert_eq!(includes.len(), 2);
5768 assert_eq!(includes[0].line(), 2);
5769 assert_eq!(includes[0].column(), 0);
5770 assert_eq!(includes[1].line(), 3);
5771 assert_eq!(includes[1].column(), 0);
5772 }
5773
5774 #[test]
5775 fn test_conditional_in_rule_vs_toplevel() {
5776 let text1 = r#"rule:
5778 command
5779ifeq (,$(X))
5780 test
5781endif
5782"#;
5783 let makefile: Makefile = text1.parse().unwrap();
5784 let rules: Vec<_> = makefile.rules().collect();
5785 let conditionals: Vec<_> = makefile.conditionals().collect();
5786
5787 assert_eq!(rules.len(), 1);
5788 assert_eq!(
5789 conditionals.len(),
5790 0,
5791 "Conditional should be part of rule, not top-level"
5792 );
5793
5794 let text2 = r#"rule:
5796 command
5797
5798ifeq (,$(X))
5799 test
5800endif
5801"#;
5802 let makefile: Makefile = text2.parse().unwrap();
5803 let rules: Vec<_> = makefile.rules().collect();
5804 let conditionals: Vec<_> = makefile.conditionals().collect();
5805
5806 assert_eq!(rules.len(), 1);
5807 assert_eq!(
5808 conditionals.len(),
5809 1,
5810 "Conditional after blank line should be top-level"
5811 );
5812 assert_eq!(conditionals[0].line(), 3);
5813 }
5814
5815 #[test]
5816 fn test_nested_conditionals_line_tracking() {
5817 let text = r#"ifdef OUTER
5818VAR1 = value1
5819ifdef INNER
5820VAR2 = value2
5821endif
5822VAR3 = value3
5823endif
5824"#;
5825 let makefile: Makefile = text.parse().unwrap();
5826
5827 let conditionals: Vec<_> = makefile.conditionals().collect();
5828 assert_eq!(
5829 conditionals.len(),
5830 1,
5831 "Only outer conditional should be top-level"
5832 );
5833 assert_eq!(conditionals[0].line(), 0);
5834 assert_eq!(conditionals[0].column(), 0);
5835 }
5836
5837 #[test]
5838 fn test_conditional_else_line_tracking() {
5839 let text = r#"VAR1 = before
5840
5841ifdef DEBUG
5842DEBUG_FLAGS = -g
5843else
5844DEBUG_FLAGS = -O2
5845endif
5846
5847VAR2 = after
5848"#;
5849 let makefile: Makefile = text.parse().unwrap();
5850
5851 let conditionals: Vec<_> = makefile.conditionals().collect();
5852 assert_eq!(conditionals.len(), 1);
5853 assert_eq!(conditionals[0].line(), 2);
5854 assert_eq!(conditionals[0].column(), 0);
5855 }
5856
5857 #[test]
5858 fn test_broken_conditional_endif_without_if() {
5859 let text = "VAR = value\nendif\n";
5861 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5862
5863 let vars: Vec<_> = makefile.variable_definitions().collect();
5865 assert_eq!(vars.len(), 1);
5866 assert_eq!(vars[0].line(), 0);
5867 }
5868
5869 #[test]
5870 fn test_broken_conditional_else_without_if() {
5871 let text = "VAR = value\nelse\nVAR2 = other\n";
5873 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5874
5875 let vars: Vec<_> = makefile.variable_definitions().collect();
5877 assert!(!vars.is_empty(), "Should parse at least the first variable");
5878 assert_eq!(vars[0].line(), 0);
5879 }
5880
5881 #[test]
5882 fn test_broken_conditional_missing_endif() {
5883 let text = r#"ifdef DEBUG
5885DEBUG_FLAGS = -g
5886VAR = value
5887"#;
5888 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5889
5890 assert!(makefile.code().contains("ifdef DEBUG"));
5892 }
5893
5894 #[test]
5895 fn test_multiple_conditionals_line_tracking() {
5896 let text = r#"ifdef A
5897VAR_A = a
5898endif
5899
5900ifdef B
5901VAR_B = b
5902endif
5903
5904ifdef C
5905VAR_C = c
5906endif
5907"#;
5908 let makefile: Makefile = text.parse().unwrap();
5909
5910 let conditionals: Vec<_> = makefile.conditionals().collect();
5911 assert_eq!(conditionals.len(), 3);
5912 assert_eq!(conditionals[0].line(), 0);
5913 assert_eq!(conditionals[1].line(), 4);
5914 assert_eq!(conditionals[2].line(), 8);
5915 }
5916
5917 #[test]
5918 fn test_conditional_with_multiple_else_ifeq() {
5919 let text = r#"ifeq ($(OS),Windows)
5920EXT = .exe
5921else ifeq ($(OS),Linux)
5922EXT = .bin
5923else
5924EXT = .out
5925endif
5926"#;
5927 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5928
5929 let conditionals: Vec<_> = makefile.conditionals().collect();
5930 assert_eq!(conditionals.len(), 1);
5931 assert_eq!(conditionals[0].line(), 0);
5932 assert_eq!(conditionals[0].column(), 0);
5933 }
5934
5935 #[test]
5936 fn test_conditional_types_line_tracking() {
5937 let text = r#"ifdef VAR1
5938A = 1
5939endif
5940
5941ifndef VAR2
5942B = 2
5943endif
5944
5945ifeq ($(X),y)
5946C = 3
5947endif
5948
5949ifneq ($(Y),n)
5950D = 4
5951endif
5952"#;
5953 let makefile: Makefile = text.parse().unwrap();
5954
5955 let conditionals: Vec<_> = makefile.conditionals().collect();
5956 assert_eq!(conditionals.len(), 4);
5957
5958 assert_eq!(conditionals[0].line(), 0); assert_eq!(
5960 conditionals[0].conditional_type(),
5961 Some("ifdef".to_string())
5962 );
5963
5964 assert_eq!(conditionals[1].line(), 4); assert_eq!(
5966 conditionals[1].conditional_type(),
5967 Some("ifndef".to_string())
5968 );
5969
5970 assert_eq!(conditionals[2].line(), 8); assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
5972
5973 assert_eq!(conditionals[3].line(), 12); assert_eq!(
5975 conditionals[3].conditional_type(),
5976 Some("ifneq".to_string())
5977 );
5978 }
5979
5980 #[test]
5981 fn test_conditional_in_rule_with_recipes() {
5982 let text = r#"test:
5983 echo "start"
5984ifdef VERBOSE
5985 echo "verbose mode"
5986endif
5987 echo "end"
5988"#;
5989 let makefile: Makefile = text.parse().unwrap();
5990
5991 let rules: Vec<_> = makefile.rules().collect();
5992 let conditionals: Vec<_> = makefile.conditionals().collect();
5993
5994 assert_eq!(rules.len(), 1);
5995 assert_eq!(rules[0].line(), 0);
5996 assert_eq!(conditionals.len(), 0);
5998 }
5999
6000 #[test]
6001 fn test_broken_conditional_double_else() {
6002 let text = r#"ifdef DEBUG
6004A = 1
6005else
6006B = 2
6007else
6008C = 3
6009endif
6010"#;
6011 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6012
6013 assert!(makefile.code().contains("ifdef DEBUG"));
6015 }
6016
6017 #[test]
6018 fn test_broken_conditional_mismatched_nesting() {
6019 let text = r#"ifdef A
6021VAR = value
6022endif
6023endif
6024"#;
6025 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6026
6027 let conditionals: Vec<_> = makefile.conditionals().collect();
6030 assert!(
6031 !conditionals.is_empty(),
6032 "Should parse at least the first conditional"
6033 );
6034 }
6035
6036 #[test]
6037 fn test_conditional_with_comment_line_tracking() {
6038 let text = r#"# This is a comment
6039ifdef DEBUG
6040# Another comment
6041CFLAGS = -g
6042endif
6043# Final comment
6044"#;
6045 let makefile: Makefile = text.parse().unwrap();
6046
6047 let conditionals: Vec<_> = makefile.conditionals().collect();
6048 assert_eq!(conditionals.len(), 1);
6049 assert_eq!(conditionals[0].line(), 1);
6050 assert_eq!(conditionals[0].column(), 0);
6051 }
6052
6053 #[test]
6054 fn test_conditional_after_variable_with_blank_lines() {
6055 let text = r#"VAR1 = value1
6056
6057
6058ifdef DEBUG
6059VAR2 = value2
6060endif
6061"#;
6062 let makefile: Makefile = text.parse().unwrap();
6063
6064 let vars: Vec<_> = makefile.variable_definitions().collect();
6065 let conditionals: Vec<_> = makefile.conditionals().collect();
6066
6067 assert_eq!(vars.len(), 1);
6068 assert_eq!(vars[0].line(), 0);
6069
6070 assert_eq!(conditionals.len(), 1);
6071 assert_eq!(conditionals[0].line(), 3);
6072 }
6073
6074 #[test]
6075 fn test_empty_conditional_line_tracking() {
6076 let text = r#"ifdef DEBUG
6077endif
6078
6079ifndef RELEASE
6080endif
6081"#;
6082 let makefile: Makefile = text.parse().unwrap();
6083
6084 let conditionals: Vec<_> = makefile.conditionals().collect();
6085 assert_eq!(conditionals.len(), 2);
6086 assert_eq!(conditionals[0].line(), 0);
6087 assert_eq!(conditionals[1].line(), 3);
6088 }
6089
6090 #[test]
6091 fn test_recipe_line_tracking() {
6092 let text = r#"build:
6093 echo "Building..."
6094 gcc -o app main.c
6095 echo "Done"
6096
6097test:
6098 ./run-tests
6099"#;
6100 let makefile: Makefile = text.parse().unwrap();
6101
6102 let rule1 = makefile.rules().next().expect("Should have first rule");
6104 let recipes: Vec<_> = rule1.recipe_nodes().collect();
6105 assert_eq!(recipes.len(), 3);
6106
6107 assert_eq!(recipes[0].text(), "echo \"Building...\"");
6108 assert_eq!(recipes[0].line(), 1);
6109 assert_eq!(recipes[0].column(), 0);
6110
6111 assert_eq!(recipes[1].text(), "gcc -o app main.c");
6112 assert_eq!(recipes[1].line(), 2);
6113 assert_eq!(recipes[1].column(), 0);
6114
6115 assert_eq!(recipes[2].text(), "echo \"Done\"");
6116 assert_eq!(recipes[2].line(), 3);
6117 assert_eq!(recipes[2].column(), 0);
6118
6119 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6121 let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6122 assert_eq!(recipes2.len(), 1);
6123
6124 assert_eq!(recipes2[0].text(), "./run-tests");
6125 assert_eq!(recipes2[0].line(), 6);
6126 assert_eq!(recipes2[0].column(), 0);
6127 }
6128
6129 #[test]
6130 fn test_recipe_with_variables_line_tracking() {
6131 let text = r#"install:
6132 mkdir -p $(DESTDIR)
6133 cp $(BINARY) $(DESTDIR)/
6134"#;
6135 let makefile: Makefile = text.parse().unwrap();
6136 let rule = makefile.rules().next().expect("Should have rule");
6137 let recipes: Vec<_> = rule.recipe_nodes().collect();
6138
6139 assert_eq!(recipes.len(), 2);
6140 assert_eq!(recipes[0].line(), 1);
6141 assert_eq!(recipes[1].line(), 2);
6142 }
6143
6144 #[test]
6145 fn test_recipe_text_no_leading_tab() {
6146 let text = "test:\n\techo hello\n\t\techo nested\n\t echo with spaces\n";
6148 let makefile: Makefile = text.parse().unwrap();
6149 let rule = makefile.rules().next().expect("Should have rule");
6150 let recipes: Vec<_> = rule.recipe_nodes().collect();
6151
6152 assert_eq!(recipes.len(), 3);
6153
6154 eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6156
6157 assert_eq!(recipes[0].text(), "echo hello");
6159
6160 eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6162 assert_eq!(recipes[1].text(), "\techo nested");
6163
6164 eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6166 assert_eq!(recipes[2].text(), " echo with spaces");
6167 }
6168
6169 #[test]
6170 fn test_recipe_parent() {
6171 let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6172 let rule = makefile.rules().next().unwrap();
6173 let recipe = rule.recipe_nodes().next().unwrap();
6174
6175 let parent = recipe.parent().expect("Recipe should have parent");
6176 assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6177 assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6178 }
6179
6180 #[test]
6181 fn test_recipe_is_silent_various_prefixes() {
6182 let makefile: Makefile = r#"test:
6183 @echo silent
6184 -echo ignore
6185 +echo always
6186 @-echo silent_ignore
6187 -@echo ignore_silent
6188 +@echo always_silent
6189 echo normal
6190"#
6191 .parse()
6192 .unwrap();
6193
6194 let rule = makefile.rules().next().unwrap();
6195 let recipes: Vec<_> = rule.recipe_nodes().collect();
6196
6197 assert_eq!(recipes.len(), 7);
6198 assert!(recipes[0].is_silent(), "@echo should be silent");
6199 assert!(!recipes[1].is_silent(), "-echo should not be silent");
6200 assert!(!recipes[2].is_silent(), "+echo should not be silent");
6201 assert!(recipes[3].is_silent(), "@-echo should be silent");
6202 assert!(recipes[4].is_silent(), "-@echo should be silent");
6203 assert!(recipes[5].is_silent(), "+@echo should be silent");
6204 assert!(!recipes[6].is_silent(), "echo should not be silent");
6205 }
6206
6207 #[test]
6208 fn test_recipe_is_ignore_errors_various_prefixes() {
6209 let makefile: Makefile = r#"test:
6210 @echo silent
6211 -echo ignore
6212 +echo always
6213 @-echo silent_ignore
6214 -@echo ignore_silent
6215 +-echo always_ignore
6216 echo normal
6217"#
6218 .parse()
6219 .unwrap();
6220
6221 let rule = makefile.rules().next().unwrap();
6222 let recipes: Vec<_> = rule.recipe_nodes().collect();
6223
6224 assert_eq!(recipes.len(), 7);
6225 assert!(
6226 !recipes[0].is_ignore_errors(),
6227 "@echo should not ignore errors"
6228 );
6229 assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6230 assert!(
6231 !recipes[2].is_ignore_errors(),
6232 "+echo should not ignore errors"
6233 );
6234 assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6235 assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6236 assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6237 assert!(
6238 !recipes[6].is_ignore_errors(),
6239 "echo should not ignore errors"
6240 );
6241 }
6242
6243 #[test]
6244 fn test_recipe_set_prefix_add() {
6245 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6246 let rule = makefile.rules().next().unwrap();
6247 let mut recipe = rule.recipe_nodes().next().unwrap();
6248
6249 recipe.set_prefix("@");
6250 assert_eq!(recipe.text(), "@echo hello");
6251 assert!(recipe.is_silent());
6252 }
6253
6254 #[test]
6255 fn test_recipe_set_prefix_change() {
6256 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6257 let rule = makefile.rules().next().unwrap();
6258 let mut recipe = rule.recipe_nodes().next().unwrap();
6259
6260 recipe.set_prefix("-");
6261 assert_eq!(recipe.text(), "-echo hello");
6262 assert!(!recipe.is_silent());
6263 assert!(recipe.is_ignore_errors());
6264 }
6265
6266 #[test]
6267 fn test_recipe_set_prefix_remove() {
6268 let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
6269 let rule = makefile.rules().next().unwrap();
6270 let mut recipe = rule.recipe_nodes().next().unwrap();
6271
6272 recipe.set_prefix("");
6273 assert_eq!(recipe.text(), "echo hello");
6274 assert!(!recipe.is_silent());
6275 assert!(!recipe.is_ignore_errors());
6276 }
6277
6278 #[test]
6279 fn test_recipe_set_prefix_combinations() {
6280 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6281 let rule = makefile.rules().next().unwrap();
6282 let mut recipe = rule.recipe_nodes().next().unwrap();
6283
6284 recipe.set_prefix("@-");
6285 assert_eq!(recipe.text(), "@-echo hello");
6286 assert!(recipe.is_silent());
6287 assert!(recipe.is_ignore_errors());
6288
6289 recipe.set_prefix("-@");
6290 assert_eq!(recipe.text(), "-@echo hello");
6291 assert!(recipe.is_silent());
6292 assert!(recipe.is_ignore_errors());
6293 }
6294
6295 #[test]
6296 fn test_recipe_replace_text_basic() {
6297 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6298 let rule = makefile.rules().next().unwrap();
6299 let mut recipe = rule.recipe_nodes().next().unwrap();
6300
6301 recipe.replace_text("echo world");
6302 assert_eq!(recipe.text(), "echo world");
6303
6304 let rule = makefile.rules().next().unwrap();
6306 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6307 }
6308
6309 #[test]
6310 fn test_recipe_replace_text_with_prefix() {
6311 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6312 let rule = makefile.rules().next().unwrap();
6313 let mut recipe = rule.recipe_nodes().next().unwrap();
6314
6315 recipe.replace_text("@echo goodbye");
6316 assert_eq!(recipe.text(), "@echo goodbye");
6317 assert!(recipe.is_silent());
6318 }
6319
6320 #[test]
6321 fn test_recipe_insert_before_single() {
6322 let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6323 let rule = makefile.rules().next().unwrap();
6324 let recipe = rule.recipe_nodes().next().unwrap();
6325
6326 recipe.insert_before("echo hello");
6327
6328 let rule = makefile.rules().next().unwrap();
6329 let recipes: Vec<_> = rule.recipes().collect();
6330 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6331 }
6332
6333 #[test]
6334 fn test_recipe_insert_before_multiple() {
6335 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6336 .parse()
6337 .unwrap();
6338 let rule = makefile.rules().next().unwrap();
6339 let recipes: Vec<_> = rule.recipe_nodes().collect();
6340
6341 recipes[1].insert_before("echo middle");
6343
6344 let rule = makefile.rules().next().unwrap();
6345 let new_recipes: Vec<_> = rule.recipes().collect();
6346 assert_eq!(
6347 new_recipes,
6348 vec!["echo one", "echo middle", "echo two", "echo three"]
6349 );
6350 }
6351
6352 #[test]
6353 fn test_recipe_insert_before_first() {
6354 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6355 let rule = makefile.rules().next().unwrap();
6356 let recipes: Vec<_> = rule.recipe_nodes().collect();
6357
6358 recipes[0].insert_before("echo zero");
6359
6360 let rule = makefile.rules().next().unwrap();
6361 let new_recipes: Vec<_> = rule.recipes().collect();
6362 assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
6363 }
6364
6365 #[test]
6366 fn test_recipe_insert_after_single() {
6367 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6368 let rule = makefile.rules().next().unwrap();
6369 let recipe = rule.recipe_nodes().next().unwrap();
6370
6371 recipe.insert_after("echo world");
6372
6373 let rule = makefile.rules().next().unwrap();
6374 let recipes: Vec<_> = rule.recipes().collect();
6375 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6376 }
6377
6378 #[test]
6379 fn test_recipe_insert_after_multiple() {
6380 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6381 .parse()
6382 .unwrap();
6383 let rule = makefile.rules().next().unwrap();
6384 let recipes: Vec<_> = rule.recipe_nodes().collect();
6385
6386 recipes[1].insert_after("echo middle");
6388
6389 let rule = makefile.rules().next().unwrap();
6390 let new_recipes: Vec<_> = rule.recipes().collect();
6391 assert_eq!(
6392 new_recipes,
6393 vec!["echo one", "echo two", "echo middle", "echo three"]
6394 );
6395 }
6396
6397 #[test]
6398 fn test_recipe_insert_after_last() {
6399 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6400 let rule = makefile.rules().next().unwrap();
6401 let recipes: Vec<_> = rule.recipe_nodes().collect();
6402
6403 recipes[1].insert_after("echo three");
6404
6405 let rule = makefile.rules().next().unwrap();
6406 let new_recipes: Vec<_> = rule.recipes().collect();
6407 assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
6408 }
6409
6410 #[test]
6411 fn test_recipe_remove_single() {
6412 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6413 let rule = makefile.rules().next().unwrap();
6414 let recipe = rule.recipe_nodes().next().unwrap();
6415
6416 recipe.remove();
6417
6418 let rule = makefile.rules().next().unwrap();
6419 assert_eq!(rule.recipes().count(), 0);
6420 }
6421
6422 #[test]
6423 fn test_recipe_remove_first() {
6424 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6425 .parse()
6426 .unwrap();
6427 let rule = makefile.rules().next().unwrap();
6428 let recipes: Vec<_> = rule.recipe_nodes().collect();
6429
6430 recipes[0].remove();
6431
6432 let rule = makefile.rules().next().unwrap();
6433 let new_recipes: Vec<_> = rule.recipes().collect();
6434 assert_eq!(new_recipes, vec!["echo two", "echo three"]);
6435 }
6436
6437 #[test]
6438 fn test_recipe_remove_middle() {
6439 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6440 .parse()
6441 .unwrap();
6442 let rule = makefile.rules().next().unwrap();
6443 let recipes: Vec<_> = rule.recipe_nodes().collect();
6444
6445 recipes[1].remove();
6446
6447 let rule = makefile.rules().next().unwrap();
6448 let new_recipes: Vec<_> = rule.recipes().collect();
6449 assert_eq!(new_recipes, vec!["echo one", "echo three"]);
6450 }
6451
6452 #[test]
6453 fn test_recipe_remove_last() {
6454 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6455 .parse()
6456 .unwrap();
6457 let rule = makefile.rules().next().unwrap();
6458 let recipes: Vec<_> = rule.recipe_nodes().collect();
6459
6460 recipes[2].remove();
6461
6462 let rule = makefile.rules().next().unwrap();
6463 let new_recipes: Vec<_> = rule.recipes().collect();
6464 assert_eq!(new_recipes, vec!["echo one", "echo two"]);
6465 }
6466
6467 #[test]
6468 fn test_recipe_multiple_operations() {
6469 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6470 let rule = makefile.rules().next().unwrap();
6471 let mut recipe = rule.recipe_nodes().next().unwrap();
6472
6473 recipe.replace_text("echo modified");
6475 assert_eq!(recipe.text(), "echo modified");
6476
6477 recipe.set_prefix("@");
6479 assert_eq!(recipe.text(), "@echo modified");
6480
6481 recipe.insert_after("echo three");
6483
6484 let rule = makefile.rules().next().unwrap();
6486 let recipes: Vec<_> = rule.recipes().collect();
6487 assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
6488 }
6489}