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 .any(|(kind, text)| *kind == OPERATOR && text == ":");
479
480 if has_colon {
481 while self.current().is_some() {
483 if self.current() == Some(OPERATOR)
484 && self.tokens.last().map(|(_, text)| text.as_str()) == Some(":")
485 {
486 self.bump();
487 return true;
488 }
489 self.bump();
490 }
491 }
492
493 self.error("expected ':'".to_string());
494 false
495 }
496
497 fn parse_rule(&mut self) {
498 self.builder.start_node(RULE.into());
499
500 self.skip_ws();
502 self.builder.start_node(TARGETS.into());
503 let has_target = self.parse_rule_targets();
504 self.builder.finish_node();
505
506 let has_colon = if has_target {
508 self.find_and_consume_colon()
509 } else {
510 false
511 };
512
513 if has_target && has_colon {
515 self.skip_ws();
516 self.parse_rule_dependencies();
517 self.expect_eol();
518
519 self.parse_rule_recipes();
521 }
522
523 self.builder.finish_node();
524 }
525
526 fn parse_rule_targets(&mut self) -> bool {
527 let has_first_target = self.parse_rule_target();
529
530 if !has_first_target {
531 return false;
532 }
533
534 loop {
536 self.skip_ws();
537
538 if self.current() == Some(OPERATOR) && self.tokens.last().unwrap().1 == ":" {
540 break;
541 }
542
543 match self.current() {
545 Some(IDENTIFIER) | Some(DOLLAR) => {
546 if !self.parse_rule_target() {
547 break;
548 }
549 }
550 _ => break,
551 }
552 }
553
554 true
555 }
556
557 fn parse_comment(&mut self) {
558 if self.current() == Some(COMMENT) {
559 self.bump(); if self.current() == Some(NEWLINE) {
563 self.bump(); } else if self.current() == Some(WHITESPACE) {
565 self.skip_ws();
567 if self.current() == Some(NEWLINE) {
568 self.bump();
569 }
570 }
571 } else {
573 self.error("expected comment".to_string());
574 }
575 }
576
577 fn parse_assignment(&mut self) {
578 self.builder.start_node(VARIABLE.into());
579
580 self.skip_ws();
582 if self.current() == Some(IDENTIFIER) && self.tokens.last().unwrap().1 == "export" {
583 self.bump();
584 self.skip_ws();
585 }
586
587 match self.current() {
589 Some(IDENTIFIER) => self.bump(),
590 Some(DOLLAR) => self.parse_variable_reference(),
591 _ => {
592 self.error("expected variable name".to_string());
593 self.builder.finish_node();
594 return;
595 }
596 }
597
598 self.skip_ws();
600 match self.current() {
601 Some(OPERATOR) => {
602 let op = &self.tokens.last().unwrap().1;
603 if ["=", ":=", "::=", ":::=", "+=", "?=", "!="].contains(&op.as_str()) {
604 self.bump();
605 self.skip_ws();
606
607 self.builder.start_node(EXPR.into());
609 while self.current().is_some() && self.current() != Some(NEWLINE) {
610 self.bump();
611 }
612 self.builder.finish_node();
613
614 if self.current() == Some(NEWLINE) {
616 self.bump();
617 } else {
618 self.error("expected newline after variable value".to_string());
619 }
620 } else {
621 self.error(format!("invalid assignment operator: {}", op));
622 }
623 }
624 _ => self.error("expected assignment operator".to_string()),
625 }
626
627 self.builder.finish_node();
628 }
629
630 fn parse_variable_reference(&mut self) {
631 self.builder.start_node(EXPR.into());
632 self.bump(); if self.current() == Some(LPAREN) {
635 self.bump(); let mut is_function = false;
639
640 if self.current() == Some(IDENTIFIER) {
641 let function_name = &self.tokens.last().unwrap().1;
642 let known_functions = [
644 "shell", "wildcard", "call", "eval", "file", "abspath", "dir",
645 ];
646 if known_functions.contains(&function_name.as_str()) {
647 is_function = true;
648 }
649 }
650
651 if is_function {
652 self.bump();
654
655 self.consume_balanced_parens(1);
657 } else {
658 self.parse_parenthesized_expr_internal(true);
660 }
661 } else {
662 self.error("expected ( after $ in variable reference".to_string());
663 }
664
665 self.builder.finish_node();
666 }
667
668 fn parse_parenthesized_expr(&mut self) {
671 self.builder.start_node(EXPR.into());
672
673 if self.current() == Some(LPAREN) {
675 self.bump(); self.parse_parenthesized_expr_internal(false);
678 } else if self.current() == Some(QUOTE) {
679 self.parse_quoted_comparison();
681 } else {
682 self.error("expected opening parenthesis or quote".to_string());
683 }
684
685 self.builder.finish_node();
686 }
687
688 fn parse_parenthesized_expr_internal(&mut self, is_variable_ref: bool) {
690 let mut paren_count = 1;
691
692 while paren_count > 0 && self.current().is_some() {
693 match self.current() {
694 Some(LPAREN) => {
695 paren_count += 1;
696 self.bump();
697 self.builder.start_node(EXPR.into());
699 }
700 Some(RPAREN) => {
701 paren_count -= 1;
702 self.bump();
703 if paren_count > 0 {
704 self.builder.finish_node();
705 }
706 }
707 Some(QUOTE) => {
708 self.parse_quoted_string();
710 }
711 Some(DOLLAR) => {
712 self.parse_variable_reference();
714 }
715 Some(_) => self.bump(),
716 None => {
717 self.error(if is_variable_ref {
718 "unclosed variable reference".to_string()
719 } else {
720 "unclosed parenthesis".to_string()
721 });
722 break;
723 }
724 }
725 }
726
727 if !is_variable_ref {
728 self.skip_ws();
729 self.expect_eol();
730 }
731 }
732
733 fn parse_quoted_comparison(&mut self) {
736 if self.current() == Some(QUOTE) {
738 self.bump(); } else {
740 self.error("expected first quoted argument".to_string());
741 }
742
743 self.skip_ws();
745
746 if self.current() == Some(QUOTE) {
748 self.bump(); } else {
750 self.error("expected second quoted argument".to_string());
751 }
752
753 self.skip_ws();
755 self.expect_eol();
756 }
757
758 fn parse_quoted_string(&mut self) {
760 self.bump(); while !self.is_at_eof() && self.current() != Some(QUOTE) {
762 self.bump();
763 }
764 if self.current() == Some(QUOTE) {
765 self.bump();
766 }
767 }
768
769 fn parse_conditional_keyword(&mut self) -> Option<String> {
770 if self.current() != Some(IDENTIFIER) {
771 self.error(
772 "expected conditional keyword (ifdef, ifndef, ifeq, or ifneq)".to_string(),
773 );
774 return None;
775 }
776
777 let token = self.tokens.last().unwrap().1.clone();
778 if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&token.as_str()) {
779 self.error(format!("unknown conditional directive: {}", token));
780 return None;
781 }
782
783 self.bump();
784 Some(token)
785 }
786
787 fn parse_simple_condition(&mut self) {
788 self.builder.start_node(EXPR.into());
789
790 self.skip_ws();
792
793 let mut found_var = false;
795
796 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
797 match self.current() {
798 Some(WHITESPACE) => self.skip_ws(),
799 Some(DOLLAR) => {
800 found_var = true;
801 self.parse_variable_reference();
802 }
803 Some(_) => {
804 found_var = true;
806 self.bump();
807 }
808 None => break,
809 }
810 }
811
812 if !found_var {
813 self.error("expected condition after conditional directive".to_string());
815 }
816
817 self.builder.finish_node();
818
819 if self.current() == Some(NEWLINE) {
821 self.bump();
822 } else if !self.is_at_eof() {
823 self.skip_until_newline();
824 }
825 }
826
827 fn is_conditional_directive(&self, token: &str) -> bool {
829 token == "ifdef"
830 || token == "ifndef"
831 || token == "ifeq"
832 || token == "ifneq"
833 || token == "else"
834 || token == "endif"
835 }
836
837 fn handle_conditional_token(&mut self, token: &str, depth: &mut usize) -> bool {
839 match token {
840 "ifdef" | "ifndef" | "ifeq" | "ifneq"
841 if matches!(self.variant, None | Some(MakefileVariant::GNUMake)) =>
842 {
843 self.parse_conditional();
846 true
847 }
848 "else" => {
849 if *depth == 0 {
851 self.error("else without matching if".to_string());
852 self.bump();
854 false
855 } else {
856 self.builder.start_node(CONDITIONAL_ELSE.into());
858
859 self.bump();
861 self.skip_ws();
862
863 if self.current() == Some(IDENTIFIER) {
865 let next_token = &self.tokens.last().unwrap().1;
866 if next_token == "ifdef"
867 || next_token == "ifndef"
868 || next_token == "ifeq"
869 || next_token == "ifneq"
870 {
871 match next_token.as_str() {
874 "ifdef" | "ifndef" => {
875 self.bump(); self.skip_ws();
877 self.parse_simple_condition();
878 }
879 "ifeq" | "ifneq" => {
880 self.bump(); self.skip_ws();
882 self.parse_parenthesized_expr();
883 }
884 _ => unreachable!(),
885 }
886 } else {
888 }
891 } else {
892 }
894
895 self.builder.finish_node(); true
897 }
898 }
899 "endif" => {
900 if *depth == 0 {
902 self.error("endif without matching if".to_string());
903 self.bump();
905 false
906 } else {
907 *depth -= 1;
908
909 self.builder.start_node(CONDITIONAL_ENDIF.into());
911
912 self.bump();
914
915 self.skip_ws();
917
918 if self.current() == Some(COMMENT) {
923 self.parse_comment();
924 } else if self.current() == Some(NEWLINE) {
925 self.bump();
926 } else if self.current() == Some(WHITESPACE) {
927 self.skip_ws();
929 if self.current() == Some(NEWLINE) {
930 self.bump();
931 }
932 } else if !self.is_at_eof() {
934 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
937 self.bump();
938 }
939 if self.current() == Some(NEWLINE) {
940 self.bump();
941 }
942 }
943 self.builder.finish_node(); true
947 }
948 }
949 _ => false,
950 }
951 }
952
953 fn parse_conditional(&mut self) {
954 self.builder.start_node(CONDITIONAL.into());
955
956 self.builder.start_node(CONDITIONAL_IF.into());
958
959 let Some(token) = self.parse_conditional_keyword() else {
961 self.skip_until_newline();
962 self.builder.finish_node(); self.builder.finish_node(); return;
965 };
966
967 self.skip_ws();
969
970 match token.as_str() {
972 "ifdef" | "ifndef" => {
973 self.parse_simple_condition();
974 }
975 "ifeq" | "ifneq" => {
976 self.parse_parenthesized_expr();
977 }
978 _ => unreachable!("Invalid conditional token"),
979 }
980
981 self.skip_ws();
983 if self.current() == Some(COMMENT) {
984 self.parse_comment();
985 }
986 self.builder.finish_node(); let mut depth = 1;
992
993 let mut position_count = std::collections::HashMap::<usize, usize>::new();
995 let max_repetitions = 15; while depth > 0 && !self.is_at_eof() {
998 let current_pos = self.tokens.len();
1000 *position_count.entry(current_pos).or_insert(0) += 1;
1001
1002 if position_count.get(¤t_pos).unwrap() > &max_repetitions {
1005 break;
1008 }
1009
1010 match self.current() {
1011 None => {
1012 self.error("unterminated conditional (missing endif)".to_string());
1013 break;
1014 }
1015 Some(IDENTIFIER) => {
1016 let token = self.tokens.last().unwrap().1.clone();
1017 if !self.handle_conditional_token(&token, &mut depth) {
1018 if token == "include" || token == "-include" || token == "sinclude" {
1019 self.parse_include();
1020 } else {
1021 self.parse_normal_content();
1022 }
1023 }
1024 }
1025 Some(INDENT) => self.parse_recipe_line(),
1026 Some(WHITESPACE) => self.bump(),
1027 Some(COMMENT) => self.parse_comment(),
1028 Some(NEWLINE) => self.bump(),
1029 Some(DOLLAR) => self.parse_normal_content(),
1030 Some(QUOTE) => self.parse_quoted_string(),
1031 Some(_) => {
1032 self.bump();
1034 }
1035 }
1036 }
1037
1038 self.builder.finish_node();
1039 }
1040
1041 fn parse_normal_content(&mut self) {
1043 self.skip_ws();
1045
1046 if self.is_assignment_line() {
1048 self.parse_assignment();
1049 } else {
1050 self.parse_rule();
1052 }
1053 }
1054
1055 fn parse_include(&mut self) {
1056 self.builder.start_node(INCLUDE.into());
1057
1058 if self.current() != Some(IDENTIFIER)
1060 || (!["include", "-include", "sinclude"]
1061 .contains(&self.tokens.last().unwrap().1.as_str()))
1062 {
1063 self.error("expected include directive".to_string());
1064 self.builder.finish_node();
1065 return;
1066 }
1067 self.bump();
1068 self.skip_ws();
1069
1070 self.builder.start_node(EXPR.into());
1072 let mut found_path = false;
1073
1074 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1075 match self.current() {
1076 Some(WHITESPACE) => self.skip_ws(),
1077 Some(DOLLAR) => {
1078 found_path = true;
1079 self.parse_variable_reference();
1080 }
1081 Some(_) => {
1082 found_path = true;
1084 self.bump();
1085 }
1086 None => break,
1087 }
1088 }
1089
1090 if !found_path {
1091 self.error("expected file path after include".to_string());
1092 }
1093
1094 self.builder.finish_node();
1095
1096 if self.current() == Some(NEWLINE) {
1098 self.bump();
1099 } else if !self.is_at_eof() {
1100 self.error("expected newline after include".to_string());
1101 self.skip_until_newline();
1102 }
1103
1104 self.builder.finish_node();
1105 }
1106
1107 fn parse_identifier_token(&mut self) -> bool {
1108 let token = &self.tokens.last().unwrap().1;
1109
1110 if token.starts_with("%") {
1112 self.parse_rule();
1113 return true;
1114 }
1115
1116 if token.starts_with("if")
1117 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1118 {
1119 self.parse_conditional();
1120 return true;
1121 }
1122
1123 if token == "include" || token == "-include" || token == "sinclude" {
1124 self.parse_include();
1125 return true;
1126 }
1127
1128 self.parse_normal_content();
1130 true
1131 }
1132
1133 fn parse_token(&mut self) -> bool {
1134 match self.current() {
1135 None => false,
1136 Some(IDENTIFIER) => {
1137 let token = &self.tokens.last().unwrap().1;
1138 if self.is_conditional_directive(token)
1139 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1140 {
1141 self.parse_conditional();
1142 true
1143 } else {
1144 self.parse_identifier_token()
1145 }
1146 }
1147 Some(DOLLAR) => {
1148 self.parse_normal_content();
1149 true
1150 }
1151 Some(NEWLINE) => {
1152 self.builder.start_node(BLANK_LINE.into());
1153 self.bump();
1154 self.builder.finish_node();
1155 true
1156 }
1157 Some(COMMENT) => {
1158 self.parse_comment();
1159 true
1160 }
1161 Some(WHITESPACE) => {
1162 if self.is_end_of_file_or_newline_after_whitespace() {
1164 self.skip_ws();
1167 return true;
1168 }
1169
1170 let look_ahead_pos = self.tokens.len().saturating_sub(1);
1173 let mut is_documentation_or_help = false;
1174
1175 if look_ahead_pos > 0 {
1176 let next_token = &self.tokens[look_ahead_pos - 1];
1177 if next_token.0 == IDENTIFIER
1180 || next_token.0 == COMMENT
1181 || next_token.0 == TEXT
1182 {
1183 is_documentation_or_help = true;
1184 }
1185 }
1186
1187 if is_documentation_or_help {
1188 self.skip_ws();
1191 while self.current().is_some() && self.current() != Some(NEWLINE) {
1192 self.bump();
1193 }
1194 if self.current() == Some(NEWLINE) {
1195 self.bump();
1196 }
1197 } else {
1198 self.skip_ws();
1199 }
1200 true
1201 }
1202 Some(INDENT) => {
1203 self.bump();
1205
1206 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1208 self.bump();
1209 }
1210 if self.current() == Some(NEWLINE) {
1211 self.bump();
1212 }
1213 true
1214 }
1215 Some(kind) => {
1216 self.error(format!("unexpected token {:?}", kind));
1217 self.bump();
1218 true
1219 }
1220 }
1221 }
1222
1223 fn parse(mut self) -> Parse {
1224 self.builder.start_node(ROOT.into());
1225
1226 while self.parse_token() {}
1227
1228 self.builder.finish_node();
1229
1230 Parse {
1231 green_node: self.builder.finish(),
1232 errors: self.errors,
1233 }
1234 }
1235
1236 fn is_assignment_line(&mut self) -> bool {
1238 let assignment_ops = ["=", ":=", "::=", ":::=", "+=", "?=", "!="];
1239 let mut pos = self.tokens.len().saturating_sub(1);
1240 let mut seen_identifier = false;
1241 let mut seen_export = false;
1242
1243 while pos > 0 {
1244 let (kind, text) = &self.tokens[pos];
1245
1246 match kind {
1247 NEWLINE => break,
1248 IDENTIFIER if text == "export" => seen_export = true,
1249 IDENTIFIER if !seen_identifier => seen_identifier = true,
1250 OPERATOR if assignment_ops.contains(&text.as_str()) => {
1251 return seen_identifier || seen_export
1252 }
1253 OPERATOR if text == ":" => return false, WHITESPACE => (),
1255 _ if seen_export => return true, _ => return false,
1257 }
1258 pos = pos.saturating_sub(1);
1259 }
1260 false
1261 }
1262
1263 fn bump(&mut self) {
1265 let (kind, text) = self.tokens.pop().unwrap();
1266 self.builder.token(kind.into(), text.as_str());
1267 }
1268 fn current(&self) -> Option<SyntaxKind> {
1270 self.tokens.last().map(|(kind, _)| *kind)
1271 }
1272
1273 fn expect_eol(&mut self) {
1274 self.skip_ws();
1276
1277 match self.current() {
1278 Some(NEWLINE) => {
1279 self.bump();
1280 }
1281 None => {
1282 }
1284 n => {
1285 self.error(format!("expected newline, got {:?}", n));
1286 self.skip_until_newline();
1288 }
1289 }
1290 }
1291
1292 fn is_at_eof(&self) -> bool {
1294 self.current().is_none()
1295 }
1296
1297 fn is_at_eof_or_only_whitespace(&self) -> bool {
1299 if self.is_at_eof() {
1300 return true;
1301 }
1302
1303 self.tokens
1305 .iter()
1306 .rev()
1307 .all(|(kind, _)| matches!(*kind, WHITESPACE | NEWLINE))
1308 }
1309
1310 fn skip_ws(&mut self) {
1311 while self.current() == Some(WHITESPACE) {
1312 self.bump()
1313 }
1314 }
1315
1316 fn skip_until_newline(&mut self) {
1317 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1318 self.bump();
1319 }
1320 if self.current() == Some(NEWLINE) {
1321 self.bump();
1322 }
1323 }
1324
1325 fn consume_balanced_parens(&mut self, start_paren_count: usize) -> usize {
1327 let mut paren_count = start_paren_count;
1328
1329 while paren_count > 0 && self.current().is_some() {
1330 match self.current() {
1331 Some(LPAREN) => {
1332 paren_count += 1;
1333 self.bump();
1334 }
1335 Some(RPAREN) => {
1336 paren_count -= 1;
1337 self.bump();
1338 if paren_count == 0 {
1339 break;
1340 }
1341 }
1342 Some(DOLLAR) => {
1343 self.parse_variable_reference();
1345 }
1346 Some(_) => self.bump(),
1347 None => {
1348 self.error("unclosed parenthesis".to_string());
1349 break;
1350 }
1351 }
1352 }
1353
1354 paren_count
1355 }
1356
1357 fn is_end_of_file_or_newline_after_whitespace(&self) -> bool {
1359 if self.is_at_eof_or_only_whitespace() {
1361 return true;
1362 }
1363
1364 if self.tokens.len() <= 1 {
1366 return true;
1367 }
1368
1369 false
1370 }
1371 }
1372
1373 let mut tokens = lex(text);
1374 tokens.reverse();
1375 Parser {
1376 tokens,
1377 builder: GreenNodeBuilder::new(),
1378 errors: Vec::new(),
1379 original_text: text.to_string(),
1380 variant,
1381 }
1382 .parse()
1383}
1384
1385pub(crate) type SyntaxNode = rowan::SyntaxNode<Lang>;
1391#[allow(unused)]
1392type SyntaxToken = rowan::SyntaxToken<Lang>;
1393#[allow(unused)]
1394pub(crate) type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
1395
1396impl Parse {
1397 fn syntax(&self) -> SyntaxNode {
1398 SyntaxNode::new_root_mut(self.green_node.clone())
1399 }
1400
1401 pub(crate) fn root(&self) -> Makefile {
1402 Makefile::cast(self.syntax()).unwrap()
1403 }
1404}
1405
1406fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
1409 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
1410 let mut line = 0;
1411 let mut last_newline_offset = rowan::TextSize::from(0);
1412
1413 for element in root.preorder_with_tokens() {
1414 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
1415 if token.text_range().start() >= offset {
1416 break;
1417 }
1418
1419 for (idx, _) in token.text().match_indices('\n') {
1421 line += 1;
1422 last_newline_offset =
1423 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
1424 }
1425 }
1426 }
1427
1428 let column: usize = (offset - last_newline_offset).into();
1429 (line, column)
1430}
1431
1432macro_rules! ast_node {
1433 ($ast:ident, $kind:ident) => {
1434 #[derive(Clone, PartialEq, Eq, Hash)]
1435 #[repr(transparent)]
1436 pub struct $ast(SyntaxNode);
1438
1439 impl AstNode for $ast {
1440 type Language = Lang;
1441
1442 fn can_cast(kind: SyntaxKind) -> bool {
1443 kind == $kind
1444 }
1445
1446 fn cast(syntax: SyntaxNode) -> Option<Self> {
1447 if Self::can_cast(syntax.kind()) {
1448 Some(Self(syntax))
1449 } else {
1450 None
1451 }
1452 }
1453
1454 fn syntax(&self) -> &SyntaxNode {
1455 &self.0
1456 }
1457 }
1458
1459 impl $ast {
1460 pub fn line(&self) -> usize {
1462 line_col_at_offset(&self.0, self.0.text_range().start()).0
1463 }
1464
1465 pub fn column(&self) -> usize {
1467 line_col_at_offset(&self.0, self.0.text_range().start()).1
1468 }
1469
1470 pub fn line_col(&self) -> (usize, usize) {
1473 line_col_at_offset(&self.0, self.0.text_range().start())
1474 }
1475 }
1476
1477 impl core::fmt::Display for $ast {
1478 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1479 write!(f, "{}", self.0.text())
1480 }
1481 }
1482 };
1483}
1484
1485ast_node!(Makefile, ROOT);
1486ast_node!(Rule, RULE);
1487ast_node!(Recipe, RECIPE);
1488ast_node!(Identifier, IDENTIFIER);
1489ast_node!(VariableDefinition, VARIABLE);
1490ast_node!(Include, INCLUDE);
1491ast_node!(ArchiveMembers, ARCHIVE_MEMBERS);
1492ast_node!(ArchiveMember, ARCHIVE_MEMBER);
1493ast_node!(Conditional, CONDITIONAL);
1494
1495impl Recipe {
1496 pub fn text(&self) -> String {
1501 self.syntax()
1502 .children_with_tokens()
1503 .filter_map(|it| {
1504 if let Some(token) = it.as_token() {
1505 if token.kind() == TEXT {
1506 return Some(token.text().to_string());
1507 }
1508 }
1509 None
1510 })
1511 .collect::<Vec<_>>()
1512 .join("")
1513 }
1514
1515 pub fn comment(&self) -> Option<String> {
1531 self.syntax()
1532 .children_with_tokens()
1533 .filter_map(|it| {
1534 if let Some(token) = it.as_token() {
1535 if token.kind() == COMMENT {
1536 return Some(token.text().to_string());
1537 }
1538 }
1539 None
1540 })
1541 .next()
1542 }
1543
1544 pub fn full(&self) -> String {
1560 self.syntax()
1561 .children_with_tokens()
1562 .filter_map(|it| {
1563 if let Some(token) = it.as_token() {
1564 if token.kind() == TEXT || token.kind() == COMMENT {
1566 return Some(token.text().to_string());
1567 }
1568 }
1569 None
1570 })
1571 .collect::<Vec<_>>()
1572 .join("")
1573 }
1574
1575 pub fn parent(&self) -> Option<Rule> {
1588 self.syntax().parent().and_then(Rule::cast)
1589 }
1590
1591 pub fn is_silent(&self) -> bool {
1604 let text = self.text();
1605 text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1606 }
1607
1608 pub fn is_ignore_errors(&self) -> bool {
1621 let text = self.text();
1622 text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
1623 }
1624
1625 pub fn set_prefix(&mut self, prefix: &str) {
1642 let text = self.text();
1643
1644 let stripped = text.trim_start_matches(['@', '-', '+']);
1646
1647 let new_text = format!("{}{}", prefix, stripped);
1649
1650 self.replace_text(&new_text);
1651 }
1652
1653 pub fn replace_text(&mut self, new_text: &str) {
1666 let node = self.syntax();
1667 let parent = node.parent().expect("Recipe node must have a parent");
1668 let node_index = node.index();
1669
1670 let mut builder = GreenNodeBuilder::new();
1672 builder.start_node(RECIPE.into());
1673
1674 if let Some(indent_token) = node
1676 .children_with_tokens()
1677 .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
1678 {
1679 builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
1680 } else {
1681 builder.token(INDENT.into(), "\t");
1682 }
1683
1684 builder.token(TEXT.into(), new_text);
1685
1686 if let Some(newline_token) = node
1688 .children_with_tokens()
1689 .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
1690 {
1691 builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
1692 } else {
1693 builder.token(NEWLINE.into(), "\n");
1694 }
1695
1696 builder.finish_node();
1697 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1698
1699 parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
1701
1702 *self = parent
1706 .children_with_tokens()
1707 .nth(node_index)
1708 .and_then(|element| element.into_node())
1709 .and_then(Recipe::cast)
1710 .expect("New recipe node should exist at the same index");
1711 }
1712
1713 pub fn insert_before(&self, text: &str) {
1726 let node = self.syntax();
1727 let parent = node.parent().expect("Recipe node must have a parent");
1728 let node_index = node.index();
1729
1730 let mut builder = GreenNodeBuilder::new();
1732 builder.start_node(RECIPE.into());
1733 builder.token(INDENT.into(), "\t");
1734 builder.token(TEXT.into(), text);
1735 builder.token(NEWLINE.into(), "\n");
1736 builder.finish_node();
1737 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1738
1739 parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
1741 }
1742
1743 pub fn insert_after(&self, text: &str) {
1756 let node = self.syntax();
1757 let parent = node.parent().expect("Recipe node must have a parent");
1758 let node_index = node.index();
1759
1760 let mut builder = GreenNodeBuilder::new();
1762 builder.start_node(RECIPE.into());
1763 builder.token(INDENT.into(), "\t");
1764 builder.token(TEXT.into(), text);
1765 builder.token(NEWLINE.into(), "\n");
1766 builder.finish_node();
1767 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
1768
1769 parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
1771 }
1772
1773 pub fn remove(&self) {
1786 let node = self.syntax();
1787 let parent = node.parent().expect("Recipe node must have a parent");
1788 let node_index = node.index();
1789
1790 parent.splice_children(node_index..node_index + 1, vec![]);
1792 }
1793}
1794
1795pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
1799 let mut newlines_to_remove = vec![];
1801 let mut current = node.last_child_or_token();
1802
1803 while let Some(element) = current {
1804 match &element {
1805 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
1806 newlines_to_remove.push(token.clone());
1807 current = token.prev_sibling_or_token();
1808 }
1809 rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
1810 let mut recipe_current = n.last_child_or_token();
1812 while let Some(recipe_element) = recipe_current {
1813 match &recipe_element {
1814 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
1815 newlines_to_remove.push(token.clone());
1816 recipe_current = token.prev_sibling_or_token();
1817 }
1818 _ => break,
1819 }
1820 }
1821 break; }
1823 _ => break,
1824 }
1825 }
1826
1827 if newlines_to_remove.len() > 1 {
1830 newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
1832
1833 for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
1834 let parent = token.parent().unwrap();
1835 let idx = token.index();
1836 parent.splice_children(idx..idx + 1, vec![]);
1837 }
1838 }
1839}
1840
1841pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
1849 let mut collected_elements = vec![];
1850 let mut found_comment = false;
1851
1852 let mut current = node.prev_sibling_or_token();
1854 while let Some(element) = current {
1855 match &element {
1856 rowan::NodeOrToken::Token(token) => match token.kind() {
1857 COMMENT => {
1858 if token.text().starts_with("#!") {
1859 break; }
1861 found_comment = true;
1862 collected_elements.push(element.clone());
1863 }
1864 NEWLINE | WHITESPACE => {
1865 collected_elements.push(element.clone());
1866 }
1867 _ => break, },
1869 rowan::NodeOrToken::Node(n) => {
1870 if n.kind() == BLANK_LINE {
1872 collected_elements.push(element.clone());
1873 } else {
1874 break; }
1876 }
1877 }
1878 current = element.prev_sibling_or_token();
1879 }
1880
1881 let mut elements_to_remove = vec![];
1884 let mut consecutive_newlines = 0;
1885 for element in collected_elements.iter().rev() {
1886 let should_remove = match element {
1887 rowan::NodeOrToken::Token(token) => match token.kind() {
1888 COMMENT => {
1889 consecutive_newlines = 0;
1890 found_comment
1891 }
1892 NEWLINE => {
1893 consecutive_newlines += 1;
1894 found_comment && consecutive_newlines <= 1
1895 }
1896 WHITESPACE => found_comment,
1897 _ => false,
1898 },
1899 rowan::NodeOrToken::Node(n) => {
1900 if n.kind() == BLANK_LINE {
1902 consecutive_newlines += 1;
1903 found_comment && consecutive_newlines <= 1
1904 } else {
1905 false
1906 }
1907 }
1908 };
1909
1910 if should_remove {
1911 elements_to_remove.push(element.clone());
1912 }
1913 }
1914
1915 let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
1918 all_to_remove.extend(elements_to_remove.into_iter().rev());
1919
1920 all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
1922
1923 for element in all_to_remove {
1924 let idx = element.index();
1925 parent.splice_children(idx..idx + 1, vec![]);
1926 }
1927}
1928
1929impl FromStr for Rule {
1930 type Err = crate::Error;
1931
1932 fn from_str(s: &str) -> Result<Self, Self::Err> {
1933 Rule::parse(s).to_rule_result()
1934 }
1935}
1936
1937impl FromStr for Makefile {
1938 type Err = crate::Error;
1939
1940 fn from_str(s: &str) -> Result<Self, Self::Err> {
1941 Makefile::parse(s).to_result()
1942 }
1943}
1944
1945#[cfg(test)]
1946mod tests {
1947 use super::*;
1948 use crate::ast::makefile::MakefileItem;
1949 use crate::pattern::matches_pattern;
1950
1951 #[test]
1952 fn test_conditionals() {
1953 let code = "ifdef DEBUG\n DEBUG_FLAG := 1\nendif\n";
1957 let mut buf = code.as_bytes();
1958 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
1959 assert!(makefile.code().contains("DEBUG_FLAG"));
1960
1961 let code =
1963 "ifeq ($(OS),Windows_NT)\n RESULT := windows\nelse\n RESULT := unix\nendif\n";
1964 let mut buf = code.as_bytes();
1965 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
1966 assert!(makefile.code().contains("RESULT"));
1967 assert!(makefile.code().contains("windows"));
1968
1969 let code = "ifdef DEBUG\n CFLAGS += -g\n ifdef VERBOSE\n CFLAGS += -v\n endif\nelse\n CFLAGS += -O2\nendif\n";
1971 let mut buf = code.as_bytes();
1972 let makefile = Makefile::read_relaxed(&mut buf)
1973 .expect("Failed to parse nested conditionals with else");
1974 assert!(makefile.code().contains("CFLAGS"));
1975 assert!(makefile.code().contains("VERBOSE"));
1976
1977 let code = "ifdef DEBUG\nendif\n";
1979 let mut buf = code.as_bytes();
1980 let makefile =
1981 Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
1982 assert!(makefile.code().contains("ifdef DEBUG"));
1983
1984 let code = "ifeq ($(OS),Windows)\n EXT := .exe\nelse ifeq ($(OS),Linux)\n EXT := .bin\nelse\n EXT := .out\nendif\n";
1986 let mut buf = code.as_bytes();
1987 let makefile =
1988 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
1989 assert!(makefile.code().contains("EXT"));
1990
1991 let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
1993 let mut buf = code.as_bytes();
1994 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
1995 assert!(makefile.code().contains("DEBUG"));
1996
1997 let code = "ifdef \nDEBUG := 1\nendif\n";
1999 let mut buf = code.as_bytes();
2000 let makefile = Makefile::read_relaxed(&mut buf)
2001 .expect("Failed to parse with recovery - missing condition");
2002 assert!(makefile.code().contains("DEBUG"));
2003 }
2004
2005 #[test]
2006 fn test_parse_simple() {
2007 const SIMPLE: &str = r#"VARIABLE = value
2008
2009rule: dependency
2010 command
2011"#;
2012 let parsed = parse(SIMPLE, None);
2013 assert!(parsed.errors.is_empty());
2014 let node = parsed.syntax();
2015 assert_eq!(
2016 format!("{:#?}", node),
2017 r#"ROOT@0..44
2018 VARIABLE@0..17
2019 IDENTIFIER@0..8 "VARIABLE"
2020 WHITESPACE@8..9 " "
2021 OPERATOR@9..10 "="
2022 WHITESPACE@10..11 " "
2023 EXPR@11..16
2024 IDENTIFIER@11..16 "value"
2025 NEWLINE@16..17 "\n"
2026 BLANK_LINE@17..18
2027 NEWLINE@17..18 "\n"
2028 RULE@18..44
2029 TARGETS@18..22
2030 IDENTIFIER@18..22 "rule"
2031 OPERATOR@22..23 ":"
2032 WHITESPACE@23..24 " "
2033 PREREQUISITES@24..34
2034 PREREQUISITE@24..34
2035 IDENTIFIER@24..34 "dependency"
2036 NEWLINE@34..35 "\n"
2037 RECIPE@35..44
2038 INDENT@35..36 "\t"
2039 TEXT@36..43 "command"
2040 NEWLINE@43..44 "\n"
2041"#
2042 );
2043
2044 let root = parsed.root();
2045
2046 let mut rules = root.rules().collect::<Vec<_>>();
2047 assert_eq!(rules.len(), 1);
2048 let rule = rules.pop().unwrap();
2049 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2050 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
2051 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2052
2053 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2054 assert_eq!(variables.len(), 1);
2055 let variable = variables.pop().unwrap();
2056 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2057 assert_eq!(variable.raw_value(), Some("value".to_string()));
2058 }
2059
2060 #[test]
2061 fn test_parse_export_assign() {
2062 const EXPORT: &str = r#"export VARIABLE := value
2063"#;
2064 let parsed = parse(EXPORT, None);
2065 assert!(parsed.errors.is_empty());
2066 let node = parsed.syntax();
2067 assert_eq!(
2068 format!("{:#?}", node),
2069 r#"ROOT@0..25
2070 VARIABLE@0..25
2071 IDENTIFIER@0..6 "export"
2072 WHITESPACE@6..7 " "
2073 IDENTIFIER@7..15 "VARIABLE"
2074 WHITESPACE@15..16 " "
2075 OPERATOR@16..18 ":="
2076 WHITESPACE@18..19 " "
2077 EXPR@19..24
2078 IDENTIFIER@19..24 "value"
2079 NEWLINE@24..25 "\n"
2080"#
2081 );
2082
2083 let root = parsed.root();
2084
2085 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2086 assert_eq!(variables.len(), 1);
2087 let variable = variables.pop().unwrap();
2088 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2089 assert_eq!(variable.raw_value(), Some("value".to_string()));
2090 }
2091
2092 #[test]
2093 fn test_parse_multiple_prerequisites() {
2094 const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2095 command
2096
2097"#;
2098 let parsed = parse(MULTIPLE_PREREQUISITES, None);
2099 assert!(parsed.errors.is_empty());
2100 let node = parsed.syntax();
2101 assert_eq!(
2102 format!("{:#?}", node),
2103 r#"ROOT@0..40
2104 RULE@0..40
2105 TARGETS@0..4
2106 IDENTIFIER@0..4 "rule"
2107 OPERATOR@4..5 ":"
2108 WHITESPACE@5..6 " "
2109 PREREQUISITES@6..29
2110 PREREQUISITE@6..17
2111 IDENTIFIER@6..17 "dependency1"
2112 WHITESPACE@17..18 " "
2113 PREREQUISITE@18..29
2114 IDENTIFIER@18..29 "dependency2"
2115 NEWLINE@29..30 "\n"
2116 RECIPE@30..39
2117 INDENT@30..31 "\t"
2118 TEXT@31..38 "command"
2119 NEWLINE@38..39 "\n"
2120 NEWLINE@39..40 "\n"
2121"#
2122 );
2123 let root = parsed.root();
2124
2125 let rule = root.rules().next().unwrap();
2126 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2127 assert_eq!(
2128 rule.prerequisites().collect::<Vec<_>>(),
2129 vec!["dependency1", "dependency2"]
2130 );
2131 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2132 }
2133
2134 #[test]
2135 fn test_add_rule() {
2136 let mut makefile = Makefile::new();
2137 let rule = makefile.add_rule("rule");
2138 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2139 assert_eq!(
2140 rule.prerequisites().collect::<Vec<_>>(),
2141 Vec::<String>::new()
2142 );
2143
2144 assert_eq!(makefile.to_string(), "rule:\n");
2145 }
2146
2147 #[test]
2148 fn test_add_rule_with_shebang() {
2149 let content = r#"#!/usr/bin/make -f
2151
2152build: blah
2153 $(MAKE) install
2154
2155clean:
2156 dh_clean
2157"#;
2158
2159 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2160 let initial_count = makefile.rules().count();
2161 assert_eq!(initial_count, 2);
2162
2163 let rule = makefile.add_rule("build-indep");
2165 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2166
2167 assert_eq!(makefile.rules().count(), initial_count + 1);
2169 }
2170
2171 #[test]
2172 fn test_add_rule_formatting() {
2173 let content = r#"build: blah
2175 $(MAKE) install
2176
2177clean:
2178 dh_clean
2179"#;
2180
2181 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2182 let mut rule = makefile.add_rule("build-indep");
2183 rule.add_prerequisite("build").unwrap();
2184
2185 let expected = r#"build: blah
2186 $(MAKE) install
2187
2188clean:
2189 dh_clean
2190
2191build-indep: build
2192"#;
2193
2194 assert_eq!(makefile.to_string(), expected);
2195 }
2196
2197 #[test]
2198 fn test_push_command() {
2199 let mut makefile = Makefile::new();
2200 let mut rule = makefile.add_rule("rule");
2201
2202 rule.push_command("command");
2204 rule.push_command("command2");
2205
2206 assert_eq!(
2208 rule.recipes().collect::<Vec<_>>(),
2209 vec!["command", "command2"]
2210 );
2211
2212 rule.push_command("command3");
2214 assert_eq!(
2215 rule.recipes().collect::<Vec<_>>(),
2216 vec!["command", "command2", "command3"]
2217 );
2218
2219 assert_eq!(
2221 makefile.to_string(),
2222 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2223 );
2224
2225 assert_eq!(
2227 rule.to_string(),
2228 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2229 );
2230 }
2231
2232 #[test]
2233 fn test_replace_command() {
2234 let mut makefile = Makefile::new();
2235 let mut rule = makefile.add_rule("rule");
2236
2237 rule.push_command("command");
2239 rule.push_command("command2");
2240
2241 assert_eq!(
2243 rule.recipes().collect::<Vec<_>>(),
2244 vec!["command", "command2"]
2245 );
2246
2247 rule.replace_command(0, "new command");
2249 assert_eq!(
2250 rule.recipes().collect::<Vec<_>>(),
2251 vec!["new command", "command2"]
2252 );
2253
2254 assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2256
2257 assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2259 }
2260
2261 #[test]
2262 fn test_replace_command_with_comments() {
2263 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";
2266
2267 let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2268
2269 let mut rule = makefile.rules().next().unwrap();
2270
2271 assert_eq!(rule.recipe_nodes().count(), 2);
2273 let recipes: Vec<_> = rule.recipe_nodes().collect();
2274 assert_eq!(recipes[0].text(), ""); assert_eq!(
2276 recipes[1].text(),
2277 "dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"
2278 );
2279
2280 assert!(rule.replace_command(1, "dh_strip"));
2282
2283 assert_eq!(rule.recipe_nodes().count(), 2);
2285 let recipes: Vec<_> = rule.recipe_nodes().collect();
2286 assert_eq!(recipes[0].text(), ""); assert_eq!(recipes[1].text(), "dh_strip");
2288 }
2289
2290 #[test]
2291 fn test_parse_rule_without_newline() {
2292 let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2293 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2294 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2295 let rule = "rule: dependency".parse::<Rule>().unwrap();
2296 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2297 assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2298 }
2299
2300 #[test]
2301 fn test_parse_makefile_without_newline() {
2302 let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2303 assert_eq!(makefile.rules().count(), 1);
2304 }
2305
2306 #[test]
2307 fn test_from_reader() {
2308 let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2309 assert_eq!(makefile.rules().count(), 1);
2310 }
2311
2312 #[test]
2313 fn test_parse_with_tab_after_last_newline() {
2314 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2315 assert_eq!(makefile.rules().count(), 1);
2316 }
2317
2318 #[test]
2319 fn test_parse_with_space_after_last_newline() {
2320 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2321 assert_eq!(makefile.rules().count(), 1);
2322 }
2323
2324 #[test]
2325 fn test_parse_with_comment_after_last_newline() {
2326 let makefile =
2327 Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2328 assert_eq!(makefile.rules().count(), 1);
2329 }
2330
2331 #[test]
2332 fn test_parse_with_variable_rule() {
2333 let makefile =
2334 Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2335 .unwrap();
2336
2337 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2339 assert_eq!(vars.len(), 1);
2340 assert_eq!(vars[0].name(), Some("RULE".to_string()));
2341 assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2342
2343 let rules = makefile.rules().collect::<Vec<_>>();
2345 assert_eq!(rules.len(), 1);
2346 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2347 assert_eq!(
2348 rules[0].prerequisites().collect::<Vec<_>>(),
2349 vec!["dependency"]
2350 );
2351 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2352 }
2353
2354 #[test]
2355 fn test_parse_with_variable_dependency() {
2356 let makefile =
2357 Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2358
2359 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2361 assert_eq!(vars.len(), 1);
2362 assert_eq!(vars[0].name(), Some("DEP".to_string()));
2363 assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2364
2365 let rules = makefile.rules().collect::<Vec<_>>();
2367 assert_eq!(rules.len(), 1);
2368 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2369 assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2370 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2371 }
2372
2373 #[test]
2374 fn test_parse_with_variable_command() {
2375 let makefile =
2376 Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2377
2378 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2380 assert_eq!(vars.len(), 1);
2381 assert_eq!(vars[0].name(), Some("COM".to_string()));
2382 assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2383
2384 let rules = makefile.rules().collect::<Vec<_>>();
2386 assert_eq!(rules.len(), 1);
2387 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2388 assert_eq!(
2389 rules[0].prerequisites().collect::<Vec<_>>(),
2390 vec!["dependency"]
2391 );
2392 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2393 }
2394
2395 #[test]
2396 fn test_regular_line_error_reporting() {
2397 let input = "rule target\n\tcommand";
2398
2399 let parsed = parse(input, None);
2401 let direct_error = &parsed.errors[0];
2402
2403 assert_eq!(direct_error.line, 2);
2405 assert!(
2406 direct_error.message.contains("expected"),
2407 "Error message should contain 'expected': {}",
2408 direct_error.message
2409 );
2410 assert_eq!(direct_error.context, "\tcommand");
2411
2412 let reader_result = Makefile::from_reader(input.as_bytes());
2414 let parse_error = match reader_result {
2415 Ok(_) => panic!("Expected Parse error from from_reader"),
2416 Err(err) => match err {
2417 self::Error::Parse(parse_err) => parse_err,
2418 _ => panic!("Expected Parse error"),
2419 },
2420 };
2421
2422 let error_text = parse_error.to_string();
2424 assert!(error_text.contains("Error at line 2:"));
2425 assert!(error_text.contains("2| \tcommand"));
2426 }
2427
2428 #[test]
2429 fn test_parsing_error_context_with_bad_syntax() {
2430 let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2432
2433 match Makefile::from_reader(input.as_bytes()) {
2435 Ok(makefile) => {
2436 assert_eq!(
2438 makefile.rules().count(),
2439 0,
2440 "Should not have found any rules"
2441 );
2442 }
2443 Err(err) => match err {
2444 self::Error::Parse(error) => {
2445 assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2447 assert!(
2448 !error.errors[0].context.is_empty(),
2449 "Error context should not be empty"
2450 );
2451 }
2452 _ => panic!("Unexpected error type"),
2453 },
2454 };
2455 }
2456
2457 #[test]
2458 fn test_error_message_format() {
2459 let parse_error = ParseError {
2461 errors: vec![ErrorInfo {
2462 message: "test error".to_string(),
2463 line: 42,
2464 context: "some problematic code".to_string(),
2465 }],
2466 };
2467
2468 let error_text = parse_error.to_string();
2469 assert!(error_text.contains("Error at line 42: test error"));
2470 assert!(error_text.contains("42| some problematic code"));
2471 }
2472
2473 #[test]
2474 fn test_line_number_calculation() {
2475 let test_cases = [
2477 ("rule dependency\n\tcommand", 2), ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2), ("var = value\n#comment\n\tindented line", 3), ];
2481
2482 for (input, expected_line) in test_cases {
2483 match input.parse::<Makefile>() {
2485 Ok(_) => {
2486 continue;
2489 }
2490 Err(err) => {
2491 if let Error::Parse(parse_err) = err {
2492 assert_eq!(
2494 parse_err.errors[0].line, expected_line,
2495 "Line number should match the expected line"
2496 );
2497
2498 if parse_err.errors[0].message.contains("indented") {
2500 assert!(
2501 parse_err.errors[0].context.starts_with('\t'),
2502 "Context for indentation errors should include the tab character"
2503 );
2504 }
2505 } else {
2506 panic!("Expected parse error, got: {:?}", err);
2507 }
2508 }
2509 }
2510 }
2511 }
2512
2513 #[test]
2514 fn test_conditional_features() {
2515 let code = r#"
2517# Set variables based on DEBUG flag
2518ifdef DEBUG
2519 CFLAGS += -g -DDEBUG
2520else
2521 CFLAGS = -O2
2522endif
2523
2524# Define a build rule
2525all: $(OBJS)
2526 $(CC) $(CFLAGS) -o $@ $^
2527"#;
2528
2529 let mut buf = code.as_bytes();
2530 let makefile =
2531 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2532
2533 assert!(!makefile.code().is_empty(), "Makefile has content");
2536
2537 let rules = makefile.rules().collect::<Vec<_>>();
2539 assert!(!rules.is_empty(), "Should have found rules");
2540
2541 assert!(code.contains("ifdef DEBUG"));
2543 assert!(code.contains("endif"));
2544
2545 let code_with_var = r#"
2547# Define a variable first
2548CC = gcc
2549
2550ifdef DEBUG
2551 CFLAGS += -g -DDEBUG
2552else
2553 CFLAGS = -O2
2554endif
2555
2556all: $(OBJS)
2557 $(CC) $(CFLAGS) -o $@ $^
2558"#;
2559
2560 let mut buf = code_with_var.as_bytes();
2561 let makefile =
2562 Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2563
2564 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2566 assert!(
2567 !vars.is_empty(),
2568 "Should have found at least the CC variable definition"
2569 );
2570 }
2571
2572 #[test]
2573 fn test_include_directive() {
2574 let parsed = parse(
2575 "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2576 None,
2577 );
2578 assert!(parsed.errors.is_empty());
2579 let node = parsed.syntax();
2580 assert!(format!("{:#?}", node).contains("INCLUDE@"));
2581 }
2582
2583 #[test]
2584 fn test_export_variables() {
2585 let parsed = parse("export SHELL := /bin/bash\n", None);
2586 assert!(parsed.errors.is_empty());
2587 let makefile = parsed.root();
2588 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2589 assert_eq!(vars.len(), 1);
2590 let shell_var = vars
2591 .iter()
2592 .find(|v| v.name() == Some("SHELL".to_string()))
2593 .unwrap();
2594 assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2595 }
2596
2597 #[test]
2598 fn test_variable_scopes() {
2599 let parsed = parse(
2600 "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
2601 None,
2602 );
2603 assert!(parsed.errors.is_empty());
2604 let makefile = parsed.root();
2605 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2606 assert_eq!(vars.len(), 4);
2607 let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
2608 assert!(var_names.contains(&"SIMPLE".to_string()));
2609 assert!(var_names.contains(&"IMMEDIATE".to_string()));
2610 assert!(var_names.contains(&"CONDITIONAL".to_string()));
2611 assert!(var_names.contains(&"APPEND".to_string()));
2612 }
2613
2614 #[test]
2615 fn test_pattern_rule_parsing() {
2616 let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
2617 assert!(parsed.errors.is_empty());
2618 let makefile = parsed.root();
2619 let rules = makefile.rules().collect::<Vec<_>>();
2620 assert_eq!(rules.len(), 1);
2621 assert_eq!(rules[0].targets().next().unwrap(), "%.o");
2622 assert!(rules[0].recipes().next().unwrap().contains("$@"));
2623 }
2624
2625 #[test]
2626 fn test_include_variants() {
2627 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
2629 let parsed = parse(makefile_str, None);
2630 assert!(parsed.errors.is_empty());
2631
2632 let node = parsed.syntax();
2634 let debug_str = format!("{:#?}", node);
2635
2636 assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
2638
2639 let makefile = parsed.root();
2641
2642 let include_count = makefile
2644 .syntax()
2645 .children()
2646 .filter(|child| child.kind() == INCLUDE)
2647 .count();
2648 assert_eq!(include_count, 4);
2649
2650 assert!(makefile
2652 .included_files()
2653 .any(|path| path.contains("$(VAR)")));
2654 }
2655
2656 #[test]
2657 fn test_include_api() {
2658 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
2660 let makefile: Makefile = makefile_str.parse().unwrap();
2661
2662 let includes: Vec<_> = makefile.includes().collect();
2664 assert_eq!(includes.len(), 3);
2665
2666 assert!(!includes[0].is_optional()); assert!(includes[1].is_optional()); assert!(includes[2].is_optional()); let files: Vec<_> = makefile.included_files().collect();
2673 assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
2674
2675 assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
2677 assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
2678 assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
2679 }
2680
2681 #[test]
2682 fn test_include_integration() {
2683 let phony_makefile = Makefile::from_reader(
2687 ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
2688 .as_bytes()
2689 ).unwrap();
2690
2691 assert_eq!(phony_makefile.rules().count(), 2);
2693
2694 let normal_rules_count = phony_makefile
2696 .rules()
2697 .filter(|r| !r.targets().any(|t| t.starts_with('.')))
2698 .count();
2699 assert_eq!(normal_rules_count, 1);
2700
2701 assert_eq!(phony_makefile.includes().count(), 1);
2703 assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
2704
2705 let simple_makefile = Makefile::from_reader(
2707 "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
2708 .as_bytes(),
2709 )
2710 .unwrap();
2711 assert_eq!(simple_makefile.rules().count(), 1);
2712 assert_eq!(simple_makefile.includes().count(), 1);
2713 }
2714
2715 #[test]
2716 fn test_real_conditional_directives() {
2717 let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
2719 let mut buf = conditional.as_bytes();
2720 let makefile =
2721 Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
2722 let code = makefile.code();
2723 assert!(code.contains("ifdef DEBUG"));
2724 assert!(code.contains("else"));
2725 assert!(code.contains("endif"));
2726
2727 let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
2729 let mut buf = nested.as_bytes();
2730 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
2731 let code = makefile.code();
2732 assert!(code.contains("ifdef DEBUG"));
2733 assert!(code.contains("ifdef VERBOSE"));
2734
2735 let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
2737 let mut buf = ifeq.as_bytes();
2738 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
2739 let code = makefile.code();
2740 assert!(code.contains("ifeq"));
2741 assert!(code.contains("Windows_NT"));
2742 }
2743
2744 #[test]
2745 fn test_indented_text_outside_rules() {
2746 let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \" help show help\"\n";
2748 let parsed = parse(help_text, None);
2749 assert!(parsed.errors.is_empty());
2750
2751 let root = parsed.root();
2753 let rules = root.rules().collect::<Vec<_>>();
2754 assert_eq!(rules.len(), 1);
2755
2756 let help_rule = &rules[0];
2757 let recipes = help_rule.recipes().collect::<Vec<_>>();
2758 assert_eq!(recipes.len(), 2);
2759 assert!(recipes[0].contains("Available targets"));
2760 assert!(recipes[1].contains("help"));
2761 }
2762
2763 #[test]
2764 fn test_comment_handling_in_recipes() {
2765 let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
2767
2768 let parsed = parse(recipe_comment, None);
2770
2771 assert!(
2773 parsed.errors.is_empty(),
2774 "Should parse recipe with comments without errors"
2775 );
2776
2777 let root = parsed.root();
2779 let rules = root.rules().collect::<Vec<_>>();
2780 assert_eq!(rules.len(), 1, "Should find exactly one rule");
2781
2782 let build_rule = &rules[0];
2784 assert_eq!(
2785 build_rule.targets().collect::<Vec<_>>(),
2786 vec!["build"],
2787 "Rule should have 'build' as target"
2788 );
2789
2790 let recipes = build_rule.recipe_nodes().collect::<Vec<_>>();
2793 assert_eq!(recipes.len(), 2, "Should find two recipe nodes");
2794
2795 assert_eq!(recipes[0].text(), "");
2797 assert_eq!(
2798 recipes[0].comment(),
2799 Some("# This is a comment".to_string())
2800 );
2801
2802 assert_eq!(recipes[1].text(), "gcc -o app main.c");
2804 assert_eq!(recipes[1].comment(), None);
2805 }
2806
2807 #[test]
2808 fn test_multiline_variables() {
2809 let multiline = "SOURCES = main.c \\\n util.c\n";
2811
2812 let parsed = parse(multiline, None);
2814
2815 let root = parsed.root();
2817 let vars = root.variable_definitions().collect::<Vec<_>>();
2818 assert!(!vars.is_empty(), "Should find at least one variable");
2819
2820 let operators = "CFLAGS := -Wall \\\n -Werror\n";
2824 let parsed_operators = parse(operators, None);
2825
2826 let root = parsed_operators.root();
2828 let vars = root.variable_definitions().collect::<Vec<_>>();
2829 assert!(
2830 !vars.is_empty(),
2831 "Should find at least one variable with := operator"
2832 );
2833
2834 let append = "LDFLAGS += -L/usr/lib \\\n -lm\n";
2836 let parsed_append = parse(append, None);
2837
2838 let root = parsed_append.root();
2840 let vars = root.variable_definitions().collect::<Vec<_>>();
2841 assert!(
2842 !vars.is_empty(),
2843 "Should find at least one variable with += operator"
2844 );
2845 }
2846
2847 #[test]
2848 fn test_whitespace_and_eof_handling() {
2849 let blank_lines = "VAR = value\n\n\n";
2851
2852 let parsed_blank = parse(blank_lines, None);
2853
2854 let root = parsed_blank.root();
2856 let vars = root.variable_definitions().collect::<Vec<_>>();
2857 assert_eq!(
2858 vars.len(),
2859 1,
2860 "Should find one variable in blank lines test"
2861 );
2862
2863 let trailing_space = "VAR = value \n";
2865
2866 let parsed_space = parse(trailing_space, None);
2867
2868 let root = parsed_space.root();
2870 let vars = root.variable_definitions().collect::<Vec<_>>();
2871 assert_eq!(
2872 vars.len(),
2873 1,
2874 "Should find one variable in trailing space test"
2875 );
2876
2877 let no_newline = "VAR = value";
2879
2880 let parsed_no_newline = parse(no_newline, None);
2881
2882 let root = parsed_no_newline.root();
2884 let vars = root.variable_definitions().collect::<Vec<_>>();
2885 assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
2886 assert_eq!(
2887 vars[0].name(),
2888 Some("VAR".to_string()),
2889 "Variable name should be VAR"
2890 );
2891 }
2892
2893 #[test]
2894 fn test_complex_variable_references() {
2895 let wildcard = "SOURCES = $(wildcard *.c)\n";
2897 let parsed = parse(wildcard, None);
2898 assert!(parsed.errors.is_empty());
2899
2900 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
2902 let parsed = parse(nested, None);
2903 assert!(parsed.errors.is_empty());
2904
2905 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
2907 let parsed = parse(patsubst, None);
2908 assert!(parsed.errors.is_empty());
2909 }
2910
2911 #[test]
2912 fn test_complex_variable_references_minimal() {
2913 let wildcard = "SOURCES = $(wildcard *.c)\n";
2915 let parsed = parse(wildcard, None);
2916 assert!(parsed.errors.is_empty());
2917
2918 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
2920 let parsed = parse(nested, None);
2921 assert!(parsed.errors.is_empty());
2922
2923 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
2925 let parsed = parse(patsubst, None);
2926 assert!(parsed.errors.is_empty());
2927 }
2928
2929 #[test]
2930 fn test_multiline_variable_with_backslash() {
2931 let content = r#"
2932LONG_VAR = This is a long variable \
2933 that continues on the next line \
2934 and even one more line
2935"#;
2936
2937 let mut buf = content.as_bytes();
2939 let makefile =
2940 Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
2941
2942 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2944 assert_eq!(
2945 vars.len(),
2946 1,
2947 "Expected 1 variable but found {}",
2948 vars.len()
2949 );
2950 let var_value = vars[0].raw_value();
2951 assert!(var_value.is_some(), "Variable value is None");
2952
2953 let value_str = var_value.unwrap();
2955 assert!(
2956 value_str.contains("long variable"),
2957 "Value doesn't contain expected content"
2958 );
2959 }
2960
2961 #[test]
2962 fn test_multiline_variable_with_mixed_operators() {
2963 let content = r#"
2964PREFIX ?= /usr/local
2965CFLAGS := -Wall -O2 \
2966 -I$(PREFIX)/include \
2967 -DDEBUG
2968"#;
2969 let mut buf = content.as_bytes();
2971 let makefile = Makefile::read_relaxed(&mut buf)
2972 .expect("Failed to parse multiline variable with operators");
2973
2974 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2976 assert!(
2977 !vars.is_empty(),
2978 "Expected at least 1 variable, found {}",
2979 vars.len()
2980 );
2981
2982 let prefix_var = vars
2984 .iter()
2985 .find(|v| v.name().unwrap_or_default() == "PREFIX");
2986 assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
2987 assert!(
2988 prefix_var.unwrap().raw_value().is_some(),
2989 "PREFIX variable has no value"
2990 );
2991
2992 let cflags_var = vars
2994 .iter()
2995 .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
2996 assert!(
2997 cflags_var.is_some(),
2998 "Expected to find CFLAGS variable (or part of it)"
2999 );
3000 }
3001
3002 #[test]
3003 fn test_indented_help_text() {
3004 let content = r#"
3005.PHONY: help
3006help:
3007 @echo "Available targets:"
3008 @echo " build - Build the project"
3009 @echo " test - Run tests"
3010 @echo " clean - Remove build artifacts"
3011"#;
3012 let mut buf = content.as_bytes();
3014 let makefile =
3015 Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
3016
3017 let rules = makefile.rules().collect::<Vec<_>>();
3019 assert!(!rules.is_empty(), "Expected at least one rule");
3020
3021 let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
3023 assert!(help_rule.is_some(), "Expected to find help rule");
3024
3025 let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
3027 assert!(
3028 !recipes.is_empty(),
3029 "Expected at least one recipe line in help rule"
3030 );
3031 assert!(
3032 recipes.iter().any(|r| r.contains("Available targets")),
3033 "Expected to find 'Available targets' in recipes"
3034 );
3035 }
3036
3037 #[test]
3038 fn test_indented_lines_in_conditionals() {
3039 let content = r#"
3040ifdef DEBUG
3041 CFLAGS += -g -DDEBUG
3042 # This is a comment inside conditional
3043 ifdef VERBOSE
3044 CFLAGS += -v
3045 endif
3046endif
3047"#;
3048 let mut buf = content.as_bytes();
3050 let makefile = Makefile::read_relaxed(&mut buf)
3051 .expect("Failed to parse indented lines in conditionals");
3052
3053 let code = makefile.code();
3055 assert!(code.contains("ifdef DEBUG"));
3056 assert!(code.contains("ifdef VERBOSE"));
3057 assert!(code.contains("endif"));
3058 }
3059
3060 #[test]
3061 fn test_recipe_with_colon() {
3062 let content = r#"
3063build:
3064 @echo "Building at: $(shell date)"
3065 gcc -o program main.c
3066"#;
3067 let parsed = parse(content, None);
3068 assert!(
3069 parsed.errors.is_empty(),
3070 "Failed to parse recipe with colon: {:?}",
3071 parsed.errors
3072 );
3073 }
3074
3075 #[test]
3076 #[ignore]
3077 fn test_double_colon_rules() {
3078 let content = r#"
3081%.o :: %.c
3082 $(CC) -c $< -o $@
3083
3084# Double colon allows multiple rules for same target
3085all:: prerequisite1
3086 @echo "First rule for all"
3087
3088all:: prerequisite2
3089 @echo "Second rule for all"
3090"#;
3091 let mut buf = content.as_bytes();
3092 let makefile =
3093 Makefile::read_relaxed(&mut buf).expect("Failed to parse double colon rules");
3094
3095 let rules = makefile.rules().collect::<Vec<_>>();
3097 assert!(!rules.is_empty(), "Expected at least one rule");
3098
3099 let all_rules = rules
3101 .iter()
3102 .filter(|r| r.targets().any(|t| t.contains("all")));
3103 assert!(
3104 all_rules.count() > 0,
3105 "Expected to find at least one rule containing 'all'"
3106 );
3107 }
3108
3109 #[test]
3110 fn test_else_conditional_directives() {
3111 let content = r#"
3113ifeq ($(OS),Windows_NT)
3114 TARGET = windows
3115else ifeq ($(OS),Darwin)
3116 TARGET = macos
3117else ifeq ($(OS),Linux)
3118 TARGET = linux
3119else
3120 TARGET = unknown
3121endif
3122"#;
3123 let mut buf = content.as_bytes();
3124 let makefile =
3125 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3126 assert!(makefile.code().contains("else ifeq"));
3127 assert!(makefile.code().contains("TARGET"));
3128
3129 let content = r#"
3131ifdef WINDOWS
3132 TARGET = windows
3133else ifdef DARWIN
3134 TARGET = macos
3135else ifdef LINUX
3136 TARGET = linux
3137else
3138 TARGET = unknown
3139endif
3140"#;
3141 let mut buf = content.as_bytes();
3142 let makefile =
3143 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3144 assert!(makefile.code().contains("else ifdef"));
3145
3146 let content = r#"
3148ifndef NOWINDOWS
3149 TARGET = windows
3150else ifndef NODARWIN
3151 TARGET = macos
3152else
3153 TARGET = linux
3154endif
3155"#;
3156 let mut buf = content.as_bytes();
3157 let makefile =
3158 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3159 assert!(makefile.code().contains("else ifndef"));
3160
3161 let content = r#"
3163ifneq ($(OS),Windows_NT)
3164 TARGET = not_windows
3165else ifneq ($(OS),Darwin)
3166 TARGET = not_macos
3167else
3168 TARGET = darwin
3169endif
3170"#;
3171 let mut buf = content.as_bytes();
3172 let makefile =
3173 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3174 assert!(makefile.code().contains("else ifneq"));
3175 }
3176
3177 #[test]
3178 fn test_complex_else_conditionals() {
3179 let content = r#"VAR1 := foo
3181VAR2 := bar
3182
3183ifeq ($(VAR1),foo)
3184 RESULT := foo_matched
3185else ifdef VAR2
3186 RESULT := var2_defined
3187else ifndef VAR3
3188 RESULT := var3_not_defined
3189else
3190 RESULT := final_else
3191endif
3192
3193all:
3194 @echo $(RESULT)
3195"#;
3196 let mut buf = content.as_bytes();
3197 let makefile =
3198 Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3199
3200 let code = makefile.code();
3202 assert!(code.contains("ifeq ($(VAR1),foo)"));
3203 assert!(code.contains("else ifdef VAR2"));
3204 assert!(code.contains("else ifndef VAR3"));
3205 assert!(code.contains("else"));
3206 assert!(code.contains("endif"));
3207 assert!(code.contains("RESULT"));
3208
3209 let rules: Vec<_> = makefile.rules().collect();
3211 assert_eq!(rules.len(), 1);
3212 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3213 }
3214
3215 #[test]
3216 fn test_conditional_token_structure() {
3217 let content = r#"ifdef VAR1
3219X := 1
3220else ifdef VAR2
3221X := 2
3222else
3223X := 3
3224endif
3225"#;
3226 let mut buf = content.as_bytes();
3227 let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3228
3229 let syntax = makefile.syntax();
3231
3232 let mut found_conditional = false;
3234 let mut found_conditional_if = false;
3235 let mut found_conditional_else = false;
3236 let mut found_conditional_endif = false;
3237
3238 fn check_node(
3239 node: &SyntaxNode,
3240 found_cond: &mut bool,
3241 found_if: &mut bool,
3242 found_else: &mut bool,
3243 found_endif: &mut bool,
3244 ) {
3245 match node.kind() {
3246 SyntaxKind::CONDITIONAL => *found_cond = true,
3247 SyntaxKind::CONDITIONAL_IF => *found_if = true,
3248 SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3249 SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3250 _ => {}
3251 }
3252
3253 for child in node.children() {
3254 check_node(&child, found_cond, found_if, found_else, found_endif);
3255 }
3256 }
3257
3258 check_node(
3259 syntax,
3260 &mut found_conditional,
3261 &mut found_conditional_if,
3262 &mut found_conditional_else,
3263 &mut found_conditional_endif,
3264 );
3265
3266 assert!(found_conditional, "Should have CONDITIONAL node");
3267 assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3268 assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3269 assert!(
3270 found_conditional_endif,
3271 "Should have CONDITIONAL_ENDIF node"
3272 );
3273 }
3274
3275 #[test]
3276 fn test_ambiguous_assignment_vs_rule() {
3277 const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3279
3280 let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3281 let makefile =
3282 Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3283
3284 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3285 let rules = makefile.rules().collect::<Vec<_>>();
3286
3287 assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3288 assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3289
3290 assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3291
3292 const SIMPLE_RULE: &str = "target: dependency\n";
3294
3295 let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3296 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3297
3298 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3299 let rules = makefile.rules().collect::<Vec<_>>();
3300
3301 assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3302 assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3303
3304 let rule = &rules[0];
3305 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3306 }
3307
3308 #[test]
3309 fn test_nested_conditionals() {
3310 let content = r#"
3311ifdef RELEASE
3312 CFLAGS += -O3
3313 ifndef DEBUG
3314 ifneq ($(ARCH),arm)
3315 CFLAGS += -march=native
3316 else
3317 CFLAGS += -mcpu=cortex-a72
3318 endif
3319 endif
3320endif
3321"#;
3322 let mut buf = content.as_bytes();
3324 let makefile =
3325 Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3326
3327 let code = makefile.code();
3329 assert!(code.contains("ifdef RELEASE"));
3330 assert!(code.contains("ifndef DEBUG"));
3331 assert!(code.contains("ifneq"));
3332 }
3333
3334 #[test]
3335 fn test_space_indented_recipes() {
3336 let content = r#"
3339build:
3340 @echo "Building with spaces instead of tabs"
3341 gcc -o program main.c
3342"#;
3343 let mut buf = content.as_bytes();
3345 let makefile =
3346 Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3347
3348 let rules = makefile.rules().collect::<Vec<_>>();
3350 assert!(!rules.is_empty(), "Expected at least one rule");
3351
3352 let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3354 assert!(build_rule.is_some(), "Expected to find build rule");
3355 }
3356
3357 #[test]
3358 fn test_complex_variable_functions() {
3359 let content = r#"
3360FILES := $(shell find . -name "*.c")
3361OBJS := $(patsubst %.c,%.o,$(FILES))
3362NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3363HEADERS := ${wildcard *.h}
3364"#;
3365 let parsed = parse(content, None);
3366 assert!(
3367 parsed.errors.is_empty(),
3368 "Failed to parse complex variable functions: {:?}",
3369 parsed.errors
3370 );
3371 }
3372
3373 #[test]
3374 fn test_nested_variable_expansions() {
3375 let content = r#"
3376VERSION = 1.0
3377PACKAGE = myapp
3378TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3379INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3380"#;
3381 let parsed = parse(content, None);
3382 assert!(
3383 parsed.errors.is_empty(),
3384 "Failed to parse nested variable expansions: {:?}",
3385 parsed.errors
3386 );
3387 }
3388
3389 #[test]
3390 fn test_special_directives() {
3391 let content = r#"
3392# Special makefile directives
3393.PHONY: all clean
3394.SUFFIXES: .c .o
3395.DEFAULT: all
3396
3397# Variable definition and export directive
3398export PATH := /usr/bin:/bin
3399"#;
3400 let mut buf = content.as_bytes();
3402 let makefile =
3403 Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3404
3405 let rules = makefile.rules().collect::<Vec<_>>();
3407
3408 let phony_rule = rules
3410 .iter()
3411 .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3412 assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3413
3414 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3416 assert!(!vars.is_empty(), "Expected to find at least one variable");
3417 }
3418
3419 #[test]
3422 fn test_comprehensive_real_world_makefile() {
3423 let content = r#"
3425# Basic variable assignment
3426VERSION = 1.0.0
3427
3428# Phony target
3429.PHONY: all clean
3430
3431# Simple rule
3432all:
3433 echo "Building version $(VERSION)"
3434
3435# Another rule with dependencies
3436clean:
3437 rm -f *.o
3438"#;
3439
3440 let parsed = parse(content, None);
3442
3443 assert!(parsed.errors.is_empty(), "Expected no parsing errors");
3445
3446 let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
3448 assert!(!variables.is_empty(), "Expected at least one variable");
3449 assert_eq!(
3450 variables[0].name(),
3451 Some("VERSION".to_string()),
3452 "Expected VERSION variable"
3453 );
3454
3455 let rules = parsed.root().rules().collect::<Vec<_>>();
3457 assert!(!rules.is_empty(), "Expected at least one rule");
3458
3459 let rule_targets: Vec<String> = rules
3461 .iter()
3462 .flat_map(|r| r.targets().collect::<Vec<_>>())
3463 .collect();
3464 assert!(
3465 rule_targets.contains(&".PHONY".to_string()),
3466 "Expected .PHONY rule"
3467 );
3468 assert!(
3469 rule_targets.contains(&"all".to_string()),
3470 "Expected 'all' rule"
3471 );
3472 assert!(
3473 rule_targets.contains(&"clean".to_string()),
3474 "Expected 'clean' rule"
3475 );
3476 }
3477
3478 #[test]
3479 fn test_indented_help_text_outside_rules() {
3480 let content = r#"
3482# Targets with help text
3483help:
3484 @echo "Available targets:"
3485 @echo " build build the project"
3486 @echo " test run tests"
3487 @echo " clean clean build artifacts"
3488
3489# Another target
3490clean:
3491 rm -rf build/
3492"#;
3493
3494 let parsed = parse(content, None);
3496
3497 assert!(
3499 parsed.errors.is_empty(),
3500 "Failed to parse indented help text"
3501 );
3502
3503 let rules = parsed.root().rules().collect::<Vec<_>>();
3505 assert_eq!(rules.len(), 2, "Expected to find two rules");
3506
3507 let help_rule = rules
3509 .iter()
3510 .find(|r| r.targets().any(|t| t == "help"))
3511 .expect("Expected to find help rule");
3512
3513 let clean_rule = rules
3514 .iter()
3515 .find(|r| r.targets().any(|t| t == "clean"))
3516 .expect("Expected to find clean rule");
3517
3518 let help_recipes = help_rule.recipes().collect::<Vec<_>>();
3520 assert!(
3521 !help_recipes.is_empty(),
3522 "Help rule should have recipe lines"
3523 );
3524 assert!(
3525 help_recipes
3526 .iter()
3527 .any(|line| line.contains("Available targets")),
3528 "Help recipes should include 'Available targets' line"
3529 );
3530
3531 let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
3533 assert!(
3534 !clean_recipes.is_empty(),
3535 "Clean rule should have recipe lines"
3536 );
3537 assert!(
3538 clean_recipes.iter().any(|line| line.contains("rm -rf")),
3539 "Clean recipes should include 'rm -rf' command"
3540 );
3541 }
3542
3543 #[test]
3544 fn test_makefile1_phony_pattern() {
3545 let content = "#line 2145\n.PHONY: $(PHONY)\n";
3547
3548 let result = parse(content, None);
3550
3551 assert!(
3553 result.errors.is_empty(),
3554 "Failed to parse .PHONY: $(PHONY) pattern"
3555 );
3556
3557 let rules = result.root().rules().collect::<Vec<_>>();
3559 assert_eq!(rules.len(), 1, "Expected 1 rule");
3560 assert_eq!(
3561 rules[0].targets().next().unwrap(),
3562 ".PHONY",
3563 "Expected .PHONY rule"
3564 );
3565
3566 let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
3568 assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
3569 assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
3570 }
3571
3572 #[test]
3573 fn test_skip_until_newline_behavior() {
3574 let input = "text without newline";
3576 let parsed = parse(input, None);
3577 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
3579
3580 let input_with_newline = "text\nafter newline";
3581 let parsed2 = parse(input_with_newline, None);
3582 assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
3583 }
3584
3585 #[test]
3586 #[ignore] fn test_error_with_indent_token() {
3588 let input = "\tinvalid indented line";
3590 let parsed = parse(input, None);
3591 assert!(!parsed.errors.is_empty());
3593
3594 let error_msg = &parsed.errors[0].message;
3595 assert!(error_msg.contains("recipe commences before first target"));
3596 }
3597
3598 #[test]
3599 fn test_conditional_token_handling() {
3600 let input = r#"
3602ifndef VAR
3603 CFLAGS = -DTEST
3604endif
3605"#;
3606 let parsed = parse(input, None);
3607 let makefile = parsed.root();
3609 let _vars = makefile.variable_definitions().collect::<Vec<_>>();
3610 let nested = r#"
3614ifdef DEBUG
3615 ifndef RELEASE
3616 CFLAGS = -g
3617 endif
3618endif
3619"#;
3620 let parsed_nested = parse(nested, None);
3621 let _makefile = parsed_nested.root();
3623 }
3624
3625 #[test]
3626 fn test_include_vs_conditional_logic() {
3627 let input = r#"
3629include file.mk
3630ifdef VAR
3631 VALUE = 1
3632endif
3633"#;
3634 let parsed = parse(input, None);
3635 let makefile = parsed.root();
3637 let includes = makefile.includes().collect::<Vec<_>>();
3638 assert!(!includes.is_empty() || !parsed.errors.is_empty());
3640
3641 let optional_include = r#"
3643-include optional.mk
3644ifndef VAR
3645 VALUE = default
3646endif
3647"#;
3648 let parsed2 = parse(optional_include, None);
3649 let _makefile = parsed2.root();
3651 }
3652
3653 #[test]
3654 fn test_balanced_parens_counting() {
3655 let input = r#"
3657VAR = $(call func,$(nested,arg),extra)
3658COMPLEX = $(if $(condition),$(then_val),$(else_val))
3659"#;
3660 let parsed = parse(input, None);
3661 assert!(parsed.errors.is_empty());
3662
3663 let makefile = parsed.root();
3664 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3665 assert_eq!(vars.len(), 2);
3666 }
3667
3668 #[test]
3669 fn test_documentation_lookahead() {
3670 let input = r#"
3672# Documentation comment
3673help:
3674 @echo "Usage instructions"
3675 @echo "More help text"
3676"#;
3677 let parsed = parse(input, None);
3678 assert!(parsed.errors.is_empty());
3679
3680 let makefile = parsed.root();
3681 let rules = makefile.rules().collect::<Vec<_>>();
3682 assert_eq!(rules.len(), 1);
3683 assert_eq!(rules[0].targets().next().unwrap(), "help");
3684 }
3685
3686 #[test]
3687 fn test_edge_case_empty_input() {
3688 let parsed = parse("", None);
3690 assert!(parsed.errors.is_empty());
3691
3692 let parsed2 = parse(" \n \n", None);
3694 let _makefile = parsed2.root();
3697 }
3698
3699 #[test]
3700 fn test_malformed_conditional_recovery() {
3701 let input = r#"
3703ifdef
3704 # Missing condition variable
3705endif
3706"#;
3707 let parsed = parse(input, None);
3708 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
3711 }
3712
3713 #[test]
3714 fn test_replace_rule() {
3715 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
3716 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3717
3718 makefile.replace_rule(0, new_rule).unwrap();
3719
3720 let targets: Vec<_> = makefile
3721 .rules()
3722 .flat_map(|r| r.targets().collect::<Vec<_>>())
3723 .collect();
3724 assert_eq!(targets, vec!["new_rule", "rule2"]);
3725
3726 let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
3727 assert_eq!(recipes, vec!["new_command"]);
3728 }
3729
3730 #[test]
3731 fn test_replace_rule_out_of_bounds() {
3732 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3733 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3734
3735 let result = makefile.replace_rule(5, new_rule);
3736 assert!(result.is_err());
3737 }
3738
3739 #[test]
3740 fn test_remove_rule() {
3741 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
3742 .parse()
3743 .unwrap();
3744
3745 let removed = makefile.remove_rule(1).unwrap();
3746 assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
3747
3748 let remaining_targets: Vec<_> = makefile
3749 .rules()
3750 .flat_map(|r| r.targets().collect::<Vec<_>>())
3751 .collect();
3752 assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
3753 assert_eq!(makefile.rules().count(), 2);
3754 }
3755
3756 #[test]
3757 fn test_remove_rule_out_of_bounds() {
3758 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3759
3760 let result = makefile.remove_rule(5);
3761 assert!(result.is_err());
3762 }
3763
3764 #[test]
3765 fn test_insert_rule() {
3766 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
3767 let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
3768
3769 makefile.insert_rule(1, new_rule).unwrap();
3770
3771 let targets: Vec<_> = makefile
3772 .rules()
3773 .flat_map(|r| r.targets().collect::<Vec<_>>())
3774 .collect();
3775 assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
3776 assert_eq!(makefile.rules().count(), 3);
3777 }
3778
3779 #[test]
3780 fn test_insert_rule_at_end() {
3781 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3782 let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
3783
3784 makefile.insert_rule(1, new_rule).unwrap();
3785
3786 let targets: Vec<_> = makefile
3787 .rules()
3788 .flat_map(|r| r.targets().collect::<Vec<_>>())
3789 .collect();
3790 assert_eq!(targets, vec!["rule1", "end_rule"]);
3791 }
3792
3793 #[test]
3794 fn test_insert_rule_out_of_bounds() {
3795 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
3796 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3797
3798 let result = makefile.insert_rule(5, new_rule);
3799 assert!(result.is_err());
3800 }
3801
3802 #[test]
3803 fn test_insert_rule_preserves_blank_line_spacing_at_end() {
3804 let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
3806 let mut makefile: Makefile = input.parse().unwrap();
3807 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
3808
3809 makefile.insert_rule(2, new_rule).unwrap();
3810
3811 let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
3812 assert_eq!(makefile.to_string(), expected);
3813 }
3814
3815 #[test]
3816 fn test_insert_rule_adds_blank_lines_when_missing() {
3817 let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
3819 let mut makefile: Makefile = input.parse().unwrap();
3820 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
3821
3822 makefile.insert_rule(2, new_rule).unwrap();
3823
3824 let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
3825 assert_eq!(makefile.to_string(), expected);
3826 }
3827
3828 #[test]
3829 fn test_remove_command() {
3830 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
3831 .parse()
3832 .unwrap();
3833
3834 rule.remove_command(1);
3835 let recipes: Vec<_> = rule.recipes().collect();
3836 assert_eq!(recipes, vec!["command1", "command3"]);
3837 assert_eq!(rule.recipe_count(), 2);
3838 }
3839
3840 #[test]
3841 fn test_remove_command_out_of_bounds() {
3842 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
3843
3844 let result = rule.remove_command(5);
3845 assert!(!result);
3846 }
3847
3848 #[test]
3849 fn test_insert_command() {
3850 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
3851
3852 rule.insert_command(1, "command2");
3853 let recipes: Vec<_> = rule.recipes().collect();
3854 assert_eq!(recipes, vec!["command1", "command2", "command3"]);
3855 }
3856
3857 #[test]
3858 fn test_insert_command_at_end() {
3859 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
3860
3861 rule.insert_command(1, "command2");
3862 let recipes: Vec<_> = rule.recipes().collect();
3863 assert_eq!(recipes, vec!["command1", "command2"]);
3864 }
3865
3866 #[test]
3867 fn test_insert_command_in_empty_rule() {
3868 let mut rule: Rule = "rule:\n".parse().unwrap();
3869
3870 rule.insert_command(0, "new_command");
3871 let recipes: Vec<_> = rule.recipes().collect();
3872 assert_eq!(recipes, vec!["new_command"]);
3873 }
3874
3875 #[test]
3876 fn test_recipe_count() {
3877 let rule1: Rule = "rule:\n".parse().unwrap();
3878 assert_eq!(rule1.recipe_count(), 0);
3879
3880 let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
3881 assert_eq!(rule2.recipe_count(), 2);
3882 }
3883
3884 #[test]
3885 fn test_clear_commands() {
3886 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
3887 .parse()
3888 .unwrap();
3889
3890 rule.clear_commands();
3891 assert_eq!(rule.recipe_count(), 0);
3892
3893 let recipes: Vec<_> = rule.recipes().collect();
3894 assert_eq!(recipes, Vec::<String>::new());
3895
3896 let targets: Vec<_> = rule.targets().collect();
3898 assert_eq!(targets, vec!["rule"]);
3899 }
3900
3901 #[test]
3902 fn test_clear_commands_empty_rule() {
3903 let mut rule: Rule = "rule:\n".parse().unwrap();
3904
3905 rule.clear_commands();
3906 assert_eq!(rule.recipe_count(), 0);
3907
3908 let targets: Vec<_> = rule.targets().collect();
3909 assert_eq!(targets, vec!["rule"]);
3910 }
3911
3912 #[test]
3913 fn test_rule_manipulation_preserves_structure() {
3914 let input = r#"# Comment
3916VAR = value
3917
3918rule1:
3919 command1
3920
3921# Another comment
3922rule2:
3923 command2
3924
3925VAR2 = value2
3926"#;
3927
3928 let mut makefile: Makefile = input.parse().unwrap();
3929 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
3930
3931 makefile.insert_rule(1, new_rule).unwrap();
3933
3934 let targets: Vec<_> = makefile
3936 .rules()
3937 .flat_map(|r| r.targets().collect::<Vec<_>>())
3938 .collect();
3939 assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
3940
3941 let vars: Vec<_> = makefile.variable_definitions().collect();
3943 assert_eq!(vars.len(), 2);
3944
3945 let output = makefile.code();
3947 assert!(output.contains("# Comment"));
3948 assert!(output.contains("VAR = value"));
3949 assert!(output.contains("# Another comment"));
3950 assert!(output.contains("VAR2 = value2"));
3951 }
3952
3953 #[test]
3954 fn test_replace_rule_with_multiple_targets() {
3955 let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
3956 let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
3957
3958 makefile.replace_rule(0, new_rule).unwrap();
3959
3960 let targets: Vec<_> = makefile
3961 .rules()
3962 .flat_map(|r| r.targets().collect::<Vec<_>>())
3963 .collect();
3964 assert_eq!(targets, vec!["new_target"]);
3965 }
3966
3967 #[test]
3968 fn test_empty_makefile_operations() {
3969 let mut makefile = Makefile::new();
3970
3971 assert!(makefile
3973 .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
3974 .is_err());
3975 assert!(makefile.remove_rule(0).is_err());
3976
3977 let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
3979 makefile.insert_rule(0, new_rule).unwrap();
3980 assert_eq!(makefile.rules().count(), 1);
3981 }
3982
3983 #[test]
3984 fn test_command_operations_preserve_indentation() {
3985 let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
3986 .parse()
3987 .unwrap();
3988
3989 rule.insert_command(1, "middle_command");
3990 let recipes: Vec<_> = rule.recipes().collect();
3991 assert_eq!(
3992 recipes,
3993 vec!["\tdeep_indent", "middle_command", "shallow_indent"]
3994 );
3995 }
3996
3997 #[test]
3998 fn test_rule_operations_with_variables_and_includes() {
3999 let input = r#"VAR1 = value1
4000include common.mk
4001
4002rule1:
4003 command1
4004
4005VAR2 = value2
4006include other.mk
4007
4008rule2:
4009 command2
4010"#;
4011
4012 let mut makefile: Makefile = input.parse().unwrap();
4013
4014 makefile.remove_rule(0).unwrap();
4016
4017 let output = makefile.code();
4019 assert!(output.contains("VAR1 = value1"));
4020 assert!(output.contains("include common.mk"));
4021 assert!(output.contains("VAR2 = value2"));
4022 assert!(output.contains("include other.mk"));
4023
4024 assert_eq!(makefile.rules().count(), 1);
4026 let remaining_targets: Vec<_> = makefile
4027 .rules()
4028 .flat_map(|r| r.targets().collect::<Vec<_>>())
4029 .collect();
4030 assert_eq!(remaining_targets, vec!["rule2"]);
4031 }
4032
4033 #[test]
4034 fn test_command_manipulation_edge_cases() {
4035 let mut empty_rule: Rule = "empty:\n".parse().unwrap();
4037 assert_eq!(empty_rule.recipe_count(), 0);
4038
4039 empty_rule.insert_command(0, "first_command");
4040 assert_eq!(empty_rule.recipe_count(), 1);
4041
4042 let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
4044 empty_rule2.clear_commands();
4045 assert_eq!(empty_rule2.recipe_count(), 0);
4046 }
4047
4048 #[test]
4049 fn test_large_makefile_performance() {
4050 let mut makefile = Makefile::new();
4052
4053 for i in 0..100 {
4055 let rule_name = format!("rule{}", i);
4056 makefile
4057 .add_rule(&rule_name)
4058 .push_command(&format!("command{}", i));
4059 }
4060
4061 assert_eq!(makefile.rules().count(), 100);
4062
4063 let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
4065 makefile.replace_rule(50, new_rule).unwrap();
4066
4067 let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
4069 assert_eq!(rule_50_targets, vec!["middle_rule"]);
4070
4071 assert_eq!(makefile.rules().count(), 100); }
4073
4074 #[test]
4075 fn test_complex_recipe_manipulation() {
4076 let mut complex_rule: Rule = r#"complex:
4077 @echo "Starting build"
4078 $(CC) $(CFLAGS) -o $@ $<
4079 @echo "Build complete"
4080 chmod +x $@
4081"#
4082 .parse()
4083 .unwrap();
4084
4085 assert_eq!(complex_rule.recipe_count(), 4);
4086
4087 complex_rule.remove_command(0); complex_rule.remove_command(1); let final_recipes: Vec<_> = complex_rule.recipes().collect();
4092 assert_eq!(final_recipes.len(), 2);
4093 assert!(final_recipes[0].contains("$(CC)"));
4094 assert!(final_recipes[1].contains("chmod"));
4095 }
4096
4097 #[test]
4098 fn test_variable_definition_remove() {
4099 let makefile: Makefile = r#"VAR1 = value1
4100VAR2 = value2
4101VAR3 = value3
4102"#
4103 .parse()
4104 .unwrap();
4105
4106 assert_eq!(makefile.variable_definitions().count(), 3);
4108
4109 let mut var2 = makefile
4111 .variable_definitions()
4112 .nth(1)
4113 .expect("Should have second variable");
4114 assert_eq!(var2.name(), Some("VAR2".to_string()));
4115 var2.remove();
4116
4117 assert_eq!(makefile.variable_definitions().count(), 2);
4119 let var_names: Vec<_> = makefile
4120 .variable_definitions()
4121 .filter_map(|v| v.name())
4122 .collect();
4123 assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4124 }
4125
4126 #[test]
4127 fn test_variable_definition_set_value() {
4128 let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4129
4130 let mut var = makefile
4131 .variable_definitions()
4132 .next()
4133 .expect("Should have variable");
4134 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4135
4136 var.set_value("new_value");
4138
4139 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4141 assert!(makefile.code().contains("VAR = new_value"));
4142 }
4143
4144 #[test]
4145 fn test_variable_definition_set_value_preserves_format() {
4146 let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4147
4148 let mut var = makefile
4149 .variable_definitions()
4150 .next()
4151 .expect("Should have variable");
4152 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4153
4154 var.set_value("new_value");
4156
4157 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4159 let code = makefile.code();
4160 assert!(code.contains("export"), "Should preserve export prefix");
4161 assert!(code.contains(":="), "Should preserve := operator");
4162 assert!(code.contains("new_value"), "Should have new value");
4163 }
4164
4165 #[test]
4166 fn test_makefile_find_variable() {
4167 let makefile: Makefile = r#"VAR1 = value1
4168VAR2 = value2
4169VAR3 = value3
4170"#
4171 .parse()
4172 .unwrap();
4173
4174 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4176 assert_eq!(vars.len(), 1);
4177 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4178 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4179
4180 assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4182 }
4183
4184 #[test]
4185 fn test_makefile_find_variable_with_export() {
4186 let makefile: Makefile = r#"VAR1 = value1
4187export VAR2 := value2
4188VAR3 = value3
4189"#
4190 .parse()
4191 .unwrap();
4192
4193 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4195 assert_eq!(vars.len(), 1);
4196 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4197 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4198 }
4199
4200 #[test]
4201 fn test_variable_definition_is_export() {
4202 let makefile: Makefile = r#"VAR1 = value1
4203export VAR2 := value2
4204export VAR3 = value3
4205VAR4 := value4
4206"#
4207 .parse()
4208 .unwrap();
4209
4210 let vars: Vec<_> = makefile.variable_definitions().collect();
4211 assert_eq!(vars.len(), 4);
4212
4213 assert!(!vars[0].is_export());
4214 assert!(vars[1].is_export());
4215 assert!(vars[2].is_export());
4216 assert!(!vars[3].is_export());
4217 }
4218
4219 #[test]
4220 fn test_makefile_find_variable_multiple() {
4221 let makefile: Makefile = r#"VAR1 = value1
4222VAR1 = value2
4223VAR2 = other
4224VAR1 = value3
4225"#
4226 .parse()
4227 .unwrap();
4228
4229 let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4231 assert_eq!(vars.len(), 3);
4232 assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4233 assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4234 assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4235
4236 let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4238 assert_eq!(var2s.len(), 1);
4239 assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4240 }
4241
4242 #[test]
4243 fn test_variable_remove_and_find() {
4244 let makefile: Makefile = r#"VAR1 = value1
4245VAR2 = value2
4246VAR3 = value3
4247"#
4248 .parse()
4249 .unwrap();
4250
4251 let mut var2 = makefile
4253 .find_variable("VAR2")
4254 .next()
4255 .expect("Should find VAR2");
4256 var2.remove();
4257
4258 assert_eq!(makefile.find_variable("VAR2").count(), 0);
4260
4261 assert_eq!(makefile.find_variable("VAR1").count(), 1);
4263 assert_eq!(makefile.find_variable("VAR3").count(), 1);
4264 }
4265
4266 #[test]
4267 fn test_variable_remove_with_comment() {
4268 let makefile: Makefile = r#"VAR1 = value1
4269# This is a comment about VAR2
4270VAR2 = value2
4271VAR3 = value3
4272"#
4273 .parse()
4274 .unwrap();
4275
4276 let mut var2 = makefile
4278 .variable_definitions()
4279 .nth(1)
4280 .expect("Should have second variable");
4281 assert_eq!(var2.name(), Some("VAR2".to_string()));
4282 var2.remove();
4283
4284 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4286 }
4287
4288 #[test]
4289 fn test_variable_remove_with_multiple_comments() {
4290 let makefile: Makefile = r#"VAR1 = value1
4291# Comment line 1
4292# Comment line 2
4293# Comment line 3
4294VAR2 = value2
4295VAR3 = value3
4296"#
4297 .parse()
4298 .unwrap();
4299
4300 let mut var2 = makefile
4302 .variable_definitions()
4303 .nth(1)
4304 .expect("Should have second variable");
4305 var2.remove();
4306
4307 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4309 }
4310
4311 #[test]
4312 fn test_variable_remove_with_empty_line() {
4313 let makefile: Makefile = r#"VAR1 = value1
4314
4315# Comment about VAR2
4316VAR2 = value2
4317VAR3 = value3
4318"#
4319 .parse()
4320 .unwrap();
4321
4322 let mut var2 = makefile
4324 .variable_definitions()
4325 .nth(1)
4326 .expect("Should have second variable");
4327 var2.remove();
4328
4329 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4332 }
4333
4334 #[test]
4335 fn test_variable_remove_with_multiple_empty_lines() {
4336 let makefile: Makefile = r#"VAR1 = value1
4337
4338
4339# Comment about VAR2
4340VAR2 = value2
4341VAR3 = value3
4342"#
4343 .parse()
4344 .unwrap();
4345
4346 let mut var2 = makefile
4348 .variable_definitions()
4349 .nth(1)
4350 .expect("Should have second variable");
4351 var2.remove();
4352
4353 assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4356 }
4357
4358 #[test]
4359 fn test_rule_remove_with_comment() {
4360 let makefile: Makefile = r#"rule1:
4361 command1
4362
4363# Comment about rule2
4364rule2:
4365 command2
4366rule3:
4367 command3
4368"#
4369 .parse()
4370 .unwrap();
4371
4372 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4374 rule2.remove().unwrap();
4375
4376 assert_eq!(
4379 makefile.code(),
4380 "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4381 );
4382 }
4383
4384 #[test]
4385 fn test_variable_remove_preserves_shebang() {
4386 let makefile: Makefile = r#"#!/usr/bin/make -f
4387# This is a regular comment
4388VAR1 = value1
4389VAR2 = value2
4390"#
4391 .parse()
4392 .unwrap();
4393
4394 let mut var1 = makefile.variable_definitions().next().unwrap();
4396 var1.remove();
4397
4398 let code = makefile.code();
4400 assert!(code.starts_with("#!/usr/bin/make -f"));
4401 assert!(!code.contains("regular comment"));
4402 assert!(!code.contains("VAR1"));
4403 assert!(code.contains("VAR2"));
4404 }
4405
4406 #[test]
4407 fn test_variable_remove_preserves_subsequent_comments() {
4408 let makefile: Makefile = r#"VAR1 = value1
4409# Comment about VAR2
4410VAR2 = value2
4411
4412# Comment about VAR3
4413VAR3 = value3
4414"#
4415 .parse()
4416 .unwrap();
4417
4418 let mut var2 = makefile
4420 .variable_definitions()
4421 .nth(1)
4422 .expect("Should have second variable");
4423 var2.remove();
4424
4425 let code = makefile.code();
4427 assert_eq!(
4428 code,
4429 "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4430 );
4431 }
4432
4433 #[test]
4434 fn test_variable_remove_after_shebang_preserves_empty_line() {
4435 let makefile: Makefile = r#"#!/usr/bin/make -f
4436export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4437
4438%:
4439 dh $@
4440"#
4441 .parse()
4442 .unwrap();
4443
4444 let mut var = makefile.variable_definitions().next().unwrap();
4446 var.remove();
4447
4448 assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
4450 }
4451
4452 #[test]
4453 fn test_rule_add_prerequisite() {
4454 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4455 rule.add_prerequisite("dep2").unwrap();
4456 assert_eq!(
4457 rule.prerequisites().collect::<Vec<_>>(),
4458 vec!["dep1", "dep2"]
4459 );
4460 assert_eq!(rule.to_string(), "target: dep1 dep2\n");
4462 }
4463
4464 #[test]
4465 fn test_rule_add_prerequisite_to_rule_without_prereqs() {
4466 let mut rule: Rule = "target:\n".parse().unwrap();
4468 rule.add_prerequisite("dep1").unwrap();
4469 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
4470 assert_eq!(rule.to_string(), "target: dep1\n");
4472 }
4473
4474 #[test]
4475 fn test_rule_remove_prerequisite() {
4476 let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
4477 assert!(rule.remove_prerequisite("dep2").unwrap());
4478 assert_eq!(
4479 rule.prerequisites().collect::<Vec<_>>(),
4480 vec!["dep1", "dep3"]
4481 );
4482 assert!(!rule.remove_prerequisite("nonexistent").unwrap());
4483 }
4484
4485 #[test]
4486 fn test_rule_set_prerequisites() {
4487 let mut rule: Rule = "target: old_dep\n".parse().unwrap();
4488 rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
4489 .unwrap();
4490 assert_eq!(
4491 rule.prerequisites().collect::<Vec<_>>(),
4492 vec!["new_dep1", "new_dep2"]
4493 );
4494 }
4495
4496 #[test]
4497 fn test_rule_set_prerequisites_empty() {
4498 let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
4499 rule.set_prerequisites(vec![]).unwrap();
4500 assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
4501 }
4502
4503 #[test]
4504 fn test_rule_add_target() {
4505 let mut rule: Rule = "target1: dep1\n".parse().unwrap();
4506 rule.add_target("target2").unwrap();
4507 assert_eq!(
4508 rule.targets().collect::<Vec<_>>(),
4509 vec!["target1", "target2"]
4510 );
4511 }
4512
4513 #[test]
4514 fn test_rule_set_targets() {
4515 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
4516 rule.set_targets(vec!["new_target1", "new_target2"])
4517 .unwrap();
4518 assert_eq!(
4519 rule.targets().collect::<Vec<_>>(),
4520 vec!["new_target1", "new_target2"]
4521 );
4522 }
4523
4524 #[test]
4525 fn test_rule_set_targets_empty() {
4526 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4527 let result = rule.set_targets(vec![]);
4528 assert!(result.is_err());
4529 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
4531 }
4532
4533 #[test]
4534 fn test_rule_has_target() {
4535 let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
4536 assert!(rule.has_target("target1"));
4537 assert!(rule.has_target("target2"));
4538 assert!(!rule.has_target("target3"));
4539 assert!(!rule.has_target("nonexistent"));
4540 }
4541
4542 #[test]
4543 fn test_rule_rename_target() {
4544 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
4545 assert!(rule.rename_target("old_target", "new_target").unwrap());
4546 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
4547 assert!(!rule.rename_target("nonexistent", "something").unwrap());
4549 }
4550
4551 #[test]
4552 fn test_rule_rename_target_multiple() {
4553 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
4554 assert!(rule.rename_target("target2", "renamed_target").unwrap());
4555 assert_eq!(
4556 rule.targets().collect::<Vec<_>>(),
4557 vec!["target1", "renamed_target", "target3"]
4558 );
4559 }
4560
4561 #[test]
4562 fn test_rule_remove_target() {
4563 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
4564 assert!(rule.remove_target("target2").unwrap());
4565 assert_eq!(
4566 rule.targets().collect::<Vec<_>>(),
4567 vec!["target1", "target3"]
4568 );
4569 assert!(!rule.remove_target("nonexistent").unwrap());
4571 }
4572
4573 #[test]
4574 fn test_rule_remove_target_last() {
4575 let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
4576 let result = rule.remove_target("single_target");
4577 assert!(result.is_err());
4578 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
4580 }
4581
4582 #[test]
4583 fn test_rule_target_manipulation_preserves_prerequisites() {
4584 let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
4585
4586 rule.remove_target("target1").unwrap();
4588 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
4589 assert_eq!(
4590 rule.prerequisites().collect::<Vec<_>>(),
4591 vec!["dep1", "dep2"]
4592 );
4593 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4594
4595 rule.add_target("target3").unwrap();
4597 assert_eq!(
4598 rule.targets().collect::<Vec<_>>(),
4599 vec!["target2", "target3"]
4600 );
4601 assert_eq!(
4602 rule.prerequisites().collect::<Vec<_>>(),
4603 vec!["dep1", "dep2"]
4604 );
4605 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4606
4607 rule.rename_target("target2", "renamed").unwrap();
4609 assert_eq!(
4610 rule.targets().collect::<Vec<_>>(),
4611 vec!["renamed", "target3"]
4612 );
4613 assert_eq!(
4614 rule.prerequisites().collect::<Vec<_>>(),
4615 vec!["dep1", "dep2"]
4616 );
4617 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
4618 }
4619
4620 #[test]
4621 fn test_rule_remove() {
4622 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4623 let rule = makefile.find_rule_by_target("rule1").unwrap();
4624 rule.remove().unwrap();
4625 assert_eq!(makefile.rules().count(), 1);
4626 assert!(makefile.find_rule_by_target("rule1").is_none());
4627 assert!(makefile.find_rule_by_target("rule2").is_some());
4628 }
4629
4630 #[test]
4631 fn test_rule_remove_last_trims_blank_lines() {
4632 let makefile: Makefile =
4634 "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
4635 .parse()
4636 .unwrap();
4637
4638 let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
4640 rule.remove().unwrap();
4641
4642 assert_eq!(makefile.code(), "%:\n\tdh $@\n");
4644 assert_eq!(makefile.rules().count(), 1);
4645 }
4646
4647 #[test]
4648 fn test_makefile_find_rule_by_target() {
4649 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4650 let rule = makefile.find_rule_by_target("rule2");
4651 assert!(rule.is_some());
4652 assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
4653 assert!(makefile.find_rule_by_target("nonexistent").is_none());
4654 }
4655
4656 #[test]
4657 fn test_makefile_find_rules_by_target() {
4658 let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
4659 .parse()
4660 .unwrap();
4661 assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
4662 assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
4663 assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
4664 }
4665
4666 #[test]
4667 fn test_makefile_find_rule_by_target_pattern_simple() {
4668 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4669 let rule = makefile.find_rule_by_target_pattern("foo.o");
4670 assert!(rule.is_some());
4671 assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
4672 }
4673
4674 #[test]
4675 fn test_makefile_find_rule_by_target_pattern_no_match() {
4676 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4677 let rule = makefile.find_rule_by_target_pattern("foo.c");
4678 assert!(rule.is_none());
4679 }
4680
4681 #[test]
4682 fn test_makefile_find_rule_by_target_pattern_exact() {
4683 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
4684 let rule = makefile.find_rule_by_target_pattern("foo.o");
4685 assert!(rule.is_some());
4686 assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
4687 }
4688
4689 #[test]
4690 fn test_makefile_find_rule_by_target_pattern_prefix() {
4691 let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
4692 let rule = makefile.find_rule_by_target_pattern("libfoo.a");
4693 assert!(rule.is_some());
4694 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
4695 }
4696
4697 #[test]
4698 fn test_makefile_find_rule_by_target_pattern_suffix() {
4699 let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
4700 let rule = makefile.find_rule_by_target_pattern("foo_test.o");
4701 assert!(rule.is_some());
4702 assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
4703 }
4704
4705 #[test]
4706 fn test_makefile_find_rule_by_target_pattern_middle() {
4707 let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
4708 let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
4709 assert!(rule.is_some());
4710 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
4711 }
4712
4713 #[test]
4714 fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
4715 let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
4716 let rule = makefile.find_rule_by_target_pattern("anything");
4717 assert!(rule.is_some());
4718 assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
4719 }
4720
4721 #[test]
4722 fn test_makefile_find_rules_by_target_pattern_multiple() {
4723 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
4724 .parse()
4725 .unwrap();
4726 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4727 assert_eq!(rules.len(), 2);
4728 }
4729
4730 #[test]
4731 fn test_makefile_find_rules_by_target_pattern_mixed() {
4732 let makefile: Makefile =
4733 "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
4734 .parse()
4735 .unwrap();
4736 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4737 assert_eq!(rules.len(), 2); let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
4739 assert_eq!(rules.len(), 1); }
4741
4742 #[test]
4743 fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
4744 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
4745 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
4746 assert_eq!(rules.len(), 1);
4747 let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
4748 assert_eq!(rules.len(), 0);
4749 }
4750
4751 #[test]
4752 fn test_matches_pattern_exact() {
4753 assert!(matches_pattern("foo.o", "foo.o"));
4754 assert!(!matches_pattern("foo.o", "bar.o"));
4755 }
4756
4757 #[test]
4758 fn test_matches_pattern_suffix() {
4759 assert!(matches_pattern("%.o", "foo.o"));
4760 assert!(matches_pattern("%.o", "bar.o"));
4761 assert!(matches_pattern("%.o", "baz/qux.o"));
4762 assert!(!matches_pattern("%.o", "foo.c"));
4763 }
4764
4765 #[test]
4766 fn test_matches_pattern_prefix() {
4767 assert!(matches_pattern("lib%.a", "libfoo.a"));
4768 assert!(matches_pattern("lib%.a", "libbar.a"));
4769 assert!(!matches_pattern("lib%.a", "foo.a"));
4770 assert!(!matches_pattern("lib%.a", "lib.a"));
4771 }
4772
4773 #[test]
4774 fn test_matches_pattern_middle() {
4775 assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
4776 assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
4777 assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
4778 assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
4779 }
4780
4781 #[test]
4782 fn test_matches_pattern_wildcard_only() {
4783 assert!(matches_pattern("%", "anything"));
4784 assert!(matches_pattern("%", "foo.o"));
4785 assert!(!matches_pattern("%", ""));
4787 }
4788
4789 #[test]
4790 fn test_matches_pattern_empty_stem() {
4791 assert!(!matches_pattern("%.o", ".o")); assert!(!matches_pattern("lib%", "lib")); assert!(!matches_pattern("lib%.a", "lib.a")); }
4796
4797 #[test]
4798 fn test_matches_pattern_multiple_wildcards_not_supported() {
4799 assert!(!matches_pattern("%foo%bar", "xfooybarz"));
4802 assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
4803 }
4804
4805 #[test]
4806 fn test_makefile_add_phony_target() {
4807 let mut makefile = Makefile::new();
4808 makefile.add_phony_target("clean").unwrap();
4809 assert!(makefile.is_phony("clean"));
4810 assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
4811 }
4812
4813 #[test]
4814 fn test_makefile_add_phony_target_existing() {
4815 let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
4816 makefile.add_phony_target("clean").unwrap();
4817 assert!(makefile.is_phony("test"));
4818 assert!(makefile.is_phony("clean"));
4819 let targets: Vec<_> = makefile.phony_targets().collect();
4820 assert!(targets.contains(&"test".to_string()));
4821 assert!(targets.contains(&"clean".to_string()));
4822 }
4823
4824 #[test]
4825 fn test_makefile_remove_phony_target() {
4826 let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
4827 assert!(makefile.remove_phony_target("clean").unwrap());
4828 assert!(!makefile.is_phony("clean"));
4829 assert!(makefile.is_phony("test"));
4830 assert!(!makefile.remove_phony_target("nonexistent").unwrap());
4831 }
4832
4833 #[test]
4834 fn test_makefile_remove_phony_target_last() {
4835 let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
4836 assert!(makefile.remove_phony_target("clean").unwrap());
4837 assert!(!makefile.is_phony("clean"));
4838 assert!(makefile.find_rule_by_target(".PHONY").is_none());
4840 }
4841
4842 #[test]
4843 fn test_makefile_is_phony() {
4844 let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
4845 assert!(makefile.is_phony("clean"));
4846 assert!(makefile.is_phony("test"));
4847 assert!(!makefile.is_phony("build"));
4848 }
4849
4850 #[test]
4851 fn test_makefile_phony_targets() {
4852 let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
4853 let phony_targets: Vec<_> = makefile.phony_targets().collect();
4854 assert_eq!(phony_targets, vec!["clean", "test", "build"]);
4855 }
4856
4857 #[test]
4858 fn test_makefile_phony_targets_empty() {
4859 let makefile = Makefile::new();
4860 assert_eq!(makefile.phony_targets().count(), 0);
4861 }
4862
4863 #[test]
4864 fn test_makefile_remove_first_phony_target_no_extra_space() {
4865 let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
4866 assert!(makefile.remove_phony_target("clean").unwrap());
4867 let result = makefile.to_string();
4868 assert_eq!(result, ".PHONY: test build\n");
4869 }
4870
4871 #[test]
4872 fn test_recipe_with_leading_comments_and_blank_lines() {
4873 let makefile_text = r#"#!/usr/bin/make
4877
4878%:
4879 dh $@
4880
4881override_dh_build:
4882 # The next line is empty
4883
4884 dh_python3
4885"#;
4886 let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
4887
4888 let rules: Vec<_> = makefile.rules().collect();
4889 assert_eq!(rules.len(), 2, "Expected 2 rules");
4890
4891 let rule0 = &rules[0];
4893 assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
4894 assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
4895
4896 let rule1 = &rules[1];
4898 assert_eq!(
4899 rule1.targets().collect::<Vec<_>>(),
4900 vec!["override_dh_build"]
4901 );
4902
4903 let recipes: Vec<_> = rule1.recipes().collect();
4905 assert!(
4906 !recipes.is_empty(),
4907 "Expected at least one recipe for override_dh_build, got none"
4908 );
4909 assert!(
4910 recipes.contains(&"dh_python3".to_string()),
4911 "Expected 'dh_python3' in recipes, got: {:?}",
4912 recipes
4913 );
4914 }
4915
4916 #[test]
4917 fn test_rule_parse_preserves_trailing_blank_lines() {
4918 let input = r#"override_dh_systemd_enable:
4921 dh_systemd_enable -pracoon
4922
4923override_dh_install:
4924 dh_install
4925"#;
4926
4927 let mut mf: Makefile = input.parse().unwrap();
4928
4929 let rule = mf.rules().next().unwrap();
4931 let rule_text = rule.to_string();
4932
4933 assert_eq!(
4935 rule_text,
4936 "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
4937 );
4938
4939 let modified =
4941 rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
4942
4943 let new_rule: Rule = modified.parse().unwrap();
4945 assert_eq!(
4946 new_rule.to_string(),
4947 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
4948 );
4949
4950 mf.replace_rule(0, new_rule).unwrap();
4952
4953 let output = mf.to_string();
4955 assert!(
4956 output.contains(
4957 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
4958 ),
4959 "Blank line between rules should be preserved. Got: {:?}",
4960 output
4961 );
4962 }
4963
4964 #[test]
4965 fn test_rule_parse_round_trip_with_trailing_newlines() {
4966 let test_cases = vec![
4968 "rule:\n\tcommand\n", "rule:\n\tcommand\n\n", "rule:\n\tcommand\n\n\n", ];
4972
4973 for rule_text in test_cases {
4974 let rule: Rule = rule_text.parse().unwrap();
4975 let result = rule.to_string();
4976 assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
4977 }
4978 }
4979
4980 #[test]
4981 fn test_rule_clone() {
4982 let rule_text = "rule:\n\tcommand\n\n";
4984 let rule: Rule = rule_text.parse().unwrap();
4985 let cloned = rule.clone();
4986
4987 assert_eq!(rule.to_string(), cloned.to_string());
4989 assert_eq!(rule.to_string(), rule_text);
4990 assert_eq!(cloned.to_string(), rule_text);
4991
4992 assert_eq!(
4994 rule.targets().collect::<Vec<_>>(),
4995 cloned.targets().collect::<Vec<_>>()
4996 );
4997 assert_eq!(
4998 rule.recipes().collect::<Vec<_>>(),
4999 cloned.recipes().collect::<Vec<_>>()
5000 );
5001 }
5002
5003 #[test]
5004 fn test_makefile_clone() {
5005 let input = "VAR = value\n\nrule:\n\tcommand\n";
5007 let makefile: Makefile = input.parse().unwrap();
5008 let cloned = makefile.clone();
5009
5010 assert_eq!(makefile.to_string(), cloned.to_string());
5012 assert_eq!(makefile.to_string(), input);
5013
5014 assert_eq!(makefile.rules().count(), cloned.rules().count());
5016
5017 assert_eq!(
5019 makefile.variable_definitions().count(),
5020 cloned.variable_definitions().count()
5021 );
5022 }
5023
5024 #[test]
5025 fn test_conditional_with_recipe_line() {
5026 let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
5028 let parsed = parse(input, None);
5029
5030 assert!(
5032 parsed.errors.is_empty(),
5033 "Expected no parse errors, but got: {:?}",
5034 parsed.errors
5035 );
5036
5037 let mf = parsed.root();
5039 assert_eq!(mf.code(), input);
5040 }
5041
5042 #[test]
5043 fn test_conditional_in_rule_recipe() {
5044 let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
5046 let parsed = parse(input, None);
5047
5048 assert!(
5050 parsed.errors.is_empty(),
5051 "Expected no parse errors, but got: {:?}",
5052 parsed.errors
5053 );
5054
5055 let mf = parsed.root();
5057 assert_eq!(mf.code(), input);
5058
5059 assert_eq!(mf.rules().count(), 1);
5061 }
5062
5063 #[test]
5064 fn test_rule_items() {
5065 use crate::RuleItem;
5066
5067 let input = r#"test:
5069 echo "before"
5070ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
5071 ./run-tests
5072endif
5073 echo "after"
5074"#;
5075 let rule: Rule = input.parse().unwrap();
5076
5077 let items: Vec<_> = rule.items().collect();
5078 assert_eq!(
5079 items.len(),
5080 3,
5081 "Expected 3 items: recipe, conditional, recipe"
5082 );
5083
5084 match &items[0] {
5086 RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
5087 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5088 }
5089
5090 match &items[1] {
5092 RuleItem::Conditional(c) => {
5093 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5094 }
5095 RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
5096 }
5097
5098 match &items[2] {
5100 RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5101 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5102 }
5103
5104 let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5106 let simple_items: Vec<_> = simple_rule.items().collect();
5107 assert_eq!(simple_items.len(), 2);
5108
5109 match &simple_items[0] {
5110 RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5111 _ => panic!("Expected recipe"),
5112 }
5113
5114 match &simple_items[1] {
5115 RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5116 _ => panic!("Expected recipe"),
5117 }
5118
5119 let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5121 .parse()
5122 .unwrap();
5123 let cond_items: Vec<_> = cond_only.items().collect();
5124 assert_eq!(cond_items.len(), 1);
5125
5126 match &cond_items[0] {
5127 RuleItem::Conditional(c) => {
5128 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5129 }
5130 _ => panic!("Expected conditional"),
5131 }
5132 }
5133
5134 #[test]
5135 fn test_conditionals_iterator() {
5136 let makefile: Makefile = r#"ifdef DEBUG
5137VAR = debug
5138endif
5139
5140ifndef RELEASE
5141OTHER = dev
5142endif
5143"#
5144 .parse()
5145 .unwrap();
5146
5147 let conditionals: Vec<_> = makefile.conditionals().collect();
5148 assert_eq!(conditionals.len(), 2);
5149
5150 assert_eq!(
5151 conditionals[0].conditional_type(),
5152 Some("ifdef".to_string())
5153 );
5154 assert_eq!(
5155 conditionals[1].conditional_type(),
5156 Some("ifndef".to_string())
5157 );
5158 }
5159
5160 #[test]
5161 fn test_conditional_type_and_condition() {
5162 let makefile: Makefile = r#"ifdef DEBUG
5163VAR = debug
5164endif
5165"#
5166 .parse()
5167 .unwrap();
5168
5169 let conditional = makefile.conditionals().next().unwrap();
5170 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5171 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5172 }
5173
5174 #[test]
5175 fn test_conditional_has_else() {
5176 let makefile_with_else: Makefile = r#"ifdef DEBUG
5177VAR = debug
5178else
5179VAR = release
5180endif
5181"#
5182 .parse()
5183 .unwrap();
5184
5185 let conditional = makefile_with_else.conditionals().next().unwrap();
5186 assert!(conditional.has_else());
5187
5188 let makefile_without_else: Makefile = r#"ifdef DEBUG
5189VAR = debug
5190endif
5191"#
5192 .parse()
5193 .unwrap();
5194
5195 let conditional = makefile_without_else.conditionals().next().unwrap();
5196 assert!(!conditional.has_else());
5197 }
5198
5199 #[test]
5200 fn test_conditional_if_body() {
5201 let makefile: Makefile = r#"ifdef DEBUG
5202VAR = debug
5203endif
5204"#
5205 .parse()
5206 .unwrap();
5207
5208 let conditional = makefile.conditionals().next().unwrap();
5209 let if_body = conditional.if_body();
5210 assert!(if_body.is_some());
5211 assert!(if_body.unwrap().contains("VAR = debug"));
5212 }
5213
5214 #[test]
5215 fn test_conditional_else_body() {
5216 let makefile: Makefile = r#"ifdef DEBUG
5217VAR = debug
5218else
5219VAR = release
5220endif
5221"#
5222 .parse()
5223 .unwrap();
5224
5225 let conditional = makefile.conditionals().next().unwrap();
5226 let else_body = conditional.else_body();
5227 assert!(else_body.is_some());
5228 assert!(else_body.unwrap().contains("VAR = release"));
5229 }
5230
5231 #[test]
5232 fn test_add_conditional_ifdef() {
5233 let mut makefile = Makefile::new();
5234 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5235 assert!(result.is_ok());
5236
5237 let code = makefile.to_string();
5238 assert!(code.contains("ifdef DEBUG"));
5239 assert!(code.contains("VAR = debug"));
5240 assert!(code.contains("endif"));
5241 }
5242
5243 #[test]
5244 fn test_add_conditional_with_else() {
5245 let mut makefile = Makefile::new();
5246 let result =
5247 makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5248 assert!(result.is_ok());
5249
5250 let code = makefile.to_string();
5251 assert!(code.contains("ifdef DEBUG"));
5252 assert!(code.contains("VAR = debug"));
5253 assert!(code.contains("else"));
5254 assert!(code.contains("VAR = release"));
5255 assert!(code.contains("endif"));
5256 }
5257
5258 #[test]
5259 fn test_add_conditional_invalid_type() {
5260 let mut makefile = Makefile::new();
5261 let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5262 assert!(result.is_err());
5263 }
5264
5265 #[test]
5266 fn test_add_conditional_formatting() {
5267 let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5268 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5269 assert!(result.is_ok());
5270
5271 let code = makefile.to_string();
5272 assert!(code.contains("\n\nifdef DEBUG"));
5274 }
5275
5276 #[test]
5277 fn test_conditional_remove() {
5278 let makefile: Makefile = r#"ifdef DEBUG
5279VAR = debug
5280endif
5281
5282VAR2 = value2
5283"#
5284 .parse()
5285 .unwrap();
5286
5287 let mut conditional = makefile.conditionals().next().unwrap();
5288 let result = conditional.remove();
5289 assert!(result.is_ok());
5290
5291 let code = makefile.to_string();
5292 assert!(!code.contains("ifdef DEBUG"));
5293 assert!(!code.contains("VAR = debug"));
5294 assert!(code.contains("VAR2 = value2"));
5295 }
5296
5297 #[test]
5298 fn test_add_conditional_ifndef() {
5299 let mut makefile = Makefile::new();
5300 let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5301 assert!(result.is_ok());
5302
5303 let code = makefile.to_string();
5304 assert!(code.contains("ifndef NDEBUG"));
5305 assert!(code.contains("VAR = enabled"));
5306 assert!(code.contains("endif"));
5307 }
5308
5309 #[test]
5310 fn test_add_conditional_ifeq() {
5311 let mut makefile = Makefile::new();
5312 let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5313 assert!(result.is_ok());
5314
5315 let code = makefile.to_string();
5316 assert!(code.contains("ifeq ($(OS),Linux)"));
5317 assert!(code.contains("VAR = linux"));
5318 assert!(code.contains("endif"));
5319 }
5320
5321 #[test]
5322 fn test_add_conditional_ifneq() {
5323 let mut makefile = Makefile::new();
5324 let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5325 assert!(result.is_ok());
5326
5327 let code = makefile.to_string();
5328 assert!(code.contains("ifneq ($(OS),Windows)"));
5329 assert!(code.contains("VAR = unix"));
5330 assert!(code.contains("endif"));
5331 }
5332
5333 #[test]
5334 fn test_conditional_api_integration() {
5335 let mut makefile: Makefile = r#"VAR1 = value1
5337
5338rule1:
5339 command1
5340"#
5341 .parse()
5342 .unwrap();
5343
5344 makefile
5346 .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5347 .unwrap();
5348
5349 assert_eq!(makefile.conditionals().count(), 1);
5351 let conditional = makefile.conditionals().next().unwrap();
5352 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5353 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5354 assert!(conditional.has_else());
5355
5356 assert_eq!(makefile.variable_definitions().count(), 1);
5358 assert_eq!(makefile.rules().count(), 1);
5359 }
5360
5361 #[test]
5362 fn test_conditional_if_items() {
5363 let makefile: Makefile = r#"ifdef DEBUG
5364VAR = debug
5365rule:
5366 command
5367endif
5368"#
5369 .parse()
5370 .unwrap();
5371
5372 let cond = makefile.conditionals().next().unwrap();
5373 let items: Vec<_> = cond.if_items().collect();
5374 assert_eq!(items.len(), 2); match &items[0] {
5377 MakefileItem::Variable(v) => {
5378 assert_eq!(v.name(), Some("VAR".to_string()));
5379 }
5380 _ => panic!("Expected variable"),
5381 }
5382
5383 match &items[1] {
5384 MakefileItem::Rule(r) => {
5385 assert!(r.targets().any(|t| t == "rule"));
5386 }
5387 _ => panic!("Expected rule"),
5388 }
5389 }
5390
5391 #[test]
5392 fn test_conditional_else_items() {
5393 let makefile: Makefile = r#"ifdef DEBUG
5394VAR = debug
5395else
5396VAR2 = release
5397rule2:
5398 command
5399endif
5400"#
5401 .parse()
5402 .unwrap();
5403
5404 let cond = makefile.conditionals().next().unwrap();
5405 let items: Vec<_> = cond.else_items().collect();
5406 assert_eq!(items.len(), 2); match &items[0] {
5409 MakefileItem::Variable(v) => {
5410 assert_eq!(v.name(), Some("VAR2".to_string()));
5411 }
5412 _ => panic!("Expected variable"),
5413 }
5414
5415 match &items[1] {
5416 MakefileItem::Rule(r) => {
5417 assert!(r.targets().any(|t| t == "rule2"));
5418 }
5419 _ => panic!("Expected rule"),
5420 }
5421 }
5422
5423 #[test]
5424 fn test_conditional_add_if_item() {
5425 let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5426 let mut cond = makefile.conditionals().next().unwrap();
5427
5428 let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5430 let var = temp.variable_definitions().next().unwrap();
5431 cond.add_if_item(MakefileItem::Variable(var));
5432
5433 let code = makefile.to_string();
5434 assert!(code.contains("CFLAGS = -g"));
5435
5436 let cond = makefile.conditionals().next().unwrap();
5438 assert_eq!(cond.if_items().count(), 1);
5439 }
5440
5441 #[test]
5442 fn test_conditional_add_else_item() {
5443 let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
5444 let mut cond = makefile.conditionals().next().unwrap();
5445
5446 let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5448 let var = temp.variable_definitions().next().unwrap();
5449 cond.add_else_item(MakefileItem::Variable(var));
5450
5451 let code = makefile.to_string();
5452 assert!(code.contains("else"));
5453 assert!(code.contains("CFLAGS = -O2"));
5454
5455 let cond = makefile.conditionals().next().unwrap();
5457 assert_eq!(cond.else_items().count(), 1);
5458 }
5459
5460 #[test]
5461 fn test_add_conditional_with_items() {
5462 let mut makefile = Makefile::new();
5463
5464 let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
5466 let var1 = temp1.variable_definitions().next().unwrap();
5467
5468 let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5469 let var2 = temp2.variable_definitions().next().unwrap();
5470
5471 let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
5472 let rule1 = temp3.rules().next().unwrap();
5473
5474 let result = makefile.add_conditional_with_items(
5475 "ifdef",
5476 "DEBUG",
5477 vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
5478 Some(vec![MakefileItem::Variable(var2)]),
5479 );
5480
5481 assert!(result.is_ok());
5482
5483 let code = makefile.to_string();
5484 assert!(code.contains("ifdef DEBUG"));
5485 assert!(code.contains("CFLAGS = -g"));
5486 assert!(code.contains("debug:"));
5487 assert!(code.contains("else"));
5488 assert!(code.contains("CFLAGS = -O2"));
5489 }
5490
5491 #[test]
5492 fn test_conditional_items_with_nested_conditional() {
5493 let makefile: Makefile = r#"ifdef DEBUG
5494VAR = debug
5495ifdef VERBOSE
5496 VAR2 = verbose
5497endif
5498endif
5499"#
5500 .parse()
5501 .unwrap();
5502
5503 let cond = makefile.conditionals().next().unwrap();
5504 let items: Vec<_> = cond.if_items().collect();
5505 assert_eq!(items.len(), 2); match &items[0] {
5508 MakefileItem::Variable(v) => {
5509 assert_eq!(v.name(), Some("VAR".to_string()));
5510 }
5511 _ => panic!("Expected variable"),
5512 }
5513
5514 match &items[1] {
5515 MakefileItem::Conditional(c) => {
5516 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
5517 }
5518 _ => panic!("Expected conditional"),
5519 }
5520 }
5521
5522 #[test]
5523 fn test_conditional_items_with_include() {
5524 let makefile: Makefile = r#"ifdef DEBUG
5525include debug.mk
5526VAR = debug
5527endif
5528"#
5529 .parse()
5530 .unwrap();
5531
5532 let cond = makefile.conditionals().next().unwrap();
5533 let items: Vec<_> = cond.if_items().collect();
5534 assert_eq!(items.len(), 2); match &items[0] {
5537 MakefileItem::Include(i) => {
5538 assert_eq!(i.path(), Some("debug.mk".to_string()));
5539 }
5540 _ => panic!("Expected include"),
5541 }
5542
5543 match &items[1] {
5544 MakefileItem::Variable(v) => {
5545 assert_eq!(v.name(), Some("VAR".to_string()));
5546 }
5547 _ => panic!("Expected variable"),
5548 }
5549 }
5550
5551 #[test]
5552 fn test_makefile_items_iterator() {
5553 let makefile: Makefile = r#"VAR = value
5554ifdef DEBUG
5555CFLAGS = -g
5556endif
5557rule:
5558 command
5559include common.mk
5560"#
5561 .parse()
5562 .unwrap();
5563
5564 assert_eq!(makefile.variable_definitions().count(), 2);
5567 assert_eq!(makefile.conditionals().count(), 1);
5568 assert_eq!(makefile.rules().count(), 1);
5569
5570 let items: Vec<_> = makefile.items().collect();
5571 assert!(
5573 items.len() >= 3,
5574 "Expected at least 3 items, got {}",
5575 items.len()
5576 );
5577
5578 match &items[0] {
5579 MakefileItem::Variable(v) => {
5580 assert_eq!(v.name(), Some("VAR".to_string()));
5581 }
5582 _ => panic!("Expected variable at position 0"),
5583 }
5584
5585 match &items[1] {
5586 MakefileItem::Conditional(c) => {
5587 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
5588 }
5589 _ => panic!("Expected conditional at position 1"),
5590 }
5591
5592 match &items[2] {
5593 MakefileItem::Rule(r) => {
5594 let targets: Vec<_> = r.targets().collect();
5595 assert_eq!(targets, vec!["rule"]);
5596 }
5597 _ => panic!("Expected rule at position 2"),
5598 }
5599 }
5600
5601 #[test]
5602 fn test_conditional_unwrap() {
5603 let makefile: Makefile = r#"ifdef DEBUG
5604VAR = debug
5605rule:
5606 command
5607endif
5608"#
5609 .parse()
5610 .unwrap();
5611
5612 let mut cond = makefile.conditionals().next().unwrap();
5613 cond.unwrap().unwrap();
5614
5615 let code = makefile.to_string();
5616 let expected = "VAR = debug\nrule:\n\tcommand\n";
5617 assert_eq!(code, expected);
5618
5619 assert_eq!(makefile.conditionals().count(), 0);
5621
5622 assert_eq!(makefile.variable_definitions().count(), 1);
5624 assert_eq!(makefile.rules().count(), 1);
5625 }
5626
5627 #[test]
5628 fn test_conditional_unwrap_with_else_fails() {
5629 let makefile: Makefile = r#"ifdef DEBUG
5630VAR = debug
5631else
5632VAR = release
5633endif
5634"#
5635 .parse()
5636 .unwrap();
5637
5638 let mut cond = makefile.conditionals().next().unwrap();
5639 let result = cond.unwrap();
5640
5641 assert!(result.is_err());
5642 assert!(result
5643 .unwrap_err()
5644 .to_string()
5645 .contains("Cannot unwrap conditional with else clause"));
5646 }
5647
5648 #[test]
5649 fn test_conditional_unwrap_nested() {
5650 let makefile: Makefile = r#"ifdef OUTER
5651VAR = outer
5652ifdef INNER
5653VAR2 = inner
5654endif
5655endif
5656"#
5657 .parse()
5658 .unwrap();
5659
5660 let mut outer_cond = makefile.conditionals().next().unwrap();
5662 outer_cond.unwrap().unwrap();
5663
5664 let code = makefile.to_string();
5665 let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
5666 assert_eq!(code, expected);
5667 }
5668
5669 #[test]
5670 fn test_conditional_unwrap_empty() {
5671 let makefile: Makefile = r#"ifdef DEBUG
5672endif
5673"#
5674 .parse()
5675 .unwrap();
5676
5677 let mut cond = makefile.conditionals().next().unwrap();
5678 cond.unwrap().unwrap();
5679
5680 let code = makefile.to_string();
5681 assert_eq!(code, "");
5682 }
5683
5684 #[test]
5685 fn test_rule_parent() {
5686 let makefile: Makefile = r#"all:
5687 echo "test"
5688"#
5689 .parse()
5690 .unwrap();
5691
5692 let rule = makefile.rules().next().unwrap();
5693 let parent = rule.parent();
5694 assert!(parent.is_none());
5696 }
5697
5698 #[test]
5699 fn test_item_parent_in_conditional() {
5700 let makefile: Makefile = r#"ifdef DEBUG
5701VAR = debug
5702rule:
5703 command
5704endif
5705"#
5706 .parse()
5707 .unwrap();
5708
5709 let cond = makefile.conditionals().next().unwrap();
5710
5711 let items: Vec<_> = cond.if_items().collect();
5713 assert_eq!(items.len(), 2);
5714
5715 if let MakefileItem::Variable(var) = &items[0] {
5717 let parent = var.parent();
5718 assert!(parent.is_some());
5719 if let Some(MakefileItem::Conditional(_)) = parent {
5720 } else {
5722 panic!("Expected variable parent to be a Conditional");
5723 }
5724 } else {
5725 panic!("Expected first item to be a Variable");
5726 }
5727
5728 if let MakefileItem::Rule(rule) = &items[1] {
5730 let parent = rule.parent();
5731 assert!(parent.is_some());
5732 if let Some(MakefileItem::Conditional(_)) = parent {
5733 } else {
5735 panic!("Expected rule parent to be a Conditional");
5736 }
5737 } else {
5738 panic!("Expected second item to be a Rule");
5739 }
5740 }
5741
5742 #[test]
5743 fn test_nested_conditional_parent() {
5744 let makefile: Makefile = r#"ifdef OUTER
5745VAR = outer
5746ifdef INNER
5747VAR2 = inner
5748endif
5749endif
5750"#
5751 .parse()
5752 .unwrap();
5753
5754 let outer_cond = makefile.conditionals().next().unwrap();
5755
5756 let items: Vec<_> = outer_cond.if_items().collect();
5758
5759 let inner_cond = items
5761 .iter()
5762 .find_map(|item| {
5763 if let MakefileItem::Conditional(c) = item {
5764 Some(c)
5765 } else {
5766 None
5767 }
5768 })
5769 .unwrap();
5770
5771 let parent = inner_cond.parent();
5773 assert!(parent.is_some());
5774 if let Some(MakefileItem::Conditional(_)) = parent {
5775 } else {
5777 panic!("Expected inner conditional's parent to be a Conditional");
5778 }
5779 }
5780
5781 #[test]
5782 fn test_line_col() {
5783 let text = r#"# Comment at line 0
5784VAR1 = value1
5785VAR2 = value2
5786
5787rule1: dep1 dep2
5788 command1
5789 command2
5790
5791rule2:
5792 command3
5793
5794ifdef DEBUG
5795CFLAGS = -g
5796endif
5797"#;
5798 let makefile: Makefile = text.parse().unwrap();
5799
5800 let vars: Vec<_> = makefile.variable_definitions().collect();
5803 assert_eq!(vars.len(), 3);
5804
5805 assert_eq!(vars[0].line(), 1);
5807 assert_eq!(vars[0].column(), 0);
5808 assert_eq!(vars[0].line_col(), (1, 0));
5809
5810 assert_eq!(vars[1].line(), 2);
5812 assert_eq!(vars[1].column(), 0);
5813
5814 assert_eq!(vars[2].line(), 12);
5816 assert_eq!(vars[2].column(), 0);
5817
5818 let rules: Vec<_> = makefile.rules().collect();
5820 assert_eq!(rules.len(), 2);
5821
5822 assert_eq!(rules[0].line(), 4);
5824 assert_eq!(rules[0].column(), 0);
5825 assert_eq!(rules[0].line_col(), (4, 0));
5826
5827 assert_eq!(rules[1].line(), 8);
5829 assert_eq!(rules[1].column(), 0);
5830
5831 let conditionals: Vec<_> = makefile.conditionals().collect();
5833 assert_eq!(conditionals.len(), 1);
5834
5835 assert_eq!(conditionals[0].line(), 11);
5837 assert_eq!(conditionals[0].column(), 0);
5838 assert_eq!(conditionals[0].line_col(), (11, 0));
5839 }
5840
5841 #[test]
5842 fn test_line_col_multiline() {
5843 let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
5844 let makefile: Makefile = text.parse().unwrap();
5845
5846 let vars: Vec<_> = makefile.variable_definitions().collect();
5848 assert_eq!(vars.len(), 1);
5849 assert_eq!(vars[0].line(), 0);
5850 assert_eq!(vars[0].column(), 0);
5851
5852 let rules: Vec<_> = makefile.rules().collect();
5854 assert_eq!(rules.len(), 1);
5855 assert_eq!(rules[0].line(), 4);
5856 assert_eq!(rules[0].column(), 0);
5857 }
5858
5859 #[test]
5860 fn test_line_col_includes() {
5861 let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
5862 let makefile: Makefile = text.parse().unwrap();
5863
5864 let vars: Vec<_> = makefile.variable_definitions().collect();
5866 assert_eq!(vars[0].line(), 0);
5867
5868 let includes: Vec<_> = makefile.includes().collect();
5870 assert_eq!(includes.len(), 2);
5871 assert_eq!(includes[0].line(), 2);
5872 assert_eq!(includes[0].column(), 0);
5873 assert_eq!(includes[1].line(), 3);
5874 assert_eq!(includes[1].column(), 0);
5875 }
5876
5877 #[test]
5878 fn test_conditional_in_rule_vs_toplevel() {
5879 let text1 = r#"rule:
5881 command
5882ifeq (,$(X))
5883 test
5884endif
5885"#;
5886 let makefile: Makefile = text1.parse().unwrap();
5887 let rules: Vec<_> = makefile.rules().collect();
5888 let conditionals: Vec<_> = makefile.conditionals().collect();
5889
5890 assert_eq!(rules.len(), 1);
5891 assert_eq!(
5892 conditionals.len(),
5893 0,
5894 "Conditional should be part of rule, not top-level"
5895 );
5896
5897 let text2 = r#"rule:
5899 command
5900
5901ifeq (,$(X))
5902 test
5903endif
5904"#;
5905 let makefile: Makefile = text2.parse().unwrap();
5906 let rules: Vec<_> = makefile.rules().collect();
5907 let conditionals: Vec<_> = makefile.conditionals().collect();
5908
5909 assert_eq!(rules.len(), 1);
5910 assert_eq!(
5911 conditionals.len(),
5912 1,
5913 "Conditional after blank line should be top-level"
5914 );
5915 assert_eq!(conditionals[0].line(), 3);
5916 }
5917
5918 #[test]
5919 fn test_nested_conditionals_line_tracking() {
5920 let text = r#"ifdef OUTER
5921VAR1 = value1
5922ifdef INNER
5923VAR2 = value2
5924endif
5925VAR3 = value3
5926endif
5927"#;
5928 let makefile: Makefile = text.parse().unwrap();
5929
5930 let conditionals: Vec<_> = makefile.conditionals().collect();
5931 assert_eq!(
5932 conditionals.len(),
5933 1,
5934 "Only outer conditional should be top-level"
5935 );
5936 assert_eq!(conditionals[0].line(), 0);
5937 assert_eq!(conditionals[0].column(), 0);
5938 }
5939
5940 #[test]
5941 fn test_conditional_else_line_tracking() {
5942 let text = r#"VAR1 = before
5943
5944ifdef DEBUG
5945DEBUG_FLAGS = -g
5946else
5947DEBUG_FLAGS = -O2
5948endif
5949
5950VAR2 = after
5951"#;
5952 let makefile: Makefile = text.parse().unwrap();
5953
5954 let conditionals: Vec<_> = makefile.conditionals().collect();
5955 assert_eq!(conditionals.len(), 1);
5956 assert_eq!(conditionals[0].line(), 2);
5957 assert_eq!(conditionals[0].column(), 0);
5958 }
5959
5960 #[test]
5961 fn test_broken_conditional_endif_without_if() {
5962 let text = "VAR = value\nendif\n";
5964 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5965
5966 let vars: Vec<_> = makefile.variable_definitions().collect();
5968 assert_eq!(vars.len(), 1);
5969 assert_eq!(vars[0].line(), 0);
5970 }
5971
5972 #[test]
5973 fn test_broken_conditional_else_without_if() {
5974 let text = "VAR = value\nelse\nVAR2 = other\n";
5976 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5977
5978 let vars: Vec<_> = makefile.variable_definitions().collect();
5980 assert!(!vars.is_empty(), "Should parse at least the first variable");
5981 assert_eq!(vars[0].line(), 0);
5982 }
5983
5984 #[test]
5985 fn test_broken_conditional_missing_endif() {
5986 let text = r#"ifdef DEBUG
5988DEBUG_FLAGS = -g
5989VAR = value
5990"#;
5991 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
5992
5993 assert!(makefile.code().contains("ifdef DEBUG"));
5995 }
5996
5997 #[test]
5998 fn test_multiple_conditionals_line_tracking() {
5999 let text = r#"ifdef A
6000VAR_A = a
6001endif
6002
6003ifdef B
6004VAR_B = b
6005endif
6006
6007ifdef C
6008VAR_C = c
6009endif
6010"#;
6011 let makefile: Makefile = text.parse().unwrap();
6012
6013 let conditionals: Vec<_> = makefile.conditionals().collect();
6014 assert_eq!(conditionals.len(), 3);
6015 assert_eq!(conditionals[0].line(), 0);
6016 assert_eq!(conditionals[1].line(), 4);
6017 assert_eq!(conditionals[2].line(), 8);
6018 }
6019
6020 #[test]
6021 fn test_conditional_with_multiple_else_ifeq() {
6022 let text = r#"ifeq ($(OS),Windows)
6023EXT = .exe
6024else ifeq ($(OS),Linux)
6025EXT = .bin
6026else
6027EXT = .out
6028endif
6029"#;
6030 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6031
6032 let conditionals: Vec<_> = makefile.conditionals().collect();
6033 assert_eq!(conditionals.len(), 1);
6034 assert_eq!(conditionals[0].line(), 0);
6035 assert_eq!(conditionals[0].column(), 0);
6036 }
6037
6038 #[test]
6039 fn test_conditional_types_line_tracking() {
6040 let text = r#"ifdef VAR1
6041A = 1
6042endif
6043
6044ifndef VAR2
6045B = 2
6046endif
6047
6048ifeq ($(X),y)
6049C = 3
6050endif
6051
6052ifneq ($(Y),n)
6053D = 4
6054endif
6055"#;
6056 let makefile: Makefile = text.parse().unwrap();
6057
6058 let conditionals: Vec<_> = makefile.conditionals().collect();
6059 assert_eq!(conditionals.len(), 4);
6060
6061 assert_eq!(conditionals[0].line(), 0); assert_eq!(
6063 conditionals[0].conditional_type(),
6064 Some("ifdef".to_string())
6065 );
6066
6067 assert_eq!(conditionals[1].line(), 4); assert_eq!(
6069 conditionals[1].conditional_type(),
6070 Some("ifndef".to_string())
6071 );
6072
6073 assert_eq!(conditionals[2].line(), 8); assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
6075
6076 assert_eq!(conditionals[3].line(), 12); assert_eq!(
6078 conditionals[3].conditional_type(),
6079 Some("ifneq".to_string())
6080 );
6081 }
6082
6083 #[test]
6084 fn test_conditional_in_rule_with_recipes() {
6085 let text = r#"test:
6086 echo "start"
6087ifdef VERBOSE
6088 echo "verbose mode"
6089endif
6090 echo "end"
6091"#;
6092 let makefile: Makefile = text.parse().unwrap();
6093
6094 let rules: Vec<_> = makefile.rules().collect();
6095 let conditionals: Vec<_> = makefile.conditionals().collect();
6096
6097 assert_eq!(rules.len(), 1);
6098 assert_eq!(rules[0].line(), 0);
6099 assert_eq!(conditionals.len(), 0);
6101 }
6102
6103 #[test]
6104 fn test_broken_conditional_double_else() {
6105 let text = r#"ifdef DEBUG
6107A = 1
6108else
6109B = 2
6110else
6111C = 3
6112endif
6113"#;
6114 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6115
6116 assert!(makefile.code().contains("ifdef DEBUG"));
6118 }
6119
6120 #[test]
6121 fn test_broken_conditional_mismatched_nesting() {
6122 let text = r#"ifdef A
6124VAR = value
6125endif
6126endif
6127"#;
6128 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6129
6130 let conditionals: Vec<_> = makefile.conditionals().collect();
6133 assert!(
6134 !conditionals.is_empty(),
6135 "Should parse at least the first conditional"
6136 );
6137 }
6138
6139 #[test]
6140 fn test_conditional_with_comment_line_tracking() {
6141 let text = r#"# This is a comment
6142ifdef DEBUG
6143# Another comment
6144CFLAGS = -g
6145endif
6146# Final comment
6147"#;
6148 let makefile: Makefile = text.parse().unwrap();
6149
6150 let conditionals: Vec<_> = makefile.conditionals().collect();
6151 assert_eq!(conditionals.len(), 1);
6152 assert_eq!(conditionals[0].line(), 1);
6153 assert_eq!(conditionals[0].column(), 0);
6154 }
6155
6156 #[test]
6157 fn test_conditional_after_variable_with_blank_lines() {
6158 let text = r#"VAR1 = value1
6159
6160
6161ifdef DEBUG
6162VAR2 = value2
6163endif
6164"#;
6165 let makefile: Makefile = text.parse().unwrap();
6166
6167 let vars: Vec<_> = makefile.variable_definitions().collect();
6168 let conditionals: Vec<_> = makefile.conditionals().collect();
6169
6170 assert_eq!(vars.len(), 2);
6172 assert_eq!(vars[0].line(), 0); assert_eq!(vars[1].line(), 4); assert_eq!(conditionals.len(), 1);
6176 assert_eq!(conditionals[0].line(), 3);
6177 }
6178
6179 #[test]
6180 fn test_empty_conditional_line_tracking() {
6181 let text = r#"ifdef DEBUG
6182endif
6183
6184ifndef RELEASE
6185endif
6186"#;
6187 let makefile: Makefile = text.parse().unwrap();
6188
6189 let conditionals: Vec<_> = makefile.conditionals().collect();
6190 assert_eq!(conditionals.len(), 2);
6191 assert_eq!(conditionals[0].line(), 0);
6192 assert_eq!(conditionals[1].line(), 3);
6193 }
6194
6195 #[test]
6196 fn test_recipe_line_tracking() {
6197 let text = r#"build:
6198 echo "Building..."
6199 gcc -o app main.c
6200 echo "Done"
6201
6202test:
6203 ./run-tests
6204"#;
6205 let makefile: Makefile = text.parse().unwrap();
6206
6207 let rule1 = makefile.rules().next().expect("Should have first rule");
6209 let recipes: Vec<_> = rule1.recipe_nodes().collect();
6210 assert_eq!(recipes.len(), 3);
6211
6212 assert_eq!(recipes[0].text(), "echo \"Building...\"");
6213 assert_eq!(recipes[0].line(), 1);
6214 assert_eq!(recipes[0].column(), 0);
6215
6216 assert_eq!(recipes[1].text(), "gcc -o app main.c");
6217 assert_eq!(recipes[1].line(), 2);
6218 assert_eq!(recipes[1].column(), 0);
6219
6220 assert_eq!(recipes[2].text(), "echo \"Done\"");
6221 assert_eq!(recipes[2].line(), 3);
6222 assert_eq!(recipes[2].column(), 0);
6223
6224 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6226 let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6227 assert_eq!(recipes2.len(), 1);
6228
6229 assert_eq!(recipes2[0].text(), "./run-tests");
6230 assert_eq!(recipes2[0].line(), 6);
6231 assert_eq!(recipes2[0].column(), 0);
6232 }
6233
6234 #[test]
6235 fn test_recipe_with_variables_line_tracking() {
6236 let text = r#"install:
6237 mkdir -p $(DESTDIR)
6238 cp $(BINARY) $(DESTDIR)/
6239"#;
6240 let makefile: Makefile = text.parse().unwrap();
6241 let rule = makefile.rules().next().expect("Should have rule");
6242 let recipes: Vec<_> = rule.recipe_nodes().collect();
6243
6244 assert_eq!(recipes.len(), 2);
6245 assert_eq!(recipes[0].line(), 1);
6246 assert_eq!(recipes[1].line(), 2);
6247 }
6248
6249 #[test]
6250 fn test_recipe_text_no_leading_tab() {
6251 let text = "test:\n\techo hello\n\t\techo nested\n\t echo with spaces\n";
6253 let makefile: Makefile = text.parse().unwrap();
6254 let rule = makefile.rules().next().expect("Should have rule");
6255 let recipes: Vec<_> = rule.recipe_nodes().collect();
6256
6257 assert_eq!(recipes.len(), 3);
6258
6259 eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6261
6262 assert_eq!(recipes[0].text(), "echo hello");
6264
6265 eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6267 assert_eq!(recipes[1].text(), "\techo nested");
6268
6269 eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6271 assert_eq!(recipes[2].text(), " echo with spaces");
6272 }
6273
6274 #[test]
6275 fn test_recipe_parent() {
6276 let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6277 let rule = makefile.rules().next().unwrap();
6278 let recipe = rule.recipe_nodes().next().unwrap();
6279
6280 let parent = recipe.parent().expect("Recipe should have parent");
6281 assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6282 assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6283 }
6284
6285 #[test]
6286 fn test_recipe_is_silent_various_prefixes() {
6287 let makefile: Makefile = r#"test:
6288 @echo silent
6289 -echo ignore
6290 +echo always
6291 @-echo silent_ignore
6292 -@echo ignore_silent
6293 +@echo always_silent
6294 echo normal
6295"#
6296 .parse()
6297 .unwrap();
6298
6299 let rule = makefile.rules().next().unwrap();
6300 let recipes: Vec<_> = rule.recipe_nodes().collect();
6301
6302 assert_eq!(recipes.len(), 7);
6303 assert!(recipes[0].is_silent(), "@echo should be silent");
6304 assert!(!recipes[1].is_silent(), "-echo should not be silent");
6305 assert!(!recipes[2].is_silent(), "+echo should not be silent");
6306 assert!(recipes[3].is_silent(), "@-echo should be silent");
6307 assert!(recipes[4].is_silent(), "-@echo should be silent");
6308 assert!(recipes[5].is_silent(), "+@echo should be silent");
6309 assert!(!recipes[6].is_silent(), "echo should not be silent");
6310 }
6311
6312 #[test]
6313 fn test_recipe_is_ignore_errors_various_prefixes() {
6314 let makefile: Makefile = r#"test:
6315 @echo silent
6316 -echo ignore
6317 +echo always
6318 @-echo silent_ignore
6319 -@echo ignore_silent
6320 +-echo always_ignore
6321 echo normal
6322"#
6323 .parse()
6324 .unwrap();
6325
6326 let rule = makefile.rules().next().unwrap();
6327 let recipes: Vec<_> = rule.recipe_nodes().collect();
6328
6329 assert_eq!(recipes.len(), 7);
6330 assert!(
6331 !recipes[0].is_ignore_errors(),
6332 "@echo should not ignore errors"
6333 );
6334 assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6335 assert!(
6336 !recipes[2].is_ignore_errors(),
6337 "+echo should not ignore errors"
6338 );
6339 assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6340 assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6341 assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6342 assert!(
6343 !recipes[6].is_ignore_errors(),
6344 "echo should not ignore errors"
6345 );
6346 }
6347
6348 #[test]
6349 fn test_recipe_set_prefix_add() {
6350 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6351 let rule = makefile.rules().next().unwrap();
6352 let mut recipe = rule.recipe_nodes().next().unwrap();
6353
6354 recipe.set_prefix("@");
6355 assert_eq!(recipe.text(), "@echo hello");
6356 assert!(recipe.is_silent());
6357 }
6358
6359 #[test]
6360 fn test_recipe_set_prefix_change() {
6361 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6362 let rule = makefile.rules().next().unwrap();
6363 let mut recipe = rule.recipe_nodes().next().unwrap();
6364
6365 recipe.set_prefix("-");
6366 assert_eq!(recipe.text(), "-echo hello");
6367 assert!(!recipe.is_silent());
6368 assert!(recipe.is_ignore_errors());
6369 }
6370
6371 #[test]
6372 fn test_recipe_set_prefix_remove() {
6373 let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
6374 let rule = makefile.rules().next().unwrap();
6375 let mut recipe = rule.recipe_nodes().next().unwrap();
6376
6377 recipe.set_prefix("");
6378 assert_eq!(recipe.text(), "echo hello");
6379 assert!(!recipe.is_silent());
6380 assert!(!recipe.is_ignore_errors());
6381 }
6382
6383 #[test]
6384 fn test_recipe_set_prefix_combinations() {
6385 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6386 let rule = makefile.rules().next().unwrap();
6387 let mut recipe = rule.recipe_nodes().next().unwrap();
6388
6389 recipe.set_prefix("@-");
6390 assert_eq!(recipe.text(), "@-echo hello");
6391 assert!(recipe.is_silent());
6392 assert!(recipe.is_ignore_errors());
6393
6394 recipe.set_prefix("-@");
6395 assert_eq!(recipe.text(), "-@echo hello");
6396 assert!(recipe.is_silent());
6397 assert!(recipe.is_ignore_errors());
6398 }
6399
6400 #[test]
6401 fn test_recipe_replace_text_basic() {
6402 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6403 let rule = makefile.rules().next().unwrap();
6404 let mut recipe = rule.recipe_nodes().next().unwrap();
6405
6406 recipe.replace_text("echo world");
6407 assert_eq!(recipe.text(), "echo world");
6408
6409 let rule = makefile.rules().next().unwrap();
6411 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6412 }
6413
6414 #[test]
6415 fn test_recipe_replace_text_with_prefix() {
6416 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6417 let rule = makefile.rules().next().unwrap();
6418 let mut recipe = rule.recipe_nodes().next().unwrap();
6419
6420 recipe.replace_text("@echo goodbye");
6421 assert_eq!(recipe.text(), "@echo goodbye");
6422 assert!(recipe.is_silent());
6423 }
6424
6425 #[test]
6426 fn test_recipe_insert_before_single() {
6427 let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6428 let rule = makefile.rules().next().unwrap();
6429 let recipe = rule.recipe_nodes().next().unwrap();
6430
6431 recipe.insert_before("echo hello");
6432
6433 let rule = makefile.rules().next().unwrap();
6434 let recipes: Vec<_> = rule.recipes().collect();
6435 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6436 }
6437
6438 #[test]
6439 fn test_recipe_insert_before_multiple() {
6440 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6441 .parse()
6442 .unwrap();
6443 let rule = makefile.rules().next().unwrap();
6444 let recipes: Vec<_> = rule.recipe_nodes().collect();
6445
6446 recipes[1].insert_before("echo middle");
6448
6449 let rule = makefile.rules().next().unwrap();
6450 let new_recipes: Vec<_> = rule.recipes().collect();
6451 assert_eq!(
6452 new_recipes,
6453 vec!["echo one", "echo middle", "echo two", "echo three"]
6454 );
6455 }
6456
6457 #[test]
6458 fn test_recipe_insert_before_first() {
6459 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6460 let rule = makefile.rules().next().unwrap();
6461 let recipes: Vec<_> = rule.recipe_nodes().collect();
6462
6463 recipes[0].insert_before("echo zero");
6464
6465 let rule = makefile.rules().next().unwrap();
6466 let new_recipes: Vec<_> = rule.recipes().collect();
6467 assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
6468 }
6469
6470 #[test]
6471 fn test_recipe_insert_after_single() {
6472 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6473 let rule = makefile.rules().next().unwrap();
6474 let recipe = rule.recipe_nodes().next().unwrap();
6475
6476 recipe.insert_after("echo world");
6477
6478 let rule = makefile.rules().next().unwrap();
6479 let recipes: Vec<_> = rule.recipes().collect();
6480 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6481 }
6482
6483 #[test]
6484 fn test_recipe_insert_after_multiple() {
6485 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6486 .parse()
6487 .unwrap();
6488 let rule = makefile.rules().next().unwrap();
6489 let recipes: Vec<_> = rule.recipe_nodes().collect();
6490
6491 recipes[1].insert_after("echo middle");
6493
6494 let rule = makefile.rules().next().unwrap();
6495 let new_recipes: Vec<_> = rule.recipes().collect();
6496 assert_eq!(
6497 new_recipes,
6498 vec!["echo one", "echo two", "echo middle", "echo three"]
6499 );
6500 }
6501
6502 #[test]
6503 fn test_recipe_insert_after_last() {
6504 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6505 let rule = makefile.rules().next().unwrap();
6506 let recipes: Vec<_> = rule.recipe_nodes().collect();
6507
6508 recipes[1].insert_after("echo three");
6509
6510 let rule = makefile.rules().next().unwrap();
6511 let new_recipes: Vec<_> = rule.recipes().collect();
6512 assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
6513 }
6514
6515 #[test]
6516 fn test_recipe_remove_single() {
6517 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6518 let rule = makefile.rules().next().unwrap();
6519 let recipe = rule.recipe_nodes().next().unwrap();
6520
6521 recipe.remove();
6522
6523 let rule = makefile.rules().next().unwrap();
6524 assert_eq!(rule.recipes().count(), 0);
6525 }
6526
6527 #[test]
6528 fn test_recipe_remove_first() {
6529 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6530 .parse()
6531 .unwrap();
6532 let rule = makefile.rules().next().unwrap();
6533 let recipes: Vec<_> = rule.recipe_nodes().collect();
6534
6535 recipes[0].remove();
6536
6537 let rule = makefile.rules().next().unwrap();
6538 let new_recipes: Vec<_> = rule.recipes().collect();
6539 assert_eq!(new_recipes, vec!["echo two", "echo three"]);
6540 }
6541
6542 #[test]
6543 fn test_recipe_remove_middle() {
6544 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6545 .parse()
6546 .unwrap();
6547 let rule = makefile.rules().next().unwrap();
6548 let recipes: Vec<_> = rule.recipe_nodes().collect();
6549
6550 recipes[1].remove();
6551
6552 let rule = makefile.rules().next().unwrap();
6553 let new_recipes: Vec<_> = rule.recipes().collect();
6554 assert_eq!(new_recipes, vec!["echo one", "echo three"]);
6555 }
6556
6557 #[test]
6558 fn test_recipe_remove_last() {
6559 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6560 .parse()
6561 .unwrap();
6562 let rule = makefile.rules().next().unwrap();
6563 let recipes: Vec<_> = rule.recipe_nodes().collect();
6564
6565 recipes[2].remove();
6566
6567 let rule = makefile.rules().next().unwrap();
6568 let new_recipes: Vec<_> = rule.recipes().collect();
6569 assert_eq!(new_recipes, vec!["echo one", "echo two"]);
6570 }
6571
6572 #[test]
6573 fn test_recipe_multiple_operations() {
6574 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6575 let rule = makefile.rules().next().unwrap();
6576 let mut recipe = rule.recipe_nodes().next().unwrap();
6577
6578 recipe.replace_text("echo modified");
6580 assert_eq!(recipe.text(), "echo modified");
6581
6582 recipe.set_prefix("@");
6584 assert_eq!(recipe.text(), "@echo modified");
6585
6586 recipe.insert_after("echo three");
6588
6589 let rule = makefile.rules().next().unwrap();
6591 let recipes: Vec<_> = rule.recipes().collect();
6592 assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
6593 }
6594}
6595
6596#[cfg(test)]
6597mod test_continuation {
6598 use super::*;
6599
6600 #[test]
6601 fn test_recipe_continuation_lines() {
6602 let makefile_content = r#"override_dh_autoreconf:
6603 set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \
6604 dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \
6605 sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs
6606 dh_autoreconf
6607"#;
6608
6609 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6610 let rule = makefile.rules().next().unwrap();
6611
6612 let recipes: Vec<_> = rule.recipe_nodes().collect();
6613
6614 assert_eq!(recipes.len(), 2);
6616
6617 let expected_first = "set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \\ dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \\ sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs";
6619 assert_eq!(recipes[0].text(), expected_first);
6620
6621 assert_eq!(recipes[1].text(), "dh_autoreconf");
6623 }
6624
6625 #[test]
6626 fn test_simple_continuation() {
6627 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n";
6628
6629 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6630 let rule = makefile.rules().next().unwrap();
6631 let recipes: Vec<_> = rule.recipe_nodes().collect();
6632
6633 assert_eq!(recipes.len(), 1);
6634 assert_eq!(recipes[0].text(), "echo hello && \\ echo world");
6635 }
6636
6637 #[test]
6638 fn test_multiple_continuations() {
6639 let makefile_content = "test:\n\techo line1 && \\\n\t echo line2 && \\\n\t echo line3 && \\\n\t echo line4\n";
6640
6641 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6642 let rule = makefile.rules().next().unwrap();
6643 let recipes: Vec<_> = rule.recipe_nodes().collect();
6644
6645 assert_eq!(recipes.len(), 1);
6646 assert_eq!(
6647 recipes[0].text(),
6648 "echo line1 && \\ echo line2 && \\ echo line3 && \\ echo line4"
6649 );
6650 }
6651
6652 #[test]
6653 fn test_continuation_round_trip() {
6654 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
6655
6656 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6657 let output = makefile.to_string();
6658
6659 assert_eq!(output, makefile_content);
6661 }
6662
6663 #[test]
6664 fn test_continuation_with_silent_prefix() {
6665 let makefile_content = "test:\n\t@echo hello && \\\n\t echo world\n";
6666
6667 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6668 let rule = makefile.rules().next().unwrap();
6669 let recipes: Vec<_> = rule.recipe_nodes().collect();
6670
6671 assert_eq!(recipes.len(), 1);
6672 assert_eq!(recipes[0].text(), "@echo hello && \\ echo world");
6673 assert!(recipes[0].is_silent());
6674 }
6675
6676 #[test]
6677 fn test_mixed_continued_and_non_continued() {
6678 let makefile_content = r#"test:
6679 echo first
6680 echo second && \
6681 echo third
6682 echo fourth
6683"#;
6684
6685 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6686 let rule = makefile.rules().next().unwrap();
6687 let recipes: Vec<_> = rule.recipe_nodes().collect();
6688
6689 assert_eq!(recipes.len(), 3);
6690 assert_eq!(recipes[0].text(), "echo first");
6691 assert_eq!(recipes[1].text(), "echo second && \\ echo third");
6692 assert_eq!(recipes[2].text(), "echo fourth");
6693 }
6694
6695 #[test]
6696 fn test_continuation_replace_command() {
6697 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
6698
6699 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6700 let mut rule = makefile.rules().next().unwrap();
6701
6702 rule.replace_command(0, "echo replaced");
6704
6705 let recipes: Vec<_> = rule.recipe_nodes().collect();
6706 assert_eq!(recipes.len(), 2);
6707 assert_eq!(recipes[0].text(), "echo replaced");
6708 assert_eq!(recipes[1].text(), "echo done");
6709 }
6710
6711 #[test]
6712 fn test_continuation_count() {
6713 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
6714
6715 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6716 let rule = makefile.rules().next().unwrap();
6717
6718 assert_eq!(rule.recipe_count(), 2);
6720 assert_eq!(rule.recipe_nodes().count(), 2);
6721
6722 let recipes_list: Vec<_> = rule.recipes().collect();
6724 assert_eq!(
6725 recipes_list,
6726 vec!["echo hello && \\ echo world", "echo done"]
6727 );
6728 }
6729
6730 #[test]
6731 fn test_backslash_in_middle_of_line() {
6732 let makefile_content = "test:\n\techo hello\\nworld\n\techo done\n";
6734
6735 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
6736 let rule = makefile.rules().next().unwrap();
6737 let recipes: Vec<_> = rule.recipe_nodes().collect();
6738
6739 assert_eq!(recipes.len(), 2);
6740 assert_eq!(recipes[0].text(), "echo hello\\nworld");
6741 assert_eq!(recipes[1].text(), "echo done");
6742 }
6743}