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, PartialEq, Eq, Hash)]
73pub struct PositionedParseError {
74 pub message: String,
76 pub range: rowan::TextRange,
78 pub code: Option<String>,
80}
81
82impl std::fmt::Display for PositionedParseError {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(f, "{}", self.message)
85 }
86}
87
88impl std::error::Error for PositionedParseError {}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
93pub enum Lang {}
94impl rowan::Language for Lang {
95 type Kind = SyntaxKind;
96 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
97 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
98 }
99 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
100 kind.into()
101 }
102}
103
104use rowan::GreenNode;
107
108use rowan::GreenNodeBuilder;
112
113#[derive(Debug)]
116pub(crate) struct Parse {
117 pub(crate) green_node: GreenNode,
118 pub(crate) errors: Vec<ErrorInfo>,
119 pub(crate) positioned_errors: Vec<PositionedParseError>,
120}
121
122pub(crate) fn parse(text: &str, variant: Option<MakefileVariant>) -> Parse {
123 struct Parser {
124 tokens: Vec<(SyntaxKind, String)>,
127 builder: GreenNodeBuilder<'static>,
129 errors: Vec<ErrorInfo>,
132 positioned_errors: Vec<PositionedParseError>,
134 token_positions: Vec<(rowan::TextSize, rowan::TextSize)>,
136 current_token_index: usize,
138 original_text: String,
140 variant: Option<MakefileVariant>,
142 }
143
144 impl Parser {
145 fn error(&mut self, msg: String) {
146 self.builder.start_node(ERROR.into());
147
148 let (line, context) = if self.current() == Some(INDENT) {
149 let lines: Vec<&str> = self.original_text.lines().collect();
151 let tab_line = lines
152 .iter()
153 .enumerate()
154 .find(|(_, line)| line.starts_with('\t'))
155 .map(|(i, _)| i + 1)
156 .unwrap_or(1);
157
158 let next_line = tab_line + 1;
160 if next_line <= lines.len() {
161 (next_line, lines[next_line - 1].to_string())
162 } else {
163 (tab_line, lines[tab_line - 1].to_string())
164 }
165 } else {
166 let line = self.get_line_number_for_position(self.tokens.len());
167 (line, self.get_context_for_line(line))
168 };
169
170 let message = if self.current() == Some(INDENT) && !msg.contains("indented") {
171 if !self.tokens.is_empty() && self.tokens[self.tokens.len() - 1].0 == IDENTIFIER {
172 "expected ':'".to_string()
173 } else {
174 "indented line not part of a rule".to_string()
175 }
176 } else {
177 msg
178 };
179
180 self.errors.push(ErrorInfo {
181 message: message.clone(),
182 line,
183 context,
184 });
185
186 self.add_positioned_error(message, None);
187
188 if self.current().is_some() {
189 self.bump();
190 }
191 self.builder.finish_node();
192 }
193
194 fn add_positioned_error(&mut self, message: String, code: Option<String>) {
196 let range = if self.current_token_index < self.token_positions.len() {
197 let (start, end) = self.token_positions[self.current_token_index];
198 rowan::TextRange::new(start, end)
199 } else {
200 let end = self
202 .token_positions
203 .last()
204 .map(|(_, end)| *end)
205 .unwrap_or_else(|| rowan::TextSize::from(0));
206 rowan::TextRange::new(end, end)
207 };
208
209 self.positioned_errors.push(PositionedParseError {
210 message,
211 range,
212 code,
213 });
214 }
215
216 fn get_line_number_for_position(&self, position: usize) -> usize {
217 if position >= self.tokens.len() {
218 return self.original_text.matches('\n').count() + 1;
219 }
220
221 self.tokens[0..position]
223 .iter()
224 .filter(|(kind, _)| *kind == NEWLINE)
225 .count()
226 + 1
227 }
228
229 fn get_context_for_line(&self, line_number: usize) -> String {
230 self.original_text
231 .lines()
232 .nth(line_number - 1)
233 .unwrap_or("")
234 .to_string()
235 }
236
237 fn parse_recipe_line(&mut self) {
238 self.builder.start_node(RECIPE.into());
239
240 if self.current() != Some(INDENT) {
242 self.error("recipe line must start with a tab".to_string());
243 self.builder.finish_node();
244 return;
245 }
246 self.bump();
247
248 loop {
250 let mut last_text_content: Option<String> = None;
251
252 while self.current().is_some() && self.current() != Some(NEWLINE) {
254 if self.current() == Some(TEXT) {
256 if let Some((_kind, text)) = self.tokens.last() {
257 last_text_content = Some(text.clone());
258 }
259 }
260 self.bump();
261 }
262
263 if self.current() == Some(NEWLINE) {
265 self.bump();
266 }
267
268 let is_continuation = last_text_content
270 .as_ref()
271 .map(|text| text.trim_end().ends_with('\\'))
272 .unwrap_or(false);
273
274 if is_continuation {
275 if self.current() == Some(INDENT) {
277 self.bump();
278 continue;
280 } else {
281 break;
283 }
284 } else {
285 break;
287 }
288 }
289
290 self.builder.finish_node();
291 }
292
293 fn parse_rule_target(&mut self) -> bool {
294 match self.current() {
295 Some(IDENTIFIER) => {
296 if self.is_archive_member() {
298 self.parse_archive_member();
299 } else {
300 self.bump();
301 }
302 true
303 }
304 Some(DOLLAR) => {
305 self.parse_variable_reference();
306 true
307 }
308 _ => {
309 self.error("expected rule target".to_string());
310 false
311 }
312 }
313 }
314
315 fn is_archive_member(&self) -> bool {
316 if self.tokens.len() < 2 {
319 return false;
320 }
321
322 let current_is_identifier = self.current() == Some(IDENTIFIER);
324 let next_is_lparen =
325 self.tokens.len() > 1 && self.tokens[self.tokens.len() - 2].0 == LPAREN;
326
327 current_is_identifier && next_is_lparen
328 }
329
330 fn parse_archive_member(&mut self) {
331 if self.current() == Some(IDENTIFIER) {
342 self.bump();
343 }
344
345 if self.current() == Some(LPAREN) {
347 self.bump();
348
349 self.builder.start_node(ARCHIVE_MEMBERS.into());
351
352 while self.current().is_some() && self.current() != Some(RPAREN) {
354 match self.current() {
355 Some(IDENTIFIER) | Some(TEXT) => {
356 self.builder.start_node(ARCHIVE_MEMBER.into());
358 self.bump();
359 self.builder.finish_node();
360 }
361 Some(WHITESPACE) => self.bump(),
362 Some(DOLLAR) => {
363 self.builder.start_node(ARCHIVE_MEMBER.into());
365 self.parse_variable_reference();
366 self.builder.finish_node();
367 }
368 _ => break,
369 }
370 }
371
372 self.builder.finish_node();
374
375 if self.current() == Some(RPAREN) {
377 self.bump();
378 } else {
379 self.error("expected ')' to close archive member".to_string());
380 }
381 }
382 }
383
384 fn parse_rule_dependencies(&mut self) {
385 self.builder.start_node(PREREQUISITES.into());
386
387 while self.current().is_some() && self.current() != Some(NEWLINE) {
388 match self.current() {
389 Some(WHITESPACE) => {
390 self.bump(); }
392 Some(IDENTIFIER) => {
393 self.builder.start_node(PREREQUISITE.into());
395
396 if self.is_archive_member() {
397 self.parse_archive_member();
398 } else {
399 self.bump(); }
401
402 self.builder.finish_node(); }
404 Some(DOLLAR) => {
405 self.builder.start_node(PREREQUISITE.into());
407 self.parse_variable_reference();
408 self.builder.finish_node(); }
410 _ => {
411 self.bump();
413 }
414 }
415 }
416
417 self.builder.finish_node(); }
419
420 fn parse_rule_recipes(&mut self) {
421 let mut conditional_depth = 0;
423 let mut newline_count = 0;
425
426 loop {
427 match self.current() {
428 Some(INDENT) => {
429 newline_count = 0;
430 self.parse_recipe_line();
431 }
432 Some(NEWLINE) => {
433 newline_count += 1;
434 self.bump();
435 }
436 Some(COMMENT) => {
437 if conditional_depth == 0 && newline_count >= 1 {
439 break;
440 }
441 newline_count = 0;
442 self.parse_comment();
443 }
444 Some(IDENTIFIER) => {
445 let token = &self.tokens.last().unwrap().1.clone();
446 if (token == "ifdef"
448 || token == "ifndef"
449 || token == "ifeq"
450 || token == "ifneq")
451 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
452 {
453 if conditional_depth == 0 && newline_count >= 1 {
456 break;
457 }
458 newline_count = 0;
459 conditional_depth += 1;
460 self.parse_conditional();
461 conditional_depth -= 1;
464 } else if token == "include" || token == "-include" || token == "sinclude" {
465 if conditional_depth == 0 && newline_count >= 1 {
467 break;
468 }
469 newline_count = 0;
470 self.parse_include();
471 } else if token == "else" || token == "endif" {
472 break;
475 } else {
476 if conditional_depth == 0 {
478 break;
479 }
480 break;
483 }
484 }
485 _ => break,
486 }
487 }
488 }
489
490 fn find_and_consume_colon(&mut self) -> bool {
491 self.skip_ws();
493
494 if self.current() == Some(OPERATOR)
496 && matches!(self.tokens.last().unwrap().1.as_str(), ":" | "::")
497 {
498 self.bump();
499 return true;
500 }
501
502 let has_colon = self
504 .tokens
505 .iter()
506 .rev()
507 .take_while(|(kind, _)| *kind != NEWLINE)
508 .any(|(kind, text)| *kind == OPERATOR && (text == ":" || text == "::"));
509
510 if has_colon {
511 while self.current().is_some() && self.current() != Some(NEWLINE) {
513 if self.current() == Some(OPERATOR)
514 && matches!(
515 self.tokens.last().map(|(_, text)| text.as_str()),
516 Some(":" | "::")
517 )
518 {
519 self.bump();
520 return true;
521 }
522 self.bump();
523 }
524 }
525
526 self.error("expected ':'".to_string());
527 false
528 }
529
530 fn parse_rule(&mut self) {
531 self.builder.start_node(RULE.into());
532
533 self.skip_ws();
535 self.builder.start_node(TARGETS.into());
536 let has_target = self.parse_rule_targets();
537 self.builder.finish_node();
538
539 let has_colon = if has_target {
541 self.find_and_consume_colon()
542 } else {
543 false
544 };
545
546 if has_target && has_colon {
548 self.skip_ws();
549 self.parse_rule_dependencies();
550 self.expect_eol();
551
552 self.parse_rule_recipes();
554 }
555
556 self.builder.finish_node();
557 }
558
559 fn parse_rule_targets(&mut self) -> bool {
560 let has_first_target = self.parse_rule_target();
562
563 if !has_first_target {
564 return false;
565 }
566
567 loop {
569 self.skip_ws();
570
571 if self.current() == Some(OPERATOR) && self.tokens.last().unwrap().1 == ":" {
573 break;
574 }
575
576 match self.current() {
578 Some(IDENTIFIER) | Some(DOLLAR) => {
579 if !self.parse_rule_target() {
580 break;
581 }
582 }
583 _ => break,
584 }
585 }
586
587 true
588 }
589
590 fn parse_comment(&mut self) {
591 if self.current() == Some(COMMENT) {
592 self.bump(); if self.current() == Some(NEWLINE) {
596 self.bump(); } else if self.current() == Some(WHITESPACE) {
598 self.skip_ws();
600 if self.current() == Some(NEWLINE) {
601 self.bump();
602 }
603 }
604 } else {
606 self.error("expected comment".to_string());
607 }
608 }
609
610 fn parse_assignment(&mut self) {
611 self.builder.start_node(VARIABLE.into());
612
613 self.skip_ws();
615 if self.current() == Some(IDENTIFIER) && self.tokens.last().unwrap().1 == "export" {
616 self.bump();
617 self.skip_ws();
618 }
619
620 match self.current() {
622 Some(IDENTIFIER) => self.bump(),
623 Some(DOLLAR) => self.parse_variable_reference(),
624 _ => {
625 self.error("expected variable name".to_string());
626 self.builder.finish_node();
627 return;
628 }
629 }
630
631 self.skip_ws();
633 match self.current() {
634 Some(OPERATOR) => {
635 let op = &self.tokens.last().unwrap().1;
636 if ["=", ":=", "::=", ":::=", "+=", "?=", "!="].contains(&op.as_str()) {
637 self.bump();
638 self.skip_ws();
639
640 self.builder.start_node(EXPR.into());
642 while self.current().is_some() && self.current() != Some(NEWLINE) {
643 if self.current() == Some(DOLLAR) {
644 self.parse_variable_reference();
645 } else {
646 self.bump();
647 }
648 }
649 self.builder.finish_node();
650
651 if self.current() == Some(NEWLINE) {
653 self.bump();
654 } else {
655 self.error("expected newline after variable value".to_string());
656 }
657 } else {
658 self.error(format!("invalid assignment operator: {}", op));
659 }
660 }
661 Some(NEWLINE) => {
663 self.bump();
664 }
665 None => {
666 }
668 _ => self.error("expected assignment operator".to_string()),
669 }
670
671 self.builder.finish_node();
672 }
673
674 fn parse_variable_reference(&mut self) {
675 self.builder.start_node(EXPR.into());
676 self.bump(); if self.current() == Some(LPAREN) || self.current() == Some(LBRACE) {
679 let is_brace = self.current() == Some(LBRACE);
680 self.bump(); if is_brace {
683 while self.current().is_some() && self.current() != Some(RBRACE) {
685 if self.current() == Some(DOLLAR) {
686 self.parse_variable_reference();
687 } else {
688 self.bump();
689 }
690 }
691 if self.current() == Some(RBRACE) {
692 self.bump(); }
694 } else {
695 let mut is_function = false;
697
698 if self.current() == Some(IDENTIFIER) {
699 let function_name = &self.tokens.last().unwrap().1;
700 let known_functions = [
702 "shell", "wildcard", "call", "eval", "file", "abspath", "dir",
703 ];
704 if known_functions.contains(&function_name.as_str()) {
705 is_function = true;
706 }
707 }
708
709 if is_function {
710 self.bump();
712
713 self.consume_balanced_parens(1);
715 } else {
716 self.parse_parenthesized_expr_internal(true);
718 }
719 }
720 } else if self.current().is_some() && self.current() != Some(NEWLINE) {
721 self.bump();
723 } else {
724 self.error("expected variable name after $".to_string());
725 }
726
727 self.builder.finish_node();
728 }
729
730 fn parse_parenthesized_expr(&mut self) {
733 self.builder.start_node(EXPR.into());
734
735 if self.current() == Some(LPAREN) {
737 self.bump(); self.parse_parenthesized_expr_internal(false);
740 } else if self.current() == Some(QUOTE) {
741 self.parse_quoted_comparison();
743 } else {
744 self.error("expected opening parenthesis or quote".to_string());
745 }
746
747 self.builder.finish_node();
748 }
749
750 fn parse_parenthesized_expr_internal(&mut self, is_variable_ref: bool) {
752 let mut paren_count = 1;
753
754 while paren_count > 0 && self.current().is_some() {
755 match self.current() {
756 Some(LPAREN) => {
757 paren_count += 1;
758 self.bump();
759 self.builder.start_node(EXPR.into());
761 }
762 Some(RPAREN) => {
763 paren_count -= 1;
764 self.bump();
765 if paren_count > 0 {
766 self.builder.finish_node();
767 }
768 }
769 Some(QUOTE) => {
770 self.parse_quoted_string();
772 }
773 Some(DOLLAR) => {
774 self.parse_variable_reference();
776 }
777 Some(_) => self.bump(),
778 None => {
779 self.error(if is_variable_ref {
780 "unclosed variable reference".to_string()
781 } else {
782 "unclosed parenthesis".to_string()
783 });
784 break;
785 }
786 }
787 }
788
789 if !is_variable_ref {
790 self.skip_ws();
791 self.expect_eol();
792 }
793 }
794
795 fn parse_quoted_comparison(&mut self) {
798 if self.current() == Some(QUOTE) {
800 self.bump(); } else {
802 self.error("expected first quoted argument".to_string());
803 }
804
805 self.skip_ws();
807
808 if self.current() == Some(QUOTE) {
810 self.bump(); } else {
812 self.error("expected second quoted argument".to_string());
813 }
814
815 self.skip_ws();
817 self.expect_eol();
818 }
819
820 fn parse_quoted_string(&mut self) {
824 if self.current() == Some(QUOTE) {
825 self.bump();
826 }
827 }
828
829 fn parse_conditional_keyword(&mut self) -> Option<String> {
830 if self.current() != Some(IDENTIFIER) {
831 self.error(
832 "expected conditional keyword (ifdef, ifndef, ifeq, or ifneq)".to_string(),
833 );
834 return None;
835 }
836
837 let token = self.tokens.last().unwrap().1.clone();
838 if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&token.as_str()) {
839 self.error(format!("unknown conditional directive: {}", token));
840 return None;
841 }
842
843 self.bump();
844 Some(token)
845 }
846
847 fn parse_simple_condition(&mut self) {
848 self.builder.start_node(EXPR.into());
849
850 self.skip_ws();
852
853 let mut found_var = false;
855
856 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
857 match self.current() {
858 Some(WHITESPACE) => self.skip_ws(),
859 Some(DOLLAR) => {
860 found_var = true;
861 self.parse_variable_reference();
862 }
863 Some(_) => {
864 found_var = true;
866 self.bump();
867 }
868 None => break,
869 }
870 }
871
872 if !found_var {
873 self.error("expected condition after conditional directive".to_string());
875 }
876
877 self.builder.finish_node();
878
879 if self.current() == Some(NEWLINE) {
881 self.bump();
882 } else if !self.is_at_eof() {
883 self.skip_until_newline();
884 }
885 }
886
887 fn is_conditional_directive(&self, token: &str) -> bool {
889 token == "ifdef"
890 || token == "ifndef"
891 || token == "ifeq"
892 || token == "ifneq"
893 || token == "else"
894 || token == "endif"
895 }
896
897 fn handle_conditional_token(&mut self, token: &str, depth: &mut usize) -> bool {
899 match token {
900 "ifdef" | "ifndef" | "ifeq" | "ifneq"
901 if matches!(self.variant, None | Some(MakefileVariant::GNUMake)) =>
902 {
903 self.parse_conditional();
906 true
907 }
908 "else" => {
909 if *depth == 0 {
911 self.error("else without matching if".to_string());
912 self.bump();
914 false
915 } else {
916 self.builder.start_node(CONDITIONAL_ELSE.into());
918
919 self.bump();
921 self.skip_ws();
922
923 if self.current() == Some(IDENTIFIER) {
925 let next_token = &self.tokens.last().unwrap().1;
926 if next_token == "ifdef"
927 || next_token == "ifndef"
928 || next_token == "ifeq"
929 || next_token == "ifneq"
930 {
931 match next_token.as_str() {
934 "ifdef" | "ifndef" => {
935 self.bump(); self.skip_ws();
937 self.parse_simple_condition();
938 }
939 "ifeq" | "ifneq" => {
940 self.bump(); self.skip_ws();
942 self.parse_parenthesized_expr();
943 }
944 _ => unreachable!(),
945 }
946 } else {
948 }
951 } else {
952 }
954
955 self.builder.finish_node(); true
957 }
958 }
959 "endif" => {
960 if *depth == 0 {
962 self.error("endif without matching if".to_string());
963 self.bump();
965 false
966 } else {
967 *depth -= 1;
968
969 self.builder.start_node(CONDITIONAL_ENDIF.into());
971
972 self.bump();
974
975 self.skip_ws();
977
978 if self.current() == Some(COMMENT) {
983 self.parse_comment();
984 } else if self.current() == Some(NEWLINE) {
985 self.bump();
986 } else if self.current() == Some(WHITESPACE) {
987 self.skip_ws();
989 if self.current() == Some(NEWLINE) {
990 self.bump();
991 }
992 } else if !self.is_at_eof() {
994 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
997 self.bump();
998 }
999 if self.current() == Some(NEWLINE) {
1000 self.bump();
1001 }
1002 }
1003 self.builder.finish_node(); true
1007 }
1008 }
1009 _ => false,
1010 }
1011 }
1012
1013 fn parse_conditional(&mut self) {
1014 self.builder.start_node(CONDITIONAL.into());
1015
1016 self.builder.start_node(CONDITIONAL_IF.into());
1018
1019 let Some(token) = self.parse_conditional_keyword() else {
1021 self.skip_until_newline();
1022 self.builder.finish_node(); self.builder.finish_node(); return;
1025 };
1026
1027 self.skip_ws();
1029
1030 match token.as_str() {
1032 "ifdef" | "ifndef" => {
1033 self.parse_simple_condition();
1034 }
1035 "ifeq" | "ifneq" => {
1036 self.parse_parenthesized_expr();
1037 }
1038 _ => unreachable!("Invalid conditional token"),
1039 }
1040
1041 self.skip_ws();
1043 if self.current() == Some(COMMENT) {
1044 self.parse_comment();
1045 }
1046 self.builder.finish_node(); let mut depth = 1;
1052
1053 let mut position_count = std::collections::HashMap::<usize, usize>::new();
1055 let max_repetitions = 15; while depth > 0 && !self.is_at_eof() {
1058 let current_pos = self.tokens.len();
1060 *position_count.entry(current_pos).or_insert(0) += 1;
1061
1062 if position_count.get(¤t_pos).unwrap() > &max_repetitions {
1065 break;
1068 }
1069
1070 match self.current() {
1071 None => {
1072 self.error("unterminated conditional (missing endif)".to_string());
1073 break;
1074 }
1075 Some(IDENTIFIER) => {
1076 let token = self.tokens.last().unwrap().1.clone();
1077 if !self.handle_conditional_token(&token, &mut depth) {
1078 if token == "include" || token == "-include" || token == "sinclude" {
1079 self.parse_include();
1080 } else {
1081 self.parse_normal_content();
1082 }
1083 }
1084 }
1085 Some(INDENT) => self.parse_recipe_line(),
1086 Some(WHITESPACE) => self.bump(),
1087 Some(COMMENT) => self.parse_comment(),
1088 Some(NEWLINE) => self.bump(),
1089 Some(DOLLAR) => self.parse_normal_content(),
1090 Some(QUOTE) => self.parse_quoted_string(),
1091 Some(_) => {
1092 self.bump();
1094 }
1095 }
1096 }
1097
1098 self.builder.finish_node();
1099 }
1100
1101 fn parse_normal_content(&mut self) {
1103 self.skip_ws();
1105
1106 if self.is_assignment_line() {
1108 self.parse_assignment();
1109 } else {
1110 self.parse_rule();
1112 }
1113 }
1114
1115 fn parse_include(&mut self) {
1116 self.builder.start_node(INCLUDE.into());
1117
1118 if self.current() != Some(IDENTIFIER)
1120 || (!["include", "-include", "sinclude"]
1121 .contains(&self.tokens.last().unwrap().1.as_str()))
1122 {
1123 self.error("expected include directive".to_string());
1124 self.builder.finish_node();
1125 return;
1126 }
1127 self.bump();
1128 self.skip_ws();
1129
1130 self.builder.start_node(EXPR.into());
1132 let mut found_path = false;
1133
1134 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1135 match self.current() {
1136 Some(WHITESPACE) => self.skip_ws(),
1137 Some(DOLLAR) => {
1138 found_path = true;
1139 self.parse_variable_reference();
1140 }
1141 Some(_) => {
1142 found_path = true;
1144 self.bump();
1145 }
1146 None => break,
1147 }
1148 }
1149
1150 if !found_path {
1151 self.error("expected file path after include".to_string());
1152 }
1153
1154 self.builder.finish_node();
1155
1156 if self.current() == Some(NEWLINE) {
1158 self.bump();
1159 } else if !self.is_at_eof() {
1160 self.error("expected newline after include".to_string());
1161 self.skip_until_newline();
1162 }
1163
1164 self.builder.finish_node();
1165 }
1166
1167 fn parse_identifier_token(&mut self) -> bool {
1168 let token = &self.tokens.last().unwrap().1;
1169
1170 if token.starts_with("%") {
1172 self.parse_rule();
1173 return true;
1174 }
1175
1176 if token.starts_with("if")
1177 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1178 {
1179 self.parse_conditional();
1180 return true;
1181 }
1182
1183 if token == "include" || token == "-include" || token == "sinclude" {
1184 self.parse_include();
1185 return true;
1186 }
1187
1188 self.parse_normal_content();
1190 true
1191 }
1192
1193 fn parse_token(&mut self) -> bool {
1194 match self.current() {
1195 None => false,
1196 Some(IDENTIFIER) => {
1197 let token = &self.tokens.last().unwrap().1;
1198 if self.is_conditional_directive(token)
1199 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1200 {
1201 self.parse_conditional();
1202 true
1203 } else {
1204 self.parse_identifier_token()
1205 }
1206 }
1207 Some(DOLLAR) => {
1208 self.parse_normal_content();
1209 true
1210 }
1211 Some(NEWLINE) => {
1212 self.builder.start_node(BLANK_LINE.into());
1213 self.bump();
1214 self.builder.finish_node();
1215 true
1216 }
1217 Some(COMMENT) => {
1218 self.parse_comment();
1219 true
1220 }
1221 Some(WHITESPACE) => {
1222 if self.is_end_of_file_or_newline_after_whitespace() {
1224 self.skip_ws();
1227 return true;
1228 }
1229
1230 let look_ahead_pos = self.tokens.len().saturating_sub(1);
1233 let mut is_documentation_or_help = false;
1234
1235 if look_ahead_pos > 0 {
1236 let next_token = &self.tokens[look_ahead_pos - 1];
1237 if next_token.0 == IDENTIFIER
1240 || next_token.0 == COMMENT
1241 || next_token.0 == TEXT
1242 {
1243 is_documentation_or_help = true;
1244 }
1245 }
1246
1247 if is_documentation_or_help {
1248 self.skip_ws();
1251 while self.current().is_some() && self.current() != Some(NEWLINE) {
1252 self.bump();
1253 }
1254 if self.current() == Some(NEWLINE) {
1255 self.bump();
1256 }
1257 } else {
1258 self.skip_ws();
1259 }
1260 true
1261 }
1262 Some(INDENT) => {
1263 self.bump();
1265
1266 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1268 self.bump();
1269 }
1270 if self.current() == Some(NEWLINE) {
1271 self.bump();
1272 }
1273 true
1274 }
1275 Some(kind) => {
1276 self.error(format!("unexpected token {:?}", kind));
1277 self.bump();
1278 true
1279 }
1280 }
1281 }
1282
1283 fn parse(mut self) -> Parse {
1284 self.builder.start_node(ROOT.into());
1285
1286 while self.parse_token() {}
1287
1288 self.builder.finish_node();
1289
1290 Parse {
1291 green_node: self.builder.finish(),
1292 errors: self.errors,
1293 positioned_errors: self.positioned_errors,
1294 }
1295 }
1296
1297 fn is_assignment_line(&mut self) -> bool {
1299 let assignment_ops = ["=", ":=", "::=", ":::=", "+=", "?=", "!="];
1300 let mut pos = self.tokens.len().saturating_sub(1);
1301 let mut seen_identifier = false;
1302 let mut seen_export = false;
1303
1304 while pos > 0 {
1305 let (kind, text) = &self.tokens[pos];
1306
1307 match kind {
1308 NEWLINE => break,
1309 IDENTIFIER if text == "export" => seen_export = true,
1310 IDENTIFIER if !seen_identifier => seen_identifier = true,
1311 OPERATOR if assignment_ops.contains(&text.as_str()) => {
1312 return seen_identifier || seen_export
1313 }
1314 OPERATOR if text == ":" || text == "::" => return false, WHITESPACE => (),
1316 _ if seen_export => return true, _ => return false,
1318 }
1319 pos = pos.saturating_sub(1);
1320 }
1321 seen_export
1323 }
1324
1325 fn bump(&mut self) {
1327 let (kind, text) = self.tokens.pop().unwrap();
1328 self.builder.token(kind.into(), text.as_str());
1329 if self.current_token_index > 0 {
1330 self.current_token_index -= 1;
1331 }
1332 }
1333 fn current(&self) -> Option<SyntaxKind> {
1335 self.tokens.last().map(|(kind, _)| *kind)
1336 }
1337
1338 fn expect_eol(&mut self) {
1339 self.skip_ws();
1341
1342 match self.current() {
1343 Some(NEWLINE) => {
1344 self.bump();
1345 }
1346 None => {
1347 }
1349 n => {
1350 self.error(format!("expected newline, got {:?}", n));
1351 self.skip_until_newline();
1353 }
1354 }
1355 }
1356
1357 fn is_at_eof(&self) -> bool {
1359 self.current().is_none()
1360 }
1361
1362 fn is_at_eof_or_only_whitespace(&self) -> bool {
1364 if self.is_at_eof() {
1365 return true;
1366 }
1367
1368 self.tokens
1370 .iter()
1371 .rev()
1372 .all(|(kind, _)| matches!(*kind, WHITESPACE | NEWLINE))
1373 }
1374
1375 fn skip_ws(&mut self) {
1376 while self.current() == Some(WHITESPACE) {
1377 self.bump()
1378 }
1379 }
1380
1381 fn skip_until_newline(&mut self) {
1382 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1383 self.bump();
1384 }
1385 if self.current() == Some(NEWLINE) {
1386 self.bump();
1387 }
1388 }
1389
1390 fn consume_balanced_parens(&mut self, start_paren_count: usize) -> usize {
1392 let mut paren_count = start_paren_count;
1393
1394 while paren_count > 0 && self.current().is_some() {
1395 match self.current() {
1396 Some(LPAREN) => {
1397 paren_count += 1;
1398 self.bump();
1399 }
1400 Some(RPAREN) => {
1401 paren_count -= 1;
1402 self.bump();
1403 if paren_count == 0 {
1404 break;
1405 }
1406 }
1407 Some(DOLLAR) => {
1408 self.parse_variable_reference();
1410 }
1411 Some(_) => self.bump(),
1412 None => {
1413 self.error("unclosed parenthesis".to_string());
1414 break;
1415 }
1416 }
1417 }
1418
1419 paren_count
1420 }
1421
1422 fn is_end_of_file_or_newline_after_whitespace(&self) -> bool {
1424 if self.is_at_eof_or_only_whitespace() {
1426 return true;
1427 }
1428
1429 if self.tokens.len() <= 1 {
1431 return true;
1432 }
1433
1434 false
1435 }
1436 }
1437
1438 let mut tokens = lex(text);
1439
1440 let mut token_positions = Vec::with_capacity(tokens.len());
1442 let mut position = rowan::TextSize::from(0);
1443 for (_kind, text) in &tokens {
1444 let start = position;
1445 let end = start + rowan::TextSize::of(text.as_str());
1446 token_positions.push((start, end));
1447 position = end;
1448 }
1449
1450 let current_token_index = tokens.len().saturating_sub(1);
1451 tokens.reverse();
1452 Parser {
1453 tokens,
1454 builder: GreenNodeBuilder::new(),
1455 errors: Vec::new(),
1456 positioned_errors: Vec::new(),
1457 token_positions,
1458 current_token_index,
1459 original_text: text.to_string(),
1460 variant,
1461 }
1462 .parse()
1463}
1464
1465pub(crate) type SyntaxNode = rowan::SyntaxNode<Lang>;
1471#[allow(unused)]
1472type SyntaxToken = rowan::SyntaxToken<Lang>;
1473#[allow(unused)]
1474pub(crate) type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
1475
1476impl Parse {
1477 fn syntax(&self) -> SyntaxNode {
1478 SyntaxNode::new_root_mut(self.green_node.clone())
1479 }
1480
1481 pub(crate) fn root(&self) -> Makefile {
1482 Makefile::cast(self.syntax()).unwrap()
1483 }
1484}
1485
1486fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
1489 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
1490 let mut line = 0;
1491 let mut last_newline_offset = rowan::TextSize::from(0);
1492
1493 for element in root.preorder_with_tokens() {
1494 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
1495 if token.text_range().start() >= offset {
1496 break;
1497 }
1498
1499 for (idx, _) in token.text().match_indices('\n') {
1501 line += 1;
1502 last_newline_offset =
1503 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
1504 }
1505 }
1506 }
1507
1508 let column: usize = (offset - last_newline_offset).into();
1509 (line, column)
1510}
1511
1512macro_rules! ast_node {
1513 ($ast:ident, $kind:ident) => {
1514 #[derive(Clone, PartialEq, Eq, Hash)]
1515 #[repr(transparent)]
1516 pub struct $ast(SyntaxNode);
1518
1519 impl AstNode for $ast {
1520 type Language = Lang;
1521
1522 fn can_cast(kind: SyntaxKind) -> bool {
1523 kind == $kind
1524 }
1525
1526 fn cast(syntax: SyntaxNode) -> Option<Self> {
1527 if Self::can_cast(syntax.kind()) {
1528 Some(Self(syntax))
1529 } else {
1530 None
1531 }
1532 }
1533
1534 fn syntax(&self) -> &SyntaxNode {
1535 &self.0
1536 }
1537 }
1538
1539 impl $ast {
1540 pub fn line(&self) -> usize {
1542 line_col_at_offset(&self.0, self.0.text_range().start()).0
1543 }
1544
1545 pub fn column(&self) -> usize {
1547 line_col_at_offset(&self.0, self.0.text_range().start()).1
1548 }
1549
1550 pub fn line_col(&self) -> (usize, usize) {
1553 line_col_at_offset(&self.0, self.0.text_range().start())
1554 }
1555 }
1556
1557 impl core::fmt::Display for $ast {
1558 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1559 write!(f, "{}", self.0.text())
1560 }
1561 }
1562 };
1563}
1564
1565ast_node!(Makefile, ROOT);
1566ast_node!(Rule, RULE);
1567ast_node!(Recipe, RECIPE);
1568ast_node!(Identifier, IDENTIFIER);
1569ast_node!(VariableDefinition, VARIABLE);
1570ast_node!(Include, INCLUDE);
1571ast_node!(ArchiveMembers, ARCHIVE_MEMBERS);
1572ast_node!(ArchiveMember, ARCHIVE_MEMBER);
1573ast_node!(Conditional, CONDITIONAL);
1574
1575#[derive(Clone, PartialEq, Eq, Hash)]
1579pub struct VariableReference(SyntaxNode);
1580
1581impl VariableReference {
1582 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
1587 if syntax.kind() != EXPR {
1588 return None;
1589 }
1590 let mut tokens = syntax
1591 .children_with_tokens()
1592 .filter_map(|it| it.into_token());
1593 let first = tokens.next()?;
1594 if first.kind() != DOLLAR {
1595 return None;
1596 }
1597 tokens.next()?;
1599 Some(Self(syntax))
1600 }
1601
1602 pub fn syntax(&self) -> &SyntaxNode {
1604 &self.0
1605 }
1606
1607 pub fn name(&self) -> Option<String> {
1624 self.0
1626 .children_with_tokens()
1627 .filter_map(|it| it.into_token())
1628 .find(|t| t.kind() == IDENTIFIER)
1629 .map(|t| t.text().to_string())
1630 }
1631
1632 pub fn is_function_call(&self) -> bool {
1645 let mut tokens = self
1646 .0
1647 .children_with_tokens()
1648 .filter_map(|it| it.into_token());
1649
1650 let Some(dollar) = tokens.next() else {
1652 return false;
1653 };
1654 if dollar.kind() != DOLLAR {
1655 return false;
1656 }
1657 let Some(open) = tokens.next() else {
1658 return false;
1659 };
1660 if open.kind() != LPAREN && open.kind() != LBRACE {
1661 return false;
1662 }
1663
1664 let Some(ident) = tokens.next() else {
1666 return false;
1667 };
1668 if ident.kind() != IDENTIFIER {
1669 return false;
1670 }
1671
1672 match tokens.next() {
1674 Some(t) => t.kind() == WHITESPACE || t.kind() == COMMA,
1675 None => false,
1676 }
1677 }
1678
1679 pub fn argument_count(&self) -> usize {
1692 if !self.is_function_call() {
1693 return 0;
1694 }
1695
1696 let mut commas = 0;
1697 let mut depth = 0;
1698 let mut past_name = false;
1699
1700 for element in self.0.children_with_tokens() {
1701 let Some(token) = element.as_token() else {
1702 continue;
1704 };
1705 match token.kind() {
1706 IDENTIFIER if !past_name => {
1707 past_name = true;
1708 }
1709 DOLLAR | LPAREN | LBRACE if !past_name => {}
1710 LPAREN => depth += 1,
1711 RPAREN if depth > 0 => depth -= 1,
1712 COMMA if depth == 0 && past_name => commas += 1,
1713 _ => {}
1714 }
1715 }
1716
1717 if past_name {
1718 commas + 1
1719 } else {
1720 0
1721 }
1722 }
1723
1724 pub fn argument_index_at_offset(&self, offset: usize) -> Option<usize> {
1740 if !self.is_function_call() {
1741 return None;
1742 }
1743
1744 let ref_start: usize = self.0.text_range().start().into();
1745 let ref_end: usize = self.0.text_range().end().into();
1746 if offset < ref_start || offset > ref_end {
1747 return None;
1748 }
1749
1750 let mut arg_index = 0;
1751 let mut depth = 0;
1752 let mut past_name = false;
1753
1754 for element in self.0.children_with_tokens() {
1755 let Some(token) = element.as_token() else {
1756 continue;
1757 };
1758 let token_end: usize = token.text_range().end().into();
1759
1760 match token.kind() {
1761 IDENTIFIER if !past_name => {
1762 past_name = true;
1763 }
1764 DOLLAR | LPAREN | LBRACE if !past_name => {}
1765 LPAREN => depth += 1,
1766 RPAREN if depth > 0 => depth -= 1,
1767 COMMA if depth == 0 && past_name => {
1768 if offset < token_end {
1769 return Some(arg_index);
1770 }
1771 arg_index += 1;
1772 }
1773 _ => {}
1774 }
1775 }
1776
1777 if past_name {
1778 Some(arg_index)
1779 } else {
1780 None
1781 }
1782 }
1783
1784 pub fn line(&self) -> usize {
1786 line_col_at_offset(&self.0, self.0.text_range().start()).0
1787 }
1788
1789 pub fn column(&self) -> usize {
1791 line_col_at_offset(&self.0, self.0.text_range().start()).1
1792 }
1793
1794 pub fn line_col(&self) -> (usize, usize) {
1796 line_col_at_offset(&self.0, self.0.text_range().start())
1797 }
1798
1799 pub fn text_range(&self) -> rowan::TextRange {
1801 self.0.text_range()
1802 }
1803}
1804
1805impl core::fmt::Display for VariableReference {
1806 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1807 write!(f, "{}", self.0.text())
1808 }
1809}
1810
1811impl Recipe {
1812 pub fn text(&self) -> String {
1824 let tokens: Vec<_> = self
1825 .syntax()
1826 .children_with_tokens()
1827 .filter_map(|it| it.as_token().cloned())
1828 .collect();
1829
1830 if tokens.is_empty() {
1831 return String::new();
1832 }
1833
1834 let start = if tokens.first().map(|t| t.kind()) == Some(INDENT) {
1836 1
1837 } else {
1838 0
1839 };
1840
1841 let end = if tokens.last().map(|t| t.kind()) == Some(NEWLINE) {
1843 tokens.len() - 1
1844 } else {
1845 tokens.len()
1846 };
1847
1848 let mut after_newline = false;
1852 tokens[start..end]
1853 .iter()
1854 .filter_map(|t| match t.kind() {
1855 TEXT => {
1856 after_newline = false;
1857 Some(t.text().to_string())
1858 }
1859 NEWLINE => {
1860 after_newline = true;
1861 Some(t.text().to_string())
1862 }
1863 INDENT if after_newline => {
1864 after_newline = false;
1865 let text = t.text();
1867 Some(text.strip_prefix('\t').unwrap_or(text).to_string())
1868 }
1869 _ => None,
1870 })
1871 .collect()
1872 }
1873
1874 pub fn indent(&self) -> Option<String> {
1889 self.syntax().children_with_tokens().find_map(|it| {
1890 if let Some(token) = it.as_token() {
1891 if token.kind() == INDENT {
1892 return Some(token.text().to_string());
1893 }
1894 }
1895 None
1896 })
1897 }
1898
1899 pub fn comment(&self) -> Option<String> {
1915 self.syntax()
1916 .children_with_tokens()
1917 .filter_map(|it| {
1918 if let Some(token) = it.as_token() {
1919 if token.kind() == COMMENT {
1920 return Some(token.text().to_string());
1921 }
1922 }
1923 None
1924 })
1925 .next()
1926 }
1927
1928 pub fn full(&self) -> String {
1944 self.syntax()
1945 .children_with_tokens()
1946 .filter_map(|it| {
1947 if let Some(token) = it.as_token() {
1948 if token.kind() == TEXT || token.kind() == COMMENT {
1950 return Some(token.text().to_string());
1951 }
1952 }
1953 None
1954 })
1955 .collect::<Vec<_>>()
1956 .join("")
1957 }
1958
1959 pub fn parent(&self) -> Option<Rule> {
1972 self.syntax().parent().and_then(Rule::cast)
1973 }
1974
1975 pub fn text_range(&self) -> rowan::TextRange {
1977 self.syntax().text_range()
1978 }
1979
1980 pub fn is_silent(&self) -> bool {
1993 let text = self.text();
1994 text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1995 }
1996
1997 pub fn is_ignore_errors(&self) -> bool {
2010 let text = self.text();
2011 text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
2012 }
2013
2014 pub fn set_prefix(&mut self, prefix: &str) {
2031 let text = self.text();
2032
2033 let stripped = text.trim_start_matches(['@', '-', '+']);
2035
2036 let new_text = format!("{}{}", prefix, stripped);
2038
2039 self.replace_text(&new_text);
2040 }
2041
2042 pub fn replace_text(&mut self, new_text: &str) {
2055 let node = self.syntax();
2056 let parent = node.parent().expect("Recipe node must have a parent");
2057 let node_index = node.index();
2058
2059 let mut builder = GreenNodeBuilder::new();
2061 builder.start_node(RECIPE.into());
2062
2063 if let Some(indent_token) = node
2065 .children_with_tokens()
2066 .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
2067 {
2068 builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
2069 } else {
2070 builder.token(INDENT.into(), "\t");
2071 }
2072
2073 builder.token(TEXT.into(), new_text);
2074
2075 if let Some(newline_token) = node
2077 .children_with_tokens()
2078 .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
2079 {
2080 builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
2081 } else {
2082 builder.token(NEWLINE.into(), "\n");
2083 }
2084
2085 builder.finish_node();
2086 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2087
2088 parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
2090
2091 *self = parent
2095 .children_with_tokens()
2096 .nth(node_index)
2097 .and_then(|element| element.into_node())
2098 .and_then(Recipe::cast)
2099 .expect("New recipe node should exist at the same index");
2100 }
2101
2102 pub fn insert_before(&self, text: &str) {
2115 let node = self.syntax();
2116 let parent = node.parent().expect("Recipe node must have a parent");
2117 let node_index = node.index();
2118
2119 let mut builder = GreenNodeBuilder::new();
2121 builder.start_node(RECIPE.into());
2122 builder.token(INDENT.into(), "\t");
2123 builder.token(TEXT.into(), text);
2124 builder.token(NEWLINE.into(), "\n");
2125 builder.finish_node();
2126 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2127
2128 parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
2130 }
2131
2132 pub fn insert_after(&self, text: &str) {
2145 let node = self.syntax();
2146 let parent = node.parent().expect("Recipe node must have a parent");
2147 let node_index = node.index();
2148
2149 let mut builder = GreenNodeBuilder::new();
2151 builder.start_node(RECIPE.into());
2152 builder.token(INDENT.into(), "\t");
2153 builder.token(TEXT.into(), text);
2154 builder.token(NEWLINE.into(), "\n");
2155 builder.finish_node();
2156 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2157
2158 parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
2160 }
2161
2162 pub fn remove(&self) {
2175 let node = self.syntax();
2176 let parent = node.parent().expect("Recipe node must have a parent");
2177 let node_index = node.index();
2178
2179 parent.splice_children(node_index..node_index + 1, vec![]);
2181 }
2182}
2183
2184pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
2188 let mut newlines_to_remove = vec![];
2190 let mut current = node.last_child_or_token();
2191
2192 while let Some(element) = current {
2193 match &element {
2194 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2195 newlines_to_remove.push(token.clone());
2196 current = token.prev_sibling_or_token();
2197 }
2198 rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
2199 let mut recipe_current = n.last_child_or_token();
2201 while let Some(recipe_element) = recipe_current {
2202 match &recipe_element {
2203 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2204 newlines_to_remove.push(token.clone());
2205 recipe_current = token.prev_sibling_or_token();
2206 }
2207 _ => break,
2208 }
2209 }
2210 break; }
2212 _ => break,
2213 }
2214 }
2215
2216 if newlines_to_remove.len() > 1 {
2219 newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
2221
2222 for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
2223 let parent = token.parent().unwrap();
2224 let idx = token.index();
2225 parent.splice_children(idx..idx + 1, vec![]);
2226 }
2227 }
2228}
2229
2230pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
2238 let mut collected_elements = vec![];
2239 let mut found_comment = false;
2240
2241 let mut current = node.prev_sibling_or_token();
2243 while let Some(element) = current {
2244 match &element {
2245 rowan::NodeOrToken::Token(token) => match token.kind() {
2246 COMMENT => {
2247 if token.text().starts_with("#!") {
2248 break; }
2250 found_comment = true;
2251 collected_elements.push(element.clone());
2252 }
2253 NEWLINE | WHITESPACE => {
2254 collected_elements.push(element.clone());
2255 }
2256 _ => break, },
2258 rowan::NodeOrToken::Node(n) => {
2259 if n.kind() == BLANK_LINE {
2261 collected_elements.push(element.clone());
2262 } else {
2263 break; }
2265 }
2266 }
2267 current = element.prev_sibling_or_token();
2268 }
2269
2270 let mut elements_to_remove = vec![];
2273 let mut consecutive_newlines = 0;
2274 for element in collected_elements.iter().rev() {
2275 let should_remove = match element {
2276 rowan::NodeOrToken::Token(token) => match token.kind() {
2277 COMMENT => {
2278 consecutive_newlines = 0;
2279 found_comment
2280 }
2281 NEWLINE => {
2282 consecutive_newlines += 1;
2283 found_comment && consecutive_newlines <= 1
2284 }
2285 WHITESPACE => found_comment,
2286 _ => false,
2287 },
2288 rowan::NodeOrToken::Node(n) => {
2289 if n.kind() == BLANK_LINE {
2291 consecutive_newlines += 1;
2292 found_comment && consecutive_newlines <= 1
2293 } else {
2294 false
2295 }
2296 }
2297 };
2298
2299 if should_remove {
2300 elements_to_remove.push(element.clone());
2301 }
2302 }
2303
2304 let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
2307 all_to_remove.extend(elements_to_remove.into_iter().rev());
2308
2309 all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
2311
2312 for element in all_to_remove {
2313 let idx = element.index();
2314 parent.splice_children(idx..idx + 1, vec![]);
2315 }
2316}
2317
2318impl FromStr for Rule {
2319 type Err = crate::Error;
2320
2321 fn from_str(s: &str) -> Result<Self, Self::Err> {
2322 Rule::parse(s).to_rule_result()
2323 }
2324}
2325
2326impl FromStr for Makefile {
2327 type Err = crate::Error;
2328
2329 fn from_str(s: &str) -> Result<Self, Self::Err> {
2330 Makefile::parse(s).to_result()
2331 }
2332}
2333
2334#[cfg(test)]
2335mod tests {
2336 use super::*;
2337 use crate::ast::makefile::MakefileItem;
2338 use crate::pattern::matches_pattern;
2339
2340 #[test]
2341 fn test_conditionals() {
2342 let code = "ifdef DEBUG\n DEBUG_FLAG := 1\nendif\n";
2346 let mut buf = code.as_bytes();
2347 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
2348 assert!(makefile.code().contains("DEBUG_FLAG"));
2349
2350 let code =
2352 "ifeq ($(OS),Windows_NT)\n RESULT := windows\nelse\n RESULT := unix\nendif\n";
2353 let mut buf = code.as_bytes();
2354 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
2355 assert!(makefile.code().contains("RESULT"));
2356 assert!(makefile.code().contains("windows"));
2357
2358 let code = "ifdef DEBUG\n CFLAGS += -g\n ifdef VERBOSE\n CFLAGS += -v\n endif\nelse\n CFLAGS += -O2\nendif\n";
2360 let mut buf = code.as_bytes();
2361 let makefile = Makefile::read_relaxed(&mut buf)
2362 .expect("Failed to parse nested conditionals with else");
2363 assert!(makefile.code().contains("CFLAGS"));
2364 assert!(makefile.code().contains("VERBOSE"));
2365
2366 let code = "ifdef DEBUG\nendif\n";
2368 let mut buf = code.as_bytes();
2369 let makefile =
2370 Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
2371 assert!(makefile.code().contains("ifdef DEBUG"));
2372
2373 let code = "ifeq ($(OS),Windows)\n EXT := .exe\nelse ifeq ($(OS),Linux)\n EXT := .bin\nelse\n EXT := .out\nendif\n";
2375 let mut buf = code.as_bytes();
2376 let makefile =
2377 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
2378 assert!(makefile.code().contains("EXT"));
2379
2380 let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
2382 let mut buf = code.as_bytes();
2383 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
2384 assert!(makefile.code().contains("DEBUG"));
2385
2386 let code = "ifdef \nDEBUG := 1\nendif\n";
2388 let mut buf = code.as_bytes();
2389 let makefile = Makefile::read_relaxed(&mut buf)
2390 .expect("Failed to parse with recovery - missing condition");
2391 assert!(makefile.code().contains("DEBUG"));
2392 }
2393
2394 #[test]
2395 fn test_parse_simple() {
2396 const SIMPLE: &str = r#"VARIABLE = value
2397
2398rule: dependency
2399 command
2400"#;
2401 let parsed = parse(SIMPLE, None);
2402 assert!(parsed.errors.is_empty());
2403 let node = parsed.syntax();
2404 assert_eq!(
2405 format!("{:#?}", node),
2406 r#"ROOT@0..44
2407 VARIABLE@0..17
2408 IDENTIFIER@0..8 "VARIABLE"
2409 WHITESPACE@8..9 " "
2410 OPERATOR@9..10 "="
2411 WHITESPACE@10..11 " "
2412 EXPR@11..16
2413 IDENTIFIER@11..16 "value"
2414 NEWLINE@16..17 "\n"
2415 BLANK_LINE@17..18
2416 NEWLINE@17..18 "\n"
2417 RULE@18..44
2418 TARGETS@18..22
2419 IDENTIFIER@18..22 "rule"
2420 OPERATOR@22..23 ":"
2421 WHITESPACE@23..24 " "
2422 PREREQUISITES@24..34
2423 PREREQUISITE@24..34
2424 IDENTIFIER@24..34 "dependency"
2425 NEWLINE@34..35 "\n"
2426 RECIPE@35..44
2427 INDENT@35..36 "\t"
2428 TEXT@36..43 "command"
2429 NEWLINE@43..44 "\n"
2430"#
2431 );
2432
2433 let root = parsed.root();
2434
2435 let mut rules = root.rules().collect::<Vec<_>>();
2436 assert_eq!(rules.len(), 1);
2437 let rule = rules.pop().unwrap();
2438 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2439 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
2440 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2441
2442 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2443 assert_eq!(variables.len(), 1);
2444 let variable = variables.pop().unwrap();
2445 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2446 assert_eq!(variable.raw_value(), Some("value".to_string()));
2447 }
2448
2449 #[test]
2450 fn test_parse_export_assign() {
2451 const EXPORT: &str = r#"export VARIABLE := value
2452"#;
2453 let parsed = parse(EXPORT, None);
2454 assert!(parsed.errors.is_empty());
2455 let node = parsed.syntax();
2456 assert_eq!(
2457 format!("{:#?}", node),
2458 r#"ROOT@0..25
2459 VARIABLE@0..25
2460 IDENTIFIER@0..6 "export"
2461 WHITESPACE@6..7 " "
2462 IDENTIFIER@7..15 "VARIABLE"
2463 WHITESPACE@15..16 " "
2464 OPERATOR@16..18 ":="
2465 WHITESPACE@18..19 " "
2466 EXPR@19..24
2467 IDENTIFIER@19..24 "value"
2468 NEWLINE@24..25 "\n"
2469"#
2470 );
2471
2472 let root = parsed.root();
2473
2474 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2475 assert_eq!(variables.len(), 1);
2476 let variable = variables.pop().unwrap();
2477 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2478 assert_eq!(variable.raw_value(), Some("value".to_string()));
2479 }
2480
2481 #[test]
2482 fn test_parse_multiple_prerequisites() {
2483 const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2484 command
2485
2486"#;
2487 let parsed = parse(MULTIPLE_PREREQUISITES, None);
2488 assert!(parsed.errors.is_empty());
2489 let node = parsed.syntax();
2490 assert_eq!(
2491 format!("{:#?}", node),
2492 r#"ROOT@0..40
2493 RULE@0..40
2494 TARGETS@0..4
2495 IDENTIFIER@0..4 "rule"
2496 OPERATOR@4..5 ":"
2497 WHITESPACE@5..6 " "
2498 PREREQUISITES@6..29
2499 PREREQUISITE@6..17
2500 IDENTIFIER@6..17 "dependency1"
2501 WHITESPACE@17..18 " "
2502 PREREQUISITE@18..29
2503 IDENTIFIER@18..29 "dependency2"
2504 NEWLINE@29..30 "\n"
2505 RECIPE@30..39
2506 INDENT@30..31 "\t"
2507 TEXT@31..38 "command"
2508 NEWLINE@38..39 "\n"
2509 NEWLINE@39..40 "\n"
2510"#
2511 );
2512 let root = parsed.root();
2513
2514 let rule = root.rules().next().unwrap();
2515 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2516 assert_eq!(
2517 rule.prerequisites().collect::<Vec<_>>(),
2518 vec!["dependency1", "dependency2"]
2519 );
2520 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2521 }
2522
2523 #[test]
2524 fn test_add_rule() {
2525 let mut makefile = Makefile::new();
2526 let rule = makefile.add_rule("rule");
2527 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2528 assert_eq!(
2529 rule.prerequisites().collect::<Vec<_>>(),
2530 Vec::<String>::new()
2531 );
2532
2533 assert_eq!(makefile.to_string(), "rule:\n");
2534 }
2535
2536 #[test]
2537 fn test_add_rule_with_shebang() {
2538 let content = r#"#!/usr/bin/make -f
2540
2541build: blah
2542 $(MAKE) install
2543
2544clean:
2545 dh_clean
2546"#;
2547
2548 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2549 let initial_count = makefile.rules().count();
2550 assert_eq!(initial_count, 2);
2551
2552 let rule = makefile.add_rule("build-indep");
2554 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2555
2556 assert_eq!(makefile.rules().count(), initial_count + 1);
2558 }
2559
2560 #[test]
2561 fn test_add_rule_formatting() {
2562 let content = r#"build: blah
2564 $(MAKE) install
2565
2566clean:
2567 dh_clean
2568"#;
2569
2570 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2571 let mut rule = makefile.add_rule("build-indep");
2572 rule.add_prerequisite("build").unwrap();
2573
2574 let expected = r#"build: blah
2575 $(MAKE) install
2576
2577clean:
2578 dh_clean
2579
2580build-indep: build
2581"#;
2582
2583 assert_eq!(makefile.to_string(), expected);
2584 }
2585
2586 #[test]
2587 fn test_push_command() {
2588 let mut makefile = Makefile::new();
2589 let mut rule = makefile.add_rule("rule");
2590
2591 rule.push_command("command");
2593 rule.push_command("command2");
2594
2595 assert_eq!(
2597 rule.recipes().collect::<Vec<_>>(),
2598 vec!["command", "command2"]
2599 );
2600
2601 rule.push_command("command3");
2603 assert_eq!(
2604 rule.recipes().collect::<Vec<_>>(),
2605 vec!["command", "command2", "command3"]
2606 );
2607
2608 assert_eq!(
2610 makefile.to_string(),
2611 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2612 );
2613
2614 assert_eq!(
2616 rule.to_string(),
2617 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2618 );
2619 }
2620
2621 #[test]
2622 fn test_replace_command() {
2623 let mut makefile = Makefile::new();
2624 let mut rule = makefile.add_rule("rule");
2625
2626 rule.push_command("command");
2628 rule.push_command("command2");
2629
2630 assert_eq!(
2632 rule.recipes().collect::<Vec<_>>(),
2633 vec!["command", "command2"]
2634 );
2635
2636 rule.replace_command(0, "new command");
2638 assert_eq!(
2639 rule.recipes().collect::<Vec<_>>(),
2640 vec!["new command", "command2"]
2641 );
2642
2643 assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2645
2646 assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2648 }
2649
2650 #[test]
2651 fn test_replace_command_with_comments() {
2652 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";
2655
2656 let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2657
2658 let mut rule = makefile.rules().next().unwrap();
2659
2660 assert_eq!(rule.recipe_nodes().count(), 2);
2662 let recipes: Vec<_> = rule.recipe_nodes().collect();
2663 assert_eq!(recipes[0].text(), ""); assert_eq!(
2665 recipes[1].text(),
2666 "dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"
2667 );
2668
2669 assert!(rule.replace_command(1, "dh_strip"));
2671
2672 assert_eq!(rule.recipe_nodes().count(), 2);
2674 let recipes: Vec<_> = rule.recipe_nodes().collect();
2675 assert_eq!(recipes[0].text(), ""); assert_eq!(recipes[1].text(), "dh_strip");
2677 }
2678
2679 #[test]
2680 fn test_parse_rule_without_newline() {
2681 let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2682 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2683 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2684 let rule = "rule: dependency".parse::<Rule>().unwrap();
2685 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2686 assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2687 }
2688
2689 #[test]
2690 fn test_parse_makefile_without_newline() {
2691 let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2692 assert_eq!(makefile.rules().count(), 1);
2693 }
2694
2695 #[test]
2696 fn test_from_reader() {
2697 let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2698 assert_eq!(makefile.rules().count(), 1);
2699 }
2700
2701 #[test]
2702 fn test_parse_with_tab_after_last_newline() {
2703 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2704 assert_eq!(makefile.rules().count(), 1);
2705 }
2706
2707 #[test]
2708 fn test_parse_with_space_after_last_newline() {
2709 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2710 assert_eq!(makefile.rules().count(), 1);
2711 }
2712
2713 #[test]
2714 fn test_parse_with_comment_after_last_newline() {
2715 let makefile =
2716 Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2717 assert_eq!(makefile.rules().count(), 1);
2718 }
2719
2720 #[test]
2721 fn test_parse_with_variable_rule() {
2722 let makefile =
2723 Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2724 .unwrap();
2725
2726 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2728 assert_eq!(vars.len(), 1);
2729 assert_eq!(vars[0].name(), Some("RULE".to_string()));
2730 assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2731
2732 let rules = makefile.rules().collect::<Vec<_>>();
2734 assert_eq!(rules.len(), 1);
2735 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2736 assert_eq!(
2737 rules[0].prerequisites().collect::<Vec<_>>(),
2738 vec!["dependency"]
2739 );
2740 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2741 }
2742
2743 #[test]
2744 fn test_parse_with_variable_dependency() {
2745 let makefile =
2746 Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2747
2748 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2750 assert_eq!(vars.len(), 1);
2751 assert_eq!(vars[0].name(), Some("DEP".to_string()));
2752 assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2753
2754 let rules = makefile.rules().collect::<Vec<_>>();
2756 assert_eq!(rules.len(), 1);
2757 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2758 assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2759 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2760 }
2761
2762 #[test]
2763 fn test_parse_with_variable_command() {
2764 let makefile =
2765 Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2766
2767 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2769 assert_eq!(vars.len(), 1);
2770 assert_eq!(vars[0].name(), Some("COM".to_string()));
2771 assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2772
2773 let rules = makefile.rules().collect::<Vec<_>>();
2775 assert_eq!(rules.len(), 1);
2776 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2777 assert_eq!(
2778 rules[0].prerequisites().collect::<Vec<_>>(),
2779 vec!["dependency"]
2780 );
2781 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2782 }
2783
2784 #[test]
2785 fn test_regular_line_error_reporting() {
2786 let input = "rule target\n\tcommand";
2787
2788 let parsed = parse(input, None);
2790 let direct_error = &parsed.errors[0];
2791
2792 assert_eq!(direct_error.line, 2);
2794 assert!(
2795 direct_error.message.contains("expected"),
2796 "Error message should contain 'expected': {}",
2797 direct_error.message
2798 );
2799 assert_eq!(direct_error.context, "\tcommand");
2800
2801 let reader_result = Makefile::from_reader(input.as_bytes());
2803 let parse_error = match reader_result {
2804 Ok(_) => panic!("Expected Parse error from from_reader"),
2805 Err(err) => match err {
2806 self::Error::Parse(parse_err) => parse_err,
2807 _ => panic!("Expected Parse error"),
2808 },
2809 };
2810
2811 let error_text = parse_error.to_string();
2813 assert!(error_text.contains("Error at line 2:"));
2814 assert!(error_text.contains("2| \tcommand"));
2815 }
2816
2817 #[test]
2818 fn test_parsing_error_context_with_bad_syntax() {
2819 let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2821
2822 match Makefile::from_reader(input.as_bytes()) {
2824 Ok(makefile) => {
2825 assert_eq!(
2827 makefile.rules().count(),
2828 0,
2829 "Should not have found any rules"
2830 );
2831 }
2832 Err(err) => match err {
2833 self::Error::Parse(error) => {
2834 assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2836 assert!(
2837 !error.errors[0].context.is_empty(),
2838 "Error context should not be empty"
2839 );
2840 }
2841 _ => panic!("Unexpected error type"),
2842 },
2843 };
2844 }
2845
2846 #[test]
2847 fn test_error_message_format() {
2848 let parse_error = ParseError {
2850 errors: vec![ErrorInfo {
2851 message: "test error".to_string(),
2852 line: 42,
2853 context: "some problematic code".to_string(),
2854 }],
2855 };
2856
2857 let error_text = parse_error.to_string();
2858 assert!(error_text.contains("Error at line 42: test error"));
2859 assert!(error_text.contains("42| some problematic code"));
2860 }
2861
2862 #[test]
2863 fn test_line_number_calculation() {
2864 let test_cases = [
2866 ("rule dependency\n\tcommand", 2), ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2), ("var = value\n#comment\n\tindented line", 3), ];
2870
2871 for (input, expected_line) in test_cases {
2872 match input.parse::<Makefile>() {
2874 Ok(_) => {
2875 continue;
2878 }
2879 Err(err) => {
2880 if let Error::Parse(parse_err) = err {
2881 assert_eq!(
2883 parse_err.errors[0].line, expected_line,
2884 "Line number should match the expected line"
2885 );
2886
2887 if parse_err.errors[0].message.contains("indented") {
2889 assert!(
2890 parse_err.errors[0].context.starts_with('\t'),
2891 "Context for indentation errors should include the tab character"
2892 );
2893 }
2894 } else {
2895 panic!("Expected parse error, got: {:?}", err);
2896 }
2897 }
2898 }
2899 }
2900 }
2901
2902 #[test]
2903 fn test_conditional_features() {
2904 let code = r#"
2906# Set variables based on DEBUG flag
2907ifdef DEBUG
2908 CFLAGS += -g -DDEBUG
2909else
2910 CFLAGS = -O2
2911endif
2912
2913# Define a build rule
2914all: $(OBJS)
2915 $(CC) $(CFLAGS) -o $@ $^
2916"#;
2917
2918 let mut buf = code.as_bytes();
2919 let makefile =
2920 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2921
2922 assert!(!makefile.code().is_empty(), "Makefile has content");
2925
2926 let rules = makefile.rules().collect::<Vec<_>>();
2928 assert!(!rules.is_empty(), "Should have found rules");
2929
2930 assert!(code.contains("ifdef DEBUG"));
2932 assert!(code.contains("endif"));
2933
2934 let code_with_var = r#"
2936# Define a variable first
2937CC = gcc
2938
2939ifdef DEBUG
2940 CFLAGS += -g -DDEBUG
2941else
2942 CFLAGS = -O2
2943endif
2944
2945all: $(OBJS)
2946 $(CC) $(CFLAGS) -o $@ $^
2947"#;
2948
2949 let mut buf = code_with_var.as_bytes();
2950 let makefile =
2951 Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2952
2953 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2955 assert!(
2956 !vars.is_empty(),
2957 "Should have found at least the CC variable definition"
2958 );
2959 }
2960
2961 #[test]
2962 fn test_include_directive() {
2963 let parsed = parse(
2964 "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2965 None,
2966 );
2967 assert!(parsed.errors.is_empty());
2968 let node = parsed.syntax();
2969 assert!(format!("{:#?}", node).contains("INCLUDE@"));
2970 }
2971
2972 #[test]
2973 fn test_export_variables() {
2974 let parsed = parse("export SHELL := /bin/bash\n", None);
2975 assert!(parsed.errors.is_empty());
2976 let makefile = parsed.root();
2977 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2978 assert_eq!(vars.len(), 1);
2979 let shell_var = vars
2980 .iter()
2981 .find(|v| v.name() == Some("SHELL".to_string()))
2982 .unwrap();
2983 assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2984 }
2985
2986 #[test]
2987 fn test_bare_export_variable() {
2988 let parsed = parse(
2991 "DEB_CFLAGS_MAINT_APPEND = -Wno-error\nexport DEB_CFLAGS_MAINT_APPEND\n\n%:\n\tdh $@\n",
2992 None,
2993 );
2994 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2995 let makefile = parsed.root();
2996 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2998 assert_eq!(vars.len(), 2);
2999 let rules = makefile.rules().collect::<Vec<_>>();
3001 assert_eq!(rules.len(), 1);
3002 assert!(rules[0].targets().any(|t| t == "%"));
3003 assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
3005 }
3006
3007 #[test]
3008 fn test_bare_export_at_eof() {
3009 let parsed = parse("VAR = value\nexport VAR", None);
3011 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3012 let makefile = parsed.root();
3013 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3014 assert_eq!(vars.len(), 2);
3015 assert_eq!(makefile.rules().count(), 0);
3016 }
3017
3018 #[test]
3019 fn test_bare_export_does_not_eat_include() {
3020 let parsed = parse("VAR = value\nexport VAR\ninclude other.mk\n", None);
3022 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3023 let makefile = parsed.root();
3024 assert_eq!(makefile.includes().count(), 1);
3025 assert_eq!(
3026 makefile.included_files().collect::<Vec<_>>(),
3027 vec!["other.mk"]
3028 );
3029 }
3030
3031 #[test]
3032 fn test_bare_export_multiple() {
3033 let parsed = parse(
3035 "A = 1\nB = 2\nexport A\nexport B\n\nall:\n\techo done\n",
3036 None,
3037 );
3038 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3039 let makefile = parsed.root();
3040 assert_eq!(makefile.variable_definitions().count(), 4);
3041 let rules = makefile.rules().collect::<Vec<_>>();
3042 assert_eq!(rules.len(), 1);
3043 assert!(rules[0].targets().any(|t| t == "all"));
3044 }
3045
3046 #[test]
3047 fn test_parse_error_does_not_cross_lines() {
3048 let parsed = parse("notarule\n\nbuild-arch:\n\techo arch\n", None);
3051 let makefile = parsed.root();
3052 let rules = makefile.rules().collect::<Vec<_>>();
3053 assert!(
3055 rules.iter().any(|r| r.targets().any(|t| t == "build-arch")),
3056 "build-arch rule should be parsed despite earlier error; rules: {:?}",
3057 rules
3058 .iter()
3059 .map(|r| r.targets().collect::<Vec<_>>())
3060 .collect::<Vec<_>>()
3061 );
3062 }
3063
3064 #[test]
3065 fn test_pyfai_rules_full() {
3066 let input = "\
3068#!/usr/bin/make -f
3069
3070export DH_VERBOSE=1
3071export PYBUILD_NAME=pyfai
3072
3073DEB_CFLAGS_MAINT_APPEND = -Wno-error=incompatible-pointer-types
3074export DEB_CFLAGS_MAINT_APPEND
3075
3076PY3VER := $(shell py3versions -dv)
3077
3078include /usr/share/dpkg/pkg-info.mk # sets SOURCE_DATE_EPOCH
3079
3080%:
3081\tdh $@ --buildsystem=pybuild
3082
3083override_dh_auto_build-arch:
3084\tPYBUILD_BUILD_ARGS=\"-Ccompile-args=--verbose\" dh_auto_build
3085
3086override_dh_auto_build-indep: override_dh_auto_build-arch
3087\tsphinx-build -N -bhtml doc/source build/html
3088
3089override_dh_auto_test:
3090
3091execute_after_dh_auto_install:
3092\tdh_install -p pyfai debian/python3-pyfai/usr/bin /usr
3093";
3094 let parsed = parse(input, None);
3095 let makefile = parsed.root();
3096
3097 assert_eq!(makefile.includes().count(), 1);
3099
3100 assert!(
3102 makefile.find_rule_by_target_pattern("build-arch").is_some(),
3103 "build-arch should match via %: pattern rule"
3104 );
3105 assert!(
3106 makefile
3107 .find_rule_by_target_pattern("build-indep")
3108 .is_some(),
3109 "build-indep should match via %: pattern rule"
3110 );
3111
3112 let rule_targets: Vec<Vec<String>> =
3114 makefile.rules().map(|r| r.targets().collect()).collect();
3115 assert!(
3116 rule_targets.iter().any(|t| t.contains(&"%".to_string())),
3117 "missing %: rule; got: {:?}",
3118 rule_targets
3119 );
3120 assert!(
3121 rule_targets
3122 .iter()
3123 .any(|t| t.contains(&"override_dh_auto_build-arch".to_string())),
3124 "missing override_dh_auto_build-arch; got: {:?}",
3125 rule_targets
3126 );
3127 assert!(
3128 rule_targets
3129 .iter()
3130 .any(|t| t.contains(&"override_dh_auto_test".to_string())),
3131 "missing override_dh_auto_test; got: {:?}",
3132 rule_targets
3133 );
3134 assert!(
3135 rule_targets
3136 .iter()
3137 .any(|t| t.contains(&"execute_after_dh_auto_install".to_string())),
3138 "missing execute_after_dh_auto_install; got: {:?}",
3139 rule_targets
3140 );
3141 }
3142
3143 #[test]
3144 fn test_variable_scopes() {
3145 let parsed = parse(
3146 "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
3147 None,
3148 );
3149 assert!(parsed.errors.is_empty());
3150 let makefile = parsed.root();
3151 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3152 assert_eq!(vars.len(), 4);
3153 let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
3154 assert!(var_names.contains(&"SIMPLE".to_string()));
3155 assert!(var_names.contains(&"IMMEDIATE".to_string()));
3156 assert!(var_names.contains(&"CONDITIONAL".to_string()));
3157 assert!(var_names.contains(&"APPEND".to_string()));
3158 }
3159
3160 #[test]
3161 fn test_pattern_rule_parsing() {
3162 let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
3163 assert!(parsed.errors.is_empty());
3164 let makefile = parsed.root();
3165 let rules = makefile.rules().collect::<Vec<_>>();
3166 assert_eq!(rules.len(), 1);
3167 assert_eq!(rules[0].targets().next().unwrap(), "%.o");
3168 assert!(rules[0].recipes().next().unwrap().contains("$@"));
3169 }
3170
3171 #[test]
3172 fn test_include_variants() {
3173 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
3175 let parsed = parse(makefile_str, None);
3176 assert!(parsed.errors.is_empty());
3177
3178 let node = parsed.syntax();
3180 let debug_str = format!("{:#?}", node);
3181
3182 assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
3184
3185 let makefile = parsed.root();
3187
3188 let include_count = makefile
3190 .syntax()
3191 .children()
3192 .filter(|child| child.kind() == INCLUDE)
3193 .count();
3194 assert_eq!(include_count, 4);
3195
3196 assert!(makefile
3198 .included_files()
3199 .any(|path| path.contains("$(VAR)")));
3200 }
3201
3202 #[test]
3203 fn test_include_api() {
3204 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
3206 let makefile: Makefile = makefile_str.parse().unwrap();
3207
3208 let includes: Vec<_> = makefile.includes().collect();
3210 assert_eq!(includes.len(), 3);
3211
3212 assert!(!includes[0].is_optional()); assert!(includes[1].is_optional()); assert!(includes[2].is_optional()); let files: Vec<_> = makefile.included_files().collect();
3219 assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
3220
3221 assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
3223 assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
3224 assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
3225 }
3226
3227 #[test]
3228 fn test_include_integration() {
3229 let phony_makefile = Makefile::from_reader(
3233 ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3234 .as_bytes()
3235 ).unwrap();
3236
3237 assert_eq!(phony_makefile.rules().count(), 2);
3239
3240 let normal_rules_count = phony_makefile
3242 .rules()
3243 .filter(|r| !r.targets().any(|t| t.starts_with('.')))
3244 .count();
3245 assert_eq!(normal_rules_count, 1);
3246
3247 assert_eq!(phony_makefile.includes().count(), 1);
3249 assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
3250
3251 let simple_makefile = Makefile::from_reader(
3253 "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3254 .as_bytes(),
3255 )
3256 .unwrap();
3257 assert_eq!(simple_makefile.rules().count(), 1);
3258 assert_eq!(simple_makefile.includes().count(), 1);
3259 }
3260
3261 #[test]
3262 fn test_real_conditional_directives() {
3263 let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
3265 let mut buf = conditional.as_bytes();
3266 let makefile =
3267 Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
3268 let code = makefile.code();
3269 assert!(code.contains("ifdef DEBUG"));
3270 assert!(code.contains("else"));
3271 assert!(code.contains("endif"));
3272
3273 let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
3275 let mut buf = nested.as_bytes();
3276 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
3277 let code = makefile.code();
3278 assert!(code.contains("ifdef DEBUG"));
3279 assert!(code.contains("ifdef VERBOSE"));
3280
3281 let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
3283 let mut buf = ifeq.as_bytes();
3284 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
3285 let code = makefile.code();
3286 assert!(code.contains("ifeq"));
3287 assert!(code.contains("Windows_NT"));
3288 }
3289
3290 #[test]
3291 fn test_indented_text_outside_rules() {
3292 let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \" help show help\"\n";
3294 let parsed = parse(help_text, None);
3295 assert!(parsed.errors.is_empty());
3296
3297 let root = parsed.root();
3299 let rules = root.rules().collect::<Vec<_>>();
3300 assert_eq!(rules.len(), 1);
3301
3302 let help_rule = &rules[0];
3303 let recipes = help_rule.recipes().collect::<Vec<_>>();
3304 assert_eq!(recipes.len(), 2);
3305 assert!(recipes[0].contains("Available targets"));
3306 assert!(recipes[1].contains("help"));
3307 }
3308
3309 #[test]
3310 fn test_comment_handling_in_recipes() {
3311 let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
3313
3314 let parsed = parse(recipe_comment, None);
3316
3317 assert!(
3319 parsed.errors.is_empty(),
3320 "Should parse recipe with comments without errors"
3321 );
3322
3323 let root = parsed.root();
3325 let rules = root.rules().collect::<Vec<_>>();
3326 assert_eq!(rules.len(), 1, "Should find exactly one rule");
3327
3328 let build_rule = &rules[0];
3330 assert_eq!(
3331 build_rule.targets().collect::<Vec<_>>(),
3332 vec!["build"],
3333 "Rule should have 'build' as target"
3334 );
3335
3336 let recipes = build_rule.recipe_nodes().collect::<Vec<_>>();
3339 assert_eq!(recipes.len(), 2, "Should find two recipe nodes");
3340
3341 assert_eq!(recipes[0].text(), "");
3343 assert_eq!(
3344 recipes[0].comment(),
3345 Some("# This is a comment".to_string())
3346 );
3347
3348 assert_eq!(recipes[1].text(), "gcc -o app main.c");
3350 assert_eq!(recipes[1].comment(), None);
3351 }
3352
3353 #[test]
3354 fn test_multiline_variables() {
3355 let multiline = "SOURCES = main.c \\\n util.c\n";
3357
3358 let parsed = parse(multiline, None);
3360
3361 let root = parsed.root();
3363 let vars = root.variable_definitions().collect::<Vec<_>>();
3364 assert!(!vars.is_empty(), "Should find at least one variable");
3365
3366 let operators = "CFLAGS := -Wall \\\n -Werror\n";
3370 let parsed_operators = parse(operators, None);
3371
3372 let root = parsed_operators.root();
3374 let vars = root.variable_definitions().collect::<Vec<_>>();
3375 assert!(
3376 !vars.is_empty(),
3377 "Should find at least one variable with := operator"
3378 );
3379
3380 let append = "LDFLAGS += -L/usr/lib \\\n -lm\n";
3382 let parsed_append = parse(append, None);
3383
3384 let root = parsed_append.root();
3386 let vars = root.variable_definitions().collect::<Vec<_>>();
3387 assert!(
3388 !vars.is_empty(),
3389 "Should find at least one variable with += operator"
3390 );
3391 }
3392
3393 #[test]
3394 fn test_whitespace_and_eof_handling() {
3395 let blank_lines = "VAR = value\n\n\n";
3397
3398 let parsed_blank = parse(blank_lines, None);
3399
3400 let root = parsed_blank.root();
3402 let vars = root.variable_definitions().collect::<Vec<_>>();
3403 assert_eq!(
3404 vars.len(),
3405 1,
3406 "Should find one variable in blank lines test"
3407 );
3408
3409 let trailing_space = "VAR = value \n";
3411
3412 let parsed_space = parse(trailing_space, None);
3413
3414 let root = parsed_space.root();
3416 let vars = root.variable_definitions().collect::<Vec<_>>();
3417 assert_eq!(
3418 vars.len(),
3419 1,
3420 "Should find one variable in trailing space test"
3421 );
3422
3423 let no_newline = "VAR = value";
3425
3426 let parsed_no_newline = parse(no_newline, None);
3427
3428 let root = parsed_no_newline.root();
3430 let vars = root.variable_definitions().collect::<Vec<_>>();
3431 assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
3432 assert_eq!(
3433 vars[0].name(),
3434 Some("VAR".to_string()),
3435 "Variable name should be VAR"
3436 );
3437 }
3438
3439 #[test]
3440 fn test_complex_variable_references() {
3441 let wildcard = "SOURCES = $(wildcard *.c)\n";
3443 let parsed = parse(wildcard, None);
3444 assert!(parsed.errors.is_empty());
3445
3446 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3448 let parsed = parse(nested, None);
3449 assert!(parsed.errors.is_empty());
3450
3451 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3453 let parsed = parse(patsubst, None);
3454 assert!(parsed.errors.is_empty());
3455 }
3456
3457 #[test]
3458 fn test_complex_variable_references_minimal() {
3459 let wildcard = "SOURCES = $(wildcard *.c)\n";
3461 let parsed = parse(wildcard, None);
3462 assert!(parsed.errors.is_empty());
3463
3464 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3466 let parsed = parse(nested, None);
3467 assert!(parsed.errors.is_empty());
3468
3469 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3471 let parsed = parse(patsubst, None);
3472 assert!(parsed.errors.is_empty());
3473 }
3474
3475 #[test]
3476 fn test_multiline_variable_with_backslash() {
3477 let content = r#"
3478LONG_VAR = This is a long variable \
3479 that continues on the next line \
3480 and even one more line
3481"#;
3482
3483 let mut buf = content.as_bytes();
3485 let makefile =
3486 Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
3487
3488 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3490 assert_eq!(
3491 vars.len(),
3492 1,
3493 "Expected 1 variable but found {}",
3494 vars.len()
3495 );
3496 let var_value = vars[0].raw_value();
3497 assert!(var_value.is_some(), "Variable value is None");
3498
3499 let value_str = var_value.unwrap();
3501 assert!(
3502 value_str.contains("long variable"),
3503 "Value doesn't contain expected content"
3504 );
3505 }
3506
3507 #[test]
3508 fn test_multiline_variable_with_mixed_operators() {
3509 let content = r#"
3510PREFIX ?= /usr/local
3511CFLAGS := -Wall -O2 \
3512 -I$(PREFIX)/include \
3513 -DDEBUG
3514"#;
3515 let mut buf = content.as_bytes();
3517 let makefile = Makefile::read_relaxed(&mut buf)
3518 .expect("Failed to parse multiline variable with operators");
3519
3520 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3522 assert!(
3523 !vars.is_empty(),
3524 "Expected at least 1 variable, found {}",
3525 vars.len()
3526 );
3527
3528 let prefix_var = vars
3530 .iter()
3531 .find(|v| v.name().unwrap_or_default() == "PREFIX");
3532 assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
3533 assert!(
3534 prefix_var.unwrap().raw_value().is_some(),
3535 "PREFIX variable has no value"
3536 );
3537
3538 let cflags_var = vars
3540 .iter()
3541 .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
3542 assert!(
3543 cflags_var.is_some(),
3544 "Expected to find CFLAGS variable (or part of it)"
3545 );
3546 }
3547
3548 #[test]
3549 fn test_indented_help_text() {
3550 let content = r#"
3551.PHONY: help
3552help:
3553 @echo "Available targets:"
3554 @echo " build - Build the project"
3555 @echo " test - Run tests"
3556 @echo " clean - Remove build artifacts"
3557"#;
3558 let mut buf = content.as_bytes();
3560 let makefile =
3561 Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
3562
3563 let rules = makefile.rules().collect::<Vec<_>>();
3565 assert!(!rules.is_empty(), "Expected at least one rule");
3566
3567 let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
3569 assert!(help_rule.is_some(), "Expected to find help rule");
3570
3571 let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
3573 assert!(
3574 !recipes.is_empty(),
3575 "Expected at least one recipe line in help rule"
3576 );
3577 assert!(
3578 recipes.iter().any(|r| r.contains("Available targets")),
3579 "Expected to find 'Available targets' in recipes"
3580 );
3581 }
3582
3583 #[test]
3584 fn test_indented_lines_in_conditionals() {
3585 let content = r#"
3586ifdef DEBUG
3587 CFLAGS += -g -DDEBUG
3588 # This is a comment inside conditional
3589 ifdef VERBOSE
3590 CFLAGS += -v
3591 endif
3592endif
3593"#;
3594 let mut buf = content.as_bytes();
3596 let makefile = Makefile::read_relaxed(&mut buf)
3597 .expect("Failed to parse indented lines in conditionals");
3598
3599 let code = makefile.code();
3601 assert!(code.contains("ifdef DEBUG"));
3602 assert!(code.contains("ifdef VERBOSE"));
3603 assert!(code.contains("endif"));
3604 }
3605
3606 #[test]
3607 fn test_recipe_with_colon() {
3608 let content = r#"
3609build:
3610 @echo "Building at: $(shell date)"
3611 gcc -o program main.c
3612"#;
3613 let parsed = parse(content, None);
3614 assert!(
3615 parsed.errors.is_empty(),
3616 "Failed to parse recipe with colon: {:?}",
3617 parsed.errors
3618 );
3619 }
3620
3621 #[test]
3622 fn test_double_colon_rules() {
3623 let content = r#"
3624%.o :: %.c
3625 $(CC) -c $< -o $@
3626
3627# Double colon allows multiple rules for same target
3628all:: prerequisite1
3629 @echo "First rule for all"
3630
3631all:: prerequisite2
3632 @echo "Second rule for all"
3633"#;
3634 let parsed = parse(content, None);
3635 assert!(
3636 parsed.errors.is_empty(),
3637 "Failed to parse double colon rules: {:?}",
3638 parsed.errors
3639 );
3640
3641 let makefile = parsed.root();
3642 let rules: Vec<_> = makefile.rules().collect();
3643 assert_eq!(rules.len(), 3);
3644
3645 for rule in &rules {
3647 assert!(rule.is_double_colon());
3648 }
3649
3650 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["%.o"]);
3652 assert_eq!(rules[1].targets().collect::<Vec<_>>(), vec!["all"]);
3653 assert_eq!(rules[2].targets().collect::<Vec<_>>(), vec!["all"]);
3654
3655 assert_eq!(
3657 rules[1].prerequisites().collect::<Vec<_>>(),
3658 vec!["prerequisite1"]
3659 );
3660 assert_eq!(
3661 rules[2].prerequisites().collect::<Vec<_>>(),
3662 vec!["prerequisite2"]
3663 );
3664 }
3665
3666 #[test]
3667 fn test_else_conditional_directives() {
3668 let content = r#"
3670ifeq ($(OS),Windows_NT)
3671 TARGET = windows
3672else ifeq ($(OS),Darwin)
3673 TARGET = macos
3674else ifeq ($(OS),Linux)
3675 TARGET = linux
3676else
3677 TARGET = unknown
3678endif
3679"#;
3680 let mut buf = content.as_bytes();
3681 let makefile =
3682 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3683 assert!(makefile.code().contains("else ifeq"));
3684 assert!(makefile.code().contains("TARGET"));
3685
3686 let content = r#"
3688ifdef WINDOWS
3689 TARGET = windows
3690else ifdef DARWIN
3691 TARGET = macos
3692else ifdef LINUX
3693 TARGET = linux
3694else
3695 TARGET = unknown
3696endif
3697"#;
3698 let mut buf = content.as_bytes();
3699 let makefile =
3700 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3701 assert!(makefile.code().contains("else ifdef"));
3702
3703 let content = r#"
3705ifndef NOWINDOWS
3706 TARGET = windows
3707else ifndef NODARWIN
3708 TARGET = macos
3709else
3710 TARGET = linux
3711endif
3712"#;
3713 let mut buf = content.as_bytes();
3714 let makefile =
3715 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3716 assert!(makefile.code().contains("else ifndef"));
3717
3718 let content = r#"
3720ifneq ($(OS),Windows_NT)
3721 TARGET = not_windows
3722else ifneq ($(OS),Darwin)
3723 TARGET = not_macos
3724else
3725 TARGET = darwin
3726endif
3727"#;
3728 let mut buf = content.as_bytes();
3729 let makefile =
3730 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3731 assert!(makefile.code().contains("else ifneq"));
3732 }
3733
3734 #[test]
3735 fn test_complex_else_conditionals() {
3736 let content = r#"VAR1 := foo
3738VAR2 := bar
3739
3740ifeq ($(VAR1),foo)
3741 RESULT := foo_matched
3742else ifdef VAR2
3743 RESULT := var2_defined
3744else ifndef VAR3
3745 RESULT := var3_not_defined
3746else
3747 RESULT := final_else
3748endif
3749
3750all:
3751 @echo $(RESULT)
3752"#;
3753 let mut buf = content.as_bytes();
3754 let makefile =
3755 Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3756
3757 let code = makefile.code();
3759 assert!(code.contains("ifeq ($(VAR1),foo)"));
3760 assert!(code.contains("else ifdef VAR2"));
3761 assert!(code.contains("else ifndef VAR3"));
3762 assert!(code.contains("else"));
3763 assert!(code.contains("endif"));
3764 assert!(code.contains("RESULT"));
3765
3766 let rules: Vec<_> = makefile.rules().collect();
3768 assert_eq!(rules.len(), 1);
3769 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3770 }
3771
3772 #[test]
3773 fn test_conditional_token_structure() {
3774 let content = r#"ifdef VAR1
3776X := 1
3777else ifdef VAR2
3778X := 2
3779else
3780X := 3
3781endif
3782"#;
3783 let mut buf = content.as_bytes();
3784 let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3785
3786 let syntax = makefile.syntax();
3788
3789 let mut found_conditional = false;
3791 let mut found_conditional_if = false;
3792 let mut found_conditional_else = false;
3793 let mut found_conditional_endif = false;
3794
3795 fn check_node(
3796 node: &SyntaxNode,
3797 found_cond: &mut bool,
3798 found_if: &mut bool,
3799 found_else: &mut bool,
3800 found_endif: &mut bool,
3801 ) {
3802 match node.kind() {
3803 SyntaxKind::CONDITIONAL => *found_cond = true,
3804 SyntaxKind::CONDITIONAL_IF => *found_if = true,
3805 SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3806 SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3807 _ => {}
3808 }
3809
3810 for child in node.children() {
3811 check_node(&child, found_cond, found_if, found_else, found_endif);
3812 }
3813 }
3814
3815 check_node(
3816 syntax,
3817 &mut found_conditional,
3818 &mut found_conditional_if,
3819 &mut found_conditional_else,
3820 &mut found_conditional_endif,
3821 );
3822
3823 assert!(found_conditional, "Should have CONDITIONAL node");
3824 assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3825 assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3826 assert!(
3827 found_conditional_endif,
3828 "Should have CONDITIONAL_ENDIF node"
3829 );
3830 }
3831
3832 #[test]
3833 fn test_ambiguous_assignment_vs_rule() {
3834 const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3836
3837 let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3838 let makefile =
3839 Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3840
3841 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3842 let rules = makefile.rules().collect::<Vec<_>>();
3843
3844 assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3845 assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3846
3847 assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3848
3849 const SIMPLE_RULE: &str = "target: dependency\n";
3851
3852 let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3853 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3854
3855 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3856 let rules = makefile.rules().collect::<Vec<_>>();
3857
3858 assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3859 assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3860
3861 let rule = &rules[0];
3862 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3863 }
3864
3865 #[test]
3866 fn test_nested_conditionals() {
3867 let content = r#"
3868ifdef RELEASE
3869 CFLAGS += -O3
3870 ifndef DEBUG
3871 ifneq ($(ARCH),arm)
3872 CFLAGS += -march=native
3873 else
3874 CFLAGS += -mcpu=cortex-a72
3875 endif
3876 endif
3877endif
3878"#;
3879 let mut buf = content.as_bytes();
3881 let makefile =
3882 Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3883
3884 let code = makefile.code();
3886 assert!(code.contains("ifdef RELEASE"));
3887 assert!(code.contains("ifndef DEBUG"));
3888 assert!(code.contains("ifneq"));
3889 }
3890
3891 #[test]
3892 fn test_space_indented_recipes() {
3893 let content = r#"
3896build:
3897 @echo "Building with spaces instead of tabs"
3898 gcc -o program main.c
3899"#;
3900 let mut buf = content.as_bytes();
3902 let makefile =
3903 Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3904
3905 let rules = makefile.rules().collect::<Vec<_>>();
3907 assert!(!rules.is_empty(), "Expected at least one rule");
3908
3909 let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3911 assert!(build_rule.is_some(), "Expected to find build rule");
3912 }
3913
3914 #[test]
3915 fn test_complex_variable_functions() {
3916 let content = r#"
3917FILES := $(shell find . -name "*.c")
3918OBJS := $(patsubst %.c,%.o,$(FILES))
3919NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3920HEADERS := ${wildcard *.h}
3921"#;
3922 let parsed = parse(content, None);
3923 assert!(
3924 parsed.errors.is_empty(),
3925 "Failed to parse complex variable functions: {:?}",
3926 parsed.errors
3927 );
3928 }
3929
3930 #[test]
3931 fn test_nested_variable_expansions() {
3932 let content = r#"
3933VERSION = 1.0
3934PACKAGE = myapp
3935TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3936INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3937"#;
3938 let parsed = parse(content, None);
3939 assert!(
3940 parsed.errors.is_empty(),
3941 "Failed to parse nested variable expansions: {:?}",
3942 parsed.errors
3943 );
3944 }
3945
3946 #[test]
3947 fn test_special_directives() {
3948 let content = r#"
3949# Special makefile directives
3950.PHONY: all clean
3951.SUFFIXES: .c .o
3952.DEFAULT: all
3953
3954# Variable definition and export directive
3955export PATH := /usr/bin:/bin
3956"#;
3957 let mut buf = content.as_bytes();
3959 let makefile =
3960 Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3961
3962 let rules = makefile.rules().collect::<Vec<_>>();
3964
3965 let phony_rule = rules
3967 .iter()
3968 .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3969 assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3970
3971 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3973 assert!(!vars.is_empty(), "Expected to find at least one variable");
3974 }
3975
3976 #[test]
3979 fn test_comprehensive_real_world_makefile() {
3980 let content = r#"
3982# Basic variable assignment
3983VERSION = 1.0.0
3984
3985# Phony target
3986.PHONY: all clean
3987
3988# Simple rule
3989all:
3990 echo "Building version $(VERSION)"
3991
3992# Another rule with dependencies
3993clean:
3994 rm -f *.o
3995"#;
3996
3997 let parsed = parse(content, None);
3999
4000 assert!(parsed.errors.is_empty(), "Expected no parsing errors");
4002
4003 let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
4005 assert!(!variables.is_empty(), "Expected at least one variable");
4006 assert_eq!(
4007 variables[0].name(),
4008 Some("VERSION".to_string()),
4009 "Expected VERSION variable"
4010 );
4011
4012 let rules = parsed.root().rules().collect::<Vec<_>>();
4014 assert!(!rules.is_empty(), "Expected at least one rule");
4015
4016 let rule_targets: Vec<String> = rules
4018 .iter()
4019 .flat_map(|r| r.targets().collect::<Vec<_>>())
4020 .collect();
4021 assert!(
4022 rule_targets.contains(&".PHONY".to_string()),
4023 "Expected .PHONY rule"
4024 );
4025 assert!(
4026 rule_targets.contains(&"all".to_string()),
4027 "Expected 'all' rule"
4028 );
4029 assert!(
4030 rule_targets.contains(&"clean".to_string()),
4031 "Expected 'clean' rule"
4032 );
4033 }
4034
4035 #[test]
4036 fn test_indented_help_text_outside_rules() {
4037 let content = r#"
4039# Targets with help text
4040help:
4041 @echo "Available targets:"
4042 @echo " build build the project"
4043 @echo " test run tests"
4044 @echo " clean clean build artifacts"
4045
4046# Another target
4047clean:
4048 rm -rf build/
4049"#;
4050
4051 let parsed = parse(content, None);
4053
4054 assert!(
4056 parsed.errors.is_empty(),
4057 "Failed to parse indented help text"
4058 );
4059
4060 let rules = parsed.root().rules().collect::<Vec<_>>();
4062 assert_eq!(rules.len(), 2, "Expected to find two rules");
4063
4064 let help_rule = rules
4066 .iter()
4067 .find(|r| r.targets().any(|t| t == "help"))
4068 .expect("Expected to find help rule");
4069
4070 let clean_rule = rules
4071 .iter()
4072 .find(|r| r.targets().any(|t| t == "clean"))
4073 .expect("Expected to find clean rule");
4074
4075 let help_recipes = help_rule.recipes().collect::<Vec<_>>();
4077 assert!(
4078 !help_recipes.is_empty(),
4079 "Help rule should have recipe lines"
4080 );
4081 assert!(
4082 help_recipes
4083 .iter()
4084 .any(|line| line.contains("Available targets")),
4085 "Help recipes should include 'Available targets' line"
4086 );
4087
4088 let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
4090 assert!(
4091 !clean_recipes.is_empty(),
4092 "Clean rule should have recipe lines"
4093 );
4094 assert!(
4095 clean_recipes.iter().any(|line| line.contains("rm -rf")),
4096 "Clean recipes should include 'rm -rf' command"
4097 );
4098 }
4099
4100 #[test]
4101 fn test_makefile1_phony_pattern() {
4102 let content = "#line 2145\n.PHONY: $(PHONY)\n";
4104
4105 let result = parse(content, None);
4107
4108 assert!(
4110 result.errors.is_empty(),
4111 "Failed to parse .PHONY: $(PHONY) pattern"
4112 );
4113
4114 let rules = result.root().rules().collect::<Vec<_>>();
4116 assert_eq!(rules.len(), 1, "Expected 1 rule");
4117 assert_eq!(
4118 rules[0].targets().next().unwrap(),
4119 ".PHONY",
4120 "Expected .PHONY rule"
4121 );
4122
4123 let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
4125 assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
4126 assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
4127 }
4128
4129 #[test]
4130 fn test_skip_until_newline_behavior() {
4131 let input = "text without newline";
4133 let parsed = parse(input, None);
4134 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4136
4137 let input_with_newline = "text\nafter newline";
4138 let parsed2 = parse(input_with_newline, None);
4139 assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
4140 }
4141
4142 #[test]
4143 #[ignore] fn test_error_with_indent_token() {
4145 let input = "\tinvalid indented line";
4147 let parsed = parse(input, None);
4148 assert!(!parsed.errors.is_empty());
4150
4151 let error_msg = &parsed.errors[0].message;
4152 assert!(error_msg.contains("recipe commences before first target"));
4153 }
4154
4155 #[test]
4156 fn test_conditional_token_handling() {
4157 let input = r#"
4159ifndef VAR
4160 CFLAGS = -DTEST
4161endif
4162"#;
4163 let parsed = parse(input, None);
4164 let makefile = parsed.root();
4166 let _vars = makefile.variable_definitions().collect::<Vec<_>>();
4167 let nested = r#"
4171ifdef DEBUG
4172 ifndef RELEASE
4173 CFLAGS = -g
4174 endif
4175endif
4176"#;
4177 let parsed_nested = parse(nested, None);
4178 let _makefile = parsed_nested.root();
4180 }
4181
4182 #[test]
4183 fn test_include_vs_conditional_logic() {
4184 let input = r#"
4186include file.mk
4187ifdef VAR
4188 VALUE = 1
4189endif
4190"#;
4191 let parsed = parse(input, None);
4192 let makefile = parsed.root();
4194 let includes = makefile.includes().collect::<Vec<_>>();
4195 assert!(!includes.is_empty() || !parsed.errors.is_empty());
4197
4198 let optional_include = r#"
4200-include optional.mk
4201ifndef VAR
4202 VALUE = default
4203endif
4204"#;
4205 let parsed2 = parse(optional_include, None);
4206 let _makefile = parsed2.root();
4208 }
4209
4210 #[test]
4211 fn test_balanced_parens_counting() {
4212 let input = r#"
4214VAR = $(call func,$(nested,arg),extra)
4215COMPLEX = $(if $(condition),$(then_val),$(else_val))
4216"#;
4217 let parsed = parse(input, None);
4218 assert!(parsed.errors.is_empty());
4219
4220 let makefile = parsed.root();
4221 let vars = makefile.variable_definitions().collect::<Vec<_>>();
4222 assert_eq!(vars.len(), 2);
4223 }
4224
4225 #[test]
4226 fn test_documentation_lookahead() {
4227 let input = r#"
4229# Documentation comment
4230help:
4231 @echo "Usage instructions"
4232 @echo "More help text"
4233"#;
4234 let parsed = parse(input, None);
4235 assert!(parsed.errors.is_empty());
4236
4237 let makefile = parsed.root();
4238 let rules = makefile.rules().collect::<Vec<_>>();
4239 assert_eq!(rules.len(), 1);
4240 assert_eq!(rules[0].targets().next().unwrap(), "help");
4241 }
4242
4243 #[test]
4244 fn test_edge_case_empty_input() {
4245 let parsed = parse("", None);
4247 assert!(parsed.errors.is_empty());
4248
4249 let parsed2 = parse(" \n \n", None);
4251 let _makefile = parsed2.root();
4254 }
4255
4256 #[test]
4257 fn test_malformed_conditional_recovery() {
4258 let input = r#"
4260ifdef
4261 # Missing condition variable
4262endif
4263"#;
4264 let parsed = parse(input, None);
4265 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4268 }
4269
4270 #[test]
4271 fn test_replace_rule() {
4272 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4273 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4274
4275 makefile.replace_rule(0, new_rule).unwrap();
4276
4277 let targets: Vec<_> = makefile
4278 .rules()
4279 .flat_map(|r| r.targets().collect::<Vec<_>>())
4280 .collect();
4281 assert_eq!(targets, vec!["new_rule", "rule2"]);
4282
4283 let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
4284 assert_eq!(recipes, vec!["new_command"]);
4285 }
4286
4287 #[test]
4288 fn test_replace_rule_out_of_bounds() {
4289 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4290 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4291
4292 let result = makefile.replace_rule(5, new_rule);
4293 assert!(result.is_err());
4294 }
4295
4296 #[test]
4297 fn test_remove_rule() {
4298 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
4299 .parse()
4300 .unwrap();
4301
4302 let removed = makefile.remove_rule(1).unwrap();
4303 assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
4304
4305 let remaining_targets: Vec<_> = makefile
4306 .rules()
4307 .flat_map(|r| r.targets().collect::<Vec<_>>())
4308 .collect();
4309 assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
4310 assert_eq!(makefile.rules().count(), 2);
4311 }
4312
4313 #[test]
4314 fn test_remove_rule_out_of_bounds() {
4315 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4316
4317 let result = makefile.remove_rule(5);
4318 assert!(result.is_err());
4319 }
4320
4321 #[test]
4322 fn test_insert_rule() {
4323 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4324 let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
4325
4326 makefile.insert_rule(1, new_rule).unwrap();
4327
4328 let targets: Vec<_> = makefile
4329 .rules()
4330 .flat_map(|r| r.targets().collect::<Vec<_>>())
4331 .collect();
4332 assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
4333 assert_eq!(makefile.rules().count(), 3);
4334 }
4335
4336 #[test]
4337 fn test_insert_rule_at_end() {
4338 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4339 let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
4340
4341 makefile.insert_rule(1, new_rule).unwrap();
4342
4343 let targets: Vec<_> = makefile
4344 .rules()
4345 .flat_map(|r| r.targets().collect::<Vec<_>>())
4346 .collect();
4347 assert_eq!(targets, vec!["rule1", "end_rule"]);
4348 }
4349
4350 #[test]
4351 fn test_insert_rule_out_of_bounds() {
4352 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4353 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4354
4355 let result = makefile.insert_rule(5, new_rule);
4356 assert!(result.is_err());
4357 }
4358
4359 #[test]
4360 fn test_insert_rule_preserves_blank_line_spacing_at_end() {
4361 let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
4363 let mut makefile: Makefile = input.parse().unwrap();
4364 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4365
4366 makefile.insert_rule(2, new_rule).unwrap();
4367
4368 let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4369 assert_eq!(makefile.to_string(), expected);
4370 }
4371
4372 #[test]
4373 fn test_insert_rule_adds_blank_lines_when_missing() {
4374 let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
4376 let mut makefile: Makefile = input.parse().unwrap();
4377 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4378
4379 makefile.insert_rule(2, new_rule).unwrap();
4380
4381 let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4382 assert_eq!(makefile.to_string(), expected);
4383 }
4384
4385 #[test]
4386 fn test_remove_command() {
4387 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4388 .parse()
4389 .unwrap();
4390
4391 rule.remove_command(1);
4392 let recipes: Vec<_> = rule.recipes().collect();
4393 assert_eq!(recipes, vec!["command1", "command3"]);
4394 assert_eq!(rule.recipe_count(), 2);
4395 }
4396
4397 #[test]
4398 fn test_remove_command_out_of_bounds() {
4399 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4400
4401 let result = rule.remove_command(5);
4402 assert!(!result);
4403 }
4404
4405 #[test]
4406 fn test_insert_command() {
4407 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
4408
4409 rule.insert_command(1, "command2");
4410 let recipes: Vec<_> = rule.recipes().collect();
4411 assert_eq!(recipes, vec!["command1", "command2", "command3"]);
4412 }
4413
4414 #[test]
4415 fn test_insert_command_at_end() {
4416 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4417
4418 rule.insert_command(1, "command2");
4419 let recipes: Vec<_> = rule.recipes().collect();
4420 assert_eq!(recipes, vec!["command1", "command2"]);
4421 }
4422
4423 #[test]
4424 fn test_insert_command_in_empty_rule() {
4425 let mut rule: Rule = "rule:\n".parse().unwrap();
4426
4427 rule.insert_command(0, "new_command");
4428 let recipes: Vec<_> = rule.recipes().collect();
4429 assert_eq!(recipes, vec!["new_command"]);
4430 }
4431
4432 #[test]
4433 fn test_recipe_count() {
4434 let rule1: Rule = "rule:\n".parse().unwrap();
4435 assert_eq!(rule1.recipe_count(), 0);
4436
4437 let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
4438 assert_eq!(rule2.recipe_count(), 2);
4439 }
4440
4441 #[test]
4442 fn test_clear_commands() {
4443 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4444 .parse()
4445 .unwrap();
4446
4447 rule.clear_commands();
4448 assert_eq!(rule.recipe_count(), 0);
4449
4450 let recipes: Vec<_> = rule.recipes().collect();
4451 assert_eq!(recipes, Vec::<String>::new());
4452
4453 let targets: Vec<_> = rule.targets().collect();
4455 assert_eq!(targets, vec!["rule"]);
4456 }
4457
4458 #[test]
4459 fn test_clear_commands_empty_rule() {
4460 let mut rule: Rule = "rule:\n".parse().unwrap();
4461
4462 rule.clear_commands();
4463 assert_eq!(rule.recipe_count(), 0);
4464
4465 let targets: Vec<_> = rule.targets().collect();
4466 assert_eq!(targets, vec!["rule"]);
4467 }
4468
4469 #[test]
4470 fn test_rule_manipulation_preserves_structure() {
4471 let input = r#"# Comment
4473VAR = value
4474
4475rule1:
4476 command1
4477
4478# Another comment
4479rule2:
4480 command2
4481
4482VAR2 = value2
4483"#;
4484
4485 let mut makefile: Makefile = input.parse().unwrap();
4486 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4487
4488 makefile.insert_rule(1, new_rule).unwrap();
4490
4491 let targets: Vec<_> = makefile
4493 .rules()
4494 .flat_map(|r| r.targets().collect::<Vec<_>>())
4495 .collect();
4496 assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
4497
4498 let vars: Vec<_> = makefile.variable_definitions().collect();
4500 assert_eq!(vars.len(), 2);
4501
4502 let output = makefile.code();
4504 assert!(output.contains("# Comment"));
4505 assert!(output.contains("VAR = value"));
4506 assert!(output.contains("# Another comment"));
4507 assert!(output.contains("VAR2 = value2"));
4508 }
4509
4510 #[test]
4511 fn test_replace_rule_with_multiple_targets() {
4512 let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
4513 let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
4514
4515 makefile.replace_rule(0, new_rule).unwrap();
4516
4517 let targets: Vec<_> = makefile
4518 .rules()
4519 .flat_map(|r| r.targets().collect::<Vec<_>>())
4520 .collect();
4521 assert_eq!(targets, vec!["new_target"]);
4522 }
4523
4524 #[test]
4525 fn test_empty_makefile_operations() {
4526 let mut makefile = Makefile::new();
4527
4528 assert!(makefile
4530 .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
4531 .is_err());
4532 assert!(makefile.remove_rule(0).is_err());
4533
4534 let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
4536 makefile.insert_rule(0, new_rule).unwrap();
4537 assert_eq!(makefile.rules().count(), 1);
4538 }
4539
4540 #[test]
4541 fn test_command_operations_preserve_indentation() {
4542 let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
4543 .parse()
4544 .unwrap();
4545
4546 rule.insert_command(1, "middle_command");
4547 let recipes: Vec<_> = rule.recipes().collect();
4548 assert_eq!(
4549 recipes,
4550 vec!["\tdeep_indent", "middle_command", "shallow_indent"]
4551 );
4552 }
4553
4554 #[test]
4555 fn test_rule_operations_with_variables_and_includes() {
4556 let input = r#"VAR1 = value1
4557include common.mk
4558
4559rule1:
4560 command1
4561
4562VAR2 = value2
4563include other.mk
4564
4565rule2:
4566 command2
4567"#;
4568
4569 let mut makefile: Makefile = input.parse().unwrap();
4570
4571 makefile.remove_rule(0).unwrap();
4573
4574 let output = makefile.code();
4576 assert!(output.contains("VAR1 = value1"));
4577 assert!(output.contains("include common.mk"));
4578 assert!(output.contains("VAR2 = value2"));
4579 assert!(output.contains("include other.mk"));
4580
4581 assert_eq!(makefile.rules().count(), 1);
4583 let remaining_targets: Vec<_> = makefile
4584 .rules()
4585 .flat_map(|r| r.targets().collect::<Vec<_>>())
4586 .collect();
4587 assert_eq!(remaining_targets, vec!["rule2"]);
4588 }
4589
4590 #[test]
4591 fn test_command_manipulation_edge_cases() {
4592 let mut empty_rule: Rule = "empty:\n".parse().unwrap();
4594 assert_eq!(empty_rule.recipe_count(), 0);
4595
4596 empty_rule.insert_command(0, "first_command");
4597 assert_eq!(empty_rule.recipe_count(), 1);
4598
4599 let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
4601 empty_rule2.clear_commands();
4602 assert_eq!(empty_rule2.recipe_count(), 0);
4603 }
4604
4605 #[test]
4606 fn test_large_makefile_performance() {
4607 let mut makefile = Makefile::new();
4609
4610 for i in 0..100 {
4612 let rule_name = format!("rule{}", i);
4613 makefile
4614 .add_rule(&rule_name)
4615 .push_command(&format!("command{}", i));
4616 }
4617
4618 assert_eq!(makefile.rules().count(), 100);
4619
4620 let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
4622 makefile.replace_rule(50, new_rule).unwrap();
4623
4624 let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
4626 assert_eq!(rule_50_targets, vec!["middle_rule"]);
4627
4628 assert_eq!(makefile.rules().count(), 100); }
4630
4631 #[test]
4632 fn test_complex_recipe_manipulation() {
4633 let mut complex_rule: Rule = r#"complex:
4634 @echo "Starting build"
4635 $(CC) $(CFLAGS) -o $@ $<
4636 @echo "Build complete"
4637 chmod +x $@
4638"#
4639 .parse()
4640 .unwrap();
4641
4642 assert_eq!(complex_rule.recipe_count(), 4);
4643
4644 complex_rule.remove_command(0); complex_rule.remove_command(1); let final_recipes: Vec<_> = complex_rule.recipes().collect();
4649 assert_eq!(final_recipes.len(), 2);
4650 assert!(final_recipes[0].contains("$(CC)"));
4651 assert!(final_recipes[1].contains("chmod"));
4652 }
4653
4654 #[test]
4655 fn test_variable_definition_remove() {
4656 let makefile: Makefile = r#"VAR1 = value1
4657VAR2 = value2
4658VAR3 = value3
4659"#
4660 .parse()
4661 .unwrap();
4662
4663 assert_eq!(makefile.variable_definitions().count(), 3);
4665
4666 let mut var2 = makefile
4668 .variable_definitions()
4669 .nth(1)
4670 .expect("Should have second variable");
4671 assert_eq!(var2.name(), Some("VAR2".to_string()));
4672 var2.remove();
4673
4674 assert_eq!(makefile.variable_definitions().count(), 2);
4676 let var_names: Vec<_> = makefile
4677 .variable_definitions()
4678 .filter_map(|v| v.name())
4679 .collect();
4680 assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4681 }
4682
4683 #[test]
4684 fn test_variable_definition_set_value() {
4685 let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4686
4687 let mut var = makefile
4688 .variable_definitions()
4689 .next()
4690 .expect("Should have variable");
4691 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4692
4693 var.set_value("new_value");
4695
4696 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4698 assert!(makefile.code().contains("VAR = new_value"));
4699 }
4700
4701 #[test]
4702 fn test_variable_definition_set_value_preserves_format() {
4703 let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4704
4705 let mut var = makefile
4706 .variable_definitions()
4707 .next()
4708 .expect("Should have variable");
4709 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4710
4711 var.set_value("new_value");
4713
4714 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4716 let code = makefile.code();
4717 assert!(code.contains("export"), "Should preserve export prefix");
4718 assert!(code.contains(":="), "Should preserve := operator");
4719 assert!(code.contains("new_value"), "Should have new value");
4720 }
4721
4722 #[test]
4723 fn test_makefile_find_variable() {
4724 let makefile: Makefile = r#"VAR1 = value1
4725VAR2 = value2
4726VAR3 = value3
4727"#
4728 .parse()
4729 .unwrap();
4730
4731 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4733 assert_eq!(vars.len(), 1);
4734 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4735 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4736
4737 assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4739 }
4740
4741 #[test]
4742 fn test_makefile_find_variable_with_export() {
4743 let makefile: Makefile = r#"VAR1 = value1
4744export VAR2 := value2
4745VAR3 = value3
4746"#
4747 .parse()
4748 .unwrap();
4749
4750 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4752 assert_eq!(vars.len(), 1);
4753 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4754 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4755 }
4756
4757 #[test]
4758 fn test_variable_definition_is_export() {
4759 let makefile: Makefile = r#"VAR1 = value1
4760export VAR2 := value2
4761export VAR3 = value3
4762VAR4 := value4
4763"#
4764 .parse()
4765 .unwrap();
4766
4767 let vars: Vec<_> = makefile.variable_definitions().collect();
4768 assert_eq!(vars.len(), 4);
4769
4770 assert!(!vars[0].is_export());
4771 assert!(vars[1].is_export());
4772 assert!(vars[2].is_export());
4773 assert!(!vars[3].is_export());
4774 }
4775
4776 #[test]
4777 fn test_makefile_find_variable_multiple() {
4778 let makefile: Makefile = r#"VAR1 = value1
4779VAR1 = value2
4780VAR2 = other
4781VAR1 = value3
4782"#
4783 .parse()
4784 .unwrap();
4785
4786 let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4788 assert_eq!(vars.len(), 3);
4789 assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4790 assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4791 assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4792
4793 let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4795 assert_eq!(var2s.len(), 1);
4796 assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4797 }
4798
4799 #[test]
4800 fn test_variable_remove_and_find() {
4801 let makefile: Makefile = r#"VAR1 = value1
4802VAR2 = value2
4803VAR3 = value3
4804"#
4805 .parse()
4806 .unwrap();
4807
4808 let mut var2 = makefile
4810 .find_variable("VAR2")
4811 .next()
4812 .expect("Should find VAR2");
4813 var2.remove();
4814
4815 assert_eq!(makefile.find_variable("VAR2").count(), 0);
4817
4818 assert_eq!(makefile.find_variable("VAR1").count(), 1);
4820 assert_eq!(makefile.find_variable("VAR3").count(), 1);
4821 }
4822
4823 #[test]
4824 fn test_variable_remove_with_comment() {
4825 let makefile: Makefile = r#"VAR1 = value1
4826# This is a comment about VAR2
4827VAR2 = value2
4828VAR3 = value3
4829"#
4830 .parse()
4831 .unwrap();
4832
4833 let mut var2 = makefile
4835 .variable_definitions()
4836 .nth(1)
4837 .expect("Should have second variable");
4838 assert_eq!(var2.name(), Some("VAR2".to_string()));
4839 var2.remove();
4840
4841 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4843 }
4844
4845 #[test]
4846 fn test_variable_remove_with_multiple_comments() {
4847 let makefile: Makefile = r#"VAR1 = value1
4848# Comment line 1
4849# Comment line 2
4850# Comment line 3
4851VAR2 = value2
4852VAR3 = value3
4853"#
4854 .parse()
4855 .unwrap();
4856
4857 let mut var2 = makefile
4859 .variable_definitions()
4860 .nth(1)
4861 .expect("Should have second variable");
4862 var2.remove();
4863
4864 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4866 }
4867
4868 #[test]
4869 fn test_variable_remove_with_empty_line() {
4870 let makefile: Makefile = r#"VAR1 = value1
4871
4872# Comment about VAR2
4873VAR2 = value2
4874VAR3 = value3
4875"#
4876 .parse()
4877 .unwrap();
4878
4879 let mut var2 = makefile
4881 .variable_definitions()
4882 .nth(1)
4883 .expect("Should have second variable");
4884 var2.remove();
4885
4886 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4889 }
4890
4891 #[test]
4892 fn test_variable_remove_with_multiple_empty_lines() {
4893 let makefile: Makefile = r#"VAR1 = value1
4894
4895
4896# Comment about VAR2
4897VAR2 = value2
4898VAR3 = value3
4899"#
4900 .parse()
4901 .unwrap();
4902
4903 let mut var2 = makefile
4905 .variable_definitions()
4906 .nth(1)
4907 .expect("Should have second variable");
4908 var2.remove();
4909
4910 assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4913 }
4914
4915 #[test]
4916 fn test_rule_remove_with_comment() {
4917 let makefile: Makefile = r#"rule1:
4918 command1
4919
4920# Comment about rule2
4921rule2:
4922 command2
4923rule3:
4924 command3
4925"#
4926 .parse()
4927 .unwrap();
4928
4929 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4931 rule2.remove().unwrap();
4932
4933 assert_eq!(
4936 makefile.code(),
4937 "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4938 );
4939 }
4940
4941 #[test]
4942 fn test_variable_remove_preserves_shebang() {
4943 let makefile: Makefile = r#"#!/usr/bin/make -f
4944# This is a regular comment
4945VAR1 = value1
4946VAR2 = value2
4947"#
4948 .parse()
4949 .unwrap();
4950
4951 let mut var1 = makefile.variable_definitions().next().unwrap();
4953 var1.remove();
4954
4955 let code = makefile.code();
4957 assert!(code.starts_with("#!/usr/bin/make -f"));
4958 assert!(!code.contains("regular comment"));
4959 assert!(!code.contains("VAR1"));
4960 assert!(code.contains("VAR2"));
4961 }
4962
4963 #[test]
4964 fn test_variable_remove_preserves_subsequent_comments() {
4965 let makefile: Makefile = r#"VAR1 = value1
4966# Comment about VAR2
4967VAR2 = value2
4968
4969# Comment about VAR3
4970VAR3 = value3
4971"#
4972 .parse()
4973 .unwrap();
4974
4975 let mut var2 = makefile
4977 .variable_definitions()
4978 .nth(1)
4979 .expect("Should have second variable");
4980 var2.remove();
4981
4982 let code = makefile.code();
4984 assert_eq!(
4985 code,
4986 "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4987 );
4988 }
4989
4990 #[test]
4991 fn test_variable_remove_after_shebang_preserves_empty_line() {
4992 let makefile: Makefile = r#"#!/usr/bin/make -f
4993export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4994
4995%:
4996 dh $@
4997"#
4998 .parse()
4999 .unwrap();
5000
5001 let mut var = makefile.variable_definitions().next().unwrap();
5003 var.remove();
5004
5005 assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
5007 }
5008
5009 #[test]
5010 fn test_rule_add_prerequisite() {
5011 let mut rule: Rule = "target: dep1\n".parse().unwrap();
5012 rule.add_prerequisite("dep2").unwrap();
5013 assert_eq!(
5014 rule.prerequisites().collect::<Vec<_>>(),
5015 vec!["dep1", "dep2"]
5016 );
5017 assert_eq!(rule.to_string(), "target: dep1 dep2\n");
5019 }
5020
5021 #[test]
5022 fn test_rule_add_prerequisite_to_rule_without_prereqs() {
5023 let mut rule: Rule = "target:\n".parse().unwrap();
5025 rule.add_prerequisite("dep1").unwrap();
5026 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
5027 assert_eq!(rule.to_string(), "target: dep1\n");
5029 }
5030
5031 #[test]
5032 fn test_rule_remove_prerequisite() {
5033 let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
5034 assert!(rule.remove_prerequisite("dep2").unwrap());
5035 assert_eq!(
5036 rule.prerequisites().collect::<Vec<_>>(),
5037 vec!["dep1", "dep3"]
5038 );
5039 assert!(!rule.remove_prerequisite("nonexistent").unwrap());
5040 }
5041
5042 #[test]
5043 fn test_rule_set_prerequisites() {
5044 let mut rule: Rule = "target: old_dep\n".parse().unwrap();
5045 rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
5046 .unwrap();
5047 assert_eq!(
5048 rule.prerequisites().collect::<Vec<_>>(),
5049 vec!["new_dep1", "new_dep2"]
5050 );
5051 }
5052
5053 #[test]
5054 fn test_rule_set_prerequisites_empty() {
5055 let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
5056 rule.set_prerequisites(vec![]).unwrap();
5057 assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
5058 }
5059
5060 #[test]
5061 fn test_rule_add_target() {
5062 let mut rule: Rule = "target1: dep1\n".parse().unwrap();
5063 rule.add_target("target2").unwrap();
5064 assert_eq!(
5065 rule.targets().collect::<Vec<_>>(),
5066 vec!["target1", "target2"]
5067 );
5068 }
5069
5070 #[test]
5071 fn test_rule_set_targets() {
5072 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5073 rule.set_targets(vec!["new_target1", "new_target2"])
5074 .unwrap();
5075 assert_eq!(
5076 rule.targets().collect::<Vec<_>>(),
5077 vec!["new_target1", "new_target2"]
5078 );
5079 }
5080
5081 #[test]
5082 fn test_rule_set_targets_empty() {
5083 let mut rule: Rule = "target: dep1\n".parse().unwrap();
5084 let result = rule.set_targets(vec![]);
5085 assert!(result.is_err());
5086 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
5088 }
5089
5090 #[test]
5091 fn test_rule_has_target() {
5092 let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
5093 assert!(rule.has_target("target1"));
5094 assert!(rule.has_target("target2"));
5095 assert!(!rule.has_target("target3"));
5096 assert!(!rule.has_target("nonexistent"));
5097 }
5098
5099 #[test]
5100 fn test_rule_rename_target() {
5101 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5102 assert!(rule.rename_target("old_target", "new_target").unwrap());
5103 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
5104 assert!(!rule.rename_target("nonexistent", "something").unwrap());
5106 }
5107
5108 #[test]
5109 fn test_rule_rename_target_multiple() {
5110 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5111 assert!(rule.rename_target("target2", "renamed_target").unwrap());
5112 assert_eq!(
5113 rule.targets().collect::<Vec<_>>(),
5114 vec!["target1", "renamed_target", "target3"]
5115 );
5116 }
5117
5118 #[test]
5119 fn test_rule_remove_target() {
5120 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5121 assert!(rule.remove_target("target2").unwrap());
5122 assert_eq!(
5123 rule.targets().collect::<Vec<_>>(),
5124 vec!["target1", "target3"]
5125 );
5126 assert!(!rule.remove_target("nonexistent").unwrap());
5128 }
5129
5130 #[test]
5131 fn test_rule_remove_target_last() {
5132 let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
5133 let result = rule.remove_target("single_target");
5134 assert!(result.is_err());
5135 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
5137 }
5138
5139 #[test]
5140 fn test_rule_target_manipulation_preserves_prerequisites() {
5141 let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
5142
5143 rule.remove_target("target1").unwrap();
5145 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
5146 assert_eq!(
5147 rule.prerequisites().collect::<Vec<_>>(),
5148 vec!["dep1", "dep2"]
5149 );
5150 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5151
5152 rule.add_target("target3").unwrap();
5154 assert_eq!(
5155 rule.targets().collect::<Vec<_>>(),
5156 vec!["target2", "target3"]
5157 );
5158 assert_eq!(
5159 rule.prerequisites().collect::<Vec<_>>(),
5160 vec!["dep1", "dep2"]
5161 );
5162 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5163
5164 rule.rename_target("target2", "renamed").unwrap();
5166 assert_eq!(
5167 rule.targets().collect::<Vec<_>>(),
5168 vec!["renamed", "target3"]
5169 );
5170 assert_eq!(
5171 rule.prerequisites().collect::<Vec<_>>(),
5172 vec!["dep1", "dep2"]
5173 );
5174 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5175 }
5176
5177 #[test]
5178 fn test_rule_remove() {
5179 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5180 let rule = makefile.find_rule_by_target("rule1").unwrap();
5181 rule.remove().unwrap();
5182 assert_eq!(makefile.rules().count(), 1);
5183 assert!(makefile.find_rule_by_target("rule1").is_none());
5184 assert!(makefile.find_rule_by_target("rule2").is_some());
5185 }
5186
5187 #[test]
5188 fn test_rule_remove_last_trims_blank_lines() {
5189 let makefile: Makefile =
5191 "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
5192 .parse()
5193 .unwrap();
5194
5195 let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
5197 rule.remove().unwrap();
5198
5199 assert_eq!(makefile.code(), "%:\n\tdh $@\n");
5201 assert_eq!(makefile.rules().count(), 1);
5202 }
5203
5204 #[test]
5205 fn test_makefile_find_rule_by_target() {
5206 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5207 let rule = makefile.find_rule_by_target("rule2");
5208 assert!(rule.is_some());
5209 assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
5210 assert!(makefile.find_rule_by_target("nonexistent").is_none());
5211 }
5212
5213 #[test]
5214 fn test_makefile_find_rules_by_target() {
5215 let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
5216 .parse()
5217 .unwrap();
5218 assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
5219 assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
5220 assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
5221 }
5222
5223 #[test]
5224 fn test_makefile_find_rule_by_target_pattern_simple() {
5225 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5226 let rule = makefile.find_rule_by_target_pattern("foo.o");
5227 assert!(rule.is_some());
5228 assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
5229 }
5230
5231 #[test]
5232 fn test_makefile_find_rule_by_target_pattern_no_match() {
5233 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5234 let rule = makefile.find_rule_by_target_pattern("foo.c");
5235 assert!(rule.is_none());
5236 }
5237
5238 #[test]
5239 fn test_makefile_find_rule_by_target_pattern_exact() {
5240 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5241 let rule = makefile.find_rule_by_target_pattern("foo.o");
5242 assert!(rule.is_some());
5243 assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
5244 }
5245
5246 #[test]
5247 fn test_makefile_find_rule_by_target_pattern_prefix() {
5248 let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5249 let rule = makefile.find_rule_by_target_pattern("libfoo.a");
5250 assert!(rule.is_some());
5251 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
5252 }
5253
5254 #[test]
5255 fn test_makefile_find_rule_by_target_pattern_suffix() {
5256 let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5257 let rule = makefile.find_rule_by_target_pattern("foo_test.o");
5258 assert!(rule.is_some());
5259 assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
5260 }
5261
5262 #[test]
5263 fn test_makefile_find_rule_by_target_pattern_middle() {
5264 let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5265 let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
5266 assert!(rule.is_some());
5267 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
5268 }
5269
5270 #[test]
5271 fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
5272 let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
5273 let rule = makefile.find_rule_by_target_pattern("anything");
5274 assert!(rule.is_some());
5275 assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
5276 }
5277
5278 #[test]
5279 fn test_makefile_find_rules_by_target_pattern_multiple() {
5280 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
5281 .parse()
5282 .unwrap();
5283 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5284 assert_eq!(rules.len(), 2);
5285 }
5286
5287 #[test]
5288 fn test_makefile_find_rules_by_target_pattern_mixed() {
5289 let makefile: Makefile =
5290 "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
5291 .parse()
5292 .unwrap();
5293 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5294 assert_eq!(rules.len(), 2); let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
5296 assert_eq!(rules.len(), 1); }
5298
5299 #[test]
5300 fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
5301 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5302 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5303 assert_eq!(rules.len(), 1);
5304 let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
5305 assert_eq!(rules.len(), 0);
5306 }
5307
5308 #[test]
5309 fn test_matches_pattern_exact() {
5310 assert!(matches_pattern("foo.o", "foo.o"));
5311 assert!(!matches_pattern("foo.o", "bar.o"));
5312 }
5313
5314 #[test]
5315 fn test_matches_pattern_suffix() {
5316 assert!(matches_pattern("%.o", "foo.o"));
5317 assert!(matches_pattern("%.o", "bar.o"));
5318 assert!(matches_pattern("%.o", "baz/qux.o"));
5319 assert!(!matches_pattern("%.o", "foo.c"));
5320 }
5321
5322 #[test]
5323 fn test_matches_pattern_prefix() {
5324 assert!(matches_pattern("lib%.a", "libfoo.a"));
5325 assert!(matches_pattern("lib%.a", "libbar.a"));
5326 assert!(!matches_pattern("lib%.a", "foo.a"));
5327 assert!(!matches_pattern("lib%.a", "lib.a"));
5328 }
5329
5330 #[test]
5331 fn test_matches_pattern_middle() {
5332 assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
5333 assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
5334 assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
5335 assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
5336 }
5337
5338 #[test]
5339 fn test_matches_pattern_wildcard_only() {
5340 assert!(matches_pattern("%", "anything"));
5341 assert!(matches_pattern("%", "foo.o"));
5342 assert!(!matches_pattern("%", ""));
5344 }
5345
5346 #[test]
5347 fn test_matches_pattern_empty_stem() {
5348 assert!(!matches_pattern("%.o", ".o")); assert!(!matches_pattern("lib%", "lib")); assert!(!matches_pattern("lib%.a", "lib.a")); }
5353
5354 #[test]
5355 fn test_matches_pattern_multiple_wildcards_not_supported() {
5356 assert!(!matches_pattern("%foo%bar", "xfooybarz"));
5359 assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
5360 }
5361
5362 #[test]
5363 fn test_makefile_add_phony_target() {
5364 let mut makefile = Makefile::new();
5365 makefile.add_phony_target("clean").unwrap();
5366 assert!(makefile.is_phony("clean"));
5367 assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
5368 }
5369
5370 #[test]
5371 fn test_makefile_add_phony_target_existing() {
5372 let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
5373 makefile.add_phony_target("clean").unwrap();
5374 assert!(makefile.is_phony("test"));
5375 assert!(makefile.is_phony("clean"));
5376 let targets: Vec<_> = makefile.phony_targets().collect();
5377 assert!(targets.contains(&"test".to_string()));
5378 assert!(targets.contains(&"clean".to_string()));
5379 }
5380
5381 #[test]
5382 fn test_makefile_remove_phony_target() {
5383 let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5384 assert!(makefile.remove_phony_target("clean").unwrap());
5385 assert!(!makefile.is_phony("clean"));
5386 assert!(makefile.is_phony("test"));
5387 assert!(!makefile.remove_phony_target("nonexistent").unwrap());
5388 }
5389
5390 #[test]
5391 fn test_makefile_remove_phony_target_last() {
5392 let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
5393 assert!(makefile.remove_phony_target("clean").unwrap());
5394 assert!(!makefile.is_phony("clean"));
5395 assert!(makefile.find_rule_by_target(".PHONY").is_none());
5397 }
5398
5399 #[test]
5400 fn test_makefile_is_phony() {
5401 let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5402 assert!(makefile.is_phony("clean"));
5403 assert!(makefile.is_phony("test"));
5404 assert!(!makefile.is_phony("build"));
5405 }
5406
5407 #[test]
5408 fn test_makefile_phony_targets() {
5409 let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5410 let phony_targets: Vec<_> = makefile.phony_targets().collect();
5411 assert_eq!(phony_targets, vec!["clean", "test", "build"]);
5412 }
5413
5414 #[test]
5415 fn test_makefile_phony_targets_empty() {
5416 let makefile = Makefile::new();
5417 assert_eq!(makefile.phony_targets().count(), 0);
5418 }
5419
5420 #[test]
5421 fn test_makefile_remove_first_phony_target_no_extra_space() {
5422 let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5423 assert!(makefile.remove_phony_target("clean").unwrap());
5424 let result = makefile.to_string();
5425 assert_eq!(result, ".PHONY: test build\n");
5426 }
5427
5428 #[test]
5429 fn test_recipe_with_leading_comments_and_blank_lines() {
5430 let makefile_text = r#"#!/usr/bin/make
5434
5435%:
5436 dh $@
5437
5438override_dh_build:
5439 # The next line is empty
5440
5441 dh_python3
5442"#;
5443 let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
5444
5445 let rules: Vec<_> = makefile.rules().collect();
5446 assert_eq!(rules.len(), 2, "Expected 2 rules");
5447
5448 let rule0 = &rules[0];
5450 assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
5451 assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
5452
5453 let rule1 = &rules[1];
5455 assert_eq!(
5456 rule1.targets().collect::<Vec<_>>(),
5457 vec!["override_dh_build"]
5458 );
5459
5460 let recipes: Vec<_> = rule1.recipes().collect();
5462 assert!(
5463 !recipes.is_empty(),
5464 "Expected at least one recipe for override_dh_build, got none"
5465 );
5466 assert!(
5467 recipes.contains(&"dh_python3".to_string()),
5468 "Expected 'dh_python3' in recipes, got: {:?}",
5469 recipes
5470 );
5471 }
5472
5473 #[test]
5474 fn test_rule_parse_preserves_trailing_blank_lines() {
5475 let input = r#"override_dh_systemd_enable:
5478 dh_systemd_enable -pracoon
5479
5480override_dh_install:
5481 dh_install
5482"#;
5483
5484 let mut mf: Makefile = input.parse().unwrap();
5485
5486 let rule = mf.rules().next().unwrap();
5488 let rule_text = rule.to_string();
5489
5490 assert_eq!(
5492 rule_text,
5493 "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
5494 );
5495
5496 let modified =
5498 rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
5499
5500 let new_rule: Rule = modified.parse().unwrap();
5502 assert_eq!(
5503 new_rule.to_string(),
5504 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
5505 );
5506
5507 mf.replace_rule(0, new_rule).unwrap();
5509
5510 let output = mf.to_string();
5512 assert!(
5513 output.contains(
5514 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
5515 ),
5516 "Blank line between rules should be preserved. Got: {:?}",
5517 output
5518 );
5519 }
5520
5521 #[test]
5522 fn test_rule_parse_round_trip_with_trailing_newlines() {
5523 let test_cases = vec![
5525 "rule:\n\tcommand\n", "rule:\n\tcommand\n\n", "rule:\n\tcommand\n\n\n", ];
5529
5530 for rule_text in test_cases {
5531 let rule: Rule = rule_text.parse().unwrap();
5532 let result = rule.to_string();
5533 assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
5534 }
5535 }
5536
5537 #[test]
5538 fn test_rule_clone() {
5539 let rule_text = "rule:\n\tcommand\n\n";
5541 let rule: Rule = rule_text.parse().unwrap();
5542 let cloned = rule.clone();
5543
5544 assert_eq!(rule.to_string(), cloned.to_string());
5546 assert_eq!(rule.to_string(), rule_text);
5547 assert_eq!(cloned.to_string(), rule_text);
5548
5549 assert_eq!(
5551 rule.targets().collect::<Vec<_>>(),
5552 cloned.targets().collect::<Vec<_>>()
5553 );
5554 assert_eq!(
5555 rule.recipes().collect::<Vec<_>>(),
5556 cloned.recipes().collect::<Vec<_>>()
5557 );
5558 }
5559
5560 #[test]
5561 fn test_makefile_clone() {
5562 let input = "VAR = value\n\nrule:\n\tcommand\n";
5564 let makefile: Makefile = input.parse().unwrap();
5565 let cloned = makefile.clone();
5566
5567 assert_eq!(makefile.to_string(), cloned.to_string());
5569 assert_eq!(makefile.to_string(), input);
5570
5571 assert_eq!(makefile.rules().count(), cloned.rules().count());
5573
5574 assert_eq!(
5576 makefile.variable_definitions().count(),
5577 cloned.variable_definitions().count()
5578 );
5579 }
5580
5581 #[test]
5582 fn test_conditional_with_recipe_line() {
5583 let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
5585 let parsed = parse(input, None);
5586
5587 assert!(
5589 parsed.errors.is_empty(),
5590 "Expected no parse errors, but got: {:?}",
5591 parsed.errors
5592 );
5593
5594 let mf = parsed.root();
5596 assert_eq!(mf.code(), input);
5597 }
5598
5599 #[test]
5600 fn test_conditional_in_rule_recipe() {
5601 let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
5603 let parsed = parse(input, None);
5604
5605 assert!(
5607 parsed.errors.is_empty(),
5608 "Expected no parse errors, but got: {:?}",
5609 parsed.errors
5610 );
5611
5612 let mf = parsed.root();
5614 assert_eq!(mf.code(), input);
5615
5616 assert_eq!(mf.rules().count(), 1);
5618 }
5619
5620 #[test]
5621 fn test_rule_items() {
5622 use crate::RuleItem;
5623
5624 let input = r#"test:
5626 echo "before"
5627ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
5628 ./run-tests
5629endif
5630 echo "after"
5631"#;
5632 let rule: Rule = input.parse().unwrap();
5633
5634 let items: Vec<_> = rule.items().collect();
5635 assert_eq!(
5636 items.len(),
5637 3,
5638 "Expected 3 items: recipe, conditional, recipe"
5639 );
5640
5641 match &items[0] {
5643 RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
5644 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5645 }
5646
5647 match &items[1] {
5649 RuleItem::Conditional(c) => {
5650 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5651 }
5652 RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
5653 }
5654
5655 match &items[2] {
5657 RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5658 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5659 }
5660
5661 let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5663 let simple_items: Vec<_> = simple_rule.items().collect();
5664 assert_eq!(simple_items.len(), 2);
5665
5666 match &simple_items[0] {
5667 RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5668 _ => panic!("Expected recipe"),
5669 }
5670
5671 match &simple_items[1] {
5672 RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5673 _ => panic!("Expected recipe"),
5674 }
5675
5676 let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5678 .parse()
5679 .unwrap();
5680 let cond_items: Vec<_> = cond_only.items().collect();
5681 assert_eq!(cond_items.len(), 1);
5682
5683 match &cond_items[0] {
5684 RuleItem::Conditional(c) => {
5685 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5686 }
5687 _ => panic!("Expected conditional"),
5688 }
5689 }
5690
5691 #[test]
5692 fn test_conditionals_iterator() {
5693 let makefile: Makefile = r#"ifdef DEBUG
5694VAR = debug
5695endif
5696
5697ifndef RELEASE
5698OTHER = dev
5699endif
5700"#
5701 .parse()
5702 .unwrap();
5703
5704 let conditionals: Vec<_> = makefile.conditionals().collect();
5705 assert_eq!(conditionals.len(), 2);
5706
5707 assert_eq!(
5708 conditionals[0].conditional_type(),
5709 Some("ifdef".to_string())
5710 );
5711 assert_eq!(
5712 conditionals[1].conditional_type(),
5713 Some("ifndef".to_string())
5714 );
5715 }
5716
5717 #[test]
5718 fn test_conditional_type_and_condition() {
5719 let makefile: Makefile = r#"ifdef DEBUG
5720VAR = debug
5721endif
5722"#
5723 .parse()
5724 .unwrap();
5725
5726 let conditional = makefile.conditionals().next().unwrap();
5727 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5728 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5729 }
5730
5731 #[test]
5732 fn test_conditional_has_else() {
5733 let makefile_with_else: Makefile = r#"ifdef DEBUG
5734VAR = debug
5735else
5736VAR = release
5737endif
5738"#
5739 .parse()
5740 .unwrap();
5741
5742 let conditional = makefile_with_else.conditionals().next().unwrap();
5743 assert!(conditional.has_else());
5744
5745 let makefile_without_else: Makefile = r#"ifdef DEBUG
5746VAR = debug
5747endif
5748"#
5749 .parse()
5750 .unwrap();
5751
5752 let conditional = makefile_without_else.conditionals().next().unwrap();
5753 assert!(!conditional.has_else());
5754 }
5755
5756 #[test]
5757 fn test_conditional_if_body() {
5758 let makefile: Makefile = r#"ifdef DEBUG
5759VAR = debug
5760endif
5761"#
5762 .parse()
5763 .unwrap();
5764
5765 let conditional = makefile.conditionals().next().unwrap();
5766 let if_body = conditional.if_body();
5767 assert!(if_body.is_some());
5768 assert!(if_body.unwrap().contains("VAR = debug"));
5769 }
5770
5771 #[test]
5772 fn test_conditional_else_body() {
5773 let makefile: Makefile = r#"ifdef DEBUG
5774VAR = debug
5775else
5776VAR = release
5777endif
5778"#
5779 .parse()
5780 .unwrap();
5781
5782 let conditional = makefile.conditionals().next().unwrap();
5783 let else_body = conditional.else_body();
5784 assert!(else_body.is_some());
5785 assert!(else_body.unwrap().contains("VAR = release"));
5786 }
5787
5788 #[test]
5789 fn test_add_conditional_ifdef() {
5790 let mut makefile = Makefile::new();
5791 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5792 assert!(result.is_ok());
5793
5794 let code = makefile.to_string();
5795 assert!(code.contains("ifdef DEBUG"));
5796 assert!(code.contains("VAR = debug"));
5797 assert!(code.contains("endif"));
5798 }
5799
5800 #[test]
5801 fn test_add_conditional_with_else() {
5802 let mut makefile = Makefile::new();
5803 let result =
5804 makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5805 assert!(result.is_ok());
5806
5807 let code = makefile.to_string();
5808 assert!(code.contains("ifdef DEBUG"));
5809 assert!(code.contains("VAR = debug"));
5810 assert!(code.contains("else"));
5811 assert!(code.contains("VAR = release"));
5812 assert!(code.contains("endif"));
5813 }
5814
5815 #[test]
5816 fn test_add_conditional_invalid_type() {
5817 let mut makefile = Makefile::new();
5818 let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5819 assert!(result.is_err());
5820 }
5821
5822 #[test]
5823 fn test_add_conditional_formatting() {
5824 let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5825 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5826 assert!(result.is_ok());
5827
5828 let code = makefile.to_string();
5829 assert!(code.contains("\n\nifdef DEBUG"));
5831 }
5832
5833 #[test]
5834 fn test_conditional_remove() {
5835 let makefile: Makefile = r#"ifdef DEBUG
5836VAR = debug
5837endif
5838
5839VAR2 = value2
5840"#
5841 .parse()
5842 .unwrap();
5843
5844 let mut conditional = makefile.conditionals().next().unwrap();
5845 let result = conditional.remove();
5846 assert!(result.is_ok());
5847
5848 let code = makefile.to_string();
5849 assert!(!code.contains("ifdef DEBUG"));
5850 assert!(!code.contains("VAR = debug"));
5851 assert!(code.contains("VAR2 = value2"));
5852 }
5853
5854 #[test]
5855 fn test_add_conditional_ifndef() {
5856 let mut makefile = Makefile::new();
5857 let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5858 assert!(result.is_ok());
5859
5860 let code = makefile.to_string();
5861 assert!(code.contains("ifndef NDEBUG"));
5862 assert!(code.contains("VAR = enabled"));
5863 assert!(code.contains("endif"));
5864 }
5865
5866 #[test]
5867 fn test_add_conditional_ifeq() {
5868 let mut makefile = Makefile::new();
5869 let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5870 assert!(result.is_ok());
5871
5872 let code = makefile.to_string();
5873 assert!(code.contains("ifeq ($(OS),Linux)"));
5874 assert!(code.contains("VAR = linux"));
5875 assert!(code.contains("endif"));
5876 }
5877
5878 #[test]
5879 fn test_add_conditional_ifneq() {
5880 let mut makefile = Makefile::new();
5881 let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5882 assert!(result.is_ok());
5883
5884 let code = makefile.to_string();
5885 assert!(code.contains("ifneq ($(OS),Windows)"));
5886 assert!(code.contains("VAR = unix"));
5887 assert!(code.contains("endif"));
5888 }
5889
5890 #[test]
5891 fn test_conditional_api_integration() {
5892 let mut makefile: Makefile = r#"VAR1 = value1
5894
5895rule1:
5896 command1
5897"#
5898 .parse()
5899 .unwrap();
5900
5901 makefile
5903 .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5904 .unwrap();
5905
5906 assert_eq!(makefile.conditionals().count(), 1);
5908 let conditional = makefile.conditionals().next().unwrap();
5909 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5910 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5911 assert!(conditional.has_else());
5912
5913 assert_eq!(makefile.variable_definitions().count(), 1);
5915 assert_eq!(makefile.rules().count(), 1);
5916 }
5917
5918 #[test]
5919 fn test_conditional_if_items() {
5920 let makefile: Makefile = r#"ifdef DEBUG
5921VAR = debug
5922rule:
5923 command
5924endif
5925"#
5926 .parse()
5927 .unwrap();
5928
5929 let cond = makefile.conditionals().next().unwrap();
5930 let items: Vec<_> = cond.if_items().collect();
5931 assert_eq!(items.len(), 2); match &items[0] {
5934 MakefileItem::Variable(v) => {
5935 assert_eq!(v.name(), Some("VAR".to_string()));
5936 }
5937 _ => panic!("Expected variable"),
5938 }
5939
5940 match &items[1] {
5941 MakefileItem::Rule(r) => {
5942 assert!(r.targets().any(|t| t == "rule"));
5943 }
5944 _ => panic!("Expected rule"),
5945 }
5946 }
5947
5948 #[test]
5949 fn test_conditional_else_items() {
5950 let makefile: Makefile = r#"ifdef DEBUG
5951VAR = debug
5952else
5953VAR2 = release
5954rule2:
5955 command
5956endif
5957"#
5958 .parse()
5959 .unwrap();
5960
5961 let cond = makefile.conditionals().next().unwrap();
5962 let items: Vec<_> = cond.else_items().collect();
5963 assert_eq!(items.len(), 2); match &items[0] {
5966 MakefileItem::Variable(v) => {
5967 assert_eq!(v.name(), Some("VAR2".to_string()));
5968 }
5969 _ => panic!("Expected variable"),
5970 }
5971
5972 match &items[1] {
5973 MakefileItem::Rule(r) => {
5974 assert!(r.targets().any(|t| t == "rule2"));
5975 }
5976 _ => panic!("Expected rule"),
5977 }
5978 }
5979
5980 #[test]
5981 fn test_conditional_add_if_item() {
5982 let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5983 let mut cond = makefile.conditionals().next().unwrap();
5984
5985 let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5987 let var = temp.variable_definitions().next().unwrap();
5988 cond.add_if_item(MakefileItem::Variable(var));
5989
5990 let code = makefile.to_string();
5991 assert!(code.contains("CFLAGS = -g"));
5992
5993 let cond = makefile.conditionals().next().unwrap();
5995 assert_eq!(cond.if_items().count(), 1);
5996 }
5997
5998 #[test]
5999 fn test_conditional_add_else_item() {
6000 let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
6001 let mut cond = makefile.conditionals().next().unwrap();
6002
6003 let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
6005 let var = temp.variable_definitions().next().unwrap();
6006 cond.add_else_item(MakefileItem::Variable(var));
6007
6008 let code = makefile.to_string();
6009 assert!(code.contains("else"));
6010 assert!(code.contains("CFLAGS = -O2"));
6011
6012 let cond = makefile.conditionals().next().unwrap();
6014 assert_eq!(cond.else_items().count(), 1);
6015 }
6016
6017 #[test]
6018 fn test_add_conditional_with_items() {
6019 let mut makefile = Makefile::new();
6020
6021 let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
6023 let var1 = temp1.variable_definitions().next().unwrap();
6024
6025 let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
6026 let var2 = temp2.variable_definitions().next().unwrap();
6027
6028 let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
6029 let rule1 = temp3.rules().next().unwrap();
6030
6031 let result = makefile.add_conditional_with_items(
6032 "ifdef",
6033 "DEBUG",
6034 vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
6035 Some(vec![MakefileItem::Variable(var2)]),
6036 );
6037
6038 assert!(result.is_ok());
6039
6040 let code = makefile.to_string();
6041 assert!(code.contains("ifdef DEBUG"));
6042 assert!(code.contains("CFLAGS = -g"));
6043 assert!(code.contains("debug:"));
6044 assert!(code.contains("else"));
6045 assert!(code.contains("CFLAGS = -O2"));
6046 }
6047
6048 #[test]
6049 fn test_conditional_items_with_nested_conditional() {
6050 let makefile: Makefile = r#"ifdef DEBUG
6051VAR = debug
6052ifdef VERBOSE
6053 VAR2 = verbose
6054endif
6055endif
6056"#
6057 .parse()
6058 .unwrap();
6059
6060 let cond = makefile.conditionals().next().unwrap();
6061 let items: Vec<_> = cond.if_items().collect();
6062 assert_eq!(items.len(), 2); match &items[0] {
6065 MakefileItem::Variable(v) => {
6066 assert_eq!(v.name(), Some("VAR".to_string()));
6067 }
6068 _ => panic!("Expected variable"),
6069 }
6070
6071 match &items[1] {
6072 MakefileItem::Conditional(c) => {
6073 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6074 }
6075 _ => panic!("Expected conditional"),
6076 }
6077 }
6078
6079 #[test]
6080 fn test_conditional_items_with_include() {
6081 let makefile: Makefile = r#"ifdef DEBUG
6082include debug.mk
6083VAR = debug
6084endif
6085"#
6086 .parse()
6087 .unwrap();
6088
6089 let cond = makefile.conditionals().next().unwrap();
6090 let items: Vec<_> = cond.if_items().collect();
6091 assert_eq!(items.len(), 2); match &items[0] {
6094 MakefileItem::Include(i) => {
6095 assert_eq!(i.path(), Some("debug.mk".to_string()));
6096 }
6097 _ => panic!("Expected include"),
6098 }
6099
6100 match &items[1] {
6101 MakefileItem::Variable(v) => {
6102 assert_eq!(v.name(), Some("VAR".to_string()));
6103 }
6104 _ => panic!("Expected variable"),
6105 }
6106 }
6107
6108 #[test]
6109 fn test_makefile_items_iterator() {
6110 let makefile: Makefile = r#"VAR = value
6111ifdef DEBUG
6112CFLAGS = -g
6113endif
6114rule:
6115 command
6116include common.mk
6117"#
6118 .parse()
6119 .unwrap();
6120
6121 assert_eq!(makefile.variable_definitions().count(), 2);
6124 assert_eq!(makefile.conditionals().count(), 1);
6125 assert_eq!(makefile.rules().count(), 1);
6126
6127 let items: Vec<_> = makefile.items().collect();
6128 assert!(
6130 items.len() >= 3,
6131 "Expected at least 3 items, got {}",
6132 items.len()
6133 );
6134
6135 match &items[0] {
6136 MakefileItem::Variable(v) => {
6137 assert_eq!(v.name(), Some("VAR".to_string()));
6138 }
6139 _ => panic!("Expected variable at position 0"),
6140 }
6141
6142 match &items[1] {
6143 MakefileItem::Conditional(c) => {
6144 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6145 }
6146 _ => panic!("Expected conditional at position 1"),
6147 }
6148
6149 match &items[2] {
6150 MakefileItem::Rule(r) => {
6151 let targets: Vec<_> = r.targets().collect();
6152 assert_eq!(targets, vec!["rule"]);
6153 }
6154 _ => panic!("Expected rule at position 2"),
6155 }
6156 }
6157
6158 #[test]
6159 fn test_conditional_unwrap() {
6160 let makefile: Makefile = r#"ifdef DEBUG
6161VAR = debug
6162rule:
6163 command
6164endif
6165"#
6166 .parse()
6167 .unwrap();
6168
6169 let mut cond = makefile.conditionals().next().unwrap();
6170 cond.unwrap().unwrap();
6171
6172 let code = makefile.to_string();
6173 let expected = "VAR = debug\nrule:\n\tcommand\n";
6174 assert_eq!(code, expected);
6175
6176 assert_eq!(makefile.conditionals().count(), 0);
6178
6179 assert_eq!(makefile.variable_definitions().count(), 1);
6181 assert_eq!(makefile.rules().count(), 1);
6182 }
6183
6184 #[test]
6185 fn test_conditional_unwrap_with_else_fails() {
6186 let makefile: Makefile = r#"ifdef DEBUG
6187VAR = debug
6188else
6189VAR = release
6190endif
6191"#
6192 .parse()
6193 .unwrap();
6194
6195 let mut cond = makefile.conditionals().next().unwrap();
6196 let result = cond.unwrap();
6197
6198 assert!(result.is_err());
6199 assert!(result
6200 .unwrap_err()
6201 .to_string()
6202 .contains("Cannot unwrap conditional with else clause"));
6203 }
6204
6205 #[test]
6206 fn test_conditional_unwrap_nested() {
6207 let makefile: Makefile = r#"ifdef OUTER
6208VAR = outer
6209ifdef INNER
6210VAR2 = inner
6211endif
6212endif
6213"#
6214 .parse()
6215 .unwrap();
6216
6217 let mut outer_cond = makefile.conditionals().next().unwrap();
6219 outer_cond.unwrap().unwrap();
6220
6221 let code = makefile.to_string();
6222 let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
6223 assert_eq!(code, expected);
6224 }
6225
6226 #[test]
6227 fn test_conditional_unwrap_empty() {
6228 let makefile: Makefile = r#"ifdef DEBUG
6229endif
6230"#
6231 .parse()
6232 .unwrap();
6233
6234 let mut cond = makefile.conditionals().next().unwrap();
6235 cond.unwrap().unwrap();
6236
6237 let code = makefile.to_string();
6238 assert_eq!(code, "");
6239 }
6240
6241 #[test]
6242 fn test_rule_parent() {
6243 let makefile: Makefile = r#"all:
6244 echo "test"
6245"#
6246 .parse()
6247 .unwrap();
6248
6249 let rule = makefile.rules().next().unwrap();
6250 let parent = rule.parent();
6251 assert!(parent.is_none());
6253 }
6254
6255 #[test]
6256 fn test_item_parent_in_conditional() {
6257 let makefile: Makefile = r#"ifdef DEBUG
6258VAR = debug
6259rule:
6260 command
6261endif
6262"#
6263 .parse()
6264 .unwrap();
6265
6266 let cond = makefile.conditionals().next().unwrap();
6267
6268 let items: Vec<_> = cond.if_items().collect();
6270 assert_eq!(items.len(), 2);
6271
6272 if let MakefileItem::Variable(var) = &items[0] {
6274 let parent = var.parent();
6275 assert!(parent.is_some());
6276 if let Some(MakefileItem::Conditional(_)) = parent {
6277 } else {
6279 panic!("Expected variable parent to be a Conditional");
6280 }
6281 } else {
6282 panic!("Expected first item to be a Variable");
6283 }
6284
6285 if let MakefileItem::Rule(rule) = &items[1] {
6287 let parent = rule.parent();
6288 assert!(parent.is_some());
6289 if let Some(MakefileItem::Conditional(_)) = parent {
6290 } else {
6292 panic!("Expected rule parent to be a Conditional");
6293 }
6294 } else {
6295 panic!("Expected second item to be a Rule");
6296 }
6297 }
6298
6299 #[test]
6300 fn test_nested_conditional_parent() {
6301 let makefile: Makefile = r#"ifdef OUTER
6302VAR = outer
6303ifdef INNER
6304VAR2 = inner
6305endif
6306endif
6307"#
6308 .parse()
6309 .unwrap();
6310
6311 let outer_cond = makefile.conditionals().next().unwrap();
6312
6313 let items: Vec<_> = outer_cond.if_items().collect();
6315
6316 let inner_cond = items
6318 .iter()
6319 .find_map(|item| {
6320 if let MakefileItem::Conditional(c) = item {
6321 Some(c)
6322 } else {
6323 None
6324 }
6325 })
6326 .unwrap();
6327
6328 let parent = inner_cond.parent();
6330 assert!(parent.is_some());
6331 if let Some(MakefileItem::Conditional(_)) = parent {
6332 } else {
6334 panic!("Expected inner conditional's parent to be a Conditional");
6335 }
6336 }
6337
6338 #[test]
6339 fn test_line_col() {
6340 let text = r#"# Comment at line 0
6341VAR1 = value1
6342VAR2 = value2
6343
6344rule1: dep1 dep2
6345 command1
6346 command2
6347
6348rule2:
6349 command3
6350
6351ifdef DEBUG
6352CFLAGS = -g
6353endif
6354"#;
6355 let makefile: Makefile = text.parse().unwrap();
6356
6357 let vars: Vec<_> = makefile.variable_definitions().collect();
6360 assert_eq!(vars.len(), 3);
6361
6362 assert_eq!(vars[0].line(), 1);
6364 assert_eq!(vars[0].column(), 0);
6365 assert_eq!(vars[0].line_col(), (1, 0));
6366
6367 assert_eq!(vars[1].line(), 2);
6369 assert_eq!(vars[1].column(), 0);
6370
6371 assert_eq!(vars[2].line(), 12);
6373 assert_eq!(vars[2].column(), 0);
6374
6375 let rules: Vec<_> = makefile.rules().collect();
6377 assert_eq!(rules.len(), 2);
6378
6379 assert_eq!(rules[0].line(), 4);
6381 assert_eq!(rules[0].column(), 0);
6382 assert_eq!(rules[0].line_col(), (4, 0));
6383
6384 assert_eq!(rules[1].line(), 8);
6386 assert_eq!(rules[1].column(), 0);
6387
6388 let conditionals: Vec<_> = makefile.conditionals().collect();
6390 assert_eq!(conditionals.len(), 1);
6391
6392 assert_eq!(conditionals[0].line(), 11);
6394 assert_eq!(conditionals[0].column(), 0);
6395 assert_eq!(conditionals[0].line_col(), (11, 0));
6396 }
6397
6398 #[test]
6399 fn test_line_col_multiline() {
6400 let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
6401 let makefile: Makefile = text.parse().unwrap();
6402
6403 let vars: Vec<_> = makefile.variable_definitions().collect();
6405 assert_eq!(vars.len(), 1);
6406 assert_eq!(vars[0].line(), 0);
6407 assert_eq!(vars[0].column(), 0);
6408
6409 let rules: Vec<_> = makefile.rules().collect();
6411 assert_eq!(rules.len(), 1);
6412 assert_eq!(rules[0].line(), 4);
6413 assert_eq!(rules[0].column(), 0);
6414 }
6415
6416 #[test]
6417 fn test_line_col_includes() {
6418 let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
6419 let makefile: Makefile = text.parse().unwrap();
6420
6421 let vars: Vec<_> = makefile.variable_definitions().collect();
6423 assert_eq!(vars[0].line(), 0);
6424
6425 let includes: Vec<_> = makefile.includes().collect();
6427 assert_eq!(includes.len(), 2);
6428 assert_eq!(includes[0].line(), 2);
6429 assert_eq!(includes[0].column(), 0);
6430 assert_eq!(includes[1].line(), 3);
6431 assert_eq!(includes[1].column(), 0);
6432 }
6433
6434 #[test]
6435 fn test_conditional_in_rule_vs_toplevel() {
6436 let text1 = r#"rule:
6438 command
6439ifeq (,$(X))
6440 test
6441endif
6442"#;
6443 let makefile: Makefile = text1.parse().unwrap();
6444 let rules: Vec<_> = makefile.rules().collect();
6445 let conditionals: Vec<_> = makefile.conditionals().collect();
6446
6447 assert_eq!(rules.len(), 1);
6448 assert_eq!(
6449 conditionals.len(),
6450 0,
6451 "Conditional should be part of rule, not top-level"
6452 );
6453
6454 let text2 = r#"rule:
6456 command
6457
6458ifeq (,$(X))
6459 test
6460endif
6461"#;
6462 let makefile: Makefile = text2.parse().unwrap();
6463 let rules: Vec<_> = makefile.rules().collect();
6464 let conditionals: Vec<_> = makefile.conditionals().collect();
6465
6466 assert_eq!(rules.len(), 1);
6467 assert_eq!(
6468 conditionals.len(),
6469 1,
6470 "Conditional after blank line should be top-level"
6471 );
6472 assert_eq!(conditionals[0].line(), 3);
6473 }
6474
6475 #[test]
6476 fn test_nested_conditionals_line_tracking() {
6477 let text = r#"ifdef OUTER
6478VAR1 = value1
6479ifdef INNER
6480VAR2 = value2
6481endif
6482VAR3 = value3
6483endif
6484"#;
6485 let makefile: Makefile = text.parse().unwrap();
6486
6487 let conditionals: Vec<_> = makefile.conditionals().collect();
6488 assert_eq!(
6489 conditionals.len(),
6490 1,
6491 "Only outer conditional should be top-level"
6492 );
6493 assert_eq!(conditionals[0].line(), 0);
6494 assert_eq!(conditionals[0].column(), 0);
6495 }
6496
6497 #[test]
6498 fn test_conditional_else_line_tracking() {
6499 let text = r#"VAR1 = before
6500
6501ifdef DEBUG
6502DEBUG_FLAGS = -g
6503else
6504DEBUG_FLAGS = -O2
6505endif
6506
6507VAR2 = after
6508"#;
6509 let makefile: Makefile = text.parse().unwrap();
6510
6511 let conditionals: Vec<_> = makefile.conditionals().collect();
6512 assert_eq!(conditionals.len(), 1);
6513 assert_eq!(conditionals[0].line(), 2);
6514 assert_eq!(conditionals[0].column(), 0);
6515 }
6516
6517 #[test]
6518 fn test_broken_conditional_endif_without_if() {
6519 let text = "VAR = value\nendif\n";
6521 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6522
6523 let vars: Vec<_> = makefile.variable_definitions().collect();
6525 assert_eq!(vars.len(), 1);
6526 assert_eq!(vars[0].line(), 0);
6527 }
6528
6529 #[test]
6530 fn test_broken_conditional_else_without_if() {
6531 let text = "VAR = value\nelse\nVAR2 = other\n";
6533 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6534
6535 let vars: Vec<_> = makefile.variable_definitions().collect();
6537 assert!(!vars.is_empty(), "Should parse at least the first variable");
6538 assert_eq!(vars[0].line(), 0);
6539 }
6540
6541 #[test]
6542 fn test_broken_conditional_missing_endif() {
6543 let text = r#"ifdef DEBUG
6545DEBUG_FLAGS = -g
6546VAR = value
6547"#;
6548 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6549
6550 assert!(makefile.code().contains("ifdef DEBUG"));
6552 }
6553
6554 #[test]
6555 fn test_multiple_conditionals_line_tracking() {
6556 let text = r#"ifdef A
6557VAR_A = a
6558endif
6559
6560ifdef B
6561VAR_B = b
6562endif
6563
6564ifdef C
6565VAR_C = c
6566endif
6567"#;
6568 let makefile: Makefile = text.parse().unwrap();
6569
6570 let conditionals: Vec<_> = makefile.conditionals().collect();
6571 assert_eq!(conditionals.len(), 3);
6572 assert_eq!(conditionals[0].line(), 0);
6573 assert_eq!(conditionals[1].line(), 4);
6574 assert_eq!(conditionals[2].line(), 8);
6575 }
6576
6577 #[test]
6578 fn test_conditional_with_multiple_else_ifeq() {
6579 let text = r#"ifeq ($(OS),Windows)
6580EXT = .exe
6581else ifeq ($(OS),Linux)
6582EXT = .bin
6583else
6584EXT = .out
6585endif
6586"#;
6587 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6588
6589 let conditionals: Vec<_> = makefile.conditionals().collect();
6590 assert_eq!(conditionals.len(), 1);
6591 assert_eq!(conditionals[0].line(), 0);
6592 assert_eq!(conditionals[0].column(), 0);
6593 }
6594
6595 #[test]
6596 fn test_conditional_types_line_tracking() {
6597 let text = r#"ifdef VAR1
6598A = 1
6599endif
6600
6601ifndef VAR2
6602B = 2
6603endif
6604
6605ifeq ($(X),y)
6606C = 3
6607endif
6608
6609ifneq ($(Y),n)
6610D = 4
6611endif
6612"#;
6613 let makefile: Makefile = text.parse().unwrap();
6614
6615 let conditionals: Vec<_> = makefile.conditionals().collect();
6616 assert_eq!(conditionals.len(), 4);
6617
6618 assert_eq!(conditionals[0].line(), 0); assert_eq!(
6620 conditionals[0].conditional_type(),
6621 Some("ifdef".to_string())
6622 );
6623
6624 assert_eq!(conditionals[1].line(), 4); assert_eq!(
6626 conditionals[1].conditional_type(),
6627 Some("ifndef".to_string())
6628 );
6629
6630 assert_eq!(conditionals[2].line(), 8); assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
6632
6633 assert_eq!(conditionals[3].line(), 12); assert_eq!(
6635 conditionals[3].conditional_type(),
6636 Some("ifneq".to_string())
6637 );
6638 }
6639
6640 #[test]
6641 fn test_conditional_in_rule_with_recipes() {
6642 let text = r#"test:
6643 echo "start"
6644ifdef VERBOSE
6645 echo "verbose mode"
6646endif
6647 echo "end"
6648"#;
6649 let makefile: Makefile = text.parse().unwrap();
6650
6651 let rules: Vec<_> = makefile.rules().collect();
6652 let conditionals: Vec<_> = makefile.conditionals().collect();
6653
6654 assert_eq!(rules.len(), 1);
6655 assert_eq!(rules[0].line(), 0);
6656 assert_eq!(conditionals.len(), 0);
6658 }
6659
6660 #[test]
6661 fn test_broken_conditional_double_else() {
6662 let text = r#"ifdef DEBUG
6664A = 1
6665else
6666B = 2
6667else
6668C = 3
6669endif
6670"#;
6671 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6672
6673 assert!(makefile.code().contains("ifdef DEBUG"));
6675 }
6676
6677 #[test]
6678 fn test_broken_conditional_mismatched_nesting() {
6679 let text = r#"ifdef A
6681VAR = value
6682endif
6683endif
6684"#;
6685 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6686
6687 let conditionals: Vec<_> = makefile.conditionals().collect();
6690 assert!(
6691 !conditionals.is_empty(),
6692 "Should parse at least the first conditional"
6693 );
6694 }
6695
6696 #[test]
6697 fn test_conditional_with_comment_line_tracking() {
6698 let text = r#"# This is a comment
6699ifdef DEBUG
6700# Another comment
6701CFLAGS = -g
6702endif
6703# Final comment
6704"#;
6705 let makefile: Makefile = text.parse().unwrap();
6706
6707 let conditionals: Vec<_> = makefile.conditionals().collect();
6708 assert_eq!(conditionals.len(), 1);
6709 assert_eq!(conditionals[0].line(), 1);
6710 assert_eq!(conditionals[0].column(), 0);
6711 }
6712
6713 #[test]
6714 fn test_conditional_after_variable_with_blank_lines() {
6715 let text = r#"VAR1 = value1
6716
6717
6718ifdef DEBUG
6719VAR2 = value2
6720endif
6721"#;
6722 let makefile: Makefile = text.parse().unwrap();
6723
6724 let vars: Vec<_> = makefile.variable_definitions().collect();
6725 let conditionals: Vec<_> = makefile.conditionals().collect();
6726
6727 assert_eq!(vars.len(), 2);
6729 assert_eq!(vars[0].line(), 0); assert_eq!(vars[1].line(), 4); assert_eq!(conditionals.len(), 1);
6733 assert_eq!(conditionals[0].line(), 3);
6734 }
6735
6736 #[test]
6737 fn test_empty_conditional_line_tracking() {
6738 let text = r#"ifdef DEBUG
6739endif
6740
6741ifndef RELEASE
6742endif
6743"#;
6744 let makefile: Makefile = text.parse().unwrap();
6745
6746 let conditionals: Vec<_> = makefile.conditionals().collect();
6747 assert_eq!(conditionals.len(), 2);
6748 assert_eq!(conditionals[0].line(), 0);
6749 assert_eq!(conditionals[1].line(), 3);
6750 }
6751
6752 #[test]
6753 fn test_recipe_line_tracking() {
6754 let text = r#"build:
6755 echo "Building..."
6756 gcc -o app main.c
6757 echo "Done"
6758
6759test:
6760 ./run-tests
6761"#;
6762 let makefile: Makefile = text.parse().unwrap();
6763
6764 let rule1 = makefile.rules().next().expect("Should have first rule");
6766 let recipes: Vec<_> = rule1.recipe_nodes().collect();
6767 assert_eq!(recipes.len(), 3);
6768
6769 assert_eq!(recipes[0].text(), "echo \"Building...\"");
6770 assert_eq!(recipes[0].line(), 1);
6771 assert_eq!(recipes[0].column(), 0);
6772
6773 assert_eq!(recipes[1].text(), "gcc -o app main.c");
6774 assert_eq!(recipes[1].line(), 2);
6775 assert_eq!(recipes[1].column(), 0);
6776
6777 assert_eq!(recipes[2].text(), "echo \"Done\"");
6778 assert_eq!(recipes[2].line(), 3);
6779 assert_eq!(recipes[2].column(), 0);
6780
6781 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6783 let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6784 assert_eq!(recipes2.len(), 1);
6785
6786 assert_eq!(recipes2[0].text(), "./run-tests");
6787 assert_eq!(recipes2[0].line(), 6);
6788 assert_eq!(recipes2[0].column(), 0);
6789 }
6790
6791 #[test]
6792 fn test_recipe_with_variables_line_tracking() {
6793 let text = r#"install:
6794 mkdir -p $(DESTDIR)
6795 cp $(BINARY) $(DESTDIR)/
6796"#;
6797 let makefile: Makefile = text.parse().unwrap();
6798 let rule = makefile.rules().next().expect("Should have rule");
6799 let recipes: Vec<_> = rule.recipe_nodes().collect();
6800
6801 assert_eq!(recipes.len(), 2);
6802 assert_eq!(recipes[0].line(), 1);
6803 assert_eq!(recipes[1].line(), 2);
6804 }
6805
6806 #[test]
6807 fn test_recipe_text_no_leading_tab() {
6808 let text = "test:\n\techo hello\n\t\techo nested\n\t echo with spaces\n";
6810 let makefile: Makefile = text.parse().unwrap();
6811 let rule = makefile.rules().next().expect("Should have rule");
6812 let recipes: Vec<_> = rule.recipe_nodes().collect();
6813
6814 assert_eq!(recipes.len(), 3);
6815
6816 eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6818
6819 assert_eq!(recipes[0].text(), "echo hello");
6821
6822 eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6824 assert_eq!(recipes[1].text(), "\techo nested");
6825
6826 eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6828 assert_eq!(recipes[2].text(), " echo with spaces");
6829 }
6830
6831 #[test]
6832 fn test_recipe_parent() {
6833 let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6834 let rule = makefile.rules().next().unwrap();
6835 let recipe = rule.recipe_nodes().next().unwrap();
6836
6837 let parent = recipe.parent().expect("Recipe should have parent");
6838 assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6839 assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6840 }
6841
6842 #[test]
6843 fn test_recipe_is_silent_various_prefixes() {
6844 let makefile: Makefile = r#"test:
6845 @echo silent
6846 -echo ignore
6847 +echo always
6848 @-echo silent_ignore
6849 -@echo ignore_silent
6850 +@echo always_silent
6851 echo normal
6852"#
6853 .parse()
6854 .unwrap();
6855
6856 let rule = makefile.rules().next().unwrap();
6857 let recipes: Vec<_> = rule.recipe_nodes().collect();
6858
6859 assert_eq!(recipes.len(), 7);
6860 assert!(recipes[0].is_silent(), "@echo should be silent");
6861 assert!(!recipes[1].is_silent(), "-echo should not be silent");
6862 assert!(!recipes[2].is_silent(), "+echo should not be silent");
6863 assert!(recipes[3].is_silent(), "@-echo should be silent");
6864 assert!(recipes[4].is_silent(), "-@echo should be silent");
6865 assert!(recipes[5].is_silent(), "+@echo should be silent");
6866 assert!(!recipes[6].is_silent(), "echo should not be silent");
6867 }
6868
6869 #[test]
6870 fn test_recipe_is_ignore_errors_various_prefixes() {
6871 let makefile: Makefile = r#"test:
6872 @echo silent
6873 -echo ignore
6874 +echo always
6875 @-echo silent_ignore
6876 -@echo ignore_silent
6877 +-echo always_ignore
6878 echo normal
6879"#
6880 .parse()
6881 .unwrap();
6882
6883 let rule = makefile.rules().next().unwrap();
6884 let recipes: Vec<_> = rule.recipe_nodes().collect();
6885
6886 assert_eq!(recipes.len(), 7);
6887 assert!(
6888 !recipes[0].is_ignore_errors(),
6889 "@echo should not ignore errors"
6890 );
6891 assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6892 assert!(
6893 !recipes[2].is_ignore_errors(),
6894 "+echo should not ignore errors"
6895 );
6896 assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6897 assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6898 assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6899 assert!(
6900 !recipes[6].is_ignore_errors(),
6901 "echo should not ignore errors"
6902 );
6903 }
6904
6905 #[test]
6906 fn test_recipe_set_prefix_add() {
6907 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6908 let rule = makefile.rules().next().unwrap();
6909 let mut recipe = rule.recipe_nodes().next().unwrap();
6910
6911 recipe.set_prefix("@");
6912 assert_eq!(recipe.text(), "@echo hello");
6913 assert!(recipe.is_silent());
6914 }
6915
6916 #[test]
6917 fn test_recipe_set_prefix_change() {
6918 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6919 let rule = makefile.rules().next().unwrap();
6920 let mut recipe = rule.recipe_nodes().next().unwrap();
6921
6922 recipe.set_prefix("-");
6923 assert_eq!(recipe.text(), "-echo hello");
6924 assert!(!recipe.is_silent());
6925 assert!(recipe.is_ignore_errors());
6926 }
6927
6928 #[test]
6929 fn test_recipe_set_prefix_remove() {
6930 let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
6931 let rule = makefile.rules().next().unwrap();
6932 let mut recipe = rule.recipe_nodes().next().unwrap();
6933
6934 recipe.set_prefix("");
6935 assert_eq!(recipe.text(), "echo hello");
6936 assert!(!recipe.is_silent());
6937 assert!(!recipe.is_ignore_errors());
6938 }
6939
6940 #[test]
6941 fn test_recipe_set_prefix_combinations() {
6942 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6943 let rule = makefile.rules().next().unwrap();
6944 let mut recipe = rule.recipe_nodes().next().unwrap();
6945
6946 recipe.set_prefix("@-");
6947 assert_eq!(recipe.text(), "@-echo hello");
6948 assert!(recipe.is_silent());
6949 assert!(recipe.is_ignore_errors());
6950
6951 recipe.set_prefix("-@");
6952 assert_eq!(recipe.text(), "-@echo hello");
6953 assert!(recipe.is_silent());
6954 assert!(recipe.is_ignore_errors());
6955 }
6956
6957 #[test]
6958 fn test_recipe_replace_text_basic() {
6959 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6960 let rule = makefile.rules().next().unwrap();
6961 let mut recipe = rule.recipe_nodes().next().unwrap();
6962
6963 recipe.replace_text("echo world");
6964 assert_eq!(recipe.text(), "echo world");
6965
6966 let rule = makefile.rules().next().unwrap();
6968 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6969 }
6970
6971 #[test]
6972 fn test_recipe_replace_text_with_prefix() {
6973 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6974 let rule = makefile.rules().next().unwrap();
6975 let mut recipe = rule.recipe_nodes().next().unwrap();
6976
6977 recipe.replace_text("@echo goodbye");
6978 assert_eq!(recipe.text(), "@echo goodbye");
6979 assert!(recipe.is_silent());
6980 }
6981
6982 #[test]
6983 fn test_recipe_insert_before_single() {
6984 let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6985 let rule = makefile.rules().next().unwrap();
6986 let recipe = rule.recipe_nodes().next().unwrap();
6987
6988 recipe.insert_before("echo hello");
6989
6990 let rule = makefile.rules().next().unwrap();
6991 let recipes: Vec<_> = rule.recipes().collect();
6992 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6993 }
6994
6995 #[test]
6996 fn test_recipe_insert_before_multiple() {
6997 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6998 .parse()
6999 .unwrap();
7000 let rule = makefile.rules().next().unwrap();
7001 let recipes: Vec<_> = rule.recipe_nodes().collect();
7002
7003 recipes[1].insert_before("echo middle");
7005
7006 let rule = makefile.rules().next().unwrap();
7007 let new_recipes: Vec<_> = rule.recipes().collect();
7008 assert_eq!(
7009 new_recipes,
7010 vec!["echo one", "echo middle", "echo two", "echo three"]
7011 );
7012 }
7013
7014 #[test]
7015 fn test_recipe_insert_before_first() {
7016 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7017 let rule = makefile.rules().next().unwrap();
7018 let recipes: Vec<_> = rule.recipe_nodes().collect();
7019
7020 recipes[0].insert_before("echo zero");
7021
7022 let rule = makefile.rules().next().unwrap();
7023 let new_recipes: Vec<_> = rule.recipes().collect();
7024 assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
7025 }
7026
7027 #[test]
7028 fn test_recipe_insert_after_single() {
7029 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7030 let rule = makefile.rules().next().unwrap();
7031 let recipe = rule.recipe_nodes().next().unwrap();
7032
7033 recipe.insert_after("echo world");
7034
7035 let rule = makefile.rules().next().unwrap();
7036 let recipes: Vec<_> = rule.recipes().collect();
7037 assert_eq!(recipes, vec!["echo hello", "echo world"]);
7038 }
7039
7040 #[test]
7041 fn test_recipe_insert_after_multiple() {
7042 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7043 .parse()
7044 .unwrap();
7045 let rule = makefile.rules().next().unwrap();
7046 let recipes: Vec<_> = rule.recipe_nodes().collect();
7047
7048 recipes[1].insert_after("echo middle");
7050
7051 let rule = makefile.rules().next().unwrap();
7052 let new_recipes: Vec<_> = rule.recipes().collect();
7053 assert_eq!(
7054 new_recipes,
7055 vec!["echo one", "echo two", "echo middle", "echo three"]
7056 );
7057 }
7058
7059 #[test]
7060 fn test_recipe_insert_after_last() {
7061 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7062 let rule = makefile.rules().next().unwrap();
7063 let recipes: Vec<_> = rule.recipe_nodes().collect();
7064
7065 recipes[1].insert_after("echo three");
7066
7067 let rule = makefile.rules().next().unwrap();
7068 let new_recipes: Vec<_> = rule.recipes().collect();
7069 assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
7070 }
7071
7072 #[test]
7073 fn test_recipe_remove_single() {
7074 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7075 let rule = makefile.rules().next().unwrap();
7076 let recipe = rule.recipe_nodes().next().unwrap();
7077
7078 recipe.remove();
7079
7080 let rule = makefile.rules().next().unwrap();
7081 assert_eq!(rule.recipes().count(), 0);
7082 }
7083
7084 #[test]
7085 fn test_recipe_remove_first() {
7086 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7087 .parse()
7088 .unwrap();
7089 let rule = makefile.rules().next().unwrap();
7090 let recipes: Vec<_> = rule.recipe_nodes().collect();
7091
7092 recipes[0].remove();
7093
7094 let rule = makefile.rules().next().unwrap();
7095 let new_recipes: Vec<_> = rule.recipes().collect();
7096 assert_eq!(new_recipes, vec!["echo two", "echo three"]);
7097 }
7098
7099 #[test]
7100 fn test_recipe_remove_middle() {
7101 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7102 .parse()
7103 .unwrap();
7104 let rule = makefile.rules().next().unwrap();
7105 let recipes: Vec<_> = rule.recipe_nodes().collect();
7106
7107 recipes[1].remove();
7108
7109 let rule = makefile.rules().next().unwrap();
7110 let new_recipes: Vec<_> = rule.recipes().collect();
7111 assert_eq!(new_recipes, vec!["echo one", "echo three"]);
7112 }
7113
7114 #[test]
7115 fn test_recipe_remove_last() {
7116 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7117 .parse()
7118 .unwrap();
7119 let rule = makefile.rules().next().unwrap();
7120 let recipes: Vec<_> = rule.recipe_nodes().collect();
7121
7122 recipes[2].remove();
7123
7124 let rule = makefile.rules().next().unwrap();
7125 let new_recipes: Vec<_> = rule.recipes().collect();
7126 assert_eq!(new_recipes, vec!["echo one", "echo two"]);
7127 }
7128
7129 #[test]
7130 fn test_recipe_multiple_operations() {
7131 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7132 let rule = makefile.rules().next().unwrap();
7133 let mut recipe = rule.recipe_nodes().next().unwrap();
7134
7135 recipe.replace_text("echo modified");
7137 assert_eq!(recipe.text(), "echo modified");
7138
7139 recipe.set_prefix("@");
7141 assert_eq!(recipe.text(), "@echo modified");
7142
7143 recipe.insert_after("echo three");
7145
7146 let rule = makefile.rules().next().unwrap();
7148 let recipes: Vec<_> = rule.recipes().collect();
7149 assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
7150 }
7151
7152 #[test]
7153 fn test_from_str_relaxed_valid() {
7154 let input = "all: foo\n\tfoo bar\n";
7155 let (makefile, errors) = Makefile::from_str_relaxed(input);
7156 assert!(errors.is_empty());
7157 assert_eq!(makefile.rules().count(), 1);
7158 assert_eq!(makefile.to_string(), input);
7159 }
7160
7161 #[test]
7162 fn test_from_str_relaxed_with_errors() {
7163 let input = "rule target\n\tcommand\n";
7165 let (makefile, errors) = Makefile::from_str_relaxed(input);
7166 assert!(!errors.is_empty());
7167 assert_eq!(makefile.to_string(), input);
7169 }
7170
7171 #[test]
7172 fn test_positioned_errors_have_valid_ranges() {
7173 let input = "rule target\n\tcommand\n";
7174 let parsed = Makefile::parse(input);
7175 assert!(!parsed.ok());
7176
7177 let positioned = parsed.positioned_errors();
7178 assert!(!positioned.is_empty());
7179
7180 for err in positioned {
7181 let start: u32 = err.range.start().into();
7183 let end: u32 = err.range.end().into();
7184 assert!(start <= end);
7185 assert!((end as usize) <= input.len());
7186 }
7187 }
7188
7189 #[test]
7190 fn test_positioned_errors_point_to_error_location() {
7191 let input = "rule target\n\tcommand\n";
7192 let parsed = Makefile::parse(input);
7193 assert!(!parsed.ok());
7194
7195 let positioned = parsed.positioned_errors();
7196 assert!(!positioned.is_empty());
7197
7198 let err = &positioned[0];
7199 let start: usize = err.range.start().into();
7200 let end: usize = err.range.end().into();
7201 let error_text = &input[start..end];
7203 assert!(!error_text.is_empty());
7204
7205 let tree = parsed.tree();
7207 assert_eq!(tree.to_string(), input);
7208 }
7209
7210 #[test]
7211 fn test_tree_with_errors_preserves_text() {
7212 let input = "rule target\n\tcommand\nVAR = value\n";
7213 let parsed = Makefile::parse(input);
7214 assert!(!parsed.ok());
7215
7216 let tree = parsed.tree();
7217 assert_eq!(tree.to_string(), input);
7218
7219 assert_eq!(tree.variable_definitions().count(), 1);
7221 }
7222}
7223
7224#[cfg(test)]
7225mod test_continuation {
7226 use super::*;
7227
7228 #[test]
7229 fn test_recipe_continuation_lines() {
7230 let makefile_content = r#"override_dh_autoreconf:
7231 set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \
7232 dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \
7233 sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs
7234 dh_autoreconf
7235"#;
7236
7237 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7238 let rule = makefile.rules().next().unwrap();
7239
7240 let recipes: Vec<_> = rule.recipe_nodes().collect();
7241
7242 assert_eq!(recipes.len(), 2);
7244
7245 let expected_first = "set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \\\n dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \\\n sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs";
7248 assert_eq!(recipes[0].text(), expected_first);
7249
7250 assert_eq!(recipes[1].text(), "dh_autoreconf");
7252 }
7253
7254 #[test]
7255 fn test_simple_continuation() {
7256 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n";
7257
7258 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7259 let rule = makefile.rules().next().unwrap();
7260 let recipes: Vec<_> = rule.recipe_nodes().collect();
7261
7262 assert_eq!(recipes.len(), 1);
7263 assert_eq!(recipes[0].text(), "echo hello && \\\n echo world");
7264 }
7265
7266 #[test]
7267 fn test_multiple_continuations() {
7268 let makefile_content = "test:\n\techo line1 && \\\n\t echo line2 && \\\n\t echo line3 && \\\n\t echo line4\n";
7269
7270 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7271 let rule = makefile.rules().next().unwrap();
7272 let recipes: Vec<_> = rule.recipe_nodes().collect();
7273
7274 assert_eq!(recipes.len(), 1);
7275 assert_eq!(
7276 recipes[0].text(),
7277 "echo line1 && \\\n echo line2 && \\\n echo line3 && \\\n echo line4"
7278 );
7279 }
7280
7281 #[test]
7282 fn test_continuation_round_trip() {
7283 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7284
7285 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7286 let output = makefile.to_string();
7287
7288 assert_eq!(output, makefile_content);
7290 }
7291
7292 #[test]
7293 fn test_continuation_with_silent_prefix() {
7294 let makefile_content = "test:\n\t@echo hello && \\\n\t echo world\n";
7295
7296 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7297 let rule = makefile.rules().next().unwrap();
7298 let recipes: Vec<_> = rule.recipe_nodes().collect();
7299
7300 assert_eq!(recipes.len(), 1);
7301 assert_eq!(recipes[0].text(), "@echo hello && \\\n echo world");
7302 assert!(recipes[0].is_silent());
7303 }
7304
7305 #[test]
7306 fn test_mixed_continued_and_non_continued() {
7307 let makefile_content = r#"test:
7308 echo first
7309 echo second && \
7310 echo third
7311 echo fourth
7312"#;
7313
7314 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7315 let rule = makefile.rules().next().unwrap();
7316 let recipes: Vec<_> = rule.recipe_nodes().collect();
7317
7318 assert_eq!(recipes.len(), 3);
7319 assert_eq!(recipes[0].text(), "echo first");
7320 assert_eq!(recipes[1].text(), "echo second && \\\n echo third");
7321 assert_eq!(recipes[2].text(), "echo fourth");
7322 }
7323
7324 #[test]
7325 fn test_continuation_replace_command() {
7326 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7327
7328 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7329 let mut rule = makefile.rules().next().unwrap();
7330
7331 rule.replace_command(0, "echo replaced");
7333
7334 let recipes: Vec<_> = rule.recipe_nodes().collect();
7335 assert_eq!(recipes.len(), 2);
7336 assert_eq!(recipes[0].text(), "echo replaced");
7337 assert_eq!(recipes[1].text(), "echo done");
7338 }
7339
7340 #[test]
7341 fn test_continuation_count() {
7342 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7343
7344 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7345 let rule = makefile.rules().next().unwrap();
7346
7347 assert_eq!(rule.recipe_count(), 2);
7349 assert_eq!(rule.recipe_nodes().count(), 2);
7350
7351 let recipes_list: Vec<_> = rule.recipes().collect();
7353 assert_eq!(
7354 recipes_list,
7355 vec!["echo hello && \\\n echo world", "echo done"]
7356 );
7357 }
7358
7359 #[test]
7360 fn test_backslash_in_middle_of_line() {
7361 let makefile_content = "test:\n\techo hello\\nworld\n\techo done\n";
7363
7364 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7365 let rule = makefile.rules().next().unwrap();
7366 let recipes: Vec<_> = rule.recipe_nodes().collect();
7367
7368 assert_eq!(recipes.len(), 2);
7369 assert_eq!(recipes[0].text(), "echo hello\\nworld");
7370 assert_eq!(recipes[1].text(), "echo done");
7371 }
7372
7373 #[test]
7374 fn test_shell_for_loop_with_continuation() {
7375 let makefile_content = r#"override_dh_installman:
7379 for i in foo bar; do \
7380 pod2man --section=1 $$i ; \
7381 done
7382"#;
7383
7384 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7385 let rule = makefile.rules().next().unwrap();
7386
7387 let recipes: Vec<_> = rule.recipe_nodes().collect();
7389 assert_eq!(recipes.len(), 1);
7390
7391 let recipe_text = recipes[0].text();
7393 let expected_recipe = "for i in foo bar; do \\\n\tpod2man --section=1 $$i ; \\\ndone";
7394 assert_eq!(recipe_text, expected_recipe);
7395
7396 let output = makefile.to_string();
7398 assert_eq!(output, makefile_content);
7399 }
7400
7401 #[test]
7402 fn test_shell_for_loop_remove_command() {
7403 let makefile_content = r#"override_dh_installman:
7406 for i in foo bar; do \
7407 pod2man --section=1 $$i ; \
7408 done
7409 echo "Done with man pages"
7410"#;
7411
7412 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7413 let mut rule = makefile.rules().next().unwrap();
7414
7415 assert_eq!(rule.recipe_count(), 2);
7417
7418 rule.remove_command(1);
7420
7421 let recipes: Vec<_> = rule.recipe_nodes().collect();
7423 assert_eq!(recipes.len(), 1);
7424
7425 let output = makefile.to_string();
7427 let expected_output = r#"override_dh_installman:
7428 for i in foo bar; do \
7429 pod2man --section=1 $$i ; \
7430 done
7431"#;
7432 assert_eq!(output, expected_output);
7433 }
7434
7435 #[test]
7436 fn test_variable_reference_paren() {
7437 let makefile: Makefile = "CFLAGS = $(BASE_FLAGS) -Wall\n".parse().unwrap();
7438 let refs: Vec<_> = makefile.variable_references().collect();
7439 assert_eq!(refs.len(), 1);
7440 assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7441 assert_eq!(refs[0].to_string(), "$(BASE_FLAGS)");
7442 }
7443
7444 #[test]
7445 fn test_variable_reference_brace() {
7446 let makefile: Makefile = "CFLAGS = ${BASE_FLAGS} -Wall\n".parse().unwrap();
7447 let refs: Vec<_> = makefile.variable_references().collect();
7448 assert_eq!(refs.len(), 1);
7449 assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7450 assert_eq!(refs[0].to_string(), "${BASE_FLAGS}");
7451 }
7452
7453 #[test]
7454 fn test_variable_reference_in_prerequisites() {
7455 let makefile: Makefile = "all: $(TARGETS)\n".parse().unwrap();
7456 let refs: Vec<_> = makefile.variable_references().collect();
7457 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7458 assert!(names.contains(&"TARGETS".to_string()));
7459 }
7460
7461 #[test]
7462 fn test_variable_reference_multiple() {
7463 let makefile: Makefile =
7464 "CFLAGS = $(BASE_FLAGS) -Wall\nLDFLAGS = $(BASE_LDFLAGS) -lm\nall: $(TARGETS)\n"
7465 .parse()
7466 .unwrap();
7467 let refs: Vec<_> = makefile.variable_references().collect();
7468 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7469 assert!(names.contains(&"BASE_FLAGS".to_string()));
7470 assert!(names.contains(&"BASE_LDFLAGS".to_string()));
7471 assert!(names.contains(&"TARGETS".to_string()));
7472 }
7473
7474 #[test]
7475 fn test_variable_reference_nested() {
7476 let makefile: Makefile = "FOO = $($(INNER))\n".parse().unwrap();
7477 let refs: Vec<_> = makefile.variable_references().collect();
7478 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7479 assert!(names.contains(&"INNER".to_string()));
7480 }
7481
7482 #[test]
7483 fn test_variable_reference_line_col() {
7484 let makefile: Makefile = "A = 1\nB = $(FOO)\n".parse().unwrap();
7485 let refs: Vec<_> = makefile.variable_references().collect();
7486 assert_eq!(refs.len(), 1);
7487 assert_eq!(refs[0].name(), Some("FOO".to_string()));
7488 assert_eq!(refs[0].line(), 1);
7489 assert_eq!(refs[0].column(), 4);
7490 assert_eq!(refs[0].line_col(), (1, 4));
7491 }
7492
7493 #[test]
7494 fn test_variable_reference_no_refs() {
7495 let makefile: Makefile = "A = hello\nall:\n\techo done\n".parse().unwrap();
7496 let refs: Vec<_> = makefile.variable_references().collect();
7497 assert_eq!(refs.len(), 0);
7498 }
7499
7500 #[test]
7501 fn test_variable_reference_mixed_styles() {
7502 let makefile: Makefile = "A = $(FOO) ${BAR}\n".parse().unwrap();
7503 let refs: Vec<_> = makefile.variable_references().collect();
7504 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7505 assert_eq!(names.len(), 2);
7506 assert!(names.contains(&"FOO".to_string()));
7507 assert!(names.contains(&"BAR".to_string()));
7508 }
7509
7510 #[test]
7511 fn test_brace_variable_in_prerequisites() {
7512 let makefile: Makefile = "all: ${OBJS}\n".parse().unwrap();
7513 let refs: Vec<_> = makefile.variable_references().collect();
7514 assert_eq!(refs.len(), 1);
7515 assert_eq!(refs[0].name(), Some("OBJS".to_string()));
7516 }
7517
7518 #[test]
7519 fn test_parse_brace_variable_roundtrip() {
7520 let input = "CFLAGS = ${BASE_FLAGS} -Wall\n";
7521 let makefile: Makefile = input.parse().unwrap();
7522 assert_eq!(makefile.to_string(), input);
7523 }
7524
7525 #[test]
7526 fn test_parse_nested_variable_in_value_roundtrip() {
7527 let input = "FOO = $(BAR) baz $(QUUX)\n";
7528 let makefile: Makefile = input.parse().unwrap();
7529 assert_eq!(makefile.to_string(), input);
7530 }
7531
7532 #[test]
7533 fn test_is_function_call() {
7534 let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7535 let refs: Vec<_> = makefile.variable_references().collect();
7536 assert_eq!(refs.len(), 1);
7537 assert!(refs[0].is_function_call());
7538 }
7539
7540 #[test]
7541 fn test_is_function_call_simple_variable() {
7542 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7543 let refs: Vec<_> = makefile.variable_references().collect();
7544 assert_eq!(refs.len(), 1);
7545 assert!(!refs[0].is_function_call());
7546 }
7547
7548 #[test]
7549 fn test_is_function_call_with_commas() {
7550 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7551 let refs: Vec<_> = makefile.variable_references().collect();
7552 assert_eq!(refs.len(), 1);
7553 assert!(refs[0].is_function_call());
7554 }
7555
7556 #[test]
7557 fn test_is_function_call_braces() {
7558 let makefile: Makefile = "FILES = ${wildcard *.c}\n".parse().unwrap();
7559 let refs: Vec<_> = makefile.variable_references().collect();
7560 assert_eq!(refs.len(), 1);
7561 assert!(refs[0].is_function_call());
7562 }
7563
7564 #[test]
7565 fn test_argument_count_simple_variable() {
7566 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7567 let refs: Vec<_> = makefile.variable_references().collect();
7568 assert_eq!(refs[0].argument_count(), 0);
7569 }
7570
7571 #[test]
7572 fn test_argument_count_one_arg() {
7573 let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7574 let refs: Vec<_> = makefile.variable_references().collect();
7575 assert_eq!(refs[0].argument_count(), 1);
7576 }
7577
7578 #[test]
7579 fn test_argument_count_three_args() {
7580 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7581 let refs: Vec<_> = makefile.variable_references().collect();
7582 assert_eq!(refs[0].argument_count(), 3);
7583 }
7584
7585 #[test]
7586 fn test_argument_index_at_offset_subst() {
7587 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7588 let refs: Vec<_> = makefile.variable_references().collect();
7589 assert_eq!(refs[0].argument_index_at_offset(12), Some(0));
7595 assert_eq!(refs[0].argument_index_at_offset(14), Some(1));
7596 assert_eq!(refs[0].argument_index_at_offset(16), Some(2));
7597 }
7598
7599 #[test]
7600 fn test_argument_index_at_offset_outside() {
7601 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7602 let refs: Vec<_> = makefile.variable_references().collect();
7603 assert_eq!(refs[0].argument_index_at_offset(0), None);
7605 assert_eq!(refs[0].argument_index_at_offset(22), None);
7607 }
7608
7609 #[test]
7610 fn test_argument_index_at_offset_simple_variable() {
7611 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7612 let refs: Vec<_> = makefile.variable_references().collect();
7613 assert_eq!(refs[0].argument_index_at_offset(11), None);
7614 }
7615
7616 #[test]
7617 fn test_lex_braces() {
7618 use crate::lex::lex;
7619 let tokens = lex("${FOO}");
7620 let kinds: Vec<_> = tokens.iter().map(|(k, _)| *k).collect();
7621 assert!(kinds.contains(&DOLLAR));
7622 assert!(kinds.contains(&LBRACE));
7623 assert!(kinds.contains(&RBRACE));
7624 }
7625
7626 #[test]
7627 fn test_parse_quoted_string_inside_function_call() {
7628 let cases = [
7633 "X = $(if a,'foo')\n",
7634 "X = $(if a,'foo (bar)')\n",
7635 "X = $(if a,'(')\n",
7636 "X = $(if a,')')\n",
7637 "X = $(if $(SKIP),-k 'not ($(call f,$(s),$(SKIP)))')\n",
7638 "X = foo'bar\nY = baz\n",
7639 "X = it's fine\n",
7640 "X = $(if a,it's)\n",
7641 "X = '\nY = bar\n",
7642 ];
7643 for src in cases {
7644 let parsed: Makefile = src.parse().unwrap_or_else(|e| {
7645 panic!("failed to parse {src:?}: {e:?}");
7646 });
7647 assert_eq!(parsed.to_string(), src, "round-trip mismatch for {src:?}");
7648 }
7649 }
7650}