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 loop {
201 let mut last_text_content: Option<String> = None;
202
203 while self.current().is_some() && self.current() != Some(NEWLINE) {
205 if self.current() == Some(TEXT) {
207 if let Some((_kind, text)) = self.tokens.last() {
208 last_text_content = Some(text.clone());
209 }
210 }
211 self.bump();
212 }
213
214 if self.current() == Some(NEWLINE) {
216 self.bump();
217 }
218
219 let is_continuation = last_text_content
221 .as_ref()
222 .map(|text| text.trim_end().ends_with('\\'))
223 .unwrap_or(false);
224
225 if is_continuation {
226 if self.current() == Some(INDENT) {
228 self.bump();
229 continue;
231 } else {
232 break;
234 }
235 } else {
236 break;
238 }
239 }
240
241 self.builder.finish_node();
242 }
243
244 fn parse_rule_target(&mut self) -> bool {
245 match self.current() {
246 Some(IDENTIFIER) => {
247 if self.is_archive_member() {
249 self.parse_archive_member();
250 } else {
251 self.bump();
252 }
253 true
254 }
255 Some(DOLLAR) => {
256 self.parse_variable_reference();
257 true
258 }
259 _ => {
260 self.error("expected rule target".to_string());
261 false
262 }
263 }
264 }
265
266 fn is_archive_member(&self) -> bool {
267 if self.tokens.len() < 2 {
270 return false;
271 }
272
273 let current_is_identifier = self.current() == Some(IDENTIFIER);
275 let next_is_lparen =
276 self.tokens.len() > 1 && self.tokens[self.tokens.len() - 2].0 == LPAREN;
277
278 current_is_identifier && next_is_lparen
279 }
280
281 fn parse_archive_member(&mut self) {
282 if self.current() == Some(IDENTIFIER) {
293 self.bump();
294 }
295
296 if self.current() == Some(LPAREN) {
298 self.bump();
299
300 self.builder.start_node(ARCHIVE_MEMBERS.into());
302
303 while self.current().is_some() && self.current() != Some(RPAREN) {
305 match self.current() {
306 Some(IDENTIFIER) | Some(TEXT) => {
307 self.builder.start_node(ARCHIVE_MEMBER.into());
309 self.bump();
310 self.builder.finish_node();
311 }
312 Some(WHITESPACE) => self.bump(),
313 Some(DOLLAR) => {
314 self.builder.start_node(ARCHIVE_MEMBER.into());
316 self.parse_variable_reference();
317 self.builder.finish_node();
318 }
319 _ => break,
320 }
321 }
322
323 self.builder.finish_node();
325
326 if self.current() == Some(RPAREN) {
328 self.bump();
329 } else {
330 self.error("expected ')' to close archive member".to_string());
331 }
332 }
333 }
334
335 fn parse_rule_dependencies(&mut self) {
336 self.builder.start_node(PREREQUISITES.into());
337
338 while self.current().is_some() && self.current() != Some(NEWLINE) {
339 match self.current() {
340 Some(WHITESPACE) => {
341 self.bump(); }
343 Some(IDENTIFIER) => {
344 self.builder.start_node(PREREQUISITE.into());
346
347 if self.is_archive_member() {
348 self.parse_archive_member();
349 } else {
350 self.bump(); }
352
353 self.builder.finish_node(); }
355 Some(DOLLAR) => {
356 self.builder.start_node(PREREQUISITE.into());
358
359 self.bump(); if self.current() == Some(LPAREN) {
363 self.bump(); let mut paren_count = 1;
365
366 while self.current().is_some() && paren_count > 0 {
367 if self.current() == Some(LPAREN) {
368 paren_count += 1;
369 } else if self.current() == Some(RPAREN) {
370 paren_count -= 1;
371 }
372 self.bump();
373 }
374 } else {
375 if self.current().is_some() {
377 self.bump();
378 }
379 }
380
381 self.builder.finish_node(); }
383 _ => {
384 self.bump();
386 }
387 }
388 }
389
390 self.builder.finish_node(); }
392
393 fn parse_rule_recipes(&mut self) {
394 let mut conditional_depth = 0;
396 let mut newline_count = 0;
398
399 loop {
400 match self.current() {
401 Some(INDENT) => {
402 newline_count = 0;
403 self.parse_recipe_line();
404 }
405 Some(NEWLINE) => {
406 newline_count += 1;
407 self.bump();
408 }
409 Some(COMMENT) => {
410 if conditional_depth == 0 && newline_count >= 1 {
412 break;
413 }
414 newline_count = 0;
415 self.parse_comment();
416 }
417 Some(IDENTIFIER) => {
418 let token = &self.tokens.last().unwrap().1.clone();
419 if (token == "ifdef"
421 || token == "ifndef"
422 || token == "ifeq"
423 || token == "ifneq")
424 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
425 {
426 if conditional_depth == 0 && newline_count >= 1 {
429 break;
430 }
431 newline_count = 0;
432 conditional_depth += 1;
433 self.parse_conditional();
434 conditional_depth -= 1;
437 } else if token == "include" || token == "-include" || token == "sinclude" {
438 if conditional_depth == 0 && newline_count >= 1 {
440 break;
441 }
442 newline_count = 0;
443 self.parse_include();
444 } else if token == "else" || token == "endif" {
445 break;
448 } else {
449 if conditional_depth == 0 {
451 break;
452 }
453 break;
456 }
457 }
458 _ => break,
459 }
460 }
461 }
462
463 fn find_and_consume_colon(&mut self) -> bool {
464 self.skip_ws();
466
467 if self.current() == Some(OPERATOR) && self.tokens.last().unwrap().1 == ":" {
469 self.bump();
470 return true;
471 }
472
473 let has_colon = self
475 .tokens
476 .iter()
477 .rev()
478 .take_while(|(kind, _)| *kind != NEWLINE)
479 .any(|(kind, text)| *kind == OPERATOR && text == ":");
480
481 if has_colon {
482 while self.current().is_some() && self.current() != Some(NEWLINE) {
484 if self.current() == Some(OPERATOR)
485 && self.tokens.last().map(|(_, text)| text.as_str()) == Some(":")
486 {
487 self.bump();
488 return true;
489 }
490 self.bump();
491 }
492 }
493
494 self.error("expected ':'".to_string());
495 false
496 }
497
498 fn parse_rule(&mut self) {
499 self.builder.start_node(RULE.into());
500
501 self.skip_ws();
503 self.builder.start_node(TARGETS.into());
504 let has_target = self.parse_rule_targets();
505 self.builder.finish_node();
506
507 let has_colon = if has_target {
509 self.find_and_consume_colon()
510 } else {
511 false
512 };
513
514 if has_target && has_colon {
516 self.skip_ws();
517 self.parse_rule_dependencies();
518 self.expect_eol();
519
520 self.parse_rule_recipes();
522 }
523
524 self.builder.finish_node();
525 }
526
527 fn parse_rule_targets(&mut self) -> bool {
528 let has_first_target = self.parse_rule_target();
530
531 if !has_first_target {
532 return false;
533 }
534
535 loop {
537 self.skip_ws();
538
539 if self.current() == Some(OPERATOR) && self.tokens.last().unwrap().1 == ":" {
541 break;
542 }
543
544 match self.current() {
546 Some(IDENTIFIER) | Some(DOLLAR) => {
547 if !self.parse_rule_target() {
548 break;
549 }
550 }
551 _ => break,
552 }
553 }
554
555 true
556 }
557
558 fn parse_comment(&mut self) {
559 if self.current() == Some(COMMENT) {
560 self.bump(); if self.current() == Some(NEWLINE) {
564 self.bump(); } else if self.current() == Some(WHITESPACE) {
566 self.skip_ws();
568 if self.current() == Some(NEWLINE) {
569 self.bump();
570 }
571 }
572 } else {
574 self.error("expected comment".to_string());
575 }
576 }
577
578 fn parse_assignment(&mut self) {
579 self.builder.start_node(VARIABLE.into());
580
581 self.skip_ws();
583 if self.current() == Some(IDENTIFIER) && self.tokens.last().unwrap().1 == "export" {
584 self.bump();
585 self.skip_ws();
586 }
587
588 match self.current() {
590 Some(IDENTIFIER) => self.bump(),
591 Some(DOLLAR) => self.parse_variable_reference(),
592 _ => {
593 self.error("expected variable name".to_string());
594 self.builder.finish_node();
595 return;
596 }
597 }
598
599 self.skip_ws();
601 match self.current() {
602 Some(OPERATOR) => {
603 let op = &self.tokens.last().unwrap().1;
604 if ["=", ":=", "::=", ":::=", "+=", "?=", "!="].contains(&op.as_str()) {
605 self.bump();
606 self.skip_ws();
607
608 self.builder.start_node(EXPR.into());
610 while self.current().is_some() && self.current() != Some(NEWLINE) {
611 self.bump();
612 }
613 self.builder.finish_node();
614
615 if self.current() == Some(NEWLINE) {
617 self.bump();
618 } else {
619 self.error("expected newline after variable value".to_string());
620 }
621 } else {
622 self.error(format!("invalid assignment operator: {}", op));
623 }
624 }
625 Some(NEWLINE) => {
627 self.bump();
628 }
629 None => {
630 }
632 _ => self.error("expected assignment operator".to_string()),
633 }
634
635 self.builder.finish_node();
636 }
637
638 fn parse_variable_reference(&mut self) {
639 self.builder.start_node(EXPR.into());
640 self.bump(); if self.current() == Some(LPAREN) {
643 self.bump(); let mut is_function = false;
647
648 if self.current() == Some(IDENTIFIER) {
649 let function_name = &self.tokens.last().unwrap().1;
650 let known_functions = [
652 "shell", "wildcard", "call", "eval", "file", "abspath", "dir",
653 ];
654 if known_functions.contains(&function_name.as_str()) {
655 is_function = true;
656 }
657 }
658
659 if is_function {
660 self.bump();
662
663 self.consume_balanced_parens(1);
665 } else {
666 self.parse_parenthesized_expr_internal(true);
668 }
669 } else {
670 self.error("expected ( after $ in variable reference".to_string());
671 }
672
673 self.builder.finish_node();
674 }
675
676 fn parse_parenthesized_expr(&mut self) {
679 self.builder.start_node(EXPR.into());
680
681 if self.current() == Some(LPAREN) {
683 self.bump(); self.parse_parenthesized_expr_internal(false);
686 } else if self.current() == Some(QUOTE) {
687 self.parse_quoted_comparison();
689 } else {
690 self.error("expected opening parenthesis or quote".to_string());
691 }
692
693 self.builder.finish_node();
694 }
695
696 fn parse_parenthesized_expr_internal(&mut self, is_variable_ref: bool) {
698 let mut paren_count = 1;
699
700 while paren_count > 0 && self.current().is_some() {
701 match self.current() {
702 Some(LPAREN) => {
703 paren_count += 1;
704 self.bump();
705 self.builder.start_node(EXPR.into());
707 }
708 Some(RPAREN) => {
709 paren_count -= 1;
710 self.bump();
711 if paren_count > 0 {
712 self.builder.finish_node();
713 }
714 }
715 Some(QUOTE) => {
716 self.parse_quoted_string();
718 }
719 Some(DOLLAR) => {
720 self.parse_variable_reference();
722 }
723 Some(_) => self.bump(),
724 None => {
725 self.error(if is_variable_ref {
726 "unclosed variable reference".to_string()
727 } else {
728 "unclosed parenthesis".to_string()
729 });
730 break;
731 }
732 }
733 }
734
735 if !is_variable_ref {
736 self.skip_ws();
737 self.expect_eol();
738 }
739 }
740
741 fn parse_quoted_comparison(&mut self) {
744 if self.current() == Some(QUOTE) {
746 self.bump(); } else {
748 self.error("expected first quoted argument".to_string());
749 }
750
751 self.skip_ws();
753
754 if self.current() == Some(QUOTE) {
756 self.bump(); } else {
758 self.error("expected second quoted argument".to_string());
759 }
760
761 self.skip_ws();
763 self.expect_eol();
764 }
765
766 fn parse_quoted_string(&mut self) {
768 self.bump(); while !self.is_at_eof() && self.current() != Some(QUOTE) {
770 self.bump();
771 }
772 if self.current() == Some(QUOTE) {
773 self.bump();
774 }
775 }
776
777 fn parse_conditional_keyword(&mut self) -> Option<String> {
778 if self.current() != Some(IDENTIFIER) {
779 self.error(
780 "expected conditional keyword (ifdef, ifndef, ifeq, or ifneq)".to_string(),
781 );
782 return None;
783 }
784
785 let token = self.tokens.last().unwrap().1.clone();
786 if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&token.as_str()) {
787 self.error(format!("unknown conditional directive: {}", token));
788 return None;
789 }
790
791 self.bump();
792 Some(token)
793 }
794
795 fn parse_simple_condition(&mut self) {
796 self.builder.start_node(EXPR.into());
797
798 self.skip_ws();
800
801 let mut found_var = false;
803
804 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
805 match self.current() {
806 Some(WHITESPACE) => self.skip_ws(),
807 Some(DOLLAR) => {
808 found_var = true;
809 self.parse_variable_reference();
810 }
811 Some(_) => {
812 found_var = true;
814 self.bump();
815 }
816 None => break,
817 }
818 }
819
820 if !found_var {
821 self.error("expected condition after conditional directive".to_string());
823 }
824
825 self.builder.finish_node();
826
827 if self.current() == Some(NEWLINE) {
829 self.bump();
830 } else if !self.is_at_eof() {
831 self.skip_until_newline();
832 }
833 }
834
835 fn is_conditional_directive(&self, token: &str) -> bool {
837 token == "ifdef"
838 || token == "ifndef"
839 || token == "ifeq"
840 || token == "ifneq"
841 || token == "else"
842 || token == "endif"
843 }
844
845 fn handle_conditional_token(&mut self, token: &str, depth: &mut usize) -> bool {
847 match token {
848 "ifdef" | "ifndef" | "ifeq" | "ifneq"
849 if matches!(self.variant, None | Some(MakefileVariant::GNUMake)) =>
850 {
851 self.parse_conditional();
854 true
855 }
856 "else" => {
857 if *depth == 0 {
859 self.error("else without matching if".to_string());
860 self.bump();
862 false
863 } else {
864 self.builder.start_node(CONDITIONAL_ELSE.into());
866
867 self.bump();
869 self.skip_ws();
870
871 if self.current() == Some(IDENTIFIER) {
873 let next_token = &self.tokens.last().unwrap().1;
874 if next_token == "ifdef"
875 || next_token == "ifndef"
876 || next_token == "ifeq"
877 || next_token == "ifneq"
878 {
879 match next_token.as_str() {
882 "ifdef" | "ifndef" => {
883 self.bump(); self.skip_ws();
885 self.parse_simple_condition();
886 }
887 "ifeq" | "ifneq" => {
888 self.bump(); self.skip_ws();
890 self.parse_parenthesized_expr();
891 }
892 _ => unreachable!(),
893 }
894 } else {
896 }
899 } else {
900 }
902
903 self.builder.finish_node(); true
905 }
906 }
907 "endif" => {
908 if *depth == 0 {
910 self.error("endif without matching if".to_string());
911 self.bump();
913 false
914 } else {
915 *depth -= 1;
916
917 self.builder.start_node(CONDITIONAL_ENDIF.into());
919
920 self.bump();
922
923 self.skip_ws();
925
926 if self.current() == Some(COMMENT) {
931 self.parse_comment();
932 } else if self.current() == Some(NEWLINE) {
933 self.bump();
934 } else if self.current() == Some(WHITESPACE) {
935 self.skip_ws();
937 if self.current() == Some(NEWLINE) {
938 self.bump();
939 }
940 } else if !self.is_at_eof() {
942 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
945 self.bump();
946 }
947 if self.current() == Some(NEWLINE) {
948 self.bump();
949 }
950 }
951 self.builder.finish_node(); true
955 }
956 }
957 _ => false,
958 }
959 }
960
961 fn parse_conditional(&mut self) {
962 self.builder.start_node(CONDITIONAL.into());
963
964 self.builder.start_node(CONDITIONAL_IF.into());
966
967 let Some(token) = self.parse_conditional_keyword() else {
969 self.skip_until_newline();
970 self.builder.finish_node(); self.builder.finish_node(); return;
973 };
974
975 self.skip_ws();
977
978 match token.as_str() {
980 "ifdef" | "ifndef" => {
981 self.parse_simple_condition();
982 }
983 "ifeq" | "ifneq" => {
984 self.parse_parenthesized_expr();
985 }
986 _ => unreachable!("Invalid conditional token"),
987 }
988
989 self.skip_ws();
991 if self.current() == Some(COMMENT) {
992 self.parse_comment();
993 }
994 self.builder.finish_node(); let mut depth = 1;
1000
1001 let mut position_count = std::collections::HashMap::<usize, usize>::new();
1003 let max_repetitions = 15; while depth > 0 && !self.is_at_eof() {
1006 let current_pos = self.tokens.len();
1008 *position_count.entry(current_pos).or_insert(0) += 1;
1009
1010 if position_count.get(¤t_pos).unwrap() > &max_repetitions {
1013 break;
1016 }
1017
1018 match self.current() {
1019 None => {
1020 self.error("unterminated conditional (missing endif)".to_string());
1021 break;
1022 }
1023 Some(IDENTIFIER) => {
1024 let token = self.tokens.last().unwrap().1.clone();
1025 if !self.handle_conditional_token(&token, &mut depth) {
1026 if token == "include" || token == "-include" || token == "sinclude" {
1027 self.parse_include();
1028 } else {
1029 self.parse_normal_content();
1030 }
1031 }
1032 }
1033 Some(INDENT) => self.parse_recipe_line(),
1034 Some(WHITESPACE) => self.bump(),
1035 Some(COMMENT) => self.parse_comment(),
1036 Some(NEWLINE) => self.bump(),
1037 Some(DOLLAR) => self.parse_normal_content(),
1038 Some(QUOTE) => self.parse_quoted_string(),
1039 Some(_) => {
1040 self.bump();
1042 }
1043 }
1044 }
1045
1046 self.builder.finish_node();
1047 }
1048
1049 fn parse_normal_content(&mut self) {
1051 self.skip_ws();
1053
1054 if self.is_assignment_line() {
1056 self.parse_assignment();
1057 } else {
1058 self.parse_rule();
1060 }
1061 }
1062
1063 fn parse_include(&mut self) {
1064 self.builder.start_node(INCLUDE.into());
1065
1066 if self.current() != Some(IDENTIFIER)
1068 || (!["include", "-include", "sinclude"]
1069 .contains(&self.tokens.last().unwrap().1.as_str()))
1070 {
1071 self.error("expected include directive".to_string());
1072 self.builder.finish_node();
1073 return;
1074 }
1075 self.bump();
1076 self.skip_ws();
1077
1078 self.builder.start_node(EXPR.into());
1080 let mut found_path = false;
1081
1082 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1083 match self.current() {
1084 Some(WHITESPACE) => self.skip_ws(),
1085 Some(DOLLAR) => {
1086 found_path = true;
1087 self.parse_variable_reference();
1088 }
1089 Some(_) => {
1090 found_path = true;
1092 self.bump();
1093 }
1094 None => break,
1095 }
1096 }
1097
1098 if !found_path {
1099 self.error("expected file path after include".to_string());
1100 }
1101
1102 self.builder.finish_node();
1103
1104 if self.current() == Some(NEWLINE) {
1106 self.bump();
1107 } else if !self.is_at_eof() {
1108 self.error("expected newline after include".to_string());
1109 self.skip_until_newline();
1110 }
1111
1112 self.builder.finish_node();
1113 }
1114
1115 fn parse_identifier_token(&mut self) -> bool {
1116 let token = &self.tokens.last().unwrap().1;
1117
1118 if token.starts_with("%") {
1120 self.parse_rule();
1121 return true;
1122 }
1123
1124 if token.starts_with("if")
1125 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1126 {
1127 self.parse_conditional();
1128 return true;
1129 }
1130
1131 if token == "include" || token == "-include" || token == "sinclude" {
1132 self.parse_include();
1133 return true;
1134 }
1135
1136 self.parse_normal_content();
1138 true
1139 }
1140
1141 fn parse_token(&mut self) -> bool {
1142 match self.current() {
1143 None => false,
1144 Some(IDENTIFIER) => {
1145 let token = &self.tokens.last().unwrap().1;
1146 if self.is_conditional_directive(token)
1147 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1148 {
1149 self.parse_conditional();
1150 true
1151 } else {
1152 self.parse_identifier_token()
1153 }
1154 }
1155 Some(DOLLAR) => {
1156 self.parse_normal_content();
1157 true
1158 }
1159 Some(NEWLINE) => {
1160 self.builder.start_node(BLANK_LINE.into());
1161 self.bump();
1162 self.builder.finish_node();
1163 true
1164 }
1165 Some(COMMENT) => {
1166 self.parse_comment();
1167 true
1168 }
1169 Some(WHITESPACE) => {
1170 if self.is_end_of_file_or_newline_after_whitespace() {
1172 self.skip_ws();
1175 return true;
1176 }
1177
1178 let look_ahead_pos = self.tokens.len().saturating_sub(1);
1181 let mut is_documentation_or_help = false;
1182
1183 if look_ahead_pos > 0 {
1184 let next_token = &self.tokens[look_ahead_pos - 1];
1185 if next_token.0 == IDENTIFIER
1188 || next_token.0 == COMMENT
1189 || next_token.0 == TEXT
1190 {
1191 is_documentation_or_help = true;
1192 }
1193 }
1194
1195 if is_documentation_or_help {
1196 self.skip_ws();
1199 while self.current().is_some() && self.current() != Some(NEWLINE) {
1200 self.bump();
1201 }
1202 if self.current() == Some(NEWLINE) {
1203 self.bump();
1204 }
1205 } else {
1206 self.skip_ws();
1207 }
1208 true
1209 }
1210 Some(INDENT) => {
1211 self.bump();
1213
1214 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1216 self.bump();
1217 }
1218 if self.current() == Some(NEWLINE) {
1219 self.bump();
1220 }
1221 true
1222 }
1223 Some(kind) => {
1224 self.error(format!("unexpected token {:?}", kind));
1225 self.bump();
1226 true
1227 }
1228 }
1229 }
1230
1231 fn parse(mut self) -> Parse {
1232 self.builder.start_node(ROOT.into());
1233
1234 while self.parse_token() {}
1235
1236 self.builder.finish_node();
1237
1238 Parse {
1239 green_node: self.builder.finish(),
1240 errors: self.errors,
1241 }
1242 }
1243
1244 fn is_assignment_line(&mut self) -> bool {
1246 let assignment_ops = ["=", ":=", "::=", ":::=", "+=", "?=", "!="];
1247 let mut pos = self.tokens.len().saturating_sub(1);
1248 let mut seen_identifier = false;
1249 let mut seen_export = false;
1250
1251 while pos > 0 {
1252 let (kind, text) = &self.tokens[pos];
1253
1254 match kind {
1255 NEWLINE => break,
1256 IDENTIFIER if text == "export" => seen_export = true,
1257 IDENTIFIER if !seen_identifier => seen_identifier = true,
1258 OPERATOR if assignment_ops.contains(&text.as_str()) => {
1259 return seen_identifier || seen_export
1260 }
1261 OPERATOR if text == ":" => return false, WHITESPACE => (),
1263 _ if seen_export => return true, _ => return false,
1265 }
1266 pos = pos.saturating_sub(1);
1267 }
1268 seen_export
1270 }
1271
1272 fn bump(&mut self) {
1274 let (kind, text) = self.tokens.pop().unwrap();
1275 self.builder.token(kind.into(), text.as_str());
1276 }
1277 fn current(&self) -> Option<SyntaxKind> {
1279 self.tokens.last().map(|(kind, _)| *kind)
1280 }
1281
1282 fn expect_eol(&mut self) {
1283 self.skip_ws();
1285
1286 match self.current() {
1287 Some(NEWLINE) => {
1288 self.bump();
1289 }
1290 None => {
1291 }
1293 n => {
1294 self.error(format!("expected newline, got {:?}", n));
1295 self.skip_until_newline();
1297 }
1298 }
1299 }
1300
1301 fn is_at_eof(&self) -> bool {
1303 self.current().is_none()
1304 }
1305
1306 fn is_at_eof_or_only_whitespace(&self) -> bool {
1308 if self.is_at_eof() {
1309 return true;
1310 }
1311
1312 self.tokens
1314 .iter()
1315 .rev()
1316 .all(|(kind, _)| matches!(*kind, WHITESPACE | NEWLINE))
1317 }
1318
1319 fn skip_ws(&mut self) {
1320 while self.current() == Some(WHITESPACE) {
1321 self.bump()
1322 }
1323 }
1324
1325 fn skip_until_newline(&mut self) {
1326 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1327 self.bump();
1328 }
1329 if self.current() == Some(NEWLINE) {
1330 self.bump();
1331 }
1332 }
1333
1334 fn consume_balanced_parens(&mut self, start_paren_count: usize) -> usize {
1336 let mut paren_count = start_paren_count;
1337
1338 while paren_count > 0 && self.current().is_some() {
1339 match self.current() {
1340 Some(LPAREN) => {
1341 paren_count += 1;
1342 self.bump();
1343 }
1344 Some(RPAREN) => {
1345 paren_count -= 1;
1346 self.bump();
1347 if paren_count == 0 {
1348 break;
1349 }
1350 }
1351 Some(DOLLAR) => {
1352 self.parse_variable_reference();
1354 }
1355 Some(_) => self.bump(),
1356 None => {
1357 self.error("unclosed parenthesis".to_string());
1358 break;
1359 }
1360 }
1361 }
1362
1363 paren_count
1364 }
1365
1366 fn is_end_of_file_or_newline_after_whitespace(&self) -> bool {
1368 if self.is_at_eof_or_only_whitespace() {
1370 return true;
1371 }
1372
1373 if self.tokens.len() <= 1 {
1375 return true;
1376 }
1377
1378 false
1379 }
1380 }
1381
1382 let mut tokens = lex(text);
1383 tokens.reverse();
1384 Parser {
1385 tokens,
1386 builder: GreenNodeBuilder::new(),
1387 errors: Vec::new(),
1388 original_text: text.to_string(),
1389 variant,
1390 }
1391 .parse()
1392}
1393
1394pub(crate) type SyntaxNode = rowan::SyntaxNode<Lang>;
1400#[allow(unused)]
1401type SyntaxToken = rowan::SyntaxToken<Lang>;
1402#[allow(unused)]
1403pub(crate) type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
1404
1405impl Parse {
1406 fn syntax(&self) -> SyntaxNode {
1407 SyntaxNode::new_root_mut(self.green_node.clone())
1408 }
1409
1410 pub(crate) fn root(&self) -> Makefile {
1411 Makefile::cast(self.syntax()).unwrap()
1412 }
1413}
1414
1415fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
1418 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
1419 let mut line = 0;
1420 let mut last_newline_offset = rowan::TextSize::from(0);
1421
1422 for element in root.preorder_with_tokens() {
1423 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
1424 if token.text_range().start() >= offset {
1425 break;
1426 }
1427
1428 for (idx, _) in token.text().match_indices('\n') {
1430 line += 1;
1431 last_newline_offset =
1432 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
1433 }
1434 }
1435 }
1436
1437 let column: usize = (offset - last_newline_offset).into();
1438 (line, column)
1439}
1440
1441macro_rules! ast_node {
1442 ($ast:ident, $kind:ident) => {
1443 #[derive(Clone, PartialEq, Eq, Hash)]
1444 #[repr(transparent)]
1445 pub struct $ast(SyntaxNode);
1447
1448 impl AstNode for $ast {
1449 type Language = Lang;
1450
1451 fn can_cast(kind: SyntaxKind) -> bool {
1452 kind == $kind
1453 }
1454
1455 fn cast(syntax: SyntaxNode) -> Option<Self> {
1456 if Self::can_cast(syntax.kind()) {
1457 Some(Self(syntax))
1458 } else {
1459 None
1460 }
1461 }
1462
1463 fn syntax(&self) -> &SyntaxNode {
1464 &self.0
1465 }
1466 }
1467
1468 impl $ast {
1469 pub fn line(&self) -> usize {
1471 line_col_at_offset(&self.0, self.0.text_range().start()).0
1472 }
1473
1474 pub fn column(&self) -> usize {
1476 line_col_at_offset(&self.0, self.0.text_range().start()).1
1477 }
1478
1479 pub fn line_col(&self) -> (usize, usize) {
1482 line_col_at_offset(&self.0, self.0.text_range().start())
1483 }
1484 }
1485
1486 impl core::fmt::Display for $ast {
1487 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1488 write!(f, "{}", self.0.text())
1489 }
1490 }
1491 };
1492}
1493
1494ast_node!(Makefile, ROOT);
1495ast_node!(Rule, RULE);
1496ast_node!(Recipe, RECIPE);
1497ast_node!(Identifier, IDENTIFIER);
1498ast_node!(VariableDefinition, VARIABLE);
1499ast_node!(Include, INCLUDE);
1500ast_node!(ArchiveMembers, ARCHIVE_MEMBERS);
1501ast_node!(ArchiveMember, ARCHIVE_MEMBER);
1502ast_node!(Conditional, CONDITIONAL);
1503
1504impl Recipe {
1505 pub fn text(&self) -> String {
1517 let tokens: Vec<_> = self
1518 .syntax()
1519 .children_with_tokens()
1520 .filter_map(|it| it.as_token().cloned())
1521 .collect();
1522
1523 if tokens.is_empty() {
1524 return String::new();
1525 }
1526
1527 let start = if tokens.first().map(|t| t.kind()) == Some(INDENT) {
1529 1
1530 } else {
1531 0
1532 };
1533
1534 let end = if tokens.last().map(|t| t.kind()) == Some(NEWLINE) {
1536 tokens.len() - 1
1537 } else {
1538 tokens.len()
1539 };
1540
1541 let mut after_newline = false;
1545 tokens[start..end]
1546 .iter()
1547 .filter_map(|t| match t.kind() {
1548 TEXT => {
1549 after_newline = false;
1550 Some(t.text().to_string())
1551 }
1552 NEWLINE => {
1553 after_newline = true;
1554 Some(t.text().to_string())
1555 }
1556 INDENT if after_newline => {
1557 after_newline = false;
1558 let text = t.text();
1560 Some(text.strip_prefix('\t').unwrap_or(text).to_string())
1561 }
1562 _ => None,
1563 })
1564 .collect()
1565 }
1566
1567 pub fn comment(&self) -> Option<String> {
1583 self.syntax()
1584 .children_with_tokens()
1585 .filter_map(|it| {
1586 if let Some(token) = it.as_token() {
1587 if token.kind() == COMMENT {
1588 return Some(token.text().to_string());
1589 }
1590 }
1591 None
1592 })
1593 .next()
1594 }
1595
1596 pub fn full(&self) -> String {
1612 self.syntax()
1613 .children_with_tokens()
1614 .filter_map(|it| {
1615 if let Some(token) = it.as_token() {
1616 if token.kind() == TEXT || token.kind() == COMMENT {
1618 return Some(token.text().to_string());
1619 }
1620 }
1621 None
1622 })
1623 .collect::<Vec<_>>()
1624 .join("")
1625 }
1626
1627 pub fn parent(&self) -> Option<Rule> {
1640 self.syntax().parent().and_then(Rule::cast)
1641 }
1642
1643 pub fn is_silent(&self) -> bool {
1656 let text = self.text();
1657 text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1658 }
1659
1660 pub fn is_ignore_errors(&self) -> bool {
1673 let text = self.text();
1674 text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
1675 }
1676
1677 pub fn set_prefix(&mut self, prefix: &str) {
1694 let text = self.text();
1695
1696 let stripped = text.trim_start_matches(['@', '-', '+']);
1698
1699 let new_text = format!("{}{}", prefix, stripped);
1701
1702 self.replace_text(&new_text);
1703 }
1704
1705 pub fn replace_text(&mut self, new_text: &str) {
1718 let node = self.syntax();
1719 let parent = node.parent().expect("Recipe node must have a parent");
1720 let node_index = node.index();
1721
1722 let mut builder = GreenNodeBuilder::new();
1724 builder.start_node(RECIPE.into());
1725
1726 if let Some(indent_token) = node
1728 .children_with_tokens()
1729 .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
1730 {
1731 builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
1732 } else {
1733 builder.token(INDENT.into(), "\t");
1734 }
1735
1736 builder.token(TEXT.into(), new_text);
1737
1738 if let Some(newline_token) = node
1740 .children_with_tokens()
1741 .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
1742 {
1743 builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
1744 } else {
1745 builder.token(NEWLINE.into(), "\n");
1746 }
1747
1748 builder.finish_node();
1749 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1750
1751 parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
1753
1754 *self = parent
1758 .children_with_tokens()
1759 .nth(node_index)
1760 .and_then(|element| element.into_node())
1761 .and_then(Recipe::cast)
1762 .expect("New recipe node should exist at the same index");
1763 }
1764
1765 pub fn insert_before(&self, text: &str) {
1778 let node = self.syntax();
1779 let parent = node.parent().expect("Recipe node must have a parent");
1780 let node_index = node.index();
1781
1782 let mut builder = GreenNodeBuilder::new();
1784 builder.start_node(RECIPE.into());
1785 builder.token(INDENT.into(), "\t");
1786 builder.token(TEXT.into(), text);
1787 builder.token(NEWLINE.into(), "\n");
1788 builder.finish_node();
1789 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1790
1791 parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
1793 }
1794
1795 pub fn insert_after(&self, text: &str) {
1808 let node = self.syntax();
1809 let parent = node.parent().expect("Recipe node must have a parent");
1810 let node_index = node.index();
1811
1812 let mut builder = GreenNodeBuilder::new();
1814 builder.start_node(RECIPE.into());
1815 builder.token(INDENT.into(), "\t");
1816 builder.token(TEXT.into(), text);
1817 builder.token(NEWLINE.into(), "\n");
1818 builder.finish_node();
1819 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1820
1821 parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
1823 }
1824
1825 pub fn remove(&self) {
1838 let node = self.syntax();
1839 let parent = node.parent().expect("Recipe node must have a parent");
1840 let node_index = node.index();
1841
1842 parent.splice_children(node_index..node_index + 1, vec![]);
1844 }
1845}
1846
1847pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
1851 let mut newlines_to_remove = vec![];
1853 let mut current = node.last_child_or_token();
1854
1855 while let Some(element) = current {
1856 match &element {
1857 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
1858 newlines_to_remove.push(token.clone());
1859 current = token.prev_sibling_or_token();
1860 }
1861 rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
1862 let mut recipe_current = n.last_child_or_token();
1864 while let Some(recipe_element) = recipe_current {
1865 match &recipe_element {
1866 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
1867 newlines_to_remove.push(token.clone());
1868 recipe_current = token.prev_sibling_or_token();
1869 }
1870 _ => break,
1871 }
1872 }
1873 break; }
1875 _ => break,
1876 }
1877 }
1878
1879 if newlines_to_remove.len() > 1 {
1882 newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
1884
1885 for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
1886 let parent = token.parent().unwrap();
1887 let idx = token.index();
1888 parent.splice_children(idx..idx + 1, vec![]);
1889 }
1890 }
1891}
1892
1893pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
1901 let mut collected_elements = vec![];
1902 let mut found_comment = false;
1903
1904 let mut current = node.prev_sibling_or_token();
1906 while let Some(element) = current {
1907 match &element {
1908 rowan::NodeOrToken::Token(token) => match token.kind() {
1909 COMMENT => {
1910 if token.text().starts_with("#!") {
1911 break; }
1913 found_comment = true;
1914 collected_elements.push(element.clone());
1915 }
1916 NEWLINE | WHITESPACE => {
1917 collected_elements.push(element.clone());
1918 }
1919 _ => break, },
1921 rowan::NodeOrToken::Node(n) => {
1922 if n.kind() == BLANK_LINE {
1924 collected_elements.push(element.clone());
1925 } else {
1926 break; }
1928 }
1929 }
1930 current = element.prev_sibling_or_token();
1931 }
1932
1933 let mut elements_to_remove = vec![];
1936 let mut consecutive_newlines = 0;
1937 for element in collected_elements.iter().rev() {
1938 let should_remove = match element {
1939 rowan::NodeOrToken::Token(token) => match token.kind() {
1940 COMMENT => {
1941 consecutive_newlines = 0;
1942 found_comment
1943 }
1944 NEWLINE => {
1945 consecutive_newlines += 1;
1946 found_comment && consecutive_newlines <= 1
1947 }
1948 WHITESPACE => found_comment,
1949 _ => false,
1950 },
1951 rowan::NodeOrToken::Node(n) => {
1952 if n.kind() == BLANK_LINE {
1954 consecutive_newlines += 1;
1955 found_comment && consecutive_newlines <= 1
1956 } else {
1957 false
1958 }
1959 }
1960 };
1961
1962 if should_remove {
1963 elements_to_remove.push(element.clone());
1964 }
1965 }
1966
1967 let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
1970 all_to_remove.extend(elements_to_remove.into_iter().rev());
1971
1972 all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
1974
1975 for element in all_to_remove {
1976 let idx = element.index();
1977 parent.splice_children(idx..idx + 1, vec![]);
1978 }
1979}
1980
1981impl FromStr for Rule {
1982 type Err = crate::Error;
1983
1984 fn from_str(s: &str) -> Result<Self, Self::Err> {
1985 Rule::parse(s).to_rule_result()
1986 }
1987}
1988
1989impl FromStr for Makefile {
1990 type Err = crate::Error;
1991
1992 fn from_str(s: &str) -> Result<Self, Self::Err> {
1993 Makefile::parse(s).to_result()
1994 }
1995}
1996
1997#[cfg(test)]
1998mod tests {
1999 use super::*;
2000 use crate::ast::makefile::MakefileItem;
2001 use crate::pattern::matches_pattern;
2002
2003 #[test]
2004 fn test_conditionals() {
2005 let code = "ifdef DEBUG\n DEBUG_FLAG := 1\nendif\n";
2009 let mut buf = code.as_bytes();
2010 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
2011 assert!(makefile.code().contains("DEBUG_FLAG"));
2012
2013 let code =
2015 "ifeq ($(OS),Windows_NT)\n RESULT := windows\nelse\n RESULT := unix\nendif\n";
2016 let mut buf = code.as_bytes();
2017 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
2018 assert!(makefile.code().contains("RESULT"));
2019 assert!(makefile.code().contains("windows"));
2020
2021 let code = "ifdef DEBUG\n CFLAGS += -g\n ifdef VERBOSE\n CFLAGS += -v\n endif\nelse\n CFLAGS += -O2\nendif\n";
2023 let mut buf = code.as_bytes();
2024 let makefile = Makefile::read_relaxed(&mut buf)
2025 .expect("Failed to parse nested conditionals with else");
2026 assert!(makefile.code().contains("CFLAGS"));
2027 assert!(makefile.code().contains("VERBOSE"));
2028
2029 let code = "ifdef DEBUG\nendif\n";
2031 let mut buf = code.as_bytes();
2032 let makefile =
2033 Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
2034 assert!(makefile.code().contains("ifdef DEBUG"));
2035
2036 let code = "ifeq ($(OS),Windows)\n EXT := .exe\nelse ifeq ($(OS),Linux)\n EXT := .bin\nelse\n EXT := .out\nendif\n";
2038 let mut buf = code.as_bytes();
2039 let makefile =
2040 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
2041 assert!(makefile.code().contains("EXT"));
2042
2043 let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
2045 let mut buf = code.as_bytes();
2046 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
2047 assert!(makefile.code().contains("DEBUG"));
2048
2049 let code = "ifdef \nDEBUG := 1\nendif\n";
2051 let mut buf = code.as_bytes();
2052 let makefile = Makefile::read_relaxed(&mut buf)
2053 .expect("Failed to parse with recovery - missing condition");
2054 assert!(makefile.code().contains("DEBUG"));
2055 }
2056
2057 #[test]
2058 fn test_parse_simple() {
2059 const SIMPLE: &str = r#"VARIABLE = value
2060
2061rule: dependency
2062 command
2063"#;
2064 let parsed = parse(SIMPLE, None);
2065 assert!(parsed.errors.is_empty());
2066 let node = parsed.syntax();
2067 assert_eq!(
2068 format!("{:#?}", node),
2069 r#"ROOT@0..44
2070 VARIABLE@0..17
2071 IDENTIFIER@0..8 "VARIABLE"
2072 WHITESPACE@8..9 " "
2073 OPERATOR@9..10 "="
2074 WHITESPACE@10..11 " "
2075 EXPR@11..16
2076 IDENTIFIER@11..16 "value"
2077 NEWLINE@16..17 "\n"
2078 BLANK_LINE@17..18
2079 NEWLINE@17..18 "\n"
2080 RULE@18..44
2081 TARGETS@18..22
2082 IDENTIFIER@18..22 "rule"
2083 OPERATOR@22..23 ":"
2084 WHITESPACE@23..24 " "
2085 PREREQUISITES@24..34
2086 PREREQUISITE@24..34
2087 IDENTIFIER@24..34 "dependency"
2088 NEWLINE@34..35 "\n"
2089 RECIPE@35..44
2090 INDENT@35..36 "\t"
2091 TEXT@36..43 "command"
2092 NEWLINE@43..44 "\n"
2093"#
2094 );
2095
2096 let root = parsed.root();
2097
2098 let mut rules = root.rules().collect::<Vec<_>>();
2099 assert_eq!(rules.len(), 1);
2100 let rule = rules.pop().unwrap();
2101 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2102 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
2103 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2104
2105 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2106 assert_eq!(variables.len(), 1);
2107 let variable = variables.pop().unwrap();
2108 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2109 assert_eq!(variable.raw_value(), Some("value".to_string()));
2110 }
2111
2112 #[test]
2113 fn test_parse_export_assign() {
2114 const EXPORT: &str = r#"export VARIABLE := value
2115"#;
2116 let parsed = parse(EXPORT, None);
2117 assert!(parsed.errors.is_empty());
2118 let node = parsed.syntax();
2119 assert_eq!(
2120 format!("{:#?}", node),
2121 r#"ROOT@0..25
2122 VARIABLE@0..25
2123 IDENTIFIER@0..6 "export"
2124 WHITESPACE@6..7 " "
2125 IDENTIFIER@7..15 "VARIABLE"
2126 WHITESPACE@15..16 " "
2127 OPERATOR@16..18 ":="
2128 WHITESPACE@18..19 " "
2129 EXPR@19..24
2130 IDENTIFIER@19..24 "value"
2131 NEWLINE@24..25 "\n"
2132"#
2133 );
2134
2135 let root = parsed.root();
2136
2137 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2138 assert_eq!(variables.len(), 1);
2139 let variable = variables.pop().unwrap();
2140 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2141 assert_eq!(variable.raw_value(), Some("value".to_string()));
2142 }
2143
2144 #[test]
2145 fn test_parse_multiple_prerequisites() {
2146 const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2147 command
2148
2149"#;
2150 let parsed = parse(MULTIPLE_PREREQUISITES, None);
2151 assert!(parsed.errors.is_empty());
2152 let node = parsed.syntax();
2153 assert_eq!(
2154 format!("{:#?}", node),
2155 r#"ROOT@0..40
2156 RULE@0..40
2157 TARGETS@0..4
2158 IDENTIFIER@0..4 "rule"
2159 OPERATOR@4..5 ":"
2160 WHITESPACE@5..6 " "
2161 PREREQUISITES@6..29
2162 PREREQUISITE@6..17
2163 IDENTIFIER@6..17 "dependency1"
2164 WHITESPACE@17..18 " "
2165 PREREQUISITE@18..29
2166 IDENTIFIER@18..29 "dependency2"
2167 NEWLINE@29..30 "\n"
2168 RECIPE@30..39
2169 INDENT@30..31 "\t"
2170 TEXT@31..38 "command"
2171 NEWLINE@38..39 "\n"
2172 NEWLINE@39..40 "\n"
2173"#
2174 );
2175 let root = parsed.root();
2176
2177 let rule = root.rules().next().unwrap();
2178 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2179 assert_eq!(
2180 rule.prerequisites().collect::<Vec<_>>(),
2181 vec!["dependency1", "dependency2"]
2182 );
2183 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2184 }
2185
2186 #[test]
2187 fn test_add_rule() {
2188 let mut makefile = Makefile::new();
2189 let rule = makefile.add_rule("rule");
2190 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2191 assert_eq!(
2192 rule.prerequisites().collect::<Vec<_>>(),
2193 Vec::<String>::new()
2194 );
2195
2196 assert_eq!(makefile.to_string(), "rule:\n");
2197 }
2198
2199 #[test]
2200 fn test_add_rule_with_shebang() {
2201 let content = r#"#!/usr/bin/make -f
2203
2204build: blah
2205 $(MAKE) install
2206
2207clean:
2208 dh_clean
2209"#;
2210
2211 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2212 let initial_count = makefile.rules().count();
2213 assert_eq!(initial_count, 2);
2214
2215 let rule = makefile.add_rule("build-indep");
2217 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2218
2219 assert_eq!(makefile.rules().count(), initial_count + 1);
2221 }
2222
2223 #[test]
2224 fn test_add_rule_formatting() {
2225 let content = r#"build: blah
2227 $(MAKE) install
2228
2229clean:
2230 dh_clean
2231"#;
2232
2233 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2234 let mut rule = makefile.add_rule("build-indep");
2235 rule.add_prerequisite("build").unwrap();
2236
2237 let expected = r#"build: blah
2238 $(MAKE) install
2239
2240clean:
2241 dh_clean
2242
2243build-indep: build
2244"#;
2245
2246 assert_eq!(makefile.to_string(), expected);
2247 }
2248
2249 #[test]
2250 fn test_push_command() {
2251 let mut makefile = Makefile::new();
2252 let mut rule = makefile.add_rule("rule");
2253
2254 rule.push_command("command");
2256 rule.push_command("command2");
2257
2258 assert_eq!(
2260 rule.recipes().collect::<Vec<_>>(),
2261 vec!["command", "command2"]
2262 );
2263
2264 rule.push_command("command3");
2266 assert_eq!(
2267 rule.recipes().collect::<Vec<_>>(),
2268 vec!["command", "command2", "command3"]
2269 );
2270
2271 assert_eq!(
2273 makefile.to_string(),
2274 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2275 );
2276
2277 assert_eq!(
2279 rule.to_string(),
2280 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2281 );
2282 }
2283
2284 #[test]
2285 fn test_replace_command() {
2286 let mut makefile = Makefile::new();
2287 let mut rule = makefile.add_rule("rule");
2288
2289 rule.push_command("command");
2291 rule.push_command("command2");
2292
2293 assert_eq!(
2295 rule.recipes().collect::<Vec<_>>(),
2296 vec!["command", "command2"]
2297 );
2298
2299 rule.replace_command(0, "new command");
2301 assert_eq!(
2302 rule.recipes().collect::<Vec<_>>(),
2303 vec!["new command", "command2"]
2304 );
2305
2306 assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2308
2309 assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2311 }
2312
2313 #[test]
2314 fn test_replace_command_with_comments() {
2315 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";
2318
2319 let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2320
2321 let mut rule = makefile.rules().next().unwrap();
2322
2323 assert_eq!(rule.recipe_nodes().count(), 2);
2325 let recipes: Vec<_> = rule.recipe_nodes().collect();
2326 assert_eq!(recipes[0].text(), ""); assert_eq!(
2328 recipes[1].text(),
2329 "dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"
2330 );
2331
2332 assert!(rule.replace_command(1, "dh_strip"));
2334
2335 assert_eq!(rule.recipe_nodes().count(), 2);
2337 let recipes: Vec<_> = rule.recipe_nodes().collect();
2338 assert_eq!(recipes[0].text(), ""); assert_eq!(recipes[1].text(), "dh_strip");
2340 }
2341
2342 #[test]
2343 fn test_parse_rule_without_newline() {
2344 let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2345 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2346 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2347 let rule = "rule: dependency".parse::<Rule>().unwrap();
2348 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2349 assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2350 }
2351
2352 #[test]
2353 fn test_parse_makefile_without_newline() {
2354 let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2355 assert_eq!(makefile.rules().count(), 1);
2356 }
2357
2358 #[test]
2359 fn test_from_reader() {
2360 let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2361 assert_eq!(makefile.rules().count(), 1);
2362 }
2363
2364 #[test]
2365 fn test_parse_with_tab_after_last_newline() {
2366 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2367 assert_eq!(makefile.rules().count(), 1);
2368 }
2369
2370 #[test]
2371 fn test_parse_with_space_after_last_newline() {
2372 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2373 assert_eq!(makefile.rules().count(), 1);
2374 }
2375
2376 #[test]
2377 fn test_parse_with_comment_after_last_newline() {
2378 let makefile =
2379 Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2380 assert_eq!(makefile.rules().count(), 1);
2381 }
2382
2383 #[test]
2384 fn test_parse_with_variable_rule() {
2385 let makefile =
2386 Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2387 .unwrap();
2388
2389 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2391 assert_eq!(vars.len(), 1);
2392 assert_eq!(vars[0].name(), Some("RULE".to_string()));
2393 assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2394
2395 let rules = makefile.rules().collect::<Vec<_>>();
2397 assert_eq!(rules.len(), 1);
2398 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2399 assert_eq!(
2400 rules[0].prerequisites().collect::<Vec<_>>(),
2401 vec!["dependency"]
2402 );
2403 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2404 }
2405
2406 #[test]
2407 fn test_parse_with_variable_dependency() {
2408 let makefile =
2409 Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2410
2411 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2413 assert_eq!(vars.len(), 1);
2414 assert_eq!(vars[0].name(), Some("DEP".to_string()));
2415 assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2416
2417 let rules = makefile.rules().collect::<Vec<_>>();
2419 assert_eq!(rules.len(), 1);
2420 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2421 assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2422 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2423 }
2424
2425 #[test]
2426 fn test_parse_with_variable_command() {
2427 let makefile =
2428 Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2429
2430 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2432 assert_eq!(vars.len(), 1);
2433 assert_eq!(vars[0].name(), Some("COM".to_string()));
2434 assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2435
2436 let rules = makefile.rules().collect::<Vec<_>>();
2438 assert_eq!(rules.len(), 1);
2439 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2440 assert_eq!(
2441 rules[0].prerequisites().collect::<Vec<_>>(),
2442 vec!["dependency"]
2443 );
2444 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2445 }
2446
2447 #[test]
2448 fn test_regular_line_error_reporting() {
2449 let input = "rule target\n\tcommand";
2450
2451 let parsed = parse(input, None);
2453 let direct_error = &parsed.errors[0];
2454
2455 assert_eq!(direct_error.line, 2);
2457 assert!(
2458 direct_error.message.contains("expected"),
2459 "Error message should contain 'expected': {}",
2460 direct_error.message
2461 );
2462 assert_eq!(direct_error.context, "\tcommand");
2463
2464 let reader_result = Makefile::from_reader(input.as_bytes());
2466 let parse_error = match reader_result {
2467 Ok(_) => panic!("Expected Parse error from from_reader"),
2468 Err(err) => match err {
2469 self::Error::Parse(parse_err) => parse_err,
2470 _ => panic!("Expected Parse error"),
2471 },
2472 };
2473
2474 let error_text = parse_error.to_string();
2476 assert!(error_text.contains("Error at line 2:"));
2477 assert!(error_text.contains("2| \tcommand"));
2478 }
2479
2480 #[test]
2481 fn test_parsing_error_context_with_bad_syntax() {
2482 let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2484
2485 match Makefile::from_reader(input.as_bytes()) {
2487 Ok(makefile) => {
2488 assert_eq!(
2490 makefile.rules().count(),
2491 0,
2492 "Should not have found any rules"
2493 );
2494 }
2495 Err(err) => match err {
2496 self::Error::Parse(error) => {
2497 assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2499 assert!(
2500 !error.errors[0].context.is_empty(),
2501 "Error context should not be empty"
2502 );
2503 }
2504 _ => panic!("Unexpected error type"),
2505 },
2506 };
2507 }
2508
2509 #[test]
2510 fn test_error_message_format() {
2511 let parse_error = ParseError {
2513 errors: vec![ErrorInfo {
2514 message: "test error".to_string(),
2515 line: 42,
2516 context: "some problematic code".to_string(),
2517 }],
2518 };
2519
2520 let error_text = parse_error.to_string();
2521 assert!(error_text.contains("Error at line 42: test error"));
2522 assert!(error_text.contains("42| some problematic code"));
2523 }
2524
2525 #[test]
2526 fn test_line_number_calculation() {
2527 let test_cases = [
2529 ("rule dependency\n\tcommand", 2), ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2), ("var = value\n#comment\n\tindented line", 3), ];
2533
2534 for (input, expected_line) in test_cases {
2535 match input.parse::<Makefile>() {
2537 Ok(_) => {
2538 continue;
2541 }
2542 Err(err) => {
2543 if let Error::Parse(parse_err) = err {
2544 assert_eq!(
2546 parse_err.errors[0].line, expected_line,
2547 "Line number should match the expected line"
2548 );
2549
2550 if parse_err.errors[0].message.contains("indented") {
2552 assert!(
2553 parse_err.errors[0].context.starts_with('\t'),
2554 "Context for indentation errors should include the tab character"
2555 );
2556 }
2557 } else {
2558 panic!("Expected parse error, got: {:?}", err);
2559 }
2560 }
2561 }
2562 }
2563 }
2564
2565 #[test]
2566 fn test_conditional_features() {
2567 let code = r#"
2569# Set variables based on DEBUG flag
2570ifdef DEBUG
2571 CFLAGS += -g -DDEBUG
2572else
2573 CFLAGS = -O2
2574endif
2575
2576# Define a build rule
2577all: $(OBJS)
2578 $(CC) $(CFLAGS) -o $@ $^
2579"#;
2580
2581 let mut buf = code.as_bytes();
2582 let makefile =
2583 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2584
2585 assert!(!makefile.code().is_empty(), "Makefile has content");
2588
2589 let rules = makefile.rules().collect::<Vec<_>>();
2591 assert!(!rules.is_empty(), "Should have found rules");
2592
2593 assert!(code.contains("ifdef DEBUG"));
2595 assert!(code.contains("endif"));
2596
2597 let code_with_var = r#"
2599# Define a variable first
2600CC = gcc
2601
2602ifdef DEBUG
2603 CFLAGS += -g -DDEBUG
2604else
2605 CFLAGS = -O2
2606endif
2607
2608all: $(OBJS)
2609 $(CC) $(CFLAGS) -o $@ $^
2610"#;
2611
2612 let mut buf = code_with_var.as_bytes();
2613 let makefile =
2614 Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2615
2616 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2618 assert!(
2619 !vars.is_empty(),
2620 "Should have found at least the CC variable definition"
2621 );
2622 }
2623
2624 #[test]
2625 fn test_include_directive() {
2626 let parsed = parse(
2627 "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2628 None,
2629 );
2630 assert!(parsed.errors.is_empty());
2631 let node = parsed.syntax();
2632 assert!(format!("{:#?}", node).contains("INCLUDE@"));
2633 }
2634
2635 #[test]
2636 fn test_export_variables() {
2637 let parsed = parse("export SHELL := /bin/bash\n", None);
2638 assert!(parsed.errors.is_empty());
2639 let makefile = parsed.root();
2640 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2641 assert_eq!(vars.len(), 1);
2642 let shell_var = vars
2643 .iter()
2644 .find(|v| v.name() == Some("SHELL".to_string()))
2645 .unwrap();
2646 assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2647 }
2648
2649 #[test]
2650 fn test_bare_export_variable() {
2651 let parsed = parse(
2654 "DEB_CFLAGS_MAINT_APPEND = -Wno-error\nexport DEB_CFLAGS_MAINT_APPEND\n\n%:\n\tdh $@\n",
2655 None,
2656 );
2657 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2658 let makefile = parsed.root();
2659 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2661 assert_eq!(vars.len(), 2);
2662 let rules = makefile.rules().collect::<Vec<_>>();
2664 assert_eq!(rules.len(), 1);
2665 assert!(rules[0].targets().any(|t| t == "%"));
2666 assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
2668 }
2669
2670 #[test]
2671 fn test_bare_export_at_eof() {
2672 let parsed = parse("VAR = value\nexport VAR", None);
2674 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2675 let makefile = parsed.root();
2676 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2677 assert_eq!(vars.len(), 2);
2678 assert_eq!(makefile.rules().count(), 0);
2679 }
2680
2681 #[test]
2682 fn test_bare_export_does_not_eat_include() {
2683 let parsed = parse(
2685 "VAR = value\nexport VAR\ninclude other.mk\n",
2686 None,
2687 );
2688 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2689 let makefile = parsed.root();
2690 assert_eq!(makefile.includes().count(), 1);
2691 assert_eq!(
2692 makefile.included_files().collect::<Vec<_>>(),
2693 vec!["other.mk"]
2694 );
2695 }
2696
2697 #[test]
2698 fn test_bare_export_multiple() {
2699 let parsed = parse(
2701 "A = 1\nB = 2\nexport A\nexport B\n\nall:\n\techo done\n",
2702 None,
2703 );
2704 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2705 let makefile = parsed.root();
2706 assert_eq!(makefile.variable_definitions().count(), 4);
2707 let rules = makefile.rules().collect::<Vec<_>>();
2708 assert_eq!(rules.len(), 1);
2709 assert!(rules[0].targets().any(|t| t == "all"));
2710 }
2711
2712 #[test]
2713 fn test_parse_error_does_not_cross_lines() {
2714 let parsed = parse(
2717 "notarule\n\nbuild-arch:\n\techo arch\n",
2718 None,
2719 );
2720 let makefile = parsed.root();
2721 let rules = makefile.rules().collect::<Vec<_>>();
2722 assert!(
2724 rules.iter().any(|r| r.targets().any(|t| t == "build-arch")),
2725 "build-arch rule should be parsed despite earlier error; rules: {:?}",
2726 rules.iter().map(|r| r.targets().collect::<Vec<_>>()).collect::<Vec<_>>()
2727 );
2728 }
2729
2730 #[test]
2731 fn test_pyfai_rules_full() {
2732 let input = "\
2734#!/usr/bin/make -f
2735
2736export DH_VERBOSE=1
2737export PYBUILD_NAME=pyfai
2738
2739DEB_CFLAGS_MAINT_APPEND = -Wno-error=incompatible-pointer-types
2740export DEB_CFLAGS_MAINT_APPEND
2741
2742PY3VER := $(shell py3versions -dv)
2743
2744include /usr/share/dpkg/pkg-info.mk # sets SOURCE_DATE_EPOCH
2745
2746%:
2747\tdh $@ --buildsystem=pybuild
2748
2749override_dh_auto_build-arch:
2750\tPYBUILD_BUILD_ARGS=\"-Ccompile-args=--verbose\" dh_auto_build
2751
2752override_dh_auto_build-indep: override_dh_auto_build-arch
2753\tsphinx-build -N -bhtml doc/source build/html
2754
2755override_dh_auto_test:
2756
2757execute_after_dh_auto_install:
2758\tdh_install -p pyfai debian/python3-pyfai/usr/bin /usr
2759";
2760 let parsed = parse(input, None);
2761 let makefile = parsed.root();
2762
2763 assert_eq!(makefile.includes().count(), 1);
2765
2766 assert!(
2768 makefile.find_rule_by_target_pattern("build-arch").is_some(),
2769 "build-arch should match via %: pattern rule"
2770 );
2771 assert!(
2772 makefile.find_rule_by_target_pattern("build-indep").is_some(),
2773 "build-indep should match via %: pattern rule"
2774 );
2775
2776 let rule_targets: Vec<Vec<String>> = makefile
2778 .rules()
2779 .map(|r| r.targets().collect())
2780 .collect();
2781 assert!(
2782 rule_targets.iter().any(|t| t.contains(&"%".to_string())),
2783 "missing %: rule; got: {:?}", rule_targets
2784 );
2785 assert!(
2786 rule_targets.iter().any(|t| t.contains(&"override_dh_auto_build-arch".to_string())),
2787 "missing override_dh_auto_build-arch; got: {:?}", rule_targets
2788 );
2789 assert!(
2790 rule_targets.iter().any(|t| t.contains(&"override_dh_auto_test".to_string())),
2791 "missing override_dh_auto_test; got: {:?}", rule_targets
2792 );
2793 assert!(
2794 rule_targets.iter().any(|t| t.contains(&"execute_after_dh_auto_install".to_string())),
2795 "missing execute_after_dh_auto_install; got: {:?}", rule_targets
2796 );
2797 }
2798
2799 #[test]
2800 fn test_variable_scopes() {
2801 let parsed = parse(
2802 "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
2803 None,
2804 );
2805 assert!(parsed.errors.is_empty());
2806 let makefile = parsed.root();
2807 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2808 assert_eq!(vars.len(), 4);
2809 let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
2810 assert!(var_names.contains(&"SIMPLE".to_string()));
2811 assert!(var_names.contains(&"IMMEDIATE".to_string()));
2812 assert!(var_names.contains(&"CONDITIONAL".to_string()));
2813 assert!(var_names.contains(&"APPEND".to_string()));
2814 }
2815
2816 #[test]
2817 fn test_pattern_rule_parsing() {
2818 let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
2819 assert!(parsed.errors.is_empty());
2820 let makefile = parsed.root();
2821 let rules = makefile.rules().collect::<Vec<_>>();
2822 assert_eq!(rules.len(), 1);
2823 assert_eq!(rules[0].targets().next().unwrap(), "%.o");
2824 assert!(rules[0].recipes().next().unwrap().contains("$@"));
2825 }
2826
2827 #[test]
2828 fn test_include_variants() {
2829 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
2831 let parsed = parse(makefile_str, None);
2832 assert!(parsed.errors.is_empty());
2833
2834 let node = parsed.syntax();
2836 let debug_str = format!("{:#?}", node);
2837
2838 assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
2840
2841 let makefile = parsed.root();
2843
2844 let include_count = makefile
2846 .syntax()
2847 .children()
2848 .filter(|child| child.kind() == INCLUDE)
2849 .count();
2850 assert_eq!(include_count, 4);
2851
2852 assert!(makefile
2854 .included_files()
2855 .any(|path| path.contains("$(VAR)")));
2856 }
2857
2858 #[test]
2859 fn test_include_api() {
2860 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
2862 let makefile: Makefile = makefile_str.parse().unwrap();
2863
2864 let includes: Vec<_> = makefile.includes().collect();
2866 assert_eq!(includes.len(), 3);
2867
2868 assert!(!includes[0].is_optional()); assert!(includes[1].is_optional()); assert!(includes[2].is_optional()); let files: Vec<_> = makefile.included_files().collect();
2875 assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
2876
2877 assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
2879 assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
2880 assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
2881 }
2882
2883 #[test]
2884 fn test_include_integration() {
2885 let phony_makefile = Makefile::from_reader(
2889 ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
2890 .as_bytes()
2891 ).unwrap();
2892
2893 assert_eq!(phony_makefile.rules().count(), 2);
2895
2896 let normal_rules_count = phony_makefile
2898 .rules()
2899 .filter(|r| !r.targets().any(|t| t.starts_with('.')))
2900 .count();
2901 assert_eq!(normal_rules_count, 1);
2902
2903 assert_eq!(phony_makefile.includes().count(), 1);
2905 assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
2906
2907 let simple_makefile = Makefile::from_reader(
2909 "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
2910 .as_bytes(),
2911 )
2912 .unwrap();
2913 assert_eq!(simple_makefile.rules().count(), 1);
2914 assert_eq!(simple_makefile.includes().count(), 1);
2915 }
2916
2917 #[test]
2918 fn test_real_conditional_directives() {
2919 let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
2921 let mut buf = conditional.as_bytes();
2922 let makefile =
2923 Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
2924 let code = makefile.code();
2925 assert!(code.contains("ifdef DEBUG"));
2926 assert!(code.contains("else"));
2927 assert!(code.contains("endif"));
2928
2929 let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
2931 let mut buf = nested.as_bytes();
2932 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
2933 let code = makefile.code();
2934 assert!(code.contains("ifdef DEBUG"));
2935 assert!(code.contains("ifdef VERBOSE"));
2936
2937 let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
2939 let mut buf = ifeq.as_bytes();
2940 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
2941 let code = makefile.code();
2942 assert!(code.contains("ifeq"));
2943 assert!(code.contains("Windows_NT"));
2944 }
2945
2946 #[test]
2947 fn test_indented_text_outside_rules() {
2948 let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \" help show help\"\n";
2950 let parsed = parse(help_text, None);
2951 assert!(parsed.errors.is_empty());
2952
2953 let root = parsed.root();
2955 let rules = root.rules().collect::<Vec<_>>();
2956 assert_eq!(rules.len(), 1);
2957
2958 let help_rule = &rules[0];
2959 let recipes = help_rule.recipes().collect::<Vec<_>>();
2960 assert_eq!(recipes.len(), 2);
2961 assert!(recipes[0].contains("Available targets"));
2962 assert!(recipes[1].contains("help"));
2963 }
2964
2965 #[test]
2966 fn test_comment_handling_in_recipes() {
2967 let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
2969
2970 let parsed = parse(recipe_comment, None);
2972
2973 assert!(
2975 parsed.errors.is_empty(),
2976 "Should parse recipe with comments without errors"
2977 );
2978
2979 let root = parsed.root();
2981 let rules = root.rules().collect::<Vec<_>>();
2982 assert_eq!(rules.len(), 1, "Should find exactly one rule");
2983
2984 let build_rule = &rules[0];
2986 assert_eq!(
2987 build_rule.targets().collect::<Vec<_>>(),
2988 vec!["build"],
2989 "Rule should have 'build' as target"
2990 );
2991
2992 let recipes = build_rule.recipe_nodes().collect::<Vec<_>>();
2995 assert_eq!(recipes.len(), 2, "Should find two recipe nodes");
2996
2997 assert_eq!(recipes[0].text(), "");
2999 assert_eq!(
3000 recipes[0].comment(),
3001 Some("# This is a comment".to_string())
3002 );
3003
3004 assert_eq!(recipes[1].text(), "gcc -o app main.c");
3006 assert_eq!(recipes[1].comment(), None);
3007 }
3008
3009 #[test]
3010 fn test_multiline_variables() {
3011 let multiline = "SOURCES = main.c \\\n util.c\n";
3013
3014 let parsed = parse(multiline, None);
3016
3017 let root = parsed.root();
3019 let vars = root.variable_definitions().collect::<Vec<_>>();
3020 assert!(!vars.is_empty(), "Should find at least one variable");
3021
3022 let operators = "CFLAGS := -Wall \\\n -Werror\n";
3026 let parsed_operators = parse(operators, None);
3027
3028 let root = parsed_operators.root();
3030 let vars = root.variable_definitions().collect::<Vec<_>>();
3031 assert!(
3032 !vars.is_empty(),
3033 "Should find at least one variable with := operator"
3034 );
3035
3036 let append = "LDFLAGS += -L/usr/lib \\\n -lm\n";
3038 let parsed_append = parse(append, None);
3039
3040 let root = parsed_append.root();
3042 let vars = root.variable_definitions().collect::<Vec<_>>();
3043 assert!(
3044 !vars.is_empty(),
3045 "Should find at least one variable with += operator"
3046 );
3047 }
3048
3049 #[test]
3050 fn test_whitespace_and_eof_handling() {
3051 let blank_lines = "VAR = value\n\n\n";
3053
3054 let parsed_blank = parse(blank_lines, None);
3055
3056 let root = parsed_blank.root();
3058 let vars = root.variable_definitions().collect::<Vec<_>>();
3059 assert_eq!(
3060 vars.len(),
3061 1,
3062 "Should find one variable in blank lines test"
3063 );
3064
3065 let trailing_space = "VAR = value \n";
3067
3068 let parsed_space = parse(trailing_space, None);
3069
3070 let root = parsed_space.root();
3072 let vars = root.variable_definitions().collect::<Vec<_>>();
3073 assert_eq!(
3074 vars.len(),
3075 1,
3076 "Should find one variable in trailing space test"
3077 );
3078
3079 let no_newline = "VAR = value";
3081
3082 let parsed_no_newline = parse(no_newline, None);
3083
3084 let root = parsed_no_newline.root();
3086 let vars = root.variable_definitions().collect::<Vec<_>>();
3087 assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
3088 assert_eq!(
3089 vars[0].name(),
3090 Some("VAR".to_string()),
3091 "Variable name should be VAR"
3092 );
3093 }
3094
3095 #[test]
3096 fn test_complex_variable_references() {
3097 let wildcard = "SOURCES = $(wildcard *.c)\n";
3099 let parsed = parse(wildcard, None);
3100 assert!(parsed.errors.is_empty());
3101
3102 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3104 let parsed = parse(nested, None);
3105 assert!(parsed.errors.is_empty());
3106
3107 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3109 let parsed = parse(patsubst, None);
3110 assert!(parsed.errors.is_empty());
3111 }
3112
3113 #[test]
3114 fn test_complex_variable_references_minimal() {
3115 let wildcard = "SOURCES = $(wildcard *.c)\n";
3117 let parsed = parse(wildcard, None);
3118 assert!(parsed.errors.is_empty());
3119
3120 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3122 let parsed = parse(nested, None);
3123 assert!(parsed.errors.is_empty());
3124
3125 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3127 let parsed = parse(patsubst, None);
3128 assert!(parsed.errors.is_empty());
3129 }
3130
3131 #[test]
3132 fn test_multiline_variable_with_backslash() {
3133 let content = r#"
3134LONG_VAR = This is a long variable \
3135 that continues on the next line \
3136 and even one more line
3137"#;
3138
3139 let mut buf = content.as_bytes();
3141 let makefile =
3142 Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
3143
3144 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3146 assert_eq!(
3147 vars.len(),
3148 1,
3149 "Expected 1 variable but found {}",
3150 vars.len()
3151 );
3152 let var_value = vars[0].raw_value();
3153 assert!(var_value.is_some(), "Variable value is None");
3154
3155 let value_str = var_value.unwrap();
3157 assert!(
3158 value_str.contains("long variable"),
3159 "Value doesn't contain expected content"
3160 );
3161 }
3162
3163 #[test]
3164 fn test_multiline_variable_with_mixed_operators() {
3165 let content = r#"
3166PREFIX ?= /usr/local
3167CFLAGS := -Wall -O2 \
3168 -I$(PREFIX)/include \
3169 -DDEBUG
3170"#;
3171 let mut buf = content.as_bytes();
3173 let makefile = Makefile::read_relaxed(&mut buf)
3174 .expect("Failed to parse multiline variable with operators");
3175
3176 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3178 assert!(
3179 !vars.is_empty(),
3180 "Expected at least 1 variable, found {}",
3181 vars.len()
3182 );
3183
3184 let prefix_var = vars
3186 .iter()
3187 .find(|v| v.name().unwrap_or_default() == "PREFIX");
3188 assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
3189 assert!(
3190 prefix_var.unwrap().raw_value().is_some(),
3191 "PREFIX variable has no value"
3192 );
3193
3194 let cflags_var = vars
3196 .iter()
3197 .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
3198 assert!(
3199 cflags_var.is_some(),
3200 "Expected to find CFLAGS variable (or part of it)"
3201 );
3202 }
3203
3204 #[test]
3205 fn test_indented_help_text() {
3206 let content = r#"
3207.PHONY: help
3208help:
3209 @echo "Available targets:"
3210 @echo " build - Build the project"
3211 @echo " test - Run tests"
3212 @echo " clean - Remove build artifacts"
3213"#;
3214 let mut buf = content.as_bytes();
3216 let makefile =
3217 Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
3218
3219 let rules = makefile.rules().collect::<Vec<_>>();
3221 assert!(!rules.is_empty(), "Expected at least one rule");
3222
3223 let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
3225 assert!(help_rule.is_some(), "Expected to find help rule");
3226
3227 let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
3229 assert!(
3230 !recipes.is_empty(),
3231 "Expected at least one recipe line in help rule"
3232 );
3233 assert!(
3234 recipes.iter().any(|r| r.contains("Available targets")),
3235 "Expected to find 'Available targets' in recipes"
3236 );
3237 }
3238
3239 #[test]
3240 fn test_indented_lines_in_conditionals() {
3241 let content = r#"
3242ifdef DEBUG
3243 CFLAGS += -g -DDEBUG
3244 # This is a comment inside conditional
3245 ifdef VERBOSE
3246 CFLAGS += -v
3247 endif
3248endif
3249"#;
3250 let mut buf = content.as_bytes();
3252 let makefile = Makefile::read_relaxed(&mut buf)
3253 .expect("Failed to parse indented lines in conditionals");
3254
3255 let code = makefile.code();
3257 assert!(code.contains("ifdef DEBUG"));
3258 assert!(code.contains("ifdef VERBOSE"));
3259 assert!(code.contains("endif"));
3260 }
3261
3262 #[test]
3263 fn test_recipe_with_colon() {
3264 let content = r#"
3265build:
3266 @echo "Building at: $(shell date)"
3267 gcc -o program main.c
3268"#;
3269 let parsed = parse(content, None);
3270 assert!(
3271 parsed.errors.is_empty(),
3272 "Failed to parse recipe with colon: {:?}",
3273 parsed.errors
3274 );
3275 }
3276
3277 #[test]
3278 #[ignore]
3279 fn test_double_colon_rules() {
3280 let content = r#"
3283%.o :: %.c
3284 $(CC) -c $< -o $@
3285
3286# Double colon allows multiple rules for same target
3287all:: prerequisite1
3288 @echo "First rule for all"
3289
3290all:: prerequisite2
3291 @echo "Second rule for all"
3292"#;
3293 let mut buf = content.as_bytes();
3294 let makefile =
3295 Makefile::read_relaxed(&mut buf).expect("Failed to parse double colon rules");
3296
3297 let rules = makefile.rules().collect::<Vec<_>>();
3299 assert!(!rules.is_empty(), "Expected at least one rule");
3300
3301 let all_rules = rules
3303 .iter()
3304 .filter(|r| r.targets().any(|t| t.contains("all")));
3305 assert!(
3306 all_rules.count() > 0,
3307 "Expected to find at least one rule containing 'all'"
3308 );
3309 }
3310
3311 #[test]
3312 fn test_else_conditional_directives() {
3313 let content = r#"
3315ifeq ($(OS),Windows_NT)
3316 TARGET = windows
3317else ifeq ($(OS),Darwin)
3318 TARGET = macos
3319else ifeq ($(OS),Linux)
3320 TARGET = linux
3321else
3322 TARGET = unknown
3323endif
3324"#;
3325 let mut buf = content.as_bytes();
3326 let makefile =
3327 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3328 assert!(makefile.code().contains("else ifeq"));
3329 assert!(makefile.code().contains("TARGET"));
3330
3331 let content = r#"
3333ifdef WINDOWS
3334 TARGET = windows
3335else ifdef DARWIN
3336 TARGET = macos
3337else ifdef LINUX
3338 TARGET = linux
3339else
3340 TARGET = unknown
3341endif
3342"#;
3343 let mut buf = content.as_bytes();
3344 let makefile =
3345 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3346 assert!(makefile.code().contains("else ifdef"));
3347
3348 let content = r#"
3350ifndef NOWINDOWS
3351 TARGET = windows
3352else ifndef NODARWIN
3353 TARGET = macos
3354else
3355 TARGET = linux
3356endif
3357"#;
3358 let mut buf = content.as_bytes();
3359 let makefile =
3360 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3361 assert!(makefile.code().contains("else ifndef"));
3362
3363 let content = r#"
3365ifneq ($(OS),Windows_NT)
3366 TARGET = not_windows
3367else ifneq ($(OS),Darwin)
3368 TARGET = not_macos
3369else
3370 TARGET = darwin
3371endif
3372"#;
3373 let mut buf = content.as_bytes();
3374 let makefile =
3375 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3376 assert!(makefile.code().contains("else ifneq"));
3377 }
3378
3379 #[test]
3380 fn test_complex_else_conditionals() {
3381 let content = r#"VAR1 := foo
3383VAR2 := bar
3384
3385ifeq ($(VAR1),foo)
3386 RESULT := foo_matched
3387else ifdef VAR2
3388 RESULT := var2_defined
3389else ifndef VAR3
3390 RESULT := var3_not_defined
3391else
3392 RESULT := final_else
3393endif
3394
3395all:
3396 @echo $(RESULT)
3397"#;
3398 let mut buf = content.as_bytes();
3399 let makefile =
3400 Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3401
3402 let code = makefile.code();
3404 assert!(code.contains("ifeq ($(VAR1),foo)"));
3405 assert!(code.contains("else ifdef VAR2"));
3406 assert!(code.contains("else ifndef VAR3"));
3407 assert!(code.contains("else"));
3408 assert!(code.contains("endif"));
3409 assert!(code.contains("RESULT"));
3410
3411 let rules: Vec<_> = makefile.rules().collect();
3413 assert_eq!(rules.len(), 1);
3414 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3415 }
3416
3417 #[test]
3418 fn test_conditional_token_structure() {
3419 let content = r#"ifdef VAR1
3421X := 1
3422else ifdef VAR2
3423X := 2
3424else
3425X := 3
3426endif
3427"#;
3428 let mut buf = content.as_bytes();
3429 let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3430
3431 let syntax = makefile.syntax();
3433
3434 let mut found_conditional = false;
3436 let mut found_conditional_if = false;
3437 let mut found_conditional_else = false;
3438 let mut found_conditional_endif = false;
3439
3440 fn check_node(
3441 node: &SyntaxNode,
3442 found_cond: &mut bool,
3443 found_if: &mut bool,
3444 found_else: &mut bool,
3445 found_endif: &mut bool,
3446 ) {
3447 match node.kind() {
3448 SyntaxKind::CONDITIONAL => *found_cond = true,
3449 SyntaxKind::CONDITIONAL_IF => *found_if = true,
3450 SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3451 SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3452 _ => {}
3453 }
3454
3455 for child in node.children() {
3456 check_node(&child, found_cond, found_if, found_else, found_endif);
3457 }
3458 }
3459
3460 check_node(
3461 syntax,
3462 &mut found_conditional,
3463 &mut found_conditional_if,
3464 &mut found_conditional_else,
3465 &mut found_conditional_endif,
3466 );
3467
3468 assert!(found_conditional, "Should have CONDITIONAL node");
3469 assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3470 assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3471 assert!(
3472 found_conditional_endif,
3473 "Should have CONDITIONAL_ENDIF node"
3474 );
3475 }
3476
3477 #[test]
3478 fn test_ambiguous_assignment_vs_rule() {
3479 const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3481
3482 let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3483 let makefile =
3484 Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3485
3486 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3487 let rules = makefile.rules().collect::<Vec<_>>();
3488
3489 assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3490 assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3491
3492 assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3493
3494 const SIMPLE_RULE: &str = "target: dependency\n";
3496
3497 let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3498 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3499
3500 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3501 let rules = makefile.rules().collect::<Vec<_>>();
3502
3503 assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3504 assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3505
3506 let rule = &rules[0];
3507 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3508 }
3509
3510 #[test]
3511 fn test_nested_conditionals() {
3512 let content = r#"
3513ifdef RELEASE
3514 CFLAGS += -O3
3515 ifndef DEBUG
3516 ifneq ($(ARCH),arm)
3517 CFLAGS += -march=native
3518 else
3519 CFLAGS += -mcpu=cortex-a72
3520 endif
3521 endif
3522endif
3523"#;
3524 let mut buf = content.as_bytes();
3526 let makefile =
3527 Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3528
3529 let code = makefile.code();
3531 assert!(code.contains("ifdef RELEASE"));
3532 assert!(code.contains("ifndef DEBUG"));
3533 assert!(code.contains("ifneq"));
3534 }
3535
3536 #[test]
3537 fn test_space_indented_recipes() {
3538 let content = r#"
3541build:
3542 @echo "Building with spaces instead of tabs"
3543 gcc -o program main.c
3544"#;
3545 let mut buf = content.as_bytes();
3547 let makefile =
3548 Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3549
3550 let rules = makefile.rules().collect::<Vec<_>>();
3552 assert!(!rules.is_empty(), "Expected at least one rule");
3553
3554 let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3556 assert!(build_rule.is_some(), "Expected to find build rule");
3557 }
3558
3559 #[test]
3560 fn test_complex_variable_functions() {
3561 let content = r#"
3562FILES := $(shell find . -name "*.c")
3563OBJS := $(patsubst %.c,%.o,$(FILES))
3564NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3565HEADERS := ${wildcard *.h}
3566"#;
3567 let parsed = parse(content, None);
3568 assert!(
3569 parsed.errors.is_empty(),
3570 "Failed to parse complex variable functions: {:?}",
3571 parsed.errors
3572 );
3573 }
3574
3575 #[test]
3576 fn test_nested_variable_expansions() {
3577 let content = r#"
3578VERSION = 1.0
3579PACKAGE = myapp
3580TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3581INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3582"#;
3583 let parsed = parse(content, None);
3584 assert!(
3585 parsed.errors.is_empty(),
3586 "Failed to parse nested variable expansions: {:?}",
3587 parsed.errors
3588 );
3589 }
3590
3591 #[test]
3592 fn test_special_directives() {
3593 let content = r#"
3594# Special makefile directives
3595.PHONY: all clean
3596.SUFFIXES: .c .o
3597.DEFAULT: all
3598
3599# Variable definition and export directive
3600export PATH := /usr/bin:/bin
3601"#;
3602 let mut buf = content.as_bytes();
3604 let makefile =
3605 Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3606
3607 let rules = makefile.rules().collect::<Vec<_>>();
3609
3610 let phony_rule = rules
3612 .iter()
3613 .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3614 assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3615
3616 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3618 assert!(!vars.is_empty(), "Expected to find at least one variable");
3619 }
3620
3621 #[test]
3624 fn test_comprehensive_real_world_makefile() {
3625 let content = r#"
3627# Basic variable assignment
3628VERSION = 1.0.0
3629
3630# Phony target
3631.PHONY: all clean
3632
3633# Simple rule
3634all:
3635 echo "Building version $(VERSION)"
3636
3637# Another rule with dependencies
3638clean:
3639 rm -f *.o
3640"#;
3641
3642 let parsed = parse(content, None);
3644
3645 assert!(parsed.errors.is_empty(), "Expected no parsing errors");
3647
3648 let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
3650 assert!(!variables.is_empty(), "Expected at least one variable");
3651 assert_eq!(
3652 variables[0].name(),
3653 Some("VERSION".to_string()),
3654 "Expected VERSION variable"
3655 );
3656
3657 let rules = parsed.root().rules().collect::<Vec<_>>();
3659 assert!(!rules.is_empty(), "Expected at least one rule");
3660
3661 let rule_targets: Vec<String> = rules
3663 .iter()
3664 .flat_map(|r| r.targets().collect::<Vec<_>>())
3665 .collect();
3666 assert!(
3667 rule_targets.contains(&".PHONY".to_string()),
3668 "Expected .PHONY rule"
3669 );
3670 assert!(
3671 rule_targets.contains(&"all".to_string()),
3672 "Expected 'all' rule"
3673 );
3674 assert!(
3675 rule_targets.contains(&"clean".to_string()),
3676 "Expected 'clean' rule"
3677 );
3678 }
3679
3680 #[test]
3681 fn test_indented_help_text_outside_rules() {
3682 let content = r#"
3684# Targets with help text
3685help:
3686 @echo "Available targets:"
3687 @echo " build build the project"
3688 @echo " test run tests"
3689 @echo " clean clean build artifacts"
3690
3691# Another target
3692clean:
3693 rm -rf build/
3694"#;
3695
3696 let parsed = parse(content, None);
3698
3699 assert!(
3701 parsed.errors.is_empty(),
3702 "Failed to parse indented help text"
3703 );
3704
3705 let rules = parsed.root().rules().collect::<Vec<_>>();
3707 assert_eq!(rules.len(), 2, "Expected to find two rules");
3708
3709 let help_rule = rules
3711 .iter()
3712 .find(|r| r.targets().any(|t| t == "help"))
3713 .expect("Expected to find help rule");
3714
3715 let clean_rule = rules
3716 .iter()
3717 .find(|r| r.targets().any(|t| t == "clean"))
3718 .expect("Expected to find clean rule");
3719
3720 let help_recipes = help_rule.recipes().collect::<Vec<_>>();
3722 assert!(
3723 !help_recipes.is_empty(),
3724 "Help rule should have recipe lines"
3725 );
3726 assert!(
3727 help_recipes
3728 .iter()
3729 .any(|line| line.contains("Available targets")),
3730 "Help recipes should include 'Available targets' line"
3731 );
3732
3733 let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
3735 assert!(
3736 !clean_recipes.is_empty(),
3737 "Clean rule should have recipe lines"
3738 );
3739 assert!(
3740 clean_recipes.iter().any(|line| line.contains("rm -rf")),
3741 "Clean recipes should include 'rm -rf' command"
3742 );
3743 }
3744
3745 #[test]
3746 fn test_makefile1_phony_pattern() {
3747 let content = "#line 2145\n.PHONY: $(PHONY)\n";
3749
3750 let result = parse(content, None);
3752
3753 assert!(
3755 result.errors.is_empty(),
3756 "Failed to parse .PHONY: $(PHONY) pattern"
3757 );
3758
3759 let rules = result.root().rules().collect::<Vec<_>>();
3761 assert_eq!(rules.len(), 1, "Expected 1 rule");
3762 assert_eq!(
3763 rules[0].targets().next().unwrap(),
3764 ".PHONY",
3765 "Expected .PHONY rule"
3766 );
3767
3768 let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
3770 assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
3771 assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
3772 }
3773
3774 #[test]
3775 fn test_skip_until_newline_behavior() {
3776 let input = "text without newline";
3778 let parsed = parse(input, None);
3779 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
3781
3782 let input_with_newline = "text\nafter newline";
3783 let parsed2 = parse(input_with_newline, None);
3784 assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
3785 }
3786
3787 #[test]
3788 #[ignore] fn test_error_with_indent_token() {
3790 let input = "\tinvalid indented line";
3792 let parsed = parse(input, None);
3793 assert!(!parsed.errors.is_empty());
3795
3796 let error_msg = &parsed.errors[0].message;
3797 assert!(error_msg.contains("recipe commences before first target"));
3798 }
3799
3800 #[test]
3801 fn test_conditional_token_handling() {
3802 let input = r#"
3804ifndef VAR
3805 CFLAGS = -DTEST
3806endif
3807"#;
3808 let parsed = parse(input, None);
3809 let makefile = parsed.root();
3811 let _vars = makefile.variable_definitions().collect::<Vec<_>>();
3812 let nested = r#"
3816ifdef DEBUG
3817 ifndef RELEASE
3818 CFLAGS = -g
3819 endif
3820endif
3821"#;
3822 let parsed_nested = parse(nested, None);
3823 let _makefile = parsed_nested.root();
3825 }
3826
3827 #[test]
3828 fn test_include_vs_conditional_logic() {
3829 let input = r#"
3831include file.mk
3832ifdef VAR
3833 VALUE = 1
3834endif
3835"#;
3836 let parsed = parse(input, None);
3837 let makefile = parsed.root();
3839 let includes = makefile.includes().collect::<Vec<_>>();
3840 assert!(!includes.is_empty() || !parsed.errors.is_empty());
3842
3843 let optional_include = r#"
3845-include optional.mk
3846ifndef VAR
3847 VALUE = default
3848endif
3849"#;
3850 let parsed2 = parse(optional_include, None);
3851 let _makefile = parsed2.root();
3853 }
3854
3855 #[test]
3856 fn test_balanced_parens_counting() {
3857 let input = r#"
3859VAR = $(call func,$(nested,arg),extra)
3860COMPLEX = $(if $(condition),$(then_val),$(else_val))
3861"#;
3862 let parsed = parse(input, None);
3863 assert!(parsed.errors.is_empty());
3864
3865 let makefile = parsed.root();
3866 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3867 assert_eq!(vars.len(), 2);
3868 }
3869
3870 #[test]
3871 fn test_documentation_lookahead() {
3872 let input = r#"
3874# Documentation comment
3875help:
3876 @echo "Usage instructions"
3877 @echo "More help text"
3878"#;
3879 let parsed = parse(input, None);
3880 assert!(parsed.errors.is_empty());
3881
3882 let makefile = parsed.root();
3883 let rules = makefile.rules().collect::<Vec<_>>();
3884 assert_eq!(rules.len(), 1);
3885 assert_eq!(rules[0].targets().next().unwrap(), "help");
3886 }
3887
3888 #[test]
3889 fn test_edge_case_empty_input() {
3890 let parsed = parse("", None);
3892 assert!(parsed.errors.is_empty());
3893
3894 let parsed2 = parse(" \n \n", None);
3896 let _makefile = parsed2.root();
3899 }
3900
3901 #[test]
3902 fn test_malformed_conditional_recovery() {
3903 let input = r#"
3905ifdef
3906 # Missing condition variable
3907endif
3908"#;
3909 let parsed = parse(input, None);
3910 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
3913 }
3914
3915 #[test]
3916 fn test_replace_rule() {
3917 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
3918 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3919
3920 makefile.replace_rule(0, new_rule).unwrap();
3921
3922 let targets: Vec<_> = makefile
3923 .rules()
3924 .flat_map(|r| r.targets().collect::<Vec<_>>())
3925 .collect();
3926 assert_eq!(targets, vec!["new_rule", "rule2"]);
3927
3928 let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
3929 assert_eq!(recipes, vec!["new_command"]);
3930 }
3931
3932 #[test]
3933 fn test_replace_rule_out_of_bounds() {
3934 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3935 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3936
3937 let result = makefile.replace_rule(5, new_rule);
3938 assert!(result.is_err());
3939 }
3940
3941 #[test]
3942 fn test_remove_rule() {
3943 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
3944 .parse()
3945 .unwrap();
3946
3947 let removed = makefile.remove_rule(1).unwrap();
3948 assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
3949
3950 let remaining_targets: Vec<_> = makefile
3951 .rules()
3952 .flat_map(|r| r.targets().collect::<Vec<_>>())
3953 .collect();
3954 assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
3955 assert_eq!(makefile.rules().count(), 2);
3956 }
3957
3958 #[test]
3959 fn test_remove_rule_out_of_bounds() {
3960 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3961
3962 let result = makefile.remove_rule(5);
3963 assert!(result.is_err());
3964 }
3965
3966 #[test]
3967 fn test_insert_rule() {
3968 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
3969 let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
3970
3971 makefile.insert_rule(1, new_rule).unwrap();
3972
3973 let targets: Vec<_> = makefile
3974 .rules()
3975 .flat_map(|r| r.targets().collect::<Vec<_>>())
3976 .collect();
3977 assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
3978 assert_eq!(makefile.rules().count(), 3);
3979 }
3980
3981 #[test]
3982 fn test_insert_rule_at_end() {
3983 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3984 let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
3985
3986 makefile.insert_rule(1, new_rule).unwrap();
3987
3988 let targets: Vec<_> = makefile
3989 .rules()
3990 .flat_map(|r| r.targets().collect::<Vec<_>>())
3991 .collect();
3992 assert_eq!(targets, vec!["rule1", "end_rule"]);
3993 }
3994
3995 #[test]
3996 fn test_insert_rule_out_of_bounds() {
3997 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3998 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3999
4000 let result = makefile.insert_rule(5, new_rule);
4001 assert!(result.is_err());
4002 }
4003
4004 #[test]
4005 fn test_insert_rule_preserves_blank_line_spacing_at_end() {
4006 let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
4008 let mut makefile: Makefile = input.parse().unwrap();
4009 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4010
4011 makefile.insert_rule(2, new_rule).unwrap();
4012
4013 let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4014 assert_eq!(makefile.to_string(), expected);
4015 }
4016
4017 #[test]
4018 fn test_insert_rule_adds_blank_lines_when_missing() {
4019 let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
4021 let mut makefile: Makefile = input.parse().unwrap();
4022 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4023
4024 makefile.insert_rule(2, new_rule).unwrap();
4025
4026 let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4027 assert_eq!(makefile.to_string(), expected);
4028 }
4029
4030 #[test]
4031 fn test_remove_command() {
4032 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4033 .parse()
4034 .unwrap();
4035
4036 rule.remove_command(1);
4037 let recipes: Vec<_> = rule.recipes().collect();
4038 assert_eq!(recipes, vec!["command1", "command3"]);
4039 assert_eq!(rule.recipe_count(), 2);
4040 }
4041
4042 #[test]
4043 fn test_remove_command_out_of_bounds() {
4044 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4045
4046 let result = rule.remove_command(5);
4047 assert!(!result);
4048 }
4049
4050 #[test]
4051 fn test_insert_command() {
4052 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
4053
4054 rule.insert_command(1, "command2");
4055 let recipes: Vec<_> = rule.recipes().collect();
4056 assert_eq!(recipes, vec!["command1", "command2", "command3"]);
4057 }
4058
4059 #[test]
4060 fn test_insert_command_at_end() {
4061 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4062
4063 rule.insert_command(1, "command2");
4064 let recipes: Vec<_> = rule.recipes().collect();
4065 assert_eq!(recipes, vec!["command1", "command2"]);
4066 }
4067
4068 #[test]
4069 fn test_insert_command_in_empty_rule() {
4070 let mut rule: Rule = "rule:\n".parse().unwrap();
4071
4072 rule.insert_command(0, "new_command");
4073 let recipes: Vec<_> = rule.recipes().collect();
4074 assert_eq!(recipes, vec!["new_command"]);
4075 }
4076
4077 #[test]
4078 fn test_recipe_count() {
4079 let rule1: Rule = "rule:\n".parse().unwrap();
4080 assert_eq!(rule1.recipe_count(), 0);
4081
4082 let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
4083 assert_eq!(rule2.recipe_count(), 2);
4084 }
4085
4086 #[test]
4087 fn test_clear_commands() {
4088 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4089 .parse()
4090 .unwrap();
4091
4092 rule.clear_commands();
4093 assert_eq!(rule.recipe_count(), 0);
4094
4095 let recipes: Vec<_> = rule.recipes().collect();
4096 assert_eq!(recipes, Vec::<String>::new());
4097
4098 let targets: Vec<_> = rule.targets().collect();
4100 assert_eq!(targets, vec!["rule"]);
4101 }
4102
4103 #[test]
4104 fn test_clear_commands_empty_rule() {
4105 let mut rule: Rule = "rule:\n".parse().unwrap();
4106
4107 rule.clear_commands();
4108 assert_eq!(rule.recipe_count(), 0);
4109
4110 let targets: Vec<_> = rule.targets().collect();
4111 assert_eq!(targets, vec!["rule"]);
4112 }
4113
4114 #[test]
4115 fn test_rule_manipulation_preserves_structure() {
4116 let input = r#"# Comment
4118VAR = value
4119
4120rule1:
4121 command1
4122
4123# Another comment
4124rule2:
4125 command2
4126
4127VAR2 = value2
4128"#;
4129
4130 let mut makefile: Makefile = input.parse().unwrap();
4131 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4132
4133 makefile.insert_rule(1, new_rule).unwrap();
4135
4136 let targets: Vec<_> = makefile
4138 .rules()
4139 .flat_map(|r| r.targets().collect::<Vec<_>>())
4140 .collect();
4141 assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
4142
4143 let vars: Vec<_> = makefile.variable_definitions().collect();
4145 assert_eq!(vars.len(), 2);
4146
4147 let output = makefile.code();
4149 assert!(output.contains("# Comment"));
4150 assert!(output.contains("VAR = value"));
4151 assert!(output.contains("# Another comment"));
4152 assert!(output.contains("VAR2 = value2"));
4153 }
4154
4155 #[test]
4156 fn test_replace_rule_with_multiple_targets() {
4157 let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
4158 let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
4159
4160 makefile.replace_rule(0, new_rule).unwrap();
4161
4162 let targets: Vec<_> = makefile
4163 .rules()
4164 .flat_map(|r| r.targets().collect::<Vec<_>>())
4165 .collect();
4166 assert_eq!(targets, vec!["new_target"]);
4167 }
4168
4169 #[test]
4170 fn test_empty_makefile_operations() {
4171 let mut makefile = Makefile::new();
4172
4173 assert!(makefile
4175 .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
4176 .is_err());
4177 assert!(makefile.remove_rule(0).is_err());
4178
4179 let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
4181 makefile.insert_rule(0, new_rule).unwrap();
4182 assert_eq!(makefile.rules().count(), 1);
4183 }
4184
4185 #[test]
4186 fn test_command_operations_preserve_indentation() {
4187 let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
4188 .parse()
4189 .unwrap();
4190
4191 rule.insert_command(1, "middle_command");
4192 let recipes: Vec<_> = rule.recipes().collect();
4193 assert_eq!(
4194 recipes,
4195 vec!["\tdeep_indent", "middle_command", "shallow_indent"]
4196 );
4197 }
4198
4199 #[test]
4200 fn test_rule_operations_with_variables_and_includes() {
4201 let input = r#"VAR1 = value1
4202include common.mk
4203
4204rule1:
4205 command1
4206
4207VAR2 = value2
4208include other.mk
4209
4210rule2:
4211 command2
4212"#;
4213
4214 let mut makefile: Makefile = input.parse().unwrap();
4215
4216 makefile.remove_rule(0).unwrap();
4218
4219 let output = makefile.code();
4221 assert!(output.contains("VAR1 = value1"));
4222 assert!(output.contains("include common.mk"));
4223 assert!(output.contains("VAR2 = value2"));
4224 assert!(output.contains("include other.mk"));
4225
4226 assert_eq!(makefile.rules().count(), 1);
4228 let remaining_targets: Vec<_> = makefile
4229 .rules()
4230 .flat_map(|r| r.targets().collect::<Vec<_>>())
4231 .collect();
4232 assert_eq!(remaining_targets, vec!["rule2"]);
4233 }
4234
4235 #[test]
4236 fn test_command_manipulation_edge_cases() {
4237 let mut empty_rule: Rule = "empty:\n".parse().unwrap();
4239 assert_eq!(empty_rule.recipe_count(), 0);
4240
4241 empty_rule.insert_command(0, "first_command");
4242 assert_eq!(empty_rule.recipe_count(), 1);
4243
4244 let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
4246 empty_rule2.clear_commands();
4247 assert_eq!(empty_rule2.recipe_count(), 0);
4248 }
4249
4250 #[test]
4251 fn test_large_makefile_performance() {
4252 let mut makefile = Makefile::new();
4254
4255 for i in 0..100 {
4257 let rule_name = format!("rule{}", i);
4258 makefile
4259 .add_rule(&rule_name)
4260 .push_command(&format!("command{}", i));
4261 }
4262
4263 assert_eq!(makefile.rules().count(), 100);
4264
4265 let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
4267 makefile.replace_rule(50, new_rule).unwrap();
4268
4269 let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
4271 assert_eq!(rule_50_targets, vec!["middle_rule"]);
4272
4273 assert_eq!(makefile.rules().count(), 100); }
4275
4276 #[test]
4277 fn test_complex_recipe_manipulation() {
4278 let mut complex_rule: Rule = r#"complex:
4279 @echo "Starting build"
4280 $(CC) $(CFLAGS) -o $@ $<
4281 @echo "Build complete"
4282 chmod +x $@
4283"#
4284 .parse()
4285 .unwrap();
4286
4287 assert_eq!(complex_rule.recipe_count(), 4);
4288
4289 complex_rule.remove_command(0); complex_rule.remove_command(1); let final_recipes: Vec<_> = complex_rule.recipes().collect();
4294 assert_eq!(final_recipes.len(), 2);
4295 assert!(final_recipes[0].contains("$(CC)"));
4296 assert!(final_recipes[1].contains("chmod"));
4297 }
4298
4299 #[test]
4300 fn test_variable_definition_remove() {
4301 let makefile: Makefile = r#"VAR1 = value1
4302VAR2 = value2
4303VAR3 = value3
4304"#
4305 .parse()
4306 .unwrap();
4307
4308 assert_eq!(makefile.variable_definitions().count(), 3);
4310
4311 let mut var2 = makefile
4313 .variable_definitions()
4314 .nth(1)
4315 .expect("Should have second variable");
4316 assert_eq!(var2.name(), Some("VAR2".to_string()));
4317 var2.remove();
4318
4319 assert_eq!(makefile.variable_definitions().count(), 2);
4321 let var_names: Vec<_> = makefile
4322 .variable_definitions()
4323 .filter_map(|v| v.name())
4324 .collect();
4325 assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4326 }
4327
4328 #[test]
4329 fn test_variable_definition_set_value() {
4330 let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4331
4332 let mut var = makefile
4333 .variable_definitions()
4334 .next()
4335 .expect("Should have variable");
4336 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4337
4338 var.set_value("new_value");
4340
4341 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4343 assert!(makefile.code().contains("VAR = new_value"));
4344 }
4345
4346 #[test]
4347 fn test_variable_definition_set_value_preserves_format() {
4348 let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4349
4350 let mut var = makefile
4351 .variable_definitions()
4352 .next()
4353 .expect("Should have variable");
4354 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4355
4356 var.set_value("new_value");
4358
4359 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4361 let code = makefile.code();
4362 assert!(code.contains("export"), "Should preserve export prefix");
4363 assert!(code.contains(":="), "Should preserve := operator");
4364 assert!(code.contains("new_value"), "Should have new value");
4365 }
4366
4367 #[test]
4368 fn test_makefile_find_variable() {
4369 let makefile: Makefile = r#"VAR1 = value1
4370VAR2 = value2
4371VAR3 = value3
4372"#
4373 .parse()
4374 .unwrap();
4375
4376 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4378 assert_eq!(vars.len(), 1);
4379 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4380 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4381
4382 assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4384 }
4385
4386 #[test]
4387 fn test_makefile_find_variable_with_export() {
4388 let makefile: Makefile = r#"VAR1 = value1
4389export VAR2 := value2
4390VAR3 = value3
4391"#
4392 .parse()
4393 .unwrap();
4394
4395 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4397 assert_eq!(vars.len(), 1);
4398 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4399 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4400 }
4401
4402 #[test]
4403 fn test_variable_definition_is_export() {
4404 let makefile: Makefile = r#"VAR1 = value1
4405export VAR2 := value2
4406export VAR3 = value3
4407VAR4 := value4
4408"#
4409 .parse()
4410 .unwrap();
4411
4412 let vars: Vec<_> = makefile.variable_definitions().collect();
4413 assert_eq!(vars.len(), 4);
4414
4415 assert!(!vars[0].is_export());
4416 assert!(vars[1].is_export());
4417 assert!(vars[2].is_export());
4418 assert!(!vars[3].is_export());
4419 }
4420
4421 #[test]
4422 fn test_makefile_find_variable_multiple() {
4423 let makefile: Makefile = r#"VAR1 = value1
4424VAR1 = value2
4425VAR2 = other
4426VAR1 = value3
4427"#
4428 .parse()
4429 .unwrap();
4430
4431 let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4433 assert_eq!(vars.len(), 3);
4434 assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4435 assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4436 assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4437
4438 let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4440 assert_eq!(var2s.len(), 1);
4441 assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4442 }
4443
4444 #[test]
4445 fn test_variable_remove_and_find() {
4446 let makefile: Makefile = r#"VAR1 = value1
4447VAR2 = value2
4448VAR3 = value3
4449"#
4450 .parse()
4451 .unwrap();
4452
4453 let mut var2 = makefile
4455 .find_variable("VAR2")
4456 .next()
4457 .expect("Should find VAR2");
4458 var2.remove();
4459
4460 assert_eq!(makefile.find_variable("VAR2").count(), 0);
4462
4463 assert_eq!(makefile.find_variable("VAR1").count(), 1);
4465 assert_eq!(makefile.find_variable("VAR3").count(), 1);
4466 }
4467
4468 #[test]
4469 fn test_variable_remove_with_comment() {
4470 let makefile: Makefile = r#"VAR1 = value1
4471# This is a comment about VAR2
4472VAR2 = value2
4473VAR3 = value3
4474"#
4475 .parse()
4476 .unwrap();
4477
4478 let mut var2 = makefile
4480 .variable_definitions()
4481 .nth(1)
4482 .expect("Should have second variable");
4483 assert_eq!(var2.name(), Some("VAR2".to_string()));
4484 var2.remove();
4485
4486 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4488 }
4489
4490 #[test]
4491 fn test_variable_remove_with_multiple_comments() {
4492 let makefile: Makefile = r#"VAR1 = value1
4493# Comment line 1
4494# Comment line 2
4495# Comment line 3
4496VAR2 = value2
4497VAR3 = value3
4498"#
4499 .parse()
4500 .unwrap();
4501
4502 let mut var2 = makefile
4504 .variable_definitions()
4505 .nth(1)
4506 .expect("Should have second variable");
4507 var2.remove();
4508
4509 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4511 }
4512
4513 #[test]
4514 fn test_variable_remove_with_empty_line() {
4515 let makefile: Makefile = r#"VAR1 = value1
4516
4517# Comment about VAR2
4518VAR2 = value2
4519VAR3 = value3
4520"#
4521 .parse()
4522 .unwrap();
4523
4524 let mut var2 = makefile
4526 .variable_definitions()
4527 .nth(1)
4528 .expect("Should have second variable");
4529 var2.remove();
4530
4531 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4534 }
4535
4536 #[test]
4537 fn test_variable_remove_with_multiple_empty_lines() {
4538 let makefile: Makefile = r#"VAR1 = value1
4539
4540
4541# Comment about VAR2
4542VAR2 = value2
4543VAR3 = value3
4544"#
4545 .parse()
4546 .unwrap();
4547
4548 let mut var2 = makefile
4550 .variable_definitions()
4551 .nth(1)
4552 .expect("Should have second variable");
4553 var2.remove();
4554
4555 assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4558 }
4559
4560 #[test]
4561 fn test_rule_remove_with_comment() {
4562 let makefile: Makefile = r#"rule1:
4563 command1
4564
4565# Comment about rule2
4566rule2:
4567 command2
4568rule3:
4569 command3
4570"#
4571 .parse()
4572 .unwrap();
4573
4574 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4576 rule2.remove().unwrap();
4577
4578 assert_eq!(
4581 makefile.code(),
4582 "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4583 );
4584 }
4585
4586 #[test]
4587 fn test_variable_remove_preserves_shebang() {
4588 let makefile: Makefile = r#"#!/usr/bin/make -f
4589# This is a regular comment
4590VAR1 = value1
4591VAR2 = value2
4592"#
4593 .parse()
4594 .unwrap();
4595
4596 let mut var1 = makefile.variable_definitions().next().unwrap();
4598 var1.remove();
4599
4600 let code = makefile.code();
4602 assert!(code.starts_with("#!/usr/bin/make -f"));
4603 assert!(!code.contains("regular comment"));
4604 assert!(!code.contains("VAR1"));
4605 assert!(code.contains("VAR2"));
4606 }
4607
4608 #[test]
4609 fn test_variable_remove_preserves_subsequent_comments() {
4610 let makefile: Makefile = r#"VAR1 = value1
4611# Comment about VAR2
4612VAR2 = value2
4613
4614# Comment about VAR3
4615VAR3 = value3
4616"#
4617 .parse()
4618 .unwrap();
4619
4620 let mut var2 = makefile
4622 .variable_definitions()
4623 .nth(1)
4624 .expect("Should have second variable");
4625 var2.remove();
4626
4627 let code = makefile.code();
4629 assert_eq!(
4630 code,
4631 "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4632 );
4633 }
4634
4635 #[test]
4636 fn test_variable_remove_after_shebang_preserves_empty_line() {
4637 let makefile: Makefile = r#"#!/usr/bin/make -f
4638export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4639
4640%:
4641 dh $@
4642"#
4643 .parse()
4644 .unwrap();
4645
4646 let mut var = makefile.variable_definitions().next().unwrap();
4648 var.remove();
4649
4650 assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
4652 }
4653
4654 #[test]
4655 fn test_rule_add_prerequisite() {
4656 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4657 rule.add_prerequisite("dep2").unwrap();
4658 assert_eq!(
4659 rule.prerequisites().collect::<Vec<_>>(),
4660 vec!["dep1", "dep2"]
4661 );
4662 assert_eq!(rule.to_string(), "target: dep1 dep2\n");
4664 }
4665
4666 #[test]
4667 fn test_rule_add_prerequisite_to_rule_without_prereqs() {
4668 let mut rule: Rule = "target:\n".parse().unwrap();
4670 rule.add_prerequisite("dep1").unwrap();
4671 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
4672 assert_eq!(rule.to_string(), "target: dep1\n");
4674 }
4675
4676 #[test]
4677 fn test_rule_remove_prerequisite() {
4678 let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
4679 assert!(rule.remove_prerequisite("dep2").unwrap());
4680 assert_eq!(
4681 rule.prerequisites().collect::<Vec<_>>(),
4682 vec!["dep1", "dep3"]
4683 );
4684 assert!(!rule.remove_prerequisite("nonexistent").unwrap());
4685 }
4686
4687 #[test]
4688 fn test_rule_set_prerequisites() {
4689 let mut rule: Rule = "target: old_dep\n".parse().unwrap();
4690 rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
4691 .unwrap();
4692 assert_eq!(
4693 rule.prerequisites().collect::<Vec<_>>(),
4694 vec!["new_dep1", "new_dep2"]
4695 );
4696 }
4697
4698 #[test]
4699 fn test_rule_set_prerequisites_empty() {
4700 let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
4701 rule.set_prerequisites(vec![]).unwrap();
4702 assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
4703 }
4704
4705 #[test]
4706 fn test_rule_add_target() {
4707 let mut rule: Rule = "target1: dep1\n".parse().unwrap();
4708 rule.add_target("target2").unwrap();
4709 assert_eq!(
4710 rule.targets().collect::<Vec<_>>(),
4711 vec!["target1", "target2"]
4712 );
4713 }
4714
4715 #[test]
4716 fn test_rule_set_targets() {
4717 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
4718 rule.set_targets(vec!["new_target1", "new_target2"])
4719 .unwrap();
4720 assert_eq!(
4721 rule.targets().collect::<Vec<_>>(),
4722 vec!["new_target1", "new_target2"]
4723 );
4724 }
4725
4726 #[test]
4727 fn test_rule_set_targets_empty() {
4728 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4729 let result = rule.set_targets(vec![]);
4730 assert!(result.is_err());
4731 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
4733 }
4734
4735 #[test]
4736 fn test_rule_has_target() {
4737 let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
4738 assert!(rule.has_target("target1"));
4739 assert!(rule.has_target("target2"));
4740 assert!(!rule.has_target("target3"));
4741 assert!(!rule.has_target("nonexistent"));
4742 }
4743
4744 #[test]
4745 fn test_rule_rename_target() {
4746 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
4747 assert!(rule.rename_target("old_target", "new_target").unwrap());
4748 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
4749 assert!(!rule.rename_target("nonexistent", "something").unwrap());
4751 }
4752
4753 #[test]
4754 fn test_rule_rename_target_multiple() {
4755 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
4756 assert!(rule.rename_target("target2", "renamed_target").unwrap());
4757 assert_eq!(
4758 rule.targets().collect::<Vec<_>>(),
4759 vec!["target1", "renamed_target", "target3"]
4760 );
4761 }
4762
4763 #[test]
4764 fn test_rule_remove_target() {
4765 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
4766 assert!(rule.remove_target("target2").unwrap());
4767 assert_eq!(
4768 rule.targets().collect::<Vec<_>>(),
4769 vec!["target1", "target3"]
4770 );
4771 assert!(!rule.remove_target("nonexistent").unwrap());
4773 }
4774
4775 #[test]
4776 fn test_rule_remove_target_last() {
4777 let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
4778 let result = rule.remove_target("single_target");
4779 assert!(result.is_err());
4780 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
4782 }
4783
4784 #[test]
4785 fn test_rule_target_manipulation_preserves_prerequisites() {
4786 let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
4787
4788 rule.remove_target("target1").unwrap();
4790 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
4791 assert_eq!(
4792 rule.prerequisites().collect::<Vec<_>>(),
4793 vec!["dep1", "dep2"]
4794 );
4795 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4796
4797 rule.add_target("target3").unwrap();
4799 assert_eq!(
4800 rule.targets().collect::<Vec<_>>(),
4801 vec!["target2", "target3"]
4802 );
4803 assert_eq!(
4804 rule.prerequisites().collect::<Vec<_>>(),
4805 vec!["dep1", "dep2"]
4806 );
4807 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4808
4809 rule.rename_target("target2", "renamed").unwrap();
4811 assert_eq!(
4812 rule.targets().collect::<Vec<_>>(),
4813 vec!["renamed", "target3"]
4814 );
4815 assert_eq!(
4816 rule.prerequisites().collect::<Vec<_>>(),
4817 vec!["dep1", "dep2"]
4818 );
4819 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4820 }
4821
4822 #[test]
4823 fn test_rule_remove() {
4824 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4825 let rule = makefile.find_rule_by_target("rule1").unwrap();
4826 rule.remove().unwrap();
4827 assert_eq!(makefile.rules().count(), 1);
4828 assert!(makefile.find_rule_by_target("rule1").is_none());
4829 assert!(makefile.find_rule_by_target("rule2").is_some());
4830 }
4831
4832 #[test]
4833 fn test_rule_remove_last_trims_blank_lines() {
4834 let makefile: Makefile =
4836 "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
4837 .parse()
4838 .unwrap();
4839
4840 let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
4842 rule.remove().unwrap();
4843
4844 assert_eq!(makefile.code(), "%:\n\tdh $@\n");
4846 assert_eq!(makefile.rules().count(), 1);
4847 }
4848
4849 #[test]
4850 fn test_makefile_find_rule_by_target() {
4851 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4852 let rule = makefile.find_rule_by_target("rule2");
4853 assert!(rule.is_some());
4854 assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
4855 assert!(makefile.find_rule_by_target("nonexistent").is_none());
4856 }
4857
4858 #[test]
4859 fn test_makefile_find_rules_by_target() {
4860 let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
4861 .parse()
4862 .unwrap();
4863 assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
4864 assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
4865 assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
4866 }
4867
4868 #[test]
4869 fn test_makefile_find_rule_by_target_pattern_simple() {
4870 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4871 let rule = makefile.find_rule_by_target_pattern("foo.o");
4872 assert!(rule.is_some());
4873 assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
4874 }
4875
4876 #[test]
4877 fn test_makefile_find_rule_by_target_pattern_no_match() {
4878 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4879 let rule = makefile.find_rule_by_target_pattern("foo.c");
4880 assert!(rule.is_none());
4881 }
4882
4883 #[test]
4884 fn test_makefile_find_rule_by_target_pattern_exact() {
4885 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
4886 let rule = makefile.find_rule_by_target_pattern("foo.o");
4887 assert!(rule.is_some());
4888 assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
4889 }
4890
4891 #[test]
4892 fn test_makefile_find_rule_by_target_pattern_prefix() {
4893 let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
4894 let rule = makefile.find_rule_by_target_pattern("libfoo.a");
4895 assert!(rule.is_some());
4896 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
4897 }
4898
4899 #[test]
4900 fn test_makefile_find_rule_by_target_pattern_suffix() {
4901 let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4902 let rule = makefile.find_rule_by_target_pattern("foo_test.o");
4903 assert!(rule.is_some());
4904 assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
4905 }
4906
4907 #[test]
4908 fn test_makefile_find_rule_by_target_pattern_middle() {
4909 let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
4910 let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
4911 assert!(rule.is_some());
4912 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
4913 }
4914
4915 #[test]
4916 fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
4917 let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
4918 let rule = makefile.find_rule_by_target_pattern("anything");
4919 assert!(rule.is_some());
4920 assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
4921 }
4922
4923 #[test]
4924 fn test_makefile_find_rules_by_target_pattern_multiple() {
4925 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
4926 .parse()
4927 .unwrap();
4928 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4929 assert_eq!(rules.len(), 2);
4930 }
4931
4932 #[test]
4933 fn test_makefile_find_rules_by_target_pattern_mixed() {
4934 let makefile: Makefile =
4935 "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
4936 .parse()
4937 .unwrap();
4938 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4939 assert_eq!(rules.len(), 2); let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
4941 assert_eq!(rules.len(), 1); }
4943
4944 #[test]
4945 fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
4946 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
4947 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4948 assert_eq!(rules.len(), 1);
4949 let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
4950 assert_eq!(rules.len(), 0);
4951 }
4952
4953 #[test]
4954 fn test_matches_pattern_exact() {
4955 assert!(matches_pattern("foo.o", "foo.o"));
4956 assert!(!matches_pattern("foo.o", "bar.o"));
4957 }
4958
4959 #[test]
4960 fn test_matches_pattern_suffix() {
4961 assert!(matches_pattern("%.o", "foo.o"));
4962 assert!(matches_pattern("%.o", "bar.o"));
4963 assert!(matches_pattern("%.o", "baz/qux.o"));
4964 assert!(!matches_pattern("%.o", "foo.c"));
4965 }
4966
4967 #[test]
4968 fn test_matches_pattern_prefix() {
4969 assert!(matches_pattern("lib%.a", "libfoo.a"));
4970 assert!(matches_pattern("lib%.a", "libbar.a"));
4971 assert!(!matches_pattern("lib%.a", "foo.a"));
4972 assert!(!matches_pattern("lib%.a", "lib.a"));
4973 }
4974
4975 #[test]
4976 fn test_matches_pattern_middle() {
4977 assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
4978 assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
4979 assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
4980 assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
4981 }
4982
4983 #[test]
4984 fn test_matches_pattern_wildcard_only() {
4985 assert!(matches_pattern("%", "anything"));
4986 assert!(matches_pattern("%", "foo.o"));
4987 assert!(!matches_pattern("%", ""));
4989 }
4990
4991 #[test]
4992 fn test_matches_pattern_empty_stem() {
4993 assert!(!matches_pattern("%.o", ".o")); assert!(!matches_pattern("lib%", "lib")); assert!(!matches_pattern("lib%.a", "lib.a")); }
4998
4999 #[test]
5000 fn test_matches_pattern_multiple_wildcards_not_supported() {
5001 assert!(!matches_pattern("%foo%bar", "xfooybarz"));
5004 assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
5005 }
5006
5007 #[test]
5008 fn test_makefile_add_phony_target() {
5009 let mut makefile = Makefile::new();
5010 makefile.add_phony_target("clean").unwrap();
5011 assert!(makefile.is_phony("clean"));
5012 assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
5013 }
5014
5015 #[test]
5016 fn test_makefile_add_phony_target_existing() {
5017 let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
5018 makefile.add_phony_target("clean").unwrap();
5019 assert!(makefile.is_phony("test"));
5020 assert!(makefile.is_phony("clean"));
5021 let targets: Vec<_> = makefile.phony_targets().collect();
5022 assert!(targets.contains(&"test".to_string()));
5023 assert!(targets.contains(&"clean".to_string()));
5024 }
5025
5026 #[test]
5027 fn test_makefile_remove_phony_target() {
5028 let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5029 assert!(makefile.remove_phony_target("clean").unwrap());
5030 assert!(!makefile.is_phony("clean"));
5031 assert!(makefile.is_phony("test"));
5032 assert!(!makefile.remove_phony_target("nonexistent").unwrap());
5033 }
5034
5035 #[test]
5036 fn test_makefile_remove_phony_target_last() {
5037 let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
5038 assert!(makefile.remove_phony_target("clean").unwrap());
5039 assert!(!makefile.is_phony("clean"));
5040 assert!(makefile.find_rule_by_target(".PHONY").is_none());
5042 }
5043
5044 #[test]
5045 fn test_makefile_is_phony() {
5046 let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5047 assert!(makefile.is_phony("clean"));
5048 assert!(makefile.is_phony("test"));
5049 assert!(!makefile.is_phony("build"));
5050 }
5051
5052 #[test]
5053 fn test_makefile_phony_targets() {
5054 let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5055 let phony_targets: Vec<_> = makefile.phony_targets().collect();
5056 assert_eq!(phony_targets, vec!["clean", "test", "build"]);
5057 }
5058
5059 #[test]
5060 fn test_makefile_phony_targets_empty() {
5061 let makefile = Makefile::new();
5062 assert_eq!(makefile.phony_targets().count(), 0);
5063 }
5064
5065 #[test]
5066 fn test_makefile_remove_first_phony_target_no_extra_space() {
5067 let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5068 assert!(makefile.remove_phony_target("clean").unwrap());
5069 let result = makefile.to_string();
5070 assert_eq!(result, ".PHONY: test build\n");
5071 }
5072
5073 #[test]
5074 fn test_recipe_with_leading_comments_and_blank_lines() {
5075 let makefile_text = r#"#!/usr/bin/make
5079
5080%:
5081 dh $@
5082
5083override_dh_build:
5084 # The next line is empty
5085
5086 dh_python3
5087"#;
5088 let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
5089
5090 let rules: Vec<_> = makefile.rules().collect();
5091 assert_eq!(rules.len(), 2, "Expected 2 rules");
5092
5093 let rule0 = &rules[0];
5095 assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
5096 assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
5097
5098 let rule1 = &rules[1];
5100 assert_eq!(
5101 rule1.targets().collect::<Vec<_>>(),
5102 vec!["override_dh_build"]
5103 );
5104
5105 let recipes: Vec<_> = rule1.recipes().collect();
5107 assert!(
5108 !recipes.is_empty(),
5109 "Expected at least one recipe for override_dh_build, got none"
5110 );
5111 assert!(
5112 recipes.contains(&"dh_python3".to_string()),
5113 "Expected 'dh_python3' in recipes, got: {:?}",
5114 recipes
5115 );
5116 }
5117
5118 #[test]
5119 fn test_rule_parse_preserves_trailing_blank_lines() {
5120 let input = r#"override_dh_systemd_enable:
5123 dh_systemd_enable -pracoon
5124
5125override_dh_install:
5126 dh_install
5127"#;
5128
5129 let mut mf: Makefile = input.parse().unwrap();
5130
5131 let rule = mf.rules().next().unwrap();
5133 let rule_text = rule.to_string();
5134
5135 assert_eq!(
5137 rule_text,
5138 "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
5139 );
5140
5141 let modified =
5143 rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
5144
5145 let new_rule: Rule = modified.parse().unwrap();
5147 assert_eq!(
5148 new_rule.to_string(),
5149 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
5150 );
5151
5152 mf.replace_rule(0, new_rule).unwrap();
5154
5155 let output = mf.to_string();
5157 assert!(
5158 output.contains(
5159 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
5160 ),
5161 "Blank line between rules should be preserved. Got: {:?}",
5162 output
5163 );
5164 }
5165
5166 #[test]
5167 fn test_rule_parse_round_trip_with_trailing_newlines() {
5168 let test_cases = vec![
5170 "rule:\n\tcommand\n", "rule:\n\tcommand\n\n", "rule:\n\tcommand\n\n\n", ];
5174
5175 for rule_text in test_cases {
5176 let rule: Rule = rule_text.parse().unwrap();
5177 let result = rule.to_string();
5178 assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
5179 }
5180 }
5181
5182 #[test]
5183 fn test_rule_clone() {
5184 let rule_text = "rule:\n\tcommand\n\n";
5186 let rule: Rule = rule_text.parse().unwrap();
5187 let cloned = rule.clone();
5188
5189 assert_eq!(rule.to_string(), cloned.to_string());
5191 assert_eq!(rule.to_string(), rule_text);
5192 assert_eq!(cloned.to_string(), rule_text);
5193
5194 assert_eq!(
5196 rule.targets().collect::<Vec<_>>(),
5197 cloned.targets().collect::<Vec<_>>()
5198 );
5199 assert_eq!(
5200 rule.recipes().collect::<Vec<_>>(),
5201 cloned.recipes().collect::<Vec<_>>()
5202 );
5203 }
5204
5205 #[test]
5206 fn test_makefile_clone() {
5207 let input = "VAR = value\n\nrule:\n\tcommand\n";
5209 let makefile: Makefile = input.parse().unwrap();
5210 let cloned = makefile.clone();
5211
5212 assert_eq!(makefile.to_string(), cloned.to_string());
5214 assert_eq!(makefile.to_string(), input);
5215
5216 assert_eq!(makefile.rules().count(), cloned.rules().count());
5218
5219 assert_eq!(
5221 makefile.variable_definitions().count(),
5222 cloned.variable_definitions().count()
5223 );
5224 }
5225
5226 #[test]
5227 fn test_conditional_with_recipe_line() {
5228 let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
5230 let parsed = parse(input, None);
5231
5232 assert!(
5234 parsed.errors.is_empty(),
5235 "Expected no parse errors, but got: {:?}",
5236 parsed.errors
5237 );
5238
5239 let mf = parsed.root();
5241 assert_eq!(mf.code(), input);
5242 }
5243
5244 #[test]
5245 fn test_conditional_in_rule_recipe() {
5246 let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
5248 let parsed = parse(input, None);
5249
5250 assert!(
5252 parsed.errors.is_empty(),
5253 "Expected no parse errors, but got: {:?}",
5254 parsed.errors
5255 );
5256
5257 let mf = parsed.root();
5259 assert_eq!(mf.code(), input);
5260
5261 assert_eq!(mf.rules().count(), 1);
5263 }
5264
5265 #[test]
5266 fn test_rule_items() {
5267 use crate::RuleItem;
5268
5269 let input = r#"test:
5271 echo "before"
5272ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
5273 ./run-tests
5274endif
5275 echo "after"
5276"#;
5277 let rule: Rule = input.parse().unwrap();
5278
5279 let items: Vec<_> = rule.items().collect();
5280 assert_eq!(
5281 items.len(),
5282 3,
5283 "Expected 3 items: recipe, conditional, recipe"
5284 );
5285
5286 match &items[0] {
5288 RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
5289 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5290 }
5291
5292 match &items[1] {
5294 RuleItem::Conditional(c) => {
5295 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5296 }
5297 RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
5298 }
5299
5300 match &items[2] {
5302 RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5303 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5304 }
5305
5306 let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5308 let simple_items: Vec<_> = simple_rule.items().collect();
5309 assert_eq!(simple_items.len(), 2);
5310
5311 match &simple_items[0] {
5312 RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5313 _ => panic!("Expected recipe"),
5314 }
5315
5316 match &simple_items[1] {
5317 RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5318 _ => panic!("Expected recipe"),
5319 }
5320
5321 let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5323 .parse()
5324 .unwrap();
5325 let cond_items: Vec<_> = cond_only.items().collect();
5326 assert_eq!(cond_items.len(), 1);
5327
5328 match &cond_items[0] {
5329 RuleItem::Conditional(c) => {
5330 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5331 }
5332 _ => panic!("Expected conditional"),
5333 }
5334 }
5335
5336 #[test]
5337 fn test_conditionals_iterator() {
5338 let makefile: Makefile = r#"ifdef DEBUG
5339VAR = debug
5340endif
5341
5342ifndef RELEASE
5343OTHER = dev
5344endif
5345"#
5346 .parse()
5347 .unwrap();
5348
5349 let conditionals: Vec<_> = makefile.conditionals().collect();
5350 assert_eq!(conditionals.len(), 2);
5351
5352 assert_eq!(
5353 conditionals[0].conditional_type(),
5354 Some("ifdef".to_string())
5355 );
5356 assert_eq!(
5357 conditionals[1].conditional_type(),
5358 Some("ifndef".to_string())
5359 );
5360 }
5361
5362 #[test]
5363 fn test_conditional_type_and_condition() {
5364 let makefile: Makefile = r#"ifdef DEBUG
5365VAR = debug
5366endif
5367"#
5368 .parse()
5369 .unwrap();
5370
5371 let conditional = makefile.conditionals().next().unwrap();
5372 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5373 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5374 }
5375
5376 #[test]
5377 fn test_conditional_has_else() {
5378 let makefile_with_else: Makefile = r#"ifdef DEBUG
5379VAR = debug
5380else
5381VAR = release
5382endif
5383"#
5384 .parse()
5385 .unwrap();
5386
5387 let conditional = makefile_with_else.conditionals().next().unwrap();
5388 assert!(conditional.has_else());
5389
5390 let makefile_without_else: Makefile = r#"ifdef DEBUG
5391VAR = debug
5392endif
5393"#
5394 .parse()
5395 .unwrap();
5396
5397 let conditional = makefile_without_else.conditionals().next().unwrap();
5398 assert!(!conditional.has_else());
5399 }
5400
5401 #[test]
5402 fn test_conditional_if_body() {
5403 let makefile: Makefile = r#"ifdef DEBUG
5404VAR = debug
5405endif
5406"#
5407 .parse()
5408 .unwrap();
5409
5410 let conditional = makefile.conditionals().next().unwrap();
5411 let if_body = conditional.if_body();
5412 assert!(if_body.is_some());
5413 assert!(if_body.unwrap().contains("VAR = debug"));
5414 }
5415
5416 #[test]
5417 fn test_conditional_else_body() {
5418 let makefile: Makefile = r#"ifdef DEBUG
5419VAR = debug
5420else
5421VAR = release
5422endif
5423"#
5424 .parse()
5425 .unwrap();
5426
5427 let conditional = makefile.conditionals().next().unwrap();
5428 let else_body = conditional.else_body();
5429 assert!(else_body.is_some());
5430 assert!(else_body.unwrap().contains("VAR = release"));
5431 }
5432
5433 #[test]
5434 fn test_add_conditional_ifdef() {
5435 let mut makefile = Makefile::new();
5436 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5437 assert!(result.is_ok());
5438
5439 let code = makefile.to_string();
5440 assert!(code.contains("ifdef DEBUG"));
5441 assert!(code.contains("VAR = debug"));
5442 assert!(code.contains("endif"));
5443 }
5444
5445 #[test]
5446 fn test_add_conditional_with_else() {
5447 let mut makefile = Makefile::new();
5448 let result =
5449 makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5450 assert!(result.is_ok());
5451
5452 let code = makefile.to_string();
5453 assert!(code.contains("ifdef DEBUG"));
5454 assert!(code.contains("VAR = debug"));
5455 assert!(code.contains("else"));
5456 assert!(code.contains("VAR = release"));
5457 assert!(code.contains("endif"));
5458 }
5459
5460 #[test]
5461 fn test_add_conditional_invalid_type() {
5462 let mut makefile = Makefile::new();
5463 let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5464 assert!(result.is_err());
5465 }
5466
5467 #[test]
5468 fn test_add_conditional_formatting() {
5469 let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5470 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5471 assert!(result.is_ok());
5472
5473 let code = makefile.to_string();
5474 assert!(code.contains("\n\nifdef DEBUG"));
5476 }
5477
5478 #[test]
5479 fn test_conditional_remove() {
5480 let makefile: Makefile = r#"ifdef DEBUG
5481VAR = debug
5482endif
5483
5484VAR2 = value2
5485"#
5486 .parse()
5487 .unwrap();
5488
5489 let mut conditional = makefile.conditionals().next().unwrap();
5490 let result = conditional.remove();
5491 assert!(result.is_ok());
5492
5493 let code = makefile.to_string();
5494 assert!(!code.contains("ifdef DEBUG"));
5495 assert!(!code.contains("VAR = debug"));
5496 assert!(code.contains("VAR2 = value2"));
5497 }
5498
5499 #[test]
5500 fn test_add_conditional_ifndef() {
5501 let mut makefile = Makefile::new();
5502 let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5503 assert!(result.is_ok());
5504
5505 let code = makefile.to_string();
5506 assert!(code.contains("ifndef NDEBUG"));
5507 assert!(code.contains("VAR = enabled"));
5508 assert!(code.contains("endif"));
5509 }
5510
5511 #[test]
5512 fn test_add_conditional_ifeq() {
5513 let mut makefile = Makefile::new();
5514 let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5515 assert!(result.is_ok());
5516
5517 let code = makefile.to_string();
5518 assert!(code.contains("ifeq ($(OS),Linux)"));
5519 assert!(code.contains("VAR = linux"));
5520 assert!(code.contains("endif"));
5521 }
5522
5523 #[test]
5524 fn test_add_conditional_ifneq() {
5525 let mut makefile = Makefile::new();
5526 let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5527 assert!(result.is_ok());
5528
5529 let code = makefile.to_string();
5530 assert!(code.contains("ifneq ($(OS),Windows)"));
5531 assert!(code.contains("VAR = unix"));
5532 assert!(code.contains("endif"));
5533 }
5534
5535 #[test]
5536 fn test_conditional_api_integration() {
5537 let mut makefile: Makefile = r#"VAR1 = value1
5539
5540rule1:
5541 command1
5542"#
5543 .parse()
5544 .unwrap();
5545
5546 makefile
5548 .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5549 .unwrap();
5550
5551 assert_eq!(makefile.conditionals().count(), 1);
5553 let conditional = makefile.conditionals().next().unwrap();
5554 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5555 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5556 assert!(conditional.has_else());
5557
5558 assert_eq!(makefile.variable_definitions().count(), 1);
5560 assert_eq!(makefile.rules().count(), 1);
5561 }
5562
5563 #[test]
5564 fn test_conditional_if_items() {
5565 let makefile: Makefile = r#"ifdef DEBUG
5566VAR = debug
5567rule:
5568 command
5569endif
5570"#
5571 .parse()
5572 .unwrap();
5573
5574 let cond = makefile.conditionals().next().unwrap();
5575 let items: Vec<_> = cond.if_items().collect();
5576 assert_eq!(items.len(), 2); match &items[0] {
5579 MakefileItem::Variable(v) => {
5580 assert_eq!(v.name(), Some("VAR".to_string()));
5581 }
5582 _ => panic!("Expected variable"),
5583 }
5584
5585 match &items[1] {
5586 MakefileItem::Rule(r) => {
5587 assert!(r.targets().any(|t| t == "rule"));
5588 }
5589 _ => panic!("Expected rule"),
5590 }
5591 }
5592
5593 #[test]
5594 fn test_conditional_else_items() {
5595 let makefile: Makefile = r#"ifdef DEBUG
5596VAR = debug
5597else
5598VAR2 = release
5599rule2:
5600 command
5601endif
5602"#
5603 .parse()
5604 .unwrap();
5605
5606 let cond = makefile.conditionals().next().unwrap();
5607 let items: Vec<_> = cond.else_items().collect();
5608 assert_eq!(items.len(), 2); match &items[0] {
5611 MakefileItem::Variable(v) => {
5612 assert_eq!(v.name(), Some("VAR2".to_string()));
5613 }
5614 _ => panic!("Expected variable"),
5615 }
5616
5617 match &items[1] {
5618 MakefileItem::Rule(r) => {
5619 assert!(r.targets().any(|t| t == "rule2"));
5620 }
5621 _ => panic!("Expected rule"),
5622 }
5623 }
5624
5625 #[test]
5626 fn test_conditional_add_if_item() {
5627 let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5628 let mut cond = makefile.conditionals().next().unwrap();
5629
5630 let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5632 let var = temp.variable_definitions().next().unwrap();
5633 cond.add_if_item(MakefileItem::Variable(var));
5634
5635 let code = makefile.to_string();
5636 assert!(code.contains("CFLAGS = -g"));
5637
5638 let cond = makefile.conditionals().next().unwrap();
5640 assert_eq!(cond.if_items().count(), 1);
5641 }
5642
5643 #[test]
5644 fn test_conditional_add_else_item() {
5645 let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
5646 let mut cond = makefile.conditionals().next().unwrap();
5647
5648 let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5650 let var = temp.variable_definitions().next().unwrap();
5651 cond.add_else_item(MakefileItem::Variable(var));
5652
5653 let code = makefile.to_string();
5654 assert!(code.contains("else"));
5655 assert!(code.contains("CFLAGS = -O2"));
5656
5657 let cond = makefile.conditionals().next().unwrap();
5659 assert_eq!(cond.else_items().count(), 1);
5660 }
5661
5662 #[test]
5663 fn test_add_conditional_with_items() {
5664 let mut makefile = Makefile::new();
5665
5666 let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
5668 let var1 = temp1.variable_definitions().next().unwrap();
5669
5670 let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5671 let var2 = temp2.variable_definitions().next().unwrap();
5672
5673 let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
5674 let rule1 = temp3.rules().next().unwrap();
5675
5676 let result = makefile.add_conditional_with_items(
5677 "ifdef",
5678 "DEBUG",
5679 vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
5680 Some(vec![MakefileItem::Variable(var2)]),
5681 );
5682
5683 assert!(result.is_ok());
5684
5685 let code = makefile.to_string();
5686 assert!(code.contains("ifdef DEBUG"));
5687 assert!(code.contains("CFLAGS = -g"));
5688 assert!(code.contains("debug:"));
5689 assert!(code.contains("else"));
5690 assert!(code.contains("CFLAGS = -O2"));
5691 }
5692
5693 #[test]
5694 fn test_conditional_items_with_nested_conditional() {
5695 let makefile: Makefile = r#"ifdef DEBUG
5696VAR = debug
5697ifdef VERBOSE
5698 VAR2 = verbose
5699endif
5700endif
5701"#
5702 .parse()
5703 .unwrap();
5704
5705 let cond = makefile.conditionals().next().unwrap();
5706 let items: Vec<_> = cond.if_items().collect();
5707 assert_eq!(items.len(), 2); match &items[0] {
5710 MakefileItem::Variable(v) => {
5711 assert_eq!(v.name(), Some("VAR".to_string()));
5712 }
5713 _ => panic!("Expected variable"),
5714 }
5715
5716 match &items[1] {
5717 MakefileItem::Conditional(c) => {
5718 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
5719 }
5720 _ => panic!("Expected conditional"),
5721 }
5722 }
5723
5724 #[test]
5725 fn test_conditional_items_with_include() {
5726 let makefile: Makefile = r#"ifdef DEBUG
5727include debug.mk
5728VAR = debug
5729endif
5730"#
5731 .parse()
5732 .unwrap();
5733
5734 let cond = makefile.conditionals().next().unwrap();
5735 let items: Vec<_> = cond.if_items().collect();
5736 assert_eq!(items.len(), 2); match &items[0] {
5739 MakefileItem::Include(i) => {
5740 assert_eq!(i.path(), Some("debug.mk".to_string()));
5741 }
5742 _ => panic!("Expected include"),
5743 }
5744
5745 match &items[1] {
5746 MakefileItem::Variable(v) => {
5747 assert_eq!(v.name(), Some("VAR".to_string()));
5748 }
5749 _ => panic!("Expected variable"),
5750 }
5751 }
5752
5753 #[test]
5754 fn test_makefile_items_iterator() {
5755 let makefile: Makefile = r#"VAR = value
5756ifdef DEBUG
5757CFLAGS = -g
5758endif
5759rule:
5760 command
5761include common.mk
5762"#
5763 .parse()
5764 .unwrap();
5765
5766 assert_eq!(makefile.variable_definitions().count(), 2);
5769 assert_eq!(makefile.conditionals().count(), 1);
5770 assert_eq!(makefile.rules().count(), 1);
5771
5772 let items: Vec<_> = makefile.items().collect();
5773 assert!(
5775 items.len() >= 3,
5776 "Expected at least 3 items, got {}",
5777 items.len()
5778 );
5779
5780 match &items[0] {
5781 MakefileItem::Variable(v) => {
5782 assert_eq!(v.name(), Some("VAR".to_string()));
5783 }
5784 _ => panic!("Expected variable at position 0"),
5785 }
5786
5787 match &items[1] {
5788 MakefileItem::Conditional(c) => {
5789 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
5790 }
5791 _ => panic!("Expected conditional at position 1"),
5792 }
5793
5794 match &items[2] {
5795 MakefileItem::Rule(r) => {
5796 let targets: Vec<_> = r.targets().collect();
5797 assert_eq!(targets, vec!["rule"]);
5798 }
5799 _ => panic!("Expected rule at position 2"),
5800 }
5801 }
5802
5803 #[test]
5804 fn test_conditional_unwrap() {
5805 let makefile: Makefile = r#"ifdef DEBUG
5806VAR = debug
5807rule:
5808 command
5809endif
5810"#
5811 .parse()
5812 .unwrap();
5813
5814 let mut cond = makefile.conditionals().next().unwrap();
5815 cond.unwrap().unwrap();
5816
5817 let code = makefile.to_string();
5818 let expected = "VAR = debug\nrule:\n\tcommand\n";
5819 assert_eq!(code, expected);
5820
5821 assert_eq!(makefile.conditionals().count(), 0);
5823
5824 assert_eq!(makefile.variable_definitions().count(), 1);
5826 assert_eq!(makefile.rules().count(), 1);
5827 }
5828
5829 #[test]
5830 fn test_conditional_unwrap_with_else_fails() {
5831 let makefile: Makefile = r#"ifdef DEBUG
5832VAR = debug
5833else
5834VAR = release
5835endif
5836"#
5837 .parse()
5838 .unwrap();
5839
5840 let mut cond = makefile.conditionals().next().unwrap();
5841 let result = cond.unwrap();
5842
5843 assert!(result.is_err());
5844 assert!(result
5845 .unwrap_err()
5846 .to_string()
5847 .contains("Cannot unwrap conditional with else clause"));
5848 }
5849
5850 #[test]
5851 fn test_conditional_unwrap_nested() {
5852 let makefile: Makefile = r#"ifdef OUTER
5853VAR = outer
5854ifdef INNER
5855VAR2 = inner
5856endif
5857endif
5858"#
5859 .parse()
5860 .unwrap();
5861
5862 let mut outer_cond = makefile.conditionals().next().unwrap();
5864 outer_cond.unwrap().unwrap();
5865
5866 let code = makefile.to_string();
5867 let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
5868 assert_eq!(code, expected);
5869 }
5870
5871 #[test]
5872 fn test_conditional_unwrap_empty() {
5873 let makefile: Makefile = r#"ifdef DEBUG
5874endif
5875"#
5876 .parse()
5877 .unwrap();
5878
5879 let mut cond = makefile.conditionals().next().unwrap();
5880 cond.unwrap().unwrap();
5881
5882 let code = makefile.to_string();
5883 assert_eq!(code, "");
5884 }
5885
5886 #[test]
5887 fn test_rule_parent() {
5888 let makefile: Makefile = r#"all:
5889 echo "test"
5890"#
5891 .parse()
5892 .unwrap();
5893
5894 let rule = makefile.rules().next().unwrap();
5895 let parent = rule.parent();
5896 assert!(parent.is_none());
5898 }
5899
5900 #[test]
5901 fn test_item_parent_in_conditional() {
5902 let makefile: Makefile = r#"ifdef DEBUG
5903VAR = debug
5904rule:
5905 command
5906endif
5907"#
5908 .parse()
5909 .unwrap();
5910
5911 let cond = makefile.conditionals().next().unwrap();
5912
5913 let items: Vec<_> = cond.if_items().collect();
5915 assert_eq!(items.len(), 2);
5916
5917 if let MakefileItem::Variable(var) = &items[0] {
5919 let parent = var.parent();
5920 assert!(parent.is_some());
5921 if let Some(MakefileItem::Conditional(_)) = parent {
5922 } else {
5924 panic!("Expected variable parent to be a Conditional");
5925 }
5926 } else {
5927 panic!("Expected first item to be a Variable");
5928 }
5929
5930 if let MakefileItem::Rule(rule) = &items[1] {
5932 let parent = rule.parent();
5933 assert!(parent.is_some());
5934 if let Some(MakefileItem::Conditional(_)) = parent {
5935 } else {
5937 panic!("Expected rule parent to be a Conditional");
5938 }
5939 } else {
5940 panic!("Expected second item to be a Rule");
5941 }
5942 }
5943
5944 #[test]
5945 fn test_nested_conditional_parent() {
5946 let makefile: Makefile = r#"ifdef OUTER
5947VAR = outer
5948ifdef INNER
5949VAR2 = inner
5950endif
5951endif
5952"#
5953 .parse()
5954 .unwrap();
5955
5956 let outer_cond = makefile.conditionals().next().unwrap();
5957
5958 let items: Vec<_> = outer_cond.if_items().collect();
5960
5961 let inner_cond = items
5963 .iter()
5964 .find_map(|item| {
5965 if let MakefileItem::Conditional(c) = item {
5966 Some(c)
5967 } else {
5968 None
5969 }
5970 })
5971 .unwrap();
5972
5973 let parent = inner_cond.parent();
5975 assert!(parent.is_some());
5976 if let Some(MakefileItem::Conditional(_)) = parent {
5977 } else {
5979 panic!("Expected inner conditional's parent to be a Conditional");
5980 }
5981 }
5982
5983 #[test]
5984 fn test_line_col() {
5985 let text = r#"# Comment at line 0
5986VAR1 = value1
5987VAR2 = value2
5988
5989rule1: dep1 dep2
5990 command1
5991 command2
5992
5993rule2:
5994 command3
5995
5996ifdef DEBUG
5997CFLAGS = -g
5998endif
5999"#;
6000 let makefile: Makefile = text.parse().unwrap();
6001
6002 let vars: Vec<_> = makefile.variable_definitions().collect();
6005 assert_eq!(vars.len(), 3);
6006
6007 assert_eq!(vars[0].line(), 1);
6009 assert_eq!(vars[0].column(), 0);
6010 assert_eq!(vars[0].line_col(), (1, 0));
6011
6012 assert_eq!(vars[1].line(), 2);
6014 assert_eq!(vars[1].column(), 0);
6015
6016 assert_eq!(vars[2].line(), 12);
6018 assert_eq!(vars[2].column(), 0);
6019
6020 let rules: Vec<_> = makefile.rules().collect();
6022 assert_eq!(rules.len(), 2);
6023
6024 assert_eq!(rules[0].line(), 4);
6026 assert_eq!(rules[0].column(), 0);
6027 assert_eq!(rules[0].line_col(), (4, 0));
6028
6029 assert_eq!(rules[1].line(), 8);
6031 assert_eq!(rules[1].column(), 0);
6032
6033 let conditionals: Vec<_> = makefile.conditionals().collect();
6035 assert_eq!(conditionals.len(), 1);
6036
6037 assert_eq!(conditionals[0].line(), 11);
6039 assert_eq!(conditionals[0].column(), 0);
6040 assert_eq!(conditionals[0].line_col(), (11, 0));
6041 }
6042
6043 #[test]
6044 fn test_line_col_multiline() {
6045 let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
6046 let makefile: Makefile = text.parse().unwrap();
6047
6048 let vars: Vec<_> = makefile.variable_definitions().collect();
6050 assert_eq!(vars.len(), 1);
6051 assert_eq!(vars[0].line(), 0);
6052 assert_eq!(vars[0].column(), 0);
6053
6054 let rules: Vec<_> = makefile.rules().collect();
6056 assert_eq!(rules.len(), 1);
6057 assert_eq!(rules[0].line(), 4);
6058 assert_eq!(rules[0].column(), 0);
6059 }
6060
6061 #[test]
6062 fn test_line_col_includes() {
6063 let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
6064 let makefile: Makefile = text.parse().unwrap();
6065
6066 let vars: Vec<_> = makefile.variable_definitions().collect();
6068 assert_eq!(vars[0].line(), 0);
6069
6070 let includes: Vec<_> = makefile.includes().collect();
6072 assert_eq!(includes.len(), 2);
6073 assert_eq!(includes[0].line(), 2);
6074 assert_eq!(includes[0].column(), 0);
6075 assert_eq!(includes[1].line(), 3);
6076 assert_eq!(includes[1].column(), 0);
6077 }
6078
6079 #[test]
6080 fn test_conditional_in_rule_vs_toplevel() {
6081 let text1 = r#"rule:
6083 command
6084ifeq (,$(X))
6085 test
6086endif
6087"#;
6088 let makefile: Makefile = text1.parse().unwrap();
6089 let rules: Vec<_> = makefile.rules().collect();
6090 let conditionals: Vec<_> = makefile.conditionals().collect();
6091
6092 assert_eq!(rules.len(), 1);
6093 assert_eq!(
6094 conditionals.len(),
6095 0,
6096 "Conditional should be part of rule, not top-level"
6097 );
6098
6099 let text2 = r#"rule:
6101 command
6102
6103ifeq (,$(X))
6104 test
6105endif
6106"#;
6107 let makefile: Makefile = text2.parse().unwrap();
6108 let rules: Vec<_> = makefile.rules().collect();
6109 let conditionals: Vec<_> = makefile.conditionals().collect();
6110
6111 assert_eq!(rules.len(), 1);
6112 assert_eq!(
6113 conditionals.len(),
6114 1,
6115 "Conditional after blank line should be top-level"
6116 );
6117 assert_eq!(conditionals[0].line(), 3);
6118 }
6119
6120 #[test]
6121 fn test_nested_conditionals_line_tracking() {
6122 let text = r#"ifdef OUTER
6123VAR1 = value1
6124ifdef INNER
6125VAR2 = value2
6126endif
6127VAR3 = value3
6128endif
6129"#;
6130 let makefile: Makefile = text.parse().unwrap();
6131
6132 let conditionals: Vec<_> = makefile.conditionals().collect();
6133 assert_eq!(
6134 conditionals.len(),
6135 1,
6136 "Only outer conditional should be top-level"
6137 );
6138 assert_eq!(conditionals[0].line(), 0);
6139 assert_eq!(conditionals[0].column(), 0);
6140 }
6141
6142 #[test]
6143 fn test_conditional_else_line_tracking() {
6144 let text = r#"VAR1 = before
6145
6146ifdef DEBUG
6147DEBUG_FLAGS = -g
6148else
6149DEBUG_FLAGS = -O2
6150endif
6151
6152VAR2 = after
6153"#;
6154 let makefile: Makefile = text.parse().unwrap();
6155
6156 let conditionals: Vec<_> = makefile.conditionals().collect();
6157 assert_eq!(conditionals.len(), 1);
6158 assert_eq!(conditionals[0].line(), 2);
6159 assert_eq!(conditionals[0].column(), 0);
6160 }
6161
6162 #[test]
6163 fn test_broken_conditional_endif_without_if() {
6164 let text = "VAR = value\nendif\n";
6166 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6167
6168 let vars: Vec<_> = makefile.variable_definitions().collect();
6170 assert_eq!(vars.len(), 1);
6171 assert_eq!(vars[0].line(), 0);
6172 }
6173
6174 #[test]
6175 fn test_broken_conditional_else_without_if() {
6176 let text = "VAR = value\nelse\nVAR2 = other\n";
6178 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6179
6180 let vars: Vec<_> = makefile.variable_definitions().collect();
6182 assert!(!vars.is_empty(), "Should parse at least the first variable");
6183 assert_eq!(vars[0].line(), 0);
6184 }
6185
6186 #[test]
6187 fn test_broken_conditional_missing_endif() {
6188 let text = r#"ifdef DEBUG
6190DEBUG_FLAGS = -g
6191VAR = value
6192"#;
6193 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6194
6195 assert!(makefile.code().contains("ifdef DEBUG"));
6197 }
6198
6199 #[test]
6200 fn test_multiple_conditionals_line_tracking() {
6201 let text = r#"ifdef A
6202VAR_A = a
6203endif
6204
6205ifdef B
6206VAR_B = b
6207endif
6208
6209ifdef C
6210VAR_C = c
6211endif
6212"#;
6213 let makefile: Makefile = text.parse().unwrap();
6214
6215 let conditionals: Vec<_> = makefile.conditionals().collect();
6216 assert_eq!(conditionals.len(), 3);
6217 assert_eq!(conditionals[0].line(), 0);
6218 assert_eq!(conditionals[1].line(), 4);
6219 assert_eq!(conditionals[2].line(), 8);
6220 }
6221
6222 #[test]
6223 fn test_conditional_with_multiple_else_ifeq() {
6224 let text = r#"ifeq ($(OS),Windows)
6225EXT = .exe
6226else ifeq ($(OS),Linux)
6227EXT = .bin
6228else
6229EXT = .out
6230endif
6231"#;
6232 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6233
6234 let conditionals: Vec<_> = makefile.conditionals().collect();
6235 assert_eq!(conditionals.len(), 1);
6236 assert_eq!(conditionals[0].line(), 0);
6237 assert_eq!(conditionals[0].column(), 0);
6238 }
6239
6240 #[test]
6241 fn test_conditional_types_line_tracking() {
6242 let text = r#"ifdef VAR1
6243A = 1
6244endif
6245
6246ifndef VAR2
6247B = 2
6248endif
6249
6250ifeq ($(X),y)
6251C = 3
6252endif
6253
6254ifneq ($(Y),n)
6255D = 4
6256endif
6257"#;
6258 let makefile: Makefile = text.parse().unwrap();
6259
6260 let conditionals: Vec<_> = makefile.conditionals().collect();
6261 assert_eq!(conditionals.len(), 4);
6262
6263 assert_eq!(conditionals[0].line(), 0); assert_eq!(
6265 conditionals[0].conditional_type(),
6266 Some("ifdef".to_string())
6267 );
6268
6269 assert_eq!(conditionals[1].line(), 4); assert_eq!(
6271 conditionals[1].conditional_type(),
6272 Some("ifndef".to_string())
6273 );
6274
6275 assert_eq!(conditionals[2].line(), 8); assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
6277
6278 assert_eq!(conditionals[3].line(), 12); assert_eq!(
6280 conditionals[3].conditional_type(),
6281 Some("ifneq".to_string())
6282 );
6283 }
6284
6285 #[test]
6286 fn test_conditional_in_rule_with_recipes() {
6287 let text = r#"test:
6288 echo "start"
6289ifdef VERBOSE
6290 echo "verbose mode"
6291endif
6292 echo "end"
6293"#;
6294 let makefile: Makefile = text.parse().unwrap();
6295
6296 let rules: Vec<_> = makefile.rules().collect();
6297 let conditionals: Vec<_> = makefile.conditionals().collect();
6298
6299 assert_eq!(rules.len(), 1);
6300 assert_eq!(rules[0].line(), 0);
6301 assert_eq!(conditionals.len(), 0);
6303 }
6304
6305 #[test]
6306 fn test_broken_conditional_double_else() {
6307 let text = r#"ifdef DEBUG
6309A = 1
6310else
6311B = 2
6312else
6313C = 3
6314endif
6315"#;
6316 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6317
6318 assert!(makefile.code().contains("ifdef DEBUG"));
6320 }
6321
6322 #[test]
6323 fn test_broken_conditional_mismatched_nesting() {
6324 let text = r#"ifdef A
6326VAR = value
6327endif
6328endif
6329"#;
6330 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6331
6332 let conditionals: Vec<_> = makefile.conditionals().collect();
6335 assert!(
6336 !conditionals.is_empty(),
6337 "Should parse at least the first conditional"
6338 );
6339 }
6340
6341 #[test]
6342 fn test_conditional_with_comment_line_tracking() {
6343 let text = r#"# This is a comment
6344ifdef DEBUG
6345# Another comment
6346CFLAGS = -g
6347endif
6348# Final comment
6349"#;
6350 let makefile: Makefile = text.parse().unwrap();
6351
6352 let conditionals: Vec<_> = makefile.conditionals().collect();
6353 assert_eq!(conditionals.len(), 1);
6354 assert_eq!(conditionals[0].line(), 1);
6355 assert_eq!(conditionals[0].column(), 0);
6356 }
6357
6358 #[test]
6359 fn test_conditional_after_variable_with_blank_lines() {
6360 let text = r#"VAR1 = value1
6361
6362
6363ifdef DEBUG
6364VAR2 = value2
6365endif
6366"#;
6367 let makefile: Makefile = text.parse().unwrap();
6368
6369 let vars: Vec<_> = makefile.variable_definitions().collect();
6370 let conditionals: Vec<_> = makefile.conditionals().collect();
6371
6372 assert_eq!(vars.len(), 2);
6374 assert_eq!(vars[0].line(), 0); assert_eq!(vars[1].line(), 4); assert_eq!(conditionals.len(), 1);
6378 assert_eq!(conditionals[0].line(), 3);
6379 }
6380
6381 #[test]
6382 fn test_empty_conditional_line_tracking() {
6383 let text = r#"ifdef DEBUG
6384endif
6385
6386ifndef RELEASE
6387endif
6388"#;
6389 let makefile: Makefile = text.parse().unwrap();
6390
6391 let conditionals: Vec<_> = makefile.conditionals().collect();
6392 assert_eq!(conditionals.len(), 2);
6393 assert_eq!(conditionals[0].line(), 0);
6394 assert_eq!(conditionals[1].line(), 3);
6395 }
6396
6397 #[test]
6398 fn test_recipe_line_tracking() {
6399 let text = r#"build:
6400 echo "Building..."
6401 gcc -o app main.c
6402 echo "Done"
6403
6404test:
6405 ./run-tests
6406"#;
6407 let makefile: Makefile = text.parse().unwrap();
6408
6409 let rule1 = makefile.rules().next().expect("Should have first rule");
6411 let recipes: Vec<_> = rule1.recipe_nodes().collect();
6412 assert_eq!(recipes.len(), 3);
6413
6414 assert_eq!(recipes[0].text(), "echo \"Building...\"");
6415 assert_eq!(recipes[0].line(), 1);
6416 assert_eq!(recipes[0].column(), 0);
6417
6418 assert_eq!(recipes[1].text(), "gcc -o app main.c");
6419 assert_eq!(recipes[1].line(), 2);
6420 assert_eq!(recipes[1].column(), 0);
6421
6422 assert_eq!(recipes[2].text(), "echo \"Done\"");
6423 assert_eq!(recipes[2].line(), 3);
6424 assert_eq!(recipes[2].column(), 0);
6425
6426 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6428 let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6429 assert_eq!(recipes2.len(), 1);
6430
6431 assert_eq!(recipes2[0].text(), "./run-tests");
6432 assert_eq!(recipes2[0].line(), 6);
6433 assert_eq!(recipes2[0].column(), 0);
6434 }
6435
6436 #[test]
6437 fn test_recipe_with_variables_line_tracking() {
6438 let text = r#"install:
6439 mkdir -p $(DESTDIR)
6440 cp $(BINARY) $(DESTDIR)/
6441"#;
6442 let makefile: Makefile = text.parse().unwrap();
6443 let rule = makefile.rules().next().expect("Should have rule");
6444 let recipes: Vec<_> = rule.recipe_nodes().collect();
6445
6446 assert_eq!(recipes.len(), 2);
6447 assert_eq!(recipes[0].line(), 1);
6448 assert_eq!(recipes[1].line(), 2);
6449 }
6450
6451 #[test]
6452 fn test_recipe_text_no_leading_tab() {
6453 let text = "test:\n\techo hello\n\t\techo nested\n\t echo with spaces\n";
6455 let makefile: Makefile = text.parse().unwrap();
6456 let rule = makefile.rules().next().expect("Should have rule");
6457 let recipes: Vec<_> = rule.recipe_nodes().collect();
6458
6459 assert_eq!(recipes.len(), 3);
6460
6461 eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6463
6464 assert_eq!(recipes[0].text(), "echo hello");
6466
6467 eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6469 assert_eq!(recipes[1].text(), "\techo nested");
6470
6471 eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6473 assert_eq!(recipes[2].text(), " echo with spaces");
6474 }
6475
6476 #[test]
6477 fn test_recipe_parent() {
6478 let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6479 let rule = makefile.rules().next().unwrap();
6480 let recipe = rule.recipe_nodes().next().unwrap();
6481
6482 let parent = recipe.parent().expect("Recipe should have parent");
6483 assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6484 assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6485 }
6486
6487 #[test]
6488 fn test_recipe_is_silent_various_prefixes() {
6489 let makefile: Makefile = r#"test:
6490 @echo silent
6491 -echo ignore
6492 +echo always
6493 @-echo silent_ignore
6494 -@echo ignore_silent
6495 +@echo always_silent
6496 echo normal
6497"#
6498 .parse()
6499 .unwrap();
6500
6501 let rule = makefile.rules().next().unwrap();
6502 let recipes: Vec<_> = rule.recipe_nodes().collect();
6503
6504 assert_eq!(recipes.len(), 7);
6505 assert!(recipes[0].is_silent(), "@echo should be silent");
6506 assert!(!recipes[1].is_silent(), "-echo should not be silent");
6507 assert!(!recipes[2].is_silent(), "+echo should not be silent");
6508 assert!(recipes[3].is_silent(), "@-echo should be silent");
6509 assert!(recipes[4].is_silent(), "-@echo should be silent");
6510 assert!(recipes[5].is_silent(), "+@echo should be silent");
6511 assert!(!recipes[6].is_silent(), "echo should not be silent");
6512 }
6513
6514 #[test]
6515 fn test_recipe_is_ignore_errors_various_prefixes() {
6516 let makefile: Makefile = r#"test:
6517 @echo silent
6518 -echo ignore
6519 +echo always
6520 @-echo silent_ignore
6521 -@echo ignore_silent
6522 +-echo always_ignore
6523 echo normal
6524"#
6525 .parse()
6526 .unwrap();
6527
6528 let rule = makefile.rules().next().unwrap();
6529 let recipes: Vec<_> = rule.recipe_nodes().collect();
6530
6531 assert_eq!(recipes.len(), 7);
6532 assert!(
6533 !recipes[0].is_ignore_errors(),
6534 "@echo should not ignore errors"
6535 );
6536 assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6537 assert!(
6538 !recipes[2].is_ignore_errors(),
6539 "+echo should not ignore errors"
6540 );
6541 assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6542 assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6543 assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6544 assert!(
6545 !recipes[6].is_ignore_errors(),
6546 "echo should not ignore errors"
6547 );
6548 }
6549
6550 #[test]
6551 fn test_recipe_set_prefix_add() {
6552 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6553 let rule = makefile.rules().next().unwrap();
6554 let mut recipe = rule.recipe_nodes().next().unwrap();
6555
6556 recipe.set_prefix("@");
6557 assert_eq!(recipe.text(), "@echo hello");
6558 assert!(recipe.is_silent());
6559 }
6560
6561 #[test]
6562 fn test_recipe_set_prefix_change() {
6563 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6564 let rule = makefile.rules().next().unwrap();
6565 let mut recipe = rule.recipe_nodes().next().unwrap();
6566
6567 recipe.set_prefix("-");
6568 assert_eq!(recipe.text(), "-echo hello");
6569 assert!(!recipe.is_silent());
6570 assert!(recipe.is_ignore_errors());
6571 }
6572
6573 #[test]
6574 fn test_recipe_set_prefix_remove() {
6575 let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
6576 let rule = makefile.rules().next().unwrap();
6577 let mut recipe = rule.recipe_nodes().next().unwrap();
6578
6579 recipe.set_prefix("");
6580 assert_eq!(recipe.text(), "echo hello");
6581 assert!(!recipe.is_silent());
6582 assert!(!recipe.is_ignore_errors());
6583 }
6584
6585 #[test]
6586 fn test_recipe_set_prefix_combinations() {
6587 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6588 let rule = makefile.rules().next().unwrap();
6589 let mut recipe = rule.recipe_nodes().next().unwrap();
6590
6591 recipe.set_prefix("@-");
6592 assert_eq!(recipe.text(), "@-echo hello");
6593 assert!(recipe.is_silent());
6594 assert!(recipe.is_ignore_errors());
6595
6596 recipe.set_prefix("-@");
6597 assert_eq!(recipe.text(), "-@echo hello");
6598 assert!(recipe.is_silent());
6599 assert!(recipe.is_ignore_errors());
6600 }
6601
6602 #[test]
6603 fn test_recipe_replace_text_basic() {
6604 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6605 let rule = makefile.rules().next().unwrap();
6606 let mut recipe = rule.recipe_nodes().next().unwrap();
6607
6608 recipe.replace_text("echo world");
6609 assert_eq!(recipe.text(), "echo world");
6610
6611 let rule = makefile.rules().next().unwrap();
6613 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6614 }
6615
6616 #[test]
6617 fn test_recipe_replace_text_with_prefix() {
6618 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6619 let rule = makefile.rules().next().unwrap();
6620 let mut recipe = rule.recipe_nodes().next().unwrap();
6621
6622 recipe.replace_text("@echo goodbye");
6623 assert_eq!(recipe.text(), "@echo goodbye");
6624 assert!(recipe.is_silent());
6625 }
6626
6627 #[test]
6628 fn test_recipe_insert_before_single() {
6629 let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6630 let rule = makefile.rules().next().unwrap();
6631 let recipe = rule.recipe_nodes().next().unwrap();
6632
6633 recipe.insert_before("echo hello");
6634
6635 let rule = makefile.rules().next().unwrap();
6636 let recipes: Vec<_> = rule.recipes().collect();
6637 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6638 }
6639
6640 #[test]
6641 fn test_recipe_insert_before_multiple() {
6642 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6643 .parse()
6644 .unwrap();
6645 let rule = makefile.rules().next().unwrap();
6646 let recipes: Vec<_> = rule.recipe_nodes().collect();
6647
6648 recipes[1].insert_before("echo middle");
6650
6651 let rule = makefile.rules().next().unwrap();
6652 let new_recipes: Vec<_> = rule.recipes().collect();
6653 assert_eq!(
6654 new_recipes,
6655 vec!["echo one", "echo middle", "echo two", "echo three"]
6656 );
6657 }
6658
6659 #[test]
6660 fn test_recipe_insert_before_first() {
6661 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6662 let rule = makefile.rules().next().unwrap();
6663 let recipes: Vec<_> = rule.recipe_nodes().collect();
6664
6665 recipes[0].insert_before("echo zero");
6666
6667 let rule = makefile.rules().next().unwrap();
6668 let new_recipes: Vec<_> = rule.recipes().collect();
6669 assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
6670 }
6671
6672 #[test]
6673 fn test_recipe_insert_after_single() {
6674 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6675 let rule = makefile.rules().next().unwrap();
6676 let recipe = rule.recipe_nodes().next().unwrap();
6677
6678 recipe.insert_after("echo world");
6679
6680 let rule = makefile.rules().next().unwrap();
6681 let recipes: Vec<_> = rule.recipes().collect();
6682 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6683 }
6684
6685 #[test]
6686 fn test_recipe_insert_after_multiple() {
6687 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6688 .parse()
6689 .unwrap();
6690 let rule = makefile.rules().next().unwrap();
6691 let recipes: Vec<_> = rule.recipe_nodes().collect();
6692
6693 recipes[1].insert_after("echo middle");
6695
6696 let rule = makefile.rules().next().unwrap();
6697 let new_recipes: Vec<_> = rule.recipes().collect();
6698 assert_eq!(
6699 new_recipes,
6700 vec!["echo one", "echo two", "echo middle", "echo three"]
6701 );
6702 }
6703
6704 #[test]
6705 fn test_recipe_insert_after_last() {
6706 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6707 let rule = makefile.rules().next().unwrap();
6708 let recipes: Vec<_> = rule.recipe_nodes().collect();
6709
6710 recipes[1].insert_after("echo three");
6711
6712 let rule = makefile.rules().next().unwrap();
6713 let new_recipes: Vec<_> = rule.recipes().collect();
6714 assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
6715 }
6716
6717 #[test]
6718 fn test_recipe_remove_single() {
6719 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6720 let rule = makefile.rules().next().unwrap();
6721 let recipe = rule.recipe_nodes().next().unwrap();
6722
6723 recipe.remove();
6724
6725 let rule = makefile.rules().next().unwrap();
6726 assert_eq!(rule.recipes().count(), 0);
6727 }
6728
6729 #[test]
6730 fn test_recipe_remove_first() {
6731 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6732 .parse()
6733 .unwrap();
6734 let rule = makefile.rules().next().unwrap();
6735 let recipes: Vec<_> = rule.recipe_nodes().collect();
6736
6737 recipes[0].remove();
6738
6739 let rule = makefile.rules().next().unwrap();
6740 let new_recipes: Vec<_> = rule.recipes().collect();
6741 assert_eq!(new_recipes, vec!["echo two", "echo three"]);
6742 }
6743
6744 #[test]
6745 fn test_recipe_remove_middle() {
6746 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6747 .parse()
6748 .unwrap();
6749 let rule = makefile.rules().next().unwrap();
6750 let recipes: Vec<_> = rule.recipe_nodes().collect();
6751
6752 recipes[1].remove();
6753
6754 let rule = makefile.rules().next().unwrap();
6755 let new_recipes: Vec<_> = rule.recipes().collect();
6756 assert_eq!(new_recipes, vec!["echo one", "echo three"]);
6757 }
6758
6759 #[test]
6760 fn test_recipe_remove_last() {
6761 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6762 .parse()
6763 .unwrap();
6764 let rule = makefile.rules().next().unwrap();
6765 let recipes: Vec<_> = rule.recipe_nodes().collect();
6766
6767 recipes[2].remove();
6768
6769 let rule = makefile.rules().next().unwrap();
6770 let new_recipes: Vec<_> = rule.recipes().collect();
6771 assert_eq!(new_recipes, vec!["echo one", "echo two"]);
6772 }
6773
6774 #[test]
6775 fn test_recipe_multiple_operations() {
6776 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6777 let rule = makefile.rules().next().unwrap();
6778 let mut recipe = rule.recipe_nodes().next().unwrap();
6779
6780 recipe.replace_text("echo modified");
6782 assert_eq!(recipe.text(), "echo modified");
6783
6784 recipe.set_prefix("@");
6786 assert_eq!(recipe.text(), "@echo modified");
6787
6788 recipe.insert_after("echo three");
6790
6791 let rule = makefile.rules().next().unwrap();
6793 let recipes: Vec<_> = rule.recipes().collect();
6794 assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
6795 }
6796}
6797
6798#[cfg(test)]
6799mod test_continuation {
6800 use super::*;
6801
6802 #[test]
6803 fn test_recipe_continuation_lines() {
6804 let makefile_content = r#"override_dh_autoreconf:
6805 set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \
6806 dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \
6807 sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs
6808 dh_autoreconf
6809"#;
6810
6811 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6812 let rule = makefile.rules().next().unwrap();
6813
6814 let recipes: Vec<_> = rule.recipe_nodes().collect();
6815
6816 assert_eq!(recipes.len(), 2);
6818
6819 let expected_first = "set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \\\n dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \\\n sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs";
6822 assert_eq!(recipes[0].text(), expected_first);
6823
6824 assert_eq!(recipes[1].text(), "dh_autoreconf");
6826 }
6827
6828 #[test]
6829 fn test_simple_continuation() {
6830 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n";
6831
6832 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6833 let rule = makefile.rules().next().unwrap();
6834 let recipes: Vec<_> = rule.recipe_nodes().collect();
6835
6836 assert_eq!(recipes.len(), 1);
6837 assert_eq!(recipes[0].text(), "echo hello && \\\n echo world");
6838 }
6839
6840 #[test]
6841 fn test_multiple_continuations() {
6842 let makefile_content = "test:\n\techo line1 && \\\n\t echo line2 && \\\n\t echo line3 && \\\n\t echo line4\n";
6843
6844 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6845 let rule = makefile.rules().next().unwrap();
6846 let recipes: Vec<_> = rule.recipe_nodes().collect();
6847
6848 assert_eq!(recipes.len(), 1);
6849 assert_eq!(
6850 recipes[0].text(),
6851 "echo line1 && \\\n echo line2 && \\\n echo line3 && \\\n echo line4"
6852 );
6853 }
6854
6855 #[test]
6856 fn test_continuation_round_trip() {
6857 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
6858
6859 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6860 let output = makefile.to_string();
6861
6862 assert_eq!(output, makefile_content);
6864 }
6865
6866 #[test]
6867 fn test_continuation_with_silent_prefix() {
6868 let makefile_content = "test:\n\t@echo hello && \\\n\t echo world\n";
6869
6870 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6871 let rule = makefile.rules().next().unwrap();
6872 let recipes: Vec<_> = rule.recipe_nodes().collect();
6873
6874 assert_eq!(recipes.len(), 1);
6875 assert_eq!(recipes[0].text(), "@echo hello && \\\n echo world");
6876 assert!(recipes[0].is_silent());
6877 }
6878
6879 #[test]
6880 fn test_mixed_continued_and_non_continued() {
6881 let makefile_content = r#"test:
6882 echo first
6883 echo second && \
6884 echo third
6885 echo fourth
6886"#;
6887
6888 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6889 let rule = makefile.rules().next().unwrap();
6890 let recipes: Vec<_> = rule.recipe_nodes().collect();
6891
6892 assert_eq!(recipes.len(), 3);
6893 assert_eq!(recipes[0].text(), "echo first");
6894 assert_eq!(recipes[1].text(), "echo second && \\\n echo third");
6895 assert_eq!(recipes[2].text(), "echo fourth");
6896 }
6897
6898 #[test]
6899 fn test_continuation_replace_command() {
6900 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
6901
6902 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6903 let mut rule = makefile.rules().next().unwrap();
6904
6905 rule.replace_command(0, "echo replaced");
6907
6908 let recipes: Vec<_> = rule.recipe_nodes().collect();
6909 assert_eq!(recipes.len(), 2);
6910 assert_eq!(recipes[0].text(), "echo replaced");
6911 assert_eq!(recipes[1].text(), "echo done");
6912 }
6913
6914 #[test]
6915 fn test_continuation_count() {
6916 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
6917
6918 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6919 let rule = makefile.rules().next().unwrap();
6920
6921 assert_eq!(rule.recipe_count(), 2);
6923 assert_eq!(rule.recipe_nodes().count(), 2);
6924
6925 let recipes_list: Vec<_> = rule.recipes().collect();
6927 assert_eq!(
6928 recipes_list,
6929 vec!["echo hello && \\\n echo world", "echo done"]
6930 );
6931 }
6932
6933 #[test]
6934 fn test_backslash_in_middle_of_line() {
6935 let makefile_content = "test:\n\techo hello\\nworld\n\techo done\n";
6937
6938 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6939 let rule = makefile.rules().next().unwrap();
6940 let recipes: Vec<_> = rule.recipe_nodes().collect();
6941
6942 assert_eq!(recipes.len(), 2);
6943 assert_eq!(recipes[0].text(), "echo hello\\nworld");
6944 assert_eq!(recipes[1].text(), "echo done");
6945 }
6946
6947 #[test]
6948 fn test_shell_for_loop_with_continuation() {
6949 let makefile_content = r#"override_dh_installman:
6953 for i in foo bar; do \
6954 pod2man --section=1 $$i ; \
6955 done
6956"#;
6957
6958 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6959 let rule = makefile.rules().next().unwrap();
6960
6961 let recipes: Vec<_> = rule.recipe_nodes().collect();
6963 assert_eq!(recipes.len(), 1);
6964
6965 let recipe_text = recipes[0].text();
6967 let expected_recipe = "for i in foo bar; do \\\n\tpod2man --section=1 $$i ; \\\ndone";
6968 assert_eq!(recipe_text, expected_recipe);
6969
6970 let output = makefile.to_string();
6972 assert_eq!(output, makefile_content);
6973 }
6974
6975 #[test]
6976 fn test_shell_for_loop_remove_command() {
6977 let makefile_content = r#"override_dh_installman:
6980 for i in foo bar; do \
6981 pod2man --section=1 $$i ; \
6982 done
6983 echo "Done with man pages"
6984"#;
6985
6986 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6987 let mut rule = makefile.rules().next().unwrap();
6988
6989 assert_eq!(rule.recipe_count(), 2);
6991
6992 rule.remove_command(1);
6994
6995 let recipes: Vec<_> = rule.recipe_nodes().collect();
6997 assert_eq!(recipes.len(), 1);
6998
6999 let output = makefile.to_string();
7001 let expected_output = r#"override_dh_installman:
7002 for i in foo bar; do \
7003 pod2man --section=1 $$i ; \
7004 done
7005"#;
7006 assert_eq!(output, expected_output);
7007 }
7008}