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 is_silent(&self) -> bool {
1988 let text = self.text();
1989 text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1990 }
1991
1992 pub fn is_ignore_errors(&self) -> bool {
2005 let text = self.text();
2006 text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
2007 }
2008
2009 pub fn set_prefix(&mut self, prefix: &str) {
2026 let text = self.text();
2027
2028 let stripped = text.trim_start_matches(['@', '-', '+']);
2030
2031 let new_text = format!("{}{}", prefix, stripped);
2033
2034 self.replace_text(&new_text);
2035 }
2036
2037 pub fn replace_text(&mut self, new_text: &str) {
2050 let node = self.syntax();
2051 let parent = node.parent().expect("Recipe node must have a parent");
2052 let node_index = node.index();
2053
2054 let mut builder = GreenNodeBuilder::new();
2056 builder.start_node(RECIPE.into());
2057
2058 if let Some(indent_token) = node
2060 .children_with_tokens()
2061 .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
2062 {
2063 builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
2064 } else {
2065 builder.token(INDENT.into(), "\t");
2066 }
2067
2068 builder.token(TEXT.into(), new_text);
2069
2070 if let Some(newline_token) = node
2072 .children_with_tokens()
2073 .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
2074 {
2075 builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
2076 } else {
2077 builder.token(NEWLINE.into(), "\n");
2078 }
2079
2080 builder.finish_node();
2081 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2082
2083 parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
2085
2086 *self = parent
2090 .children_with_tokens()
2091 .nth(node_index)
2092 .and_then(|element| element.into_node())
2093 .and_then(Recipe::cast)
2094 .expect("New recipe node should exist at the same index");
2095 }
2096
2097 pub fn insert_before(&self, text: &str) {
2110 let node = self.syntax();
2111 let parent = node.parent().expect("Recipe node must have a parent");
2112 let node_index = node.index();
2113
2114 let mut builder = GreenNodeBuilder::new();
2116 builder.start_node(RECIPE.into());
2117 builder.token(INDENT.into(), "\t");
2118 builder.token(TEXT.into(), text);
2119 builder.token(NEWLINE.into(), "\n");
2120 builder.finish_node();
2121 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2122
2123 parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
2125 }
2126
2127 pub fn insert_after(&self, text: &str) {
2140 let node = self.syntax();
2141 let parent = node.parent().expect("Recipe node must have a parent");
2142 let node_index = node.index();
2143
2144 let mut builder = GreenNodeBuilder::new();
2146 builder.start_node(RECIPE.into());
2147 builder.token(INDENT.into(), "\t");
2148 builder.token(TEXT.into(), text);
2149 builder.token(NEWLINE.into(), "\n");
2150 builder.finish_node();
2151 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2152
2153 parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
2155 }
2156
2157 pub fn remove(&self) {
2170 let node = self.syntax();
2171 let parent = node.parent().expect("Recipe node must have a parent");
2172 let node_index = node.index();
2173
2174 parent.splice_children(node_index..node_index + 1, vec![]);
2176 }
2177}
2178
2179pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
2183 let mut newlines_to_remove = vec![];
2185 let mut current = node.last_child_or_token();
2186
2187 while let Some(element) = current {
2188 match &element {
2189 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2190 newlines_to_remove.push(token.clone());
2191 current = token.prev_sibling_or_token();
2192 }
2193 rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
2194 let mut recipe_current = n.last_child_or_token();
2196 while let Some(recipe_element) = recipe_current {
2197 match &recipe_element {
2198 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2199 newlines_to_remove.push(token.clone());
2200 recipe_current = token.prev_sibling_or_token();
2201 }
2202 _ => break,
2203 }
2204 }
2205 break; }
2207 _ => break,
2208 }
2209 }
2210
2211 if newlines_to_remove.len() > 1 {
2214 newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
2216
2217 for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
2218 let parent = token.parent().unwrap();
2219 let idx = token.index();
2220 parent.splice_children(idx..idx + 1, vec![]);
2221 }
2222 }
2223}
2224
2225pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
2233 let mut collected_elements = vec![];
2234 let mut found_comment = false;
2235
2236 let mut current = node.prev_sibling_or_token();
2238 while let Some(element) = current {
2239 match &element {
2240 rowan::NodeOrToken::Token(token) => match token.kind() {
2241 COMMENT => {
2242 if token.text().starts_with("#!") {
2243 break; }
2245 found_comment = true;
2246 collected_elements.push(element.clone());
2247 }
2248 NEWLINE | WHITESPACE => {
2249 collected_elements.push(element.clone());
2250 }
2251 _ => break, },
2253 rowan::NodeOrToken::Node(n) => {
2254 if n.kind() == BLANK_LINE {
2256 collected_elements.push(element.clone());
2257 } else {
2258 break; }
2260 }
2261 }
2262 current = element.prev_sibling_or_token();
2263 }
2264
2265 let mut elements_to_remove = vec![];
2268 let mut consecutive_newlines = 0;
2269 for element in collected_elements.iter().rev() {
2270 let should_remove = match element {
2271 rowan::NodeOrToken::Token(token) => match token.kind() {
2272 COMMENT => {
2273 consecutive_newlines = 0;
2274 found_comment
2275 }
2276 NEWLINE => {
2277 consecutive_newlines += 1;
2278 found_comment && consecutive_newlines <= 1
2279 }
2280 WHITESPACE => found_comment,
2281 _ => false,
2282 },
2283 rowan::NodeOrToken::Node(n) => {
2284 if n.kind() == BLANK_LINE {
2286 consecutive_newlines += 1;
2287 found_comment && consecutive_newlines <= 1
2288 } else {
2289 false
2290 }
2291 }
2292 };
2293
2294 if should_remove {
2295 elements_to_remove.push(element.clone());
2296 }
2297 }
2298
2299 let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
2302 all_to_remove.extend(elements_to_remove.into_iter().rev());
2303
2304 all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
2306
2307 for element in all_to_remove {
2308 let idx = element.index();
2309 parent.splice_children(idx..idx + 1, vec![]);
2310 }
2311}
2312
2313impl FromStr for Rule {
2314 type Err = crate::Error;
2315
2316 fn from_str(s: &str) -> Result<Self, Self::Err> {
2317 Rule::parse(s).to_rule_result()
2318 }
2319}
2320
2321impl FromStr for Makefile {
2322 type Err = crate::Error;
2323
2324 fn from_str(s: &str) -> Result<Self, Self::Err> {
2325 Makefile::parse(s).to_result()
2326 }
2327}
2328
2329#[cfg(test)]
2330mod tests {
2331 use super::*;
2332 use crate::ast::makefile::MakefileItem;
2333 use crate::pattern::matches_pattern;
2334
2335 #[test]
2336 fn test_conditionals() {
2337 let code = "ifdef DEBUG\n DEBUG_FLAG := 1\nendif\n";
2341 let mut buf = code.as_bytes();
2342 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
2343 assert!(makefile.code().contains("DEBUG_FLAG"));
2344
2345 let code =
2347 "ifeq ($(OS),Windows_NT)\n RESULT := windows\nelse\n RESULT := unix\nendif\n";
2348 let mut buf = code.as_bytes();
2349 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
2350 assert!(makefile.code().contains("RESULT"));
2351 assert!(makefile.code().contains("windows"));
2352
2353 let code = "ifdef DEBUG\n CFLAGS += -g\n ifdef VERBOSE\n CFLAGS += -v\n endif\nelse\n CFLAGS += -O2\nendif\n";
2355 let mut buf = code.as_bytes();
2356 let makefile = Makefile::read_relaxed(&mut buf)
2357 .expect("Failed to parse nested conditionals with else");
2358 assert!(makefile.code().contains("CFLAGS"));
2359 assert!(makefile.code().contains("VERBOSE"));
2360
2361 let code = "ifdef DEBUG\nendif\n";
2363 let mut buf = code.as_bytes();
2364 let makefile =
2365 Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
2366 assert!(makefile.code().contains("ifdef DEBUG"));
2367
2368 let code = "ifeq ($(OS),Windows)\n EXT := .exe\nelse ifeq ($(OS),Linux)\n EXT := .bin\nelse\n EXT := .out\nendif\n";
2370 let mut buf = code.as_bytes();
2371 let makefile =
2372 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
2373 assert!(makefile.code().contains("EXT"));
2374
2375 let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
2377 let mut buf = code.as_bytes();
2378 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
2379 assert!(makefile.code().contains("DEBUG"));
2380
2381 let code = "ifdef \nDEBUG := 1\nendif\n";
2383 let mut buf = code.as_bytes();
2384 let makefile = Makefile::read_relaxed(&mut buf)
2385 .expect("Failed to parse with recovery - missing condition");
2386 assert!(makefile.code().contains("DEBUG"));
2387 }
2388
2389 #[test]
2390 fn test_parse_simple() {
2391 const SIMPLE: &str = r#"VARIABLE = value
2392
2393rule: dependency
2394 command
2395"#;
2396 let parsed = parse(SIMPLE, None);
2397 assert!(parsed.errors.is_empty());
2398 let node = parsed.syntax();
2399 assert_eq!(
2400 format!("{:#?}", node),
2401 r#"ROOT@0..44
2402 VARIABLE@0..17
2403 IDENTIFIER@0..8 "VARIABLE"
2404 WHITESPACE@8..9 " "
2405 OPERATOR@9..10 "="
2406 WHITESPACE@10..11 " "
2407 EXPR@11..16
2408 IDENTIFIER@11..16 "value"
2409 NEWLINE@16..17 "\n"
2410 BLANK_LINE@17..18
2411 NEWLINE@17..18 "\n"
2412 RULE@18..44
2413 TARGETS@18..22
2414 IDENTIFIER@18..22 "rule"
2415 OPERATOR@22..23 ":"
2416 WHITESPACE@23..24 " "
2417 PREREQUISITES@24..34
2418 PREREQUISITE@24..34
2419 IDENTIFIER@24..34 "dependency"
2420 NEWLINE@34..35 "\n"
2421 RECIPE@35..44
2422 INDENT@35..36 "\t"
2423 TEXT@36..43 "command"
2424 NEWLINE@43..44 "\n"
2425"#
2426 );
2427
2428 let root = parsed.root();
2429
2430 let mut rules = root.rules().collect::<Vec<_>>();
2431 assert_eq!(rules.len(), 1);
2432 let rule = rules.pop().unwrap();
2433 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2434 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
2435 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2436
2437 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2438 assert_eq!(variables.len(), 1);
2439 let variable = variables.pop().unwrap();
2440 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2441 assert_eq!(variable.raw_value(), Some("value".to_string()));
2442 }
2443
2444 #[test]
2445 fn test_parse_export_assign() {
2446 const EXPORT: &str = r#"export VARIABLE := value
2447"#;
2448 let parsed = parse(EXPORT, None);
2449 assert!(parsed.errors.is_empty());
2450 let node = parsed.syntax();
2451 assert_eq!(
2452 format!("{:#?}", node),
2453 r#"ROOT@0..25
2454 VARIABLE@0..25
2455 IDENTIFIER@0..6 "export"
2456 WHITESPACE@6..7 " "
2457 IDENTIFIER@7..15 "VARIABLE"
2458 WHITESPACE@15..16 " "
2459 OPERATOR@16..18 ":="
2460 WHITESPACE@18..19 " "
2461 EXPR@19..24
2462 IDENTIFIER@19..24 "value"
2463 NEWLINE@24..25 "\n"
2464"#
2465 );
2466
2467 let root = parsed.root();
2468
2469 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2470 assert_eq!(variables.len(), 1);
2471 let variable = variables.pop().unwrap();
2472 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2473 assert_eq!(variable.raw_value(), Some("value".to_string()));
2474 }
2475
2476 #[test]
2477 fn test_parse_multiple_prerequisites() {
2478 const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2479 command
2480
2481"#;
2482 let parsed = parse(MULTIPLE_PREREQUISITES, None);
2483 assert!(parsed.errors.is_empty());
2484 let node = parsed.syntax();
2485 assert_eq!(
2486 format!("{:#?}", node),
2487 r#"ROOT@0..40
2488 RULE@0..40
2489 TARGETS@0..4
2490 IDENTIFIER@0..4 "rule"
2491 OPERATOR@4..5 ":"
2492 WHITESPACE@5..6 " "
2493 PREREQUISITES@6..29
2494 PREREQUISITE@6..17
2495 IDENTIFIER@6..17 "dependency1"
2496 WHITESPACE@17..18 " "
2497 PREREQUISITE@18..29
2498 IDENTIFIER@18..29 "dependency2"
2499 NEWLINE@29..30 "\n"
2500 RECIPE@30..39
2501 INDENT@30..31 "\t"
2502 TEXT@31..38 "command"
2503 NEWLINE@38..39 "\n"
2504 NEWLINE@39..40 "\n"
2505"#
2506 );
2507 let root = parsed.root();
2508
2509 let rule = root.rules().next().unwrap();
2510 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2511 assert_eq!(
2512 rule.prerequisites().collect::<Vec<_>>(),
2513 vec!["dependency1", "dependency2"]
2514 );
2515 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2516 }
2517
2518 #[test]
2519 fn test_add_rule() {
2520 let mut makefile = Makefile::new();
2521 let rule = makefile.add_rule("rule");
2522 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2523 assert_eq!(
2524 rule.prerequisites().collect::<Vec<_>>(),
2525 Vec::<String>::new()
2526 );
2527
2528 assert_eq!(makefile.to_string(), "rule:\n");
2529 }
2530
2531 #[test]
2532 fn test_add_rule_with_shebang() {
2533 let content = r#"#!/usr/bin/make -f
2535
2536build: blah
2537 $(MAKE) install
2538
2539clean:
2540 dh_clean
2541"#;
2542
2543 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2544 let initial_count = makefile.rules().count();
2545 assert_eq!(initial_count, 2);
2546
2547 let rule = makefile.add_rule("build-indep");
2549 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2550
2551 assert_eq!(makefile.rules().count(), initial_count + 1);
2553 }
2554
2555 #[test]
2556 fn test_add_rule_formatting() {
2557 let content = r#"build: blah
2559 $(MAKE) install
2560
2561clean:
2562 dh_clean
2563"#;
2564
2565 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2566 let mut rule = makefile.add_rule("build-indep");
2567 rule.add_prerequisite("build").unwrap();
2568
2569 let expected = r#"build: blah
2570 $(MAKE) install
2571
2572clean:
2573 dh_clean
2574
2575build-indep: build
2576"#;
2577
2578 assert_eq!(makefile.to_string(), expected);
2579 }
2580
2581 #[test]
2582 fn test_push_command() {
2583 let mut makefile = Makefile::new();
2584 let mut rule = makefile.add_rule("rule");
2585
2586 rule.push_command("command");
2588 rule.push_command("command2");
2589
2590 assert_eq!(
2592 rule.recipes().collect::<Vec<_>>(),
2593 vec!["command", "command2"]
2594 );
2595
2596 rule.push_command("command3");
2598 assert_eq!(
2599 rule.recipes().collect::<Vec<_>>(),
2600 vec!["command", "command2", "command3"]
2601 );
2602
2603 assert_eq!(
2605 makefile.to_string(),
2606 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2607 );
2608
2609 assert_eq!(
2611 rule.to_string(),
2612 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2613 );
2614 }
2615
2616 #[test]
2617 fn test_replace_command() {
2618 let mut makefile = Makefile::new();
2619 let mut rule = makefile.add_rule("rule");
2620
2621 rule.push_command("command");
2623 rule.push_command("command2");
2624
2625 assert_eq!(
2627 rule.recipes().collect::<Vec<_>>(),
2628 vec!["command", "command2"]
2629 );
2630
2631 rule.replace_command(0, "new command");
2633 assert_eq!(
2634 rule.recipes().collect::<Vec<_>>(),
2635 vec!["new command", "command2"]
2636 );
2637
2638 assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2640
2641 assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2643 }
2644
2645 #[test]
2646 fn test_replace_command_with_comments() {
2647 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";
2650
2651 let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2652
2653 let mut rule = makefile.rules().next().unwrap();
2654
2655 assert_eq!(rule.recipe_nodes().count(), 2);
2657 let recipes: Vec<_> = rule.recipe_nodes().collect();
2658 assert_eq!(recipes[0].text(), ""); assert_eq!(
2660 recipes[1].text(),
2661 "dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"
2662 );
2663
2664 assert!(rule.replace_command(1, "dh_strip"));
2666
2667 assert_eq!(rule.recipe_nodes().count(), 2);
2669 let recipes: Vec<_> = rule.recipe_nodes().collect();
2670 assert_eq!(recipes[0].text(), ""); assert_eq!(recipes[1].text(), "dh_strip");
2672 }
2673
2674 #[test]
2675 fn test_parse_rule_without_newline() {
2676 let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2677 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2678 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2679 let rule = "rule: dependency".parse::<Rule>().unwrap();
2680 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2681 assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2682 }
2683
2684 #[test]
2685 fn test_parse_makefile_without_newline() {
2686 let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2687 assert_eq!(makefile.rules().count(), 1);
2688 }
2689
2690 #[test]
2691 fn test_from_reader() {
2692 let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2693 assert_eq!(makefile.rules().count(), 1);
2694 }
2695
2696 #[test]
2697 fn test_parse_with_tab_after_last_newline() {
2698 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2699 assert_eq!(makefile.rules().count(), 1);
2700 }
2701
2702 #[test]
2703 fn test_parse_with_space_after_last_newline() {
2704 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2705 assert_eq!(makefile.rules().count(), 1);
2706 }
2707
2708 #[test]
2709 fn test_parse_with_comment_after_last_newline() {
2710 let makefile =
2711 Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2712 assert_eq!(makefile.rules().count(), 1);
2713 }
2714
2715 #[test]
2716 fn test_parse_with_variable_rule() {
2717 let makefile =
2718 Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2719 .unwrap();
2720
2721 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2723 assert_eq!(vars.len(), 1);
2724 assert_eq!(vars[0].name(), Some("RULE".to_string()));
2725 assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2726
2727 let rules = makefile.rules().collect::<Vec<_>>();
2729 assert_eq!(rules.len(), 1);
2730 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2731 assert_eq!(
2732 rules[0].prerequisites().collect::<Vec<_>>(),
2733 vec!["dependency"]
2734 );
2735 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2736 }
2737
2738 #[test]
2739 fn test_parse_with_variable_dependency() {
2740 let makefile =
2741 Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2742
2743 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2745 assert_eq!(vars.len(), 1);
2746 assert_eq!(vars[0].name(), Some("DEP".to_string()));
2747 assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2748
2749 let rules = makefile.rules().collect::<Vec<_>>();
2751 assert_eq!(rules.len(), 1);
2752 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2753 assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2754 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2755 }
2756
2757 #[test]
2758 fn test_parse_with_variable_command() {
2759 let makefile =
2760 Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2761
2762 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2764 assert_eq!(vars.len(), 1);
2765 assert_eq!(vars[0].name(), Some("COM".to_string()));
2766 assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2767
2768 let rules = makefile.rules().collect::<Vec<_>>();
2770 assert_eq!(rules.len(), 1);
2771 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2772 assert_eq!(
2773 rules[0].prerequisites().collect::<Vec<_>>(),
2774 vec!["dependency"]
2775 );
2776 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2777 }
2778
2779 #[test]
2780 fn test_regular_line_error_reporting() {
2781 let input = "rule target\n\tcommand";
2782
2783 let parsed = parse(input, None);
2785 let direct_error = &parsed.errors[0];
2786
2787 assert_eq!(direct_error.line, 2);
2789 assert!(
2790 direct_error.message.contains("expected"),
2791 "Error message should contain 'expected': {}",
2792 direct_error.message
2793 );
2794 assert_eq!(direct_error.context, "\tcommand");
2795
2796 let reader_result = Makefile::from_reader(input.as_bytes());
2798 let parse_error = match reader_result {
2799 Ok(_) => panic!("Expected Parse error from from_reader"),
2800 Err(err) => match err {
2801 self::Error::Parse(parse_err) => parse_err,
2802 _ => panic!("Expected Parse error"),
2803 },
2804 };
2805
2806 let error_text = parse_error.to_string();
2808 assert!(error_text.contains("Error at line 2:"));
2809 assert!(error_text.contains("2| \tcommand"));
2810 }
2811
2812 #[test]
2813 fn test_parsing_error_context_with_bad_syntax() {
2814 let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2816
2817 match Makefile::from_reader(input.as_bytes()) {
2819 Ok(makefile) => {
2820 assert_eq!(
2822 makefile.rules().count(),
2823 0,
2824 "Should not have found any rules"
2825 );
2826 }
2827 Err(err) => match err {
2828 self::Error::Parse(error) => {
2829 assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2831 assert!(
2832 !error.errors[0].context.is_empty(),
2833 "Error context should not be empty"
2834 );
2835 }
2836 _ => panic!("Unexpected error type"),
2837 },
2838 };
2839 }
2840
2841 #[test]
2842 fn test_error_message_format() {
2843 let parse_error = ParseError {
2845 errors: vec![ErrorInfo {
2846 message: "test error".to_string(),
2847 line: 42,
2848 context: "some problematic code".to_string(),
2849 }],
2850 };
2851
2852 let error_text = parse_error.to_string();
2853 assert!(error_text.contains("Error at line 42: test error"));
2854 assert!(error_text.contains("42| some problematic code"));
2855 }
2856
2857 #[test]
2858 fn test_line_number_calculation() {
2859 let test_cases = [
2861 ("rule dependency\n\tcommand", 2), ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2), ("var = value\n#comment\n\tindented line", 3), ];
2865
2866 for (input, expected_line) in test_cases {
2867 match input.parse::<Makefile>() {
2869 Ok(_) => {
2870 continue;
2873 }
2874 Err(err) => {
2875 if let Error::Parse(parse_err) = err {
2876 assert_eq!(
2878 parse_err.errors[0].line, expected_line,
2879 "Line number should match the expected line"
2880 );
2881
2882 if parse_err.errors[0].message.contains("indented") {
2884 assert!(
2885 parse_err.errors[0].context.starts_with('\t'),
2886 "Context for indentation errors should include the tab character"
2887 );
2888 }
2889 } else {
2890 panic!("Expected parse error, got: {:?}", err);
2891 }
2892 }
2893 }
2894 }
2895 }
2896
2897 #[test]
2898 fn test_conditional_features() {
2899 let code = r#"
2901# Set variables based on DEBUG flag
2902ifdef DEBUG
2903 CFLAGS += -g -DDEBUG
2904else
2905 CFLAGS = -O2
2906endif
2907
2908# Define a build rule
2909all: $(OBJS)
2910 $(CC) $(CFLAGS) -o $@ $^
2911"#;
2912
2913 let mut buf = code.as_bytes();
2914 let makefile =
2915 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2916
2917 assert!(!makefile.code().is_empty(), "Makefile has content");
2920
2921 let rules = makefile.rules().collect::<Vec<_>>();
2923 assert!(!rules.is_empty(), "Should have found rules");
2924
2925 assert!(code.contains("ifdef DEBUG"));
2927 assert!(code.contains("endif"));
2928
2929 let code_with_var = r#"
2931# Define a variable first
2932CC = gcc
2933
2934ifdef DEBUG
2935 CFLAGS += -g -DDEBUG
2936else
2937 CFLAGS = -O2
2938endif
2939
2940all: $(OBJS)
2941 $(CC) $(CFLAGS) -o $@ $^
2942"#;
2943
2944 let mut buf = code_with_var.as_bytes();
2945 let makefile =
2946 Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2947
2948 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2950 assert!(
2951 !vars.is_empty(),
2952 "Should have found at least the CC variable definition"
2953 );
2954 }
2955
2956 #[test]
2957 fn test_include_directive() {
2958 let parsed = parse(
2959 "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2960 None,
2961 );
2962 assert!(parsed.errors.is_empty());
2963 let node = parsed.syntax();
2964 assert!(format!("{:#?}", node).contains("INCLUDE@"));
2965 }
2966
2967 #[test]
2968 fn test_export_variables() {
2969 let parsed = parse("export SHELL := /bin/bash\n", None);
2970 assert!(parsed.errors.is_empty());
2971 let makefile = parsed.root();
2972 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2973 assert_eq!(vars.len(), 1);
2974 let shell_var = vars
2975 .iter()
2976 .find(|v| v.name() == Some("SHELL".to_string()))
2977 .unwrap();
2978 assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2979 }
2980
2981 #[test]
2982 fn test_bare_export_variable() {
2983 let parsed = parse(
2986 "DEB_CFLAGS_MAINT_APPEND = -Wno-error\nexport DEB_CFLAGS_MAINT_APPEND\n\n%:\n\tdh $@\n",
2987 None,
2988 );
2989 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2990 let makefile = parsed.root();
2991 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2993 assert_eq!(vars.len(), 2);
2994 let rules = makefile.rules().collect::<Vec<_>>();
2996 assert_eq!(rules.len(), 1);
2997 assert!(rules[0].targets().any(|t| t == "%"));
2998 assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
3000 }
3001
3002 #[test]
3003 fn test_bare_export_at_eof() {
3004 let parsed = parse("VAR = value\nexport VAR", None);
3006 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3007 let makefile = parsed.root();
3008 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3009 assert_eq!(vars.len(), 2);
3010 assert_eq!(makefile.rules().count(), 0);
3011 }
3012
3013 #[test]
3014 fn test_bare_export_does_not_eat_include() {
3015 let parsed = parse("VAR = value\nexport VAR\ninclude other.mk\n", None);
3017 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3018 let makefile = parsed.root();
3019 assert_eq!(makefile.includes().count(), 1);
3020 assert_eq!(
3021 makefile.included_files().collect::<Vec<_>>(),
3022 vec!["other.mk"]
3023 );
3024 }
3025
3026 #[test]
3027 fn test_bare_export_multiple() {
3028 let parsed = parse(
3030 "A = 1\nB = 2\nexport A\nexport B\n\nall:\n\techo done\n",
3031 None,
3032 );
3033 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3034 let makefile = parsed.root();
3035 assert_eq!(makefile.variable_definitions().count(), 4);
3036 let rules = makefile.rules().collect::<Vec<_>>();
3037 assert_eq!(rules.len(), 1);
3038 assert!(rules[0].targets().any(|t| t == "all"));
3039 }
3040
3041 #[test]
3042 fn test_parse_error_does_not_cross_lines() {
3043 let parsed = parse("notarule\n\nbuild-arch:\n\techo arch\n", None);
3046 let makefile = parsed.root();
3047 let rules = makefile.rules().collect::<Vec<_>>();
3048 assert!(
3050 rules.iter().any(|r| r.targets().any(|t| t == "build-arch")),
3051 "build-arch rule should be parsed despite earlier error; rules: {:?}",
3052 rules
3053 .iter()
3054 .map(|r| r.targets().collect::<Vec<_>>())
3055 .collect::<Vec<_>>()
3056 );
3057 }
3058
3059 #[test]
3060 fn test_pyfai_rules_full() {
3061 let input = "\
3063#!/usr/bin/make -f
3064
3065export DH_VERBOSE=1
3066export PYBUILD_NAME=pyfai
3067
3068DEB_CFLAGS_MAINT_APPEND = -Wno-error=incompatible-pointer-types
3069export DEB_CFLAGS_MAINT_APPEND
3070
3071PY3VER := $(shell py3versions -dv)
3072
3073include /usr/share/dpkg/pkg-info.mk # sets SOURCE_DATE_EPOCH
3074
3075%:
3076\tdh $@ --buildsystem=pybuild
3077
3078override_dh_auto_build-arch:
3079\tPYBUILD_BUILD_ARGS=\"-Ccompile-args=--verbose\" dh_auto_build
3080
3081override_dh_auto_build-indep: override_dh_auto_build-arch
3082\tsphinx-build -N -bhtml doc/source build/html
3083
3084override_dh_auto_test:
3085
3086execute_after_dh_auto_install:
3087\tdh_install -p pyfai debian/python3-pyfai/usr/bin /usr
3088";
3089 let parsed = parse(input, None);
3090 let makefile = parsed.root();
3091
3092 assert_eq!(makefile.includes().count(), 1);
3094
3095 assert!(
3097 makefile.find_rule_by_target_pattern("build-arch").is_some(),
3098 "build-arch should match via %: pattern rule"
3099 );
3100 assert!(
3101 makefile
3102 .find_rule_by_target_pattern("build-indep")
3103 .is_some(),
3104 "build-indep should match via %: pattern rule"
3105 );
3106
3107 let rule_targets: Vec<Vec<String>> =
3109 makefile.rules().map(|r| r.targets().collect()).collect();
3110 assert!(
3111 rule_targets.iter().any(|t| t.contains(&"%".to_string())),
3112 "missing %: rule; got: {:?}",
3113 rule_targets
3114 );
3115 assert!(
3116 rule_targets
3117 .iter()
3118 .any(|t| t.contains(&"override_dh_auto_build-arch".to_string())),
3119 "missing override_dh_auto_build-arch; got: {:?}",
3120 rule_targets
3121 );
3122 assert!(
3123 rule_targets
3124 .iter()
3125 .any(|t| t.contains(&"override_dh_auto_test".to_string())),
3126 "missing override_dh_auto_test; got: {:?}",
3127 rule_targets
3128 );
3129 assert!(
3130 rule_targets
3131 .iter()
3132 .any(|t| t.contains(&"execute_after_dh_auto_install".to_string())),
3133 "missing execute_after_dh_auto_install; got: {:?}",
3134 rule_targets
3135 );
3136 }
3137
3138 #[test]
3139 fn test_variable_scopes() {
3140 let parsed = parse(
3141 "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
3142 None,
3143 );
3144 assert!(parsed.errors.is_empty());
3145 let makefile = parsed.root();
3146 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3147 assert_eq!(vars.len(), 4);
3148 let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
3149 assert!(var_names.contains(&"SIMPLE".to_string()));
3150 assert!(var_names.contains(&"IMMEDIATE".to_string()));
3151 assert!(var_names.contains(&"CONDITIONAL".to_string()));
3152 assert!(var_names.contains(&"APPEND".to_string()));
3153 }
3154
3155 #[test]
3156 fn test_pattern_rule_parsing() {
3157 let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
3158 assert!(parsed.errors.is_empty());
3159 let makefile = parsed.root();
3160 let rules = makefile.rules().collect::<Vec<_>>();
3161 assert_eq!(rules.len(), 1);
3162 assert_eq!(rules[0].targets().next().unwrap(), "%.o");
3163 assert!(rules[0].recipes().next().unwrap().contains("$@"));
3164 }
3165
3166 #[test]
3167 fn test_include_variants() {
3168 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
3170 let parsed = parse(makefile_str, None);
3171 assert!(parsed.errors.is_empty());
3172
3173 let node = parsed.syntax();
3175 let debug_str = format!("{:#?}", node);
3176
3177 assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
3179
3180 let makefile = parsed.root();
3182
3183 let include_count = makefile
3185 .syntax()
3186 .children()
3187 .filter(|child| child.kind() == INCLUDE)
3188 .count();
3189 assert_eq!(include_count, 4);
3190
3191 assert!(makefile
3193 .included_files()
3194 .any(|path| path.contains("$(VAR)")));
3195 }
3196
3197 #[test]
3198 fn test_include_api() {
3199 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
3201 let makefile: Makefile = makefile_str.parse().unwrap();
3202
3203 let includes: Vec<_> = makefile.includes().collect();
3205 assert_eq!(includes.len(), 3);
3206
3207 assert!(!includes[0].is_optional()); assert!(includes[1].is_optional()); assert!(includes[2].is_optional()); let files: Vec<_> = makefile.included_files().collect();
3214 assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
3215
3216 assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
3218 assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
3219 assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
3220 }
3221
3222 #[test]
3223 fn test_include_integration() {
3224 let phony_makefile = Makefile::from_reader(
3228 ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3229 .as_bytes()
3230 ).unwrap();
3231
3232 assert_eq!(phony_makefile.rules().count(), 2);
3234
3235 let normal_rules_count = phony_makefile
3237 .rules()
3238 .filter(|r| !r.targets().any(|t| t.starts_with('.')))
3239 .count();
3240 assert_eq!(normal_rules_count, 1);
3241
3242 assert_eq!(phony_makefile.includes().count(), 1);
3244 assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
3245
3246 let simple_makefile = Makefile::from_reader(
3248 "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3249 .as_bytes(),
3250 )
3251 .unwrap();
3252 assert_eq!(simple_makefile.rules().count(), 1);
3253 assert_eq!(simple_makefile.includes().count(), 1);
3254 }
3255
3256 #[test]
3257 fn test_real_conditional_directives() {
3258 let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
3260 let mut buf = conditional.as_bytes();
3261 let makefile =
3262 Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
3263 let code = makefile.code();
3264 assert!(code.contains("ifdef DEBUG"));
3265 assert!(code.contains("else"));
3266 assert!(code.contains("endif"));
3267
3268 let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
3270 let mut buf = nested.as_bytes();
3271 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
3272 let code = makefile.code();
3273 assert!(code.contains("ifdef DEBUG"));
3274 assert!(code.contains("ifdef VERBOSE"));
3275
3276 let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
3278 let mut buf = ifeq.as_bytes();
3279 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
3280 let code = makefile.code();
3281 assert!(code.contains("ifeq"));
3282 assert!(code.contains("Windows_NT"));
3283 }
3284
3285 #[test]
3286 fn test_indented_text_outside_rules() {
3287 let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \" help show help\"\n";
3289 let parsed = parse(help_text, None);
3290 assert!(parsed.errors.is_empty());
3291
3292 let root = parsed.root();
3294 let rules = root.rules().collect::<Vec<_>>();
3295 assert_eq!(rules.len(), 1);
3296
3297 let help_rule = &rules[0];
3298 let recipes = help_rule.recipes().collect::<Vec<_>>();
3299 assert_eq!(recipes.len(), 2);
3300 assert!(recipes[0].contains("Available targets"));
3301 assert!(recipes[1].contains("help"));
3302 }
3303
3304 #[test]
3305 fn test_comment_handling_in_recipes() {
3306 let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
3308
3309 let parsed = parse(recipe_comment, None);
3311
3312 assert!(
3314 parsed.errors.is_empty(),
3315 "Should parse recipe with comments without errors"
3316 );
3317
3318 let root = parsed.root();
3320 let rules = root.rules().collect::<Vec<_>>();
3321 assert_eq!(rules.len(), 1, "Should find exactly one rule");
3322
3323 let build_rule = &rules[0];
3325 assert_eq!(
3326 build_rule.targets().collect::<Vec<_>>(),
3327 vec!["build"],
3328 "Rule should have 'build' as target"
3329 );
3330
3331 let recipes = build_rule.recipe_nodes().collect::<Vec<_>>();
3334 assert_eq!(recipes.len(), 2, "Should find two recipe nodes");
3335
3336 assert_eq!(recipes[0].text(), "");
3338 assert_eq!(
3339 recipes[0].comment(),
3340 Some("# This is a comment".to_string())
3341 );
3342
3343 assert_eq!(recipes[1].text(), "gcc -o app main.c");
3345 assert_eq!(recipes[1].comment(), None);
3346 }
3347
3348 #[test]
3349 fn test_multiline_variables() {
3350 let multiline = "SOURCES = main.c \\\n util.c\n";
3352
3353 let parsed = parse(multiline, None);
3355
3356 let root = parsed.root();
3358 let vars = root.variable_definitions().collect::<Vec<_>>();
3359 assert!(!vars.is_empty(), "Should find at least one variable");
3360
3361 let operators = "CFLAGS := -Wall \\\n -Werror\n";
3365 let parsed_operators = parse(operators, None);
3366
3367 let root = parsed_operators.root();
3369 let vars = root.variable_definitions().collect::<Vec<_>>();
3370 assert!(
3371 !vars.is_empty(),
3372 "Should find at least one variable with := operator"
3373 );
3374
3375 let append = "LDFLAGS += -L/usr/lib \\\n -lm\n";
3377 let parsed_append = parse(append, None);
3378
3379 let root = parsed_append.root();
3381 let vars = root.variable_definitions().collect::<Vec<_>>();
3382 assert!(
3383 !vars.is_empty(),
3384 "Should find at least one variable with += operator"
3385 );
3386 }
3387
3388 #[test]
3389 fn test_whitespace_and_eof_handling() {
3390 let blank_lines = "VAR = value\n\n\n";
3392
3393 let parsed_blank = parse(blank_lines, None);
3394
3395 let root = parsed_blank.root();
3397 let vars = root.variable_definitions().collect::<Vec<_>>();
3398 assert_eq!(
3399 vars.len(),
3400 1,
3401 "Should find one variable in blank lines test"
3402 );
3403
3404 let trailing_space = "VAR = value \n";
3406
3407 let parsed_space = parse(trailing_space, None);
3408
3409 let root = parsed_space.root();
3411 let vars = root.variable_definitions().collect::<Vec<_>>();
3412 assert_eq!(
3413 vars.len(),
3414 1,
3415 "Should find one variable in trailing space test"
3416 );
3417
3418 let no_newline = "VAR = value";
3420
3421 let parsed_no_newline = parse(no_newline, None);
3422
3423 let root = parsed_no_newline.root();
3425 let vars = root.variable_definitions().collect::<Vec<_>>();
3426 assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
3427 assert_eq!(
3428 vars[0].name(),
3429 Some("VAR".to_string()),
3430 "Variable name should be VAR"
3431 );
3432 }
3433
3434 #[test]
3435 fn test_complex_variable_references() {
3436 let wildcard = "SOURCES = $(wildcard *.c)\n";
3438 let parsed = parse(wildcard, None);
3439 assert!(parsed.errors.is_empty());
3440
3441 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3443 let parsed = parse(nested, None);
3444 assert!(parsed.errors.is_empty());
3445
3446 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3448 let parsed = parse(patsubst, None);
3449 assert!(parsed.errors.is_empty());
3450 }
3451
3452 #[test]
3453 fn test_complex_variable_references_minimal() {
3454 let wildcard = "SOURCES = $(wildcard *.c)\n";
3456 let parsed = parse(wildcard, None);
3457 assert!(parsed.errors.is_empty());
3458
3459 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3461 let parsed = parse(nested, None);
3462 assert!(parsed.errors.is_empty());
3463
3464 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3466 let parsed = parse(patsubst, None);
3467 assert!(parsed.errors.is_empty());
3468 }
3469
3470 #[test]
3471 fn test_multiline_variable_with_backslash() {
3472 let content = r#"
3473LONG_VAR = This is a long variable \
3474 that continues on the next line \
3475 and even one more line
3476"#;
3477
3478 let mut buf = content.as_bytes();
3480 let makefile =
3481 Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
3482
3483 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3485 assert_eq!(
3486 vars.len(),
3487 1,
3488 "Expected 1 variable but found {}",
3489 vars.len()
3490 );
3491 let var_value = vars[0].raw_value();
3492 assert!(var_value.is_some(), "Variable value is None");
3493
3494 let value_str = var_value.unwrap();
3496 assert!(
3497 value_str.contains("long variable"),
3498 "Value doesn't contain expected content"
3499 );
3500 }
3501
3502 #[test]
3503 fn test_multiline_variable_with_mixed_operators() {
3504 let content = r#"
3505PREFIX ?= /usr/local
3506CFLAGS := -Wall -O2 \
3507 -I$(PREFIX)/include \
3508 -DDEBUG
3509"#;
3510 let mut buf = content.as_bytes();
3512 let makefile = Makefile::read_relaxed(&mut buf)
3513 .expect("Failed to parse multiline variable with operators");
3514
3515 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3517 assert!(
3518 !vars.is_empty(),
3519 "Expected at least 1 variable, found {}",
3520 vars.len()
3521 );
3522
3523 let prefix_var = vars
3525 .iter()
3526 .find(|v| v.name().unwrap_or_default() == "PREFIX");
3527 assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
3528 assert!(
3529 prefix_var.unwrap().raw_value().is_some(),
3530 "PREFIX variable has no value"
3531 );
3532
3533 let cflags_var = vars
3535 .iter()
3536 .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
3537 assert!(
3538 cflags_var.is_some(),
3539 "Expected to find CFLAGS variable (or part of it)"
3540 );
3541 }
3542
3543 #[test]
3544 fn test_indented_help_text() {
3545 let content = r#"
3546.PHONY: help
3547help:
3548 @echo "Available targets:"
3549 @echo " build - Build the project"
3550 @echo " test - Run tests"
3551 @echo " clean - Remove build artifacts"
3552"#;
3553 let mut buf = content.as_bytes();
3555 let makefile =
3556 Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
3557
3558 let rules = makefile.rules().collect::<Vec<_>>();
3560 assert!(!rules.is_empty(), "Expected at least one rule");
3561
3562 let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
3564 assert!(help_rule.is_some(), "Expected to find help rule");
3565
3566 let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
3568 assert!(
3569 !recipes.is_empty(),
3570 "Expected at least one recipe line in help rule"
3571 );
3572 assert!(
3573 recipes.iter().any(|r| r.contains("Available targets")),
3574 "Expected to find 'Available targets' in recipes"
3575 );
3576 }
3577
3578 #[test]
3579 fn test_indented_lines_in_conditionals() {
3580 let content = r#"
3581ifdef DEBUG
3582 CFLAGS += -g -DDEBUG
3583 # This is a comment inside conditional
3584 ifdef VERBOSE
3585 CFLAGS += -v
3586 endif
3587endif
3588"#;
3589 let mut buf = content.as_bytes();
3591 let makefile = Makefile::read_relaxed(&mut buf)
3592 .expect("Failed to parse indented lines in conditionals");
3593
3594 let code = makefile.code();
3596 assert!(code.contains("ifdef DEBUG"));
3597 assert!(code.contains("ifdef VERBOSE"));
3598 assert!(code.contains("endif"));
3599 }
3600
3601 #[test]
3602 fn test_recipe_with_colon() {
3603 let content = r#"
3604build:
3605 @echo "Building at: $(shell date)"
3606 gcc -o program main.c
3607"#;
3608 let parsed = parse(content, None);
3609 assert!(
3610 parsed.errors.is_empty(),
3611 "Failed to parse recipe with colon: {:?}",
3612 parsed.errors
3613 );
3614 }
3615
3616 #[test]
3617 fn test_double_colon_rules() {
3618 let content = r#"
3619%.o :: %.c
3620 $(CC) -c $< -o $@
3621
3622# Double colon allows multiple rules for same target
3623all:: prerequisite1
3624 @echo "First rule for all"
3625
3626all:: prerequisite2
3627 @echo "Second rule for all"
3628"#;
3629 let parsed = parse(content, None);
3630 assert!(
3631 parsed.errors.is_empty(),
3632 "Failed to parse double colon rules: {:?}",
3633 parsed.errors
3634 );
3635
3636 let makefile = parsed.root();
3637 let rules: Vec<_> = makefile.rules().collect();
3638 assert_eq!(rules.len(), 3);
3639
3640 for rule in &rules {
3642 assert!(rule.is_double_colon());
3643 }
3644
3645 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["%.o"]);
3647 assert_eq!(rules[1].targets().collect::<Vec<_>>(), vec!["all"]);
3648 assert_eq!(rules[2].targets().collect::<Vec<_>>(), vec!["all"]);
3649
3650 assert_eq!(
3652 rules[1].prerequisites().collect::<Vec<_>>(),
3653 vec!["prerequisite1"]
3654 );
3655 assert_eq!(
3656 rules[2].prerequisites().collect::<Vec<_>>(),
3657 vec!["prerequisite2"]
3658 );
3659 }
3660
3661 #[test]
3662 fn test_else_conditional_directives() {
3663 let content = r#"
3665ifeq ($(OS),Windows_NT)
3666 TARGET = windows
3667else ifeq ($(OS),Darwin)
3668 TARGET = macos
3669else ifeq ($(OS),Linux)
3670 TARGET = linux
3671else
3672 TARGET = unknown
3673endif
3674"#;
3675 let mut buf = content.as_bytes();
3676 let makefile =
3677 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3678 assert!(makefile.code().contains("else ifeq"));
3679 assert!(makefile.code().contains("TARGET"));
3680
3681 let content = r#"
3683ifdef WINDOWS
3684 TARGET = windows
3685else ifdef DARWIN
3686 TARGET = macos
3687else ifdef LINUX
3688 TARGET = linux
3689else
3690 TARGET = unknown
3691endif
3692"#;
3693 let mut buf = content.as_bytes();
3694 let makefile =
3695 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3696 assert!(makefile.code().contains("else ifdef"));
3697
3698 let content = r#"
3700ifndef NOWINDOWS
3701 TARGET = windows
3702else ifndef NODARWIN
3703 TARGET = macos
3704else
3705 TARGET = linux
3706endif
3707"#;
3708 let mut buf = content.as_bytes();
3709 let makefile =
3710 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3711 assert!(makefile.code().contains("else ifndef"));
3712
3713 let content = r#"
3715ifneq ($(OS),Windows_NT)
3716 TARGET = not_windows
3717else ifneq ($(OS),Darwin)
3718 TARGET = not_macos
3719else
3720 TARGET = darwin
3721endif
3722"#;
3723 let mut buf = content.as_bytes();
3724 let makefile =
3725 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3726 assert!(makefile.code().contains("else ifneq"));
3727 }
3728
3729 #[test]
3730 fn test_complex_else_conditionals() {
3731 let content = r#"VAR1 := foo
3733VAR2 := bar
3734
3735ifeq ($(VAR1),foo)
3736 RESULT := foo_matched
3737else ifdef VAR2
3738 RESULT := var2_defined
3739else ifndef VAR3
3740 RESULT := var3_not_defined
3741else
3742 RESULT := final_else
3743endif
3744
3745all:
3746 @echo $(RESULT)
3747"#;
3748 let mut buf = content.as_bytes();
3749 let makefile =
3750 Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3751
3752 let code = makefile.code();
3754 assert!(code.contains("ifeq ($(VAR1),foo)"));
3755 assert!(code.contains("else ifdef VAR2"));
3756 assert!(code.contains("else ifndef VAR3"));
3757 assert!(code.contains("else"));
3758 assert!(code.contains("endif"));
3759 assert!(code.contains("RESULT"));
3760
3761 let rules: Vec<_> = makefile.rules().collect();
3763 assert_eq!(rules.len(), 1);
3764 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3765 }
3766
3767 #[test]
3768 fn test_conditional_token_structure() {
3769 let content = r#"ifdef VAR1
3771X := 1
3772else ifdef VAR2
3773X := 2
3774else
3775X := 3
3776endif
3777"#;
3778 let mut buf = content.as_bytes();
3779 let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3780
3781 let syntax = makefile.syntax();
3783
3784 let mut found_conditional = false;
3786 let mut found_conditional_if = false;
3787 let mut found_conditional_else = false;
3788 let mut found_conditional_endif = false;
3789
3790 fn check_node(
3791 node: &SyntaxNode,
3792 found_cond: &mut bool,
3793 found_if: &mut bool,
3794 found_else: &mut bool,
3795 found_endif: &mut bool,
3796 ) {
3797 match node.kind() {
3798 SyntaxKind::CONDITIONAL => *found_cond = true,
3799 SyntaxKind::CONDITIONAL_IF => *found_if = true,
3800 SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3801 SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3802 _ => {}
3803 }
3804
3805 for child in node.children() {
3806 check_node(&child, found_cond, found_if, found_else, found_endif);
3807 }
3808 }
3809
3810 check_node(
3811 syntax,
3812 &mut found_conditional,
3813 &mut found_conditional_if,
3814 &mut found_conditional_else,
3815 &mut found_conditional_endif,
3816 );
3817
3818 assert!(found_conditional, "Should have CONDITIONAL node");
3819 assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3820 assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3821 assert!(
3822 found_conditional_endif,
3823 "Should have CONDITIONAL_ENDIF node"
3824 );
3825 }
3826
3827 #[test]
3828 fn test_ambiguous_assignment_vs_rule() {
3829 const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3831
3832 let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3833 let makefile =
3834 Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3835
3836 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3837 let rules = makefile.rules().collect::<Vec<_>>();
3838
3839 assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3840 assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3841
3842 assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3843
3844 const SIMPLE_RULE: &str = "target: dependency\n";
3846
3847 let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3848 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3849
3850 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3851 let rules = makefile.rules().collect::<Vec<_>>();
3852
3853 assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3854 assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3855
3856 let rule = &rules[0];
3857 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3858 }
3859
3860 #[test]
3861 fn test_nested_conditionals() {
3862 let content = r#"
3863ifdef RELEASE
3864 CFLAGS += -O3
3865 ifndef DEBUG
3866 ifneq ($(ARCH),arm)
3867 CFLAGS += -march=native
3868 else
3869 CFLAGS += -mcpu=cortex-a72
3870 endif
3871 endif
3872endif
3873"#;
3874 let mut buf = content.as_bytes();
3876 let makefile =
3877 Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3878
3879 let code = makefile.code();
3881 assert!(code.contains("ifdef RELEASE"));
3882 assert!(code.contains("ifndef DEBUG"));
3883 assert!(code.contains("ifneq"));
3884 }
3885
3886 #[test]
3887 fn test_space_indented_recipes() {
3888 let content = r#"
3891build:
3892 @echo "Building with spaces instead of tabs"
3893 gcc -o program main.c
3894"#;
3895 let mut buf = content.as_bytes();
3897 let makefile =
3898 Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3899
3900 let rules = makefile.rules().collect::<Vec<_>>();
3902 assert!(!rules.is_empty(), "Expected at least one rule");
3903
3904 let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3906 assert!(build_rule.is_some(), "Expected to find build rule");
3907 }
3908
3909 #[test]
3910 fn test_complex_variable_functions() {
3911 let content = r#"
3912FILES := $(shell find . -name "*.c")
3913OBJS := $(patsubst %.c,%.o,$(FILES))
3914NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3915HEADERS := ${wildcard *.h}
3916"#;
3917 let parsed = parse(content, None);
3918 assert!(
3919 parsed.errors.is_empty(),
3920 "Failed to parse complex variable functions: {:?}",
3921 parsed.errors
3922 );
3923 }
3924
3925 #[test]
3926 fn test_nested_variable_expansions() {
3927 let content = r#"
3928VERSION = 1.0
3929PACKAGE = myapp
3930TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3931INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3932"#;
3933 let parsed = parse(content, None);
3934 assert!(
3935 parsed.errors.is_empty(),
3936 "Failed to parse nested variable expansions: {:?}",
3937 parsed.errors
3938 );
3939 }
3940
3941 #[test]
3942 fn test_special_directives() {
3943 let content = r#"
3944# Special makefile directives
3945.PHONY: all clean
3946.SUFFIXES: .c .o
3947.DEFAULT: all
3948
3949# Variable definition and export directive
3950export PATH := /usr/bin:/bin
3951"#;
3952 let mut buf = content.as_bytes();
3954 let makefile =
3955 Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3956
3957 let rules = makefile.rules().collect::<Vec<_>>();
3959
3960 let phony_rule = rules
3962 .iter()
3963 .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3964 assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3965
3966 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3968 assert!(!vars.is_empty(), "Expected to find at least one variable");
3969 }
3970
3971 #[test]
3974 fn test_comprehensive_real_world_makefile() {
3975 let content = r#"
3977# Basic variable assignment
3978VERSION = 1.0.0
3979
3980# Phony target
3981.PHONY: all clean
3982
3983# Simple rule
3984all:
3985 echo "Building version $(VERSION)"
3986
3987# Another rule with dependencies
3988clean:
3989 rm -f *.o
3990"#;
3991
3992 let parsed = parse(content, None);
3994
3995 assert!(parsed.errors.is_empty(), "Expected no parsing errors");
3997
3998 let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
4000 assert!(!variables.is_empty(), "Expected at least one variable");
4001 assert_eq!(
4002 variables[0].name(),
4003 Some("VERSION".to_string()),
4004 "Expected VERSION variable"
4005 );
4006
4007 let rules = parsed.root().rules().collect::<Vec<_>>();
4009 assert!(!rules.is_empty(), "Expected at least one rule");
4010
4011 let rule_targets: Vec<String> = rules
4013 .iter()
4014 .flat_map(|r| r.targets().collect::<Vec<_>>())
4015 .collect();
4016 assert!(
4017 rule_targets.contains(&".PHONY".to_string()),
4018 "Expected .PHONY rule"
4019 );
4020 assert!(
4021 rule_targets.contains(&"all".to_string()),
4022 "Expected 'all' rule"
4023 );
4024 assert!(
4025 rule_targets.contains(&"clean".to_string()),
4026 "Expected 'clean' rule"
4027 );
4028 }
4029
4030 #[test]
4031 fn test_indented_help_text_outside_rules() {
4032 let content = r#"
4034# Targets with help text
4035help:
4036 @echo "Available targets:"
4037 @echo " build build the project"
4038 @echo " test run tests"
4039 @echo " clean clean build artifacts"
4040
4041# Another target
4042clean:
4043 rm -rf build/
4044"#;
4045
4046 let parsed = parse(content, None);
4048
4049 assert!(
4051 parsed.errors.is_empty(),
4052 "Failed to parse indented help text"
4053 );
4054
4055 let rules = parsed.root().rules().collect::<Vec<_>>();
4057 assert_eq!(rules.len(), 2, "Expected to find two rules");
4058
4059 let help_rule = rules
4061 .iter()
4062 .find(|r| r.targets().any(|t| t == "help"))
4063 .expect("Expected to find help rule");
4064
4065 let clean_rule = rules
4066 .iter()
4067 .find(|r| r.targets().any(|t| t == "clean"))
4068 .expect("Expected to find clean rule");
4069
4070 let help_recipes = help_rule.recipes().collect::<Vec<_>>();
4072 assert!(
4073 !help_recipes.is_empty(),
4074 "Help rule should have recipe lines"
4075 );
4076 assert!(
4077 help_recipes
4078 .iter()
4079 .any(|line| line.contains("Available targets")),
4080 "Help recipes should include 'Available targets' line"
4081 );
4082
4083 let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
4085 assert!(
4086 !clean_recipes.is_empty(),
4087 "Clean rule should have recipe lines"
4088 );
4089 assert!(
4090 clean_recipes.iter().any(|line| line.contains("rm -rf")),
4091 "Clean recipes should include 'rm -rf' command"
4092 );
4093 }
4094
4095 #[test]
4096 fn test_makefile1_phony_pattern() {
4097 let content = "#line 2145\n.PHONY: $(PHONY)\n";
4099
4100 let result = parse(content, None);
4102
4103 assert!(
4105 result.errors.is_empty(),
4106 "Failed to parse .PHONY: $(PHONY) pattern"
4107 );
4108
4109 let rules = result.root().rules().collect::<Vec<_>>();
4111 assert_eq!(rules.len(), 1, "Expected 1 rule");
4112 assert_eq!(
4113 rules[0].targets().next().unwrap(),
4114 ".PHONY",
4115 "Expected .PHONY rule"
4116 );
4117
4118 let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
4120 assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
4121 assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
4122 }
4123
4124 #[test]
4125 fn test_skip_until_newline_behavior() {
4126 let input = "text without newline";
4128 let parsed = parse(input, None);
4129 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4131
4132 let input_with_newline = "text\nafter newline";
4133 let parsed2 = parse(input_with_newline, None);
4134 assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
4135 }
4136
4137 #[test]
4138 #[ignore] fn test_error_with_indent_token() {
4140 let input = "\tinvalid indented line";
4142 let parsed = parse(input, None);
4143 assert!(!parsed.errors.is_empty());
4145
4146 let error_msg = &parsed.errors[0].message;
4147 assert!(error_msg.contains("recipe commences before first target"));
4148 }
4149
4150 #[test]
4151 fn test_conditional_token_handling() {
4152 let input = r#"
4154ifndef VAR
4155 CFLAGS = -DTEST
4156endif
4157"#;
4158 let parsed = parse(input, None);
4159 let makefile = parsed.root();
4161 let _vars = makefile.variable_definitions().collect::<Vec<_>>();
4162 let nested = r#"
4166ifdef DEBUG
4167 ifndef RELEASE
4168 CFLAGS = -g
4169 endif
4170endif
4171"#;
4172 let parsed_nested = parse(nested, None);
4173 let _makefile = parsed_nested.root();
4175 }
4176
4177 #[test]
4178 fn test_include_vs_conditional_logic() {
4179 let input = r#"
4181include file.mk
4182ifdef VAR
4183 VALUE = 1
4184endif
4185"#;
4186 let parsed = parse(input, None);
4187 let makefile = parsed.root();
4189 let includes = makefile.includes().collect::<Vec<_>>();
4190 assert!(!includes.is_empty() || !parsed.errors.is_empty());
4192
4193 let optional_include = r#"
4195-include optional.mk
4196ifndef VAR
4197 VALUE = default
4198endif
4199"#;
4200 let parsed2 = parse(optional_include, None);
4201 let _makefile = parsed2.root();
4203 }
4204
4205 #[test]
4206 fn test_balanced_parens_counting() {
4207 let input = r#"
4209VAR = $(call func,$(nested,arg),extra)
4210COMPLEX = $(if $(condition),$(then_val),$(else_val))
4211"#;
4212 let parsed = parse(input, None);
4213 assert!(parsed.errors.is_empty());
4214
4215 let makefile = parsed.root();
4216 let vars = makefile.variable_definitions().collect::<Vec<_>>();
4217 assert_eq!(vars.len(), 2);
4218 }
4219
4220 #[test]
4221 fn test_documentation_lookahead() {
4222 let input = r#"
4224# Documentation comment
4225help:
4226 @echo "Usage instructions"
4227 @echo "More help text"
4228"#;
4229 let parsed = parse(input, None);
4230 assert!(parsed.errors.is_empty());
4231
4232 let makefile = parsed.root();
4233 let rules = makefile.rules().collect::<Vec<_>>();
4234 assert_eq!(rules.len(), 1);
4235 assert_eq!(rules[0].targets().next().unwrap(), "help");
4236 }
4237
4238 #[test]
4239 fn test_edge_case_empty_input() {
4240 let parsed = parse("", None);
4242 assert!(parsed.errors.is_empty());
4243
4244 let parsed2 = parse(" \n \n", None);
4246 let _makefile = parsed2.root();
4249 }
4250
4251 #[test]
4252 fn test_malformed_conditional_recovery() {
4253 let input = r#"
4255ifdef
4256 # Missing condition variable
4257endif
4258"#;
4259 let parsed = parse(input, None);
4260 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4263 }
4264
4265 #[test]
4266 fn test_replace_rule() {
4267 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4268 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4269
4270 makefile.replace_rule(0, new_rule).unwrap();
4271
4272 let targets: Vec<_> = makefile
4273 .rules()
4274 .flat_map(|r| r.targets().collect::<Vec<_>>())
4275 .collect();
4276 assert_eq!(targets, vec!["new_rule", "rule2"]);
4277
4278 let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
4279 assert_eq!(recipes, vec!["new_command"]);
4280 }
4281
4282 #[test]
4283 fn test_replace_rule_out_of_bounds() {
4284 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4285 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4286
4287 let result = makefile.replace_rule(5, new_rule);
4288 assert!(result.is_err());
4289 }
4290
4291 #[test]
4292 fn test_remove_rule() {
4293 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
4294 .parse()
4295 .unwrap();
4296
4297 let removed = makefile.remove_rule(1).unwrap();
4298 assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
4299
4300 let remaining_targets: Vec<_> = makefile
4301 .rules()
4302 .flat_map(|r| r.targets().collect::<Vec<_>>())
4303 .collect();
4304 assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
4305 assert_eq!(makefile.rules().count(), 2);
4306 }
4307
4308 #[test]
4309 fn test_remove_rule_out_of_bounds() {
4310 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4311
4312 let result = makefile.remove_rule(5);
4313 assert!(result.is_err());
4314 }
4315
4316 #[test]
4317 fn test_insert_rule() {
4318 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4319 let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
4320
4321 makefile.insert_rule(1, new_rule).unwrap();
4322
4323 let targets: Vec<_> = makefile
4324 .rules()
4325 .flat_map(|r| r.targets().collect::<Vec<_>>())
4326 .collect();
4327 assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
4328 assert_eq!(makefile.rules().count(), 3);
4329 }
4330
4331 #[test]
4332 fn test_insert_rule_at_end() {
4333 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4334 let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
4335
4336 makefile.insert_rule(1, new_rule).unwrap();
4337
4338 let targets: Vec<_> = makefile
4339 .rules()
4340 .flat_map(|r| r.targets().collect::<Vec<_>>())
4341 .collect();
4342 assert_eq!(targets, vec!["rule1", "end_rule"]);
4343 }
4344
4345 #[test]
4346 fn test_insert_rule_out_of_bounds() {
4347 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4348 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4349
4350 let result = makefile.insert_rule(5, new_rule);
4351 assert!(result.is_err());
4352 }
4353
4354 #[test]
4355 fn test_insert_rule_preserves_blank_line_spacing_at_end() {
4356 let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
4358 let mut makefile: Makefile = input.parse().unwrap();
4359 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4360
4361 makefile.insert_rule(2, new_rule).unwrap();
4362
4363 let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4364 assert_eq!(makefile.to_string(), expected);
4365 }
4366
4367 #[test]
4368 fn test_insert_rule_adds_blank_lines_when_missing() {
4369 let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
4371 let mut makefile: Makefile = input.parse().unwrap();
4372 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4373
4374 makefile.insert_rule(2, new_rule).unwrap();
4375
4376 let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4377 assert_eq!(makefile.to_string(), expected);
4378 }
4379
4380 #[test]
4381 fn test_remove_command() {
4382 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4383 .parse()
4384 .unwrap();
4385
4386 rule.remove_command(1);
4387 let recipes: Vec<_> = rule.recipes().collect();
4388 assert_eq!(recipes, vec!["command1", "command3"]);
4389 assert_eq!(rule.recipe_count(), 2);
4390 }
4391
4392 #[test]
4393 fn test_remove_command_out_of_bounds() {
4394 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4395
4396 let result = rule.remove_command(5);
4397 assert!(!result);
4398 }
4399
4400 #[test]
4401 fn test_insert_command() {
4402 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
4403
4404 rule.insert_command(1, "command2");
4405 let recipes: Vec<_> = rule.recipes().collect();
4406 assert_eq!(recipes, vec!["command1", "command2", "command3"]);
4407 }
4408
4409 #[test]
4410 fn test_insert_command_at_end() {
4411 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4412
4413 rule.insert_command(1, "command2");
4414 let recipes: Vec<_> = rule.recipes().collect();
4415 assert_eq!(recipes, vec!["command1", "command2"]);
4416 }
4417
4418 #[test]
4419 fn test_insert_command_in_empty_rule() {
4420 let mut rule: Rule = "rule:\n".parse().unwrap();
4421
4422 rule.insert_command(0, "new_command");
4423 let recipes: Vec<_> = rule.recipes().collect();
4424 assert_eq!(recipes, vec!["new_command"]);
4425 }
4426
4427 #[test]
4428 fn test_recipe_count() {
4429 let rule1: Rule = "rule:\n".parse().unwrap();
4430 assert_eq!(rule1.recipe_count(), 0);
4431
4432 let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
4433 assert_eq!(rule2.recipe_count(), 2);
4434 }
4435
4436 #[test]
4437 fn test_clear_commands() {
4438 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4439 .parse()
4440 .unwrap();
4441
4442 rule.clear_commands();
4443 assert_eq!(rule.recipe_count(), 0);
4444
4445 let recipes: Vec<_> = rule.recipes().collect();
4446 assert_eq!(recipes, Vec::<String>::new());
4447
4448 let targets: Vec<_> = rule.targets().collect();
4450 assert_eq!(targets, vec!["rule"]);
4451 }
4452
4453 #[test]
4454 fn test_clear_commands_empty_rule() {
4455 let mut rule: Rule = "rule:\n".parse().unwrap();
4456
4457 rule.clear_commands();
4458 assert_eq!(rule.recipe_count(), 0);
4459
4460 let targets: Vec<_> = rule.targets().collect();
4461 assert_eq!(targets, vec!["rule"]);
4462 }
4463
4464 #[test]
4465 fn test_rule_manipulation_preserves_structure() {
4466 let input = r#"# Comment
4468VAR = value
4469
4470rule1:
4471 command1
4472
4473# Another comment
4474rule2:
4475 command2
4476
4477VAR2 = value2
4478"#;
4479
4480 let mut makefile: Makefile = input.parse().unwrap();
4481 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4482
4483 makefile.insert_rule(1, new_rule).unwrap();
4485
4486 let targets: Vec<_> = makefile
4488 .rules()
4489 .flat_map(|r| r.targets().collect::<Vec<_>>())
4490 .collect();
4491 assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
4492
4493 let vars: Vec<_> = makefile.variable_definitions().collect();
4495 assert_eq!(vars.len(), 2);
4496
4497 let output = makefile.code();
4499 assert!(output.contains("# Comment"));
4500 assert!(output.contains("VAR = value"));
4501 assert!(output.contains("# Another comment"));
4502 assert!(output.contains("VAR2 = value2"));
4503 }
4504
4505 #[test]
4506 fn test_replace_rule_with_multiple_targets() {
4507 let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
4508 let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
4509
4510 makefile.replace_rule(0, new_rule).unwrap();
4511
4512 let targets: Vec<_> = makefile
4513 .rules()
4514 .flat_map(|r| r.targets().collect::<Vec<_>>())
4515 .collect();
4516 assert_eq!(targets, vec!["new_target"]);
4517 }
4518
4519 #[test]
4520 fn test_empty_makefile_operations() {
4521 let mut makefile = Makefile::new();
4522
4523 assert!(makefile
4525 .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
4526 .is_err());
4527 assert!(makefile.remove_rule(0).is_err());
4528
4529 let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
4531 makefile.insert_rule(0, new_rule).unwrap();
4532 assert_eq!(makefile.rules().count(), 1);
4533 }
4534
4535 #[test]
4536 fn test_command_operations_preserve_indentation() {
4537 let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
4538 .parse()
4539 .unwrap();
4540
4541 rule.insert_command(1, "middle_command");
4542 let recipes: Vec<_> = rule.recipes().collect();
4543 assert_eq!(
4544 recipes,
4545 vec!["\tdeep_indent", "middle_command", "shallow_indent"]
4546 );
4547 }
4548
4549 #[test]
4550 fn test_rule_operations_with_variables_and_includes() {
4551 let input = r#"VAR1 = value1
4552include common.mk
4553
4554rule1:
4555 command1
4556
4557VAR2 = value2
4558include other.mk
4559
4560rule2:
4561 command2
4562"#;
4563
4564 let mut makefile: Makefile = input.parse().unwrap();
4565
4566 makefile.remove_rule(0).unwrap();
4568
4569 let output = makefile.code();
4571 assert!(output.contains("VAR1 = value1"));
4572 assert!(output.contains("include common.mk"));
4573 assert!(output.contains("VAR2 = value2"));
4574 assert!(output.contains("include other.mk"));
4575
4576 assert_eq!(makefile.rules().count(), 1);
4578 let remaining_targets: Vec<_> = makefile
4579 .rules()
4580 .flat_map(|r| r.targets().collect::<Vec<_>>())
4581 .collect();
4582 assert_eq!(remaining_targets, vec!["rule2"]);
4583 }
4584
4585 #[test]
4586 fn test_command_manipulation_edge_cases() {
4587 let mut empty_rule: Rule = "empty:\n".parse().unwrap();
4589 assert_eq!(empty_rule.recipe_count(), 0);
4590
4591 empty_rule.insert_command(0, "first_command");
4592 assert_eq!(empty_rule.recipe_count(), 1);
4593
4594 let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
4596 empty_rule2.clear_commands();
4597 assert_eq!(empty_rule2.recipe_count(), 0);
4598 }
4599
4600 #[test]
4601 fn test_large_makefile_performance() {
4602 let mut makefile = Makefile::new();
4604
4605 for i in 0..100 {
4607 let rule_name = format!("rule{}", i);
4608 makefile
4609 .add_rule(&rule_name)
4610 .push_command(&format!("command{}", i));
4611 }
4612
4613 assert_eq!(makefile.rules().count(), 100);
4614
4615 let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
4617 makefile.replace_rule(50, new_rule).unwrap();
4618
4619 let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
4621 assert_eq!(rule_50_targets, vec!["middle_rule"]);
4622
4623 assert_eq!(makefile.rules().count(), 100); }
4625
4626 #[test]
4627 fn test_complex_recipe_manipulation() {
4628 let mut complex_rule: Rule = r#"complex:
4629 @echo "Starting build"
4630 $(CC) $(CFLAGS) -o $@ $<
4631 @echo "Build complete"
4632 chmod +x $@
4633"#
4634 .parse()
4635 .unwrap();
4636
4637 assert_eq!(complex_rule.recipe_count(), 4);
4638
4639 complex_rule.remove_command(0); complex_rule.remove_command(1); let final_recipes: Vec<_> = complex_rule.recipes().collect();
4644 assert_eq!(final_recipes.len(), 2);
4645 assert!(final_recipes[0].contains("$(CC)"));
4646 assert!(final_recipes[1].contains("chmod"));
4647 }
4648
4649 #[test]
4650 fn test_variable_definition_remove() {
4651 let makefile: Makefile = r#"VAR1 = value1
4652VAR2 = value2
4653VAR3 = value3
4654"#
4655 .parse()
4656 .unwrap();
4657
4658 assert_eq!(makefile.variable_definitions().count(), 3);
4660
4661 let mut var2 = makefile
4663 .variable_definitions()
4664 .nth(1)
4665 .expect("Should have second variable");
4666 assert_eq!(var2.name(), Some("VAR2".to_string()));
4667 var2.remove();
4668
4669 assert_eq!(makefile.variable_definitions().count(), 2);
4671 let var_names: Vec<_> = makefile
4672 .variable_definitions()
4673 .filter_map(|v| v.name())
4674 .collect();
4675 assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4676 }
4677
4678 #[test]
4679 fn test_variable_definition_set_value() {
4680 let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4681
4682 let mut var = makefile
4683 .variable_definitions()
4684 .next()
4685 .expect("Should have variable");
4686 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4687
4688 var.set_value("new_value");
4690
4691 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4693 assert!(makefile.code().contains("VAR = new_value"));
4694 }
4695
4696 #[test]
4697 fn test_variable_definition_set_value_preserves_format() {
4698 let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4699
4700 let mut var = makefile
4701 .variable_definitions()
4702 .next()
4703 .expect("Should have variable");
4704 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4705
4706 var.set_value("new_value");
4708
4709 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4711 let code = makefile.code();
4712 assert!(code.contains("export"), "Should preserve export prefix");
4713 assert!(code.contains(":="), "Should preserve := operator");
4714 assert!(code.contains("new_value"), "Should have new value");
4715 }
4716
4717 #[test]
4718 fn test_makefile_find_variable() {
4719 let makefile: Makefile = r#"VAR1 = value1
4720VAR2 = value2
4721VAR3 = value3
4722"#
4723 .parse()
4724 .unwrap();
4725
4726 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4728 assert_eq!(vars.len(), 1);
4729 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4730 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4731
4732 assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4734 }
4735
4736 #[test]
4737 fn test_makefile_find_variable_with_export() {
4738 let makefile: Makefile = r#"VAR1 = value1
4739export VAR2 := value2
4740VAR3 = value3
4741"#
4742 .parse()
4743 .unwrap();
4744
4745 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4747 assert_eq!(vars.len(), 1);
4748 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4749 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4750 }
4751
4752 #[test]
4753 fn test_variable_definition_is_export() {
4754 let makefile: Makefile = r#"VAR1 = value1
4755export VAR2 := value2
4756export VAR3 = value3
4757VAR4 := value4
4758"#
4759 .parse()
4760 .unwrap();
4761
4762 let vars: Vec<_> = makefile.variable_definitions().collect();
4763 assert_eq!(vars.len(), 4);
4764
4765 assert!(!vars[0].is_export());
4766 assert!(vars[1].is_export());
4767 assert!(vars[2].is_export());
4768 assert!(!vars[3].is_export());
4769 }
4770
4771 #[test]
4772 fn test_makefile_find_variable_multiple() {
4773 let makefile: Makefile = r#"VAR1 = value1
4774VAR1 = value2
4775VAR2 = other
4776VAR1 = value3
4777"#
4778 .parse()
4779 .unwrap();
4780
4781 let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4783 assert_eq!(vars.len(), 3);
4784 assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4785 assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4786 assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4787
4788 let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4790 assert_eq!(var2s.len(), 1);
4791 assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4792 }
4793
4794 #[test]
4795 fn test_variable_remove_and_find() {
4796 let makefile: Makefile = r#"VAR1 = value1
4797VAR2 = value2
4798VAR3 = value3
4799"#
4800 .parse()
4801 .unwrap();
4802
4803 let mut var2 = makefile
4805 .find_variable("VAR2")
4806 .next()
4807 .expect("Should find VAR2");
4808 var2.remove();
4809
4810 assert_eq!(makefile.find_variable("VAR2").count(), 0);
4812
4813 assert_eq!(makefile.find_variable("VAR1").count(), 1);
4815 assert_eq!(makefile.find_variable("VAR3").count(), 1);
4816 }
4817
4818 #[test]
4819 fn test_variable_remove_with_comment() {
4820 let makefile: Makefile = r#"VAR1 = value1
4821# This is a comment about VAR2
4822VAR2 = value2
4823VAR3 = value3
4824"#
4825 .parse()
4826 .unwrap();
4827
4828 let mut var2 = makefile
4830 .variable_definitions()
4831 .nth(1)
4832 .expect("Should have second variable");
4833 assert_eq!(var2.name(), Some("VAR2".to_string()));
4834 var2.remove();
4835
4836 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4838 }
4839
4840 #[test]
4841 fn test_variable_remove_with_multiple_comments() {
4842 let makefile: Makefile = r#"VAR1 = value1
4843# Comment line 1
4844# Comment line 2
4845# Comment line 3
4846VAR2 = value2
4847VAR3 = value3
4848"#
4849 .parse()
4850 .unwrap();
4851
4852 let mut var2 = makefile
4854 .variable_definitions()
4855 .nth(1)
4856 .expect("Should have second variable");
4857 var2.remove();
4858
4859 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4861 }
4862
4863 #[test]
4864 fn test_variable_remove_with_empty_line() {
4865 let makefile: Makefile = r#"VAR1 = value1
4866
4867# Comment about VAR2
4868VAR2 = value2
4869VAR3 = value3
4870"#
4871 .parse()
4872 .unwrap();
4873
4874 let mut var2 = makefile
4876 .variable_definitions()
4877 .nth(1)
4878 .expect("Should have second variable");
4879 var2.remove();
4880
4881 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4884 }
4885
4886 #[test]
4887 fn test_variable_remove_with_multiple_empty_lines() {
4888 let makefile: Makefile = r#"VAR1 = value1
4889
4890
4891# Comment about VAR2
4892VAR2 = value2
4893VAR3 = value3
4894"#
4895 .parse()
4896 .unwrap();
4897
4898 let mut var2 = makefile
4900 .variable_definitions()
4901 .nth(1)
4902 .expect("Should have second variable");
4903 var2.remove();
4904
4905 assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4908 }
4909
4910 #[test]
4911 fn test_rule_remove_with_comment() {
4912 let makefile: Makefile = r#"rule1:
4913 command1
4914
4915# Comment about rule2
4916rule2:
4917 command2
4918rule3:
4919 command3
4920"#
4921 .parse()
4922 .unwrap();
4923
4924 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4926 rule2.remove().unwrap();
4927
4928 assert_eq!(
4931 makefile.code(),
4932 "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4933 );
4934 }
4935
4936 #[test]
4937 fn test_variable_remove_preserves_shebang() {
4938 let makefile: Makefile = r#"#!/usr/bin/make -f
4939# This is a regular comment
4940VAR1 = value1
4941VAR2 = value2
4942"#
4943 .parse()
4944 .unwrap();
4945
4946 let mut var1 = makefile.variable_definitions().next().unwrap();
4948 var1.remove();
4949
4950 let code = makefile.code();
4952 assert!(code.starts_with("#!/usr/bin/make -f"));
4953 assert!(!code.contains("regular comment"));
4954 assert!(!code.contains("VAR1"));
4955 assert!(code.contains("VAR2"));
4956 }
4957
4958 #[test]
4959 fn test_variable_remove_preserves_subsequent_comments() {
4960 let makefile: Makefile = r#"VAR1 = value1
4961# Comment about VAR2
4962VAR2 = value2
4963
4964# Comment about VAR3
4965VAR3 = value3
4966"#
4967 .parse()
4968 .unwrap();
4969
4970 let mut var2 = makefile
4972 .variable_definitions()
4973 .nth(1)
4974 .expect("Should have second variable");
4975 var2.remove();
4976
4977 let code = makefile.code();
4979 assert_eq!(
4980 code,
4981 "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4982 );
4983 }
4984
4985 #[test]
4986 fn test_variable_remove_after_shebang_preserves_empty_line() {
4987 let makefile: Makefile = r#"#!/usr/bin/make -f
4988export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4989
4990%:
4991 dh $@
4992"#
4993 .parse()
4994 .unwrap();
4995
4996 let mut var = makefile.variable_definitions().next().unwrap();
4998 var.remove();
4999
5000 assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
5002 }
5003
5004 #[test]
5005 fn test_rule_add_prerequisite() {
5006 let mut rule: Rule = "target: dep1\n".parse().unwrap();
5007 rule.add_prerequisite("dep2").unwrap();
5008 assert_eq!(
5009 rule.prerequisites().collect::<Vec<_>>(),
5010 vec!["dep1", "dep2"]
5011 );
5012 assert_eq!(rule.to_string(), "target: dep1 dep2\n");
5014 }
5015
5016 #[test]
5017 fn test_rule_add_prerequisite_to_rule_without_prereqs() {
5018 let mut rule: Rule = "target:\n".parse().unwrap();
5020 rule.add_prerequisite("dep1").unwrap();
5021 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
5022 assert_eq!(rule.to_string(), "target: dep1\n");
5024 }
5025
5026 #[test]
5027 fn test_rule_remove_prerequisite() {
5028 let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
5029 assert!(rule.remove_prerequisite("dep2").unwrap());
5030 assert_eq!(
5031 rule.prerequisites().collect::<Vec<_>>(),
5032 vec!["dep1", "dep3"]
5033 );
5034 assert!(!rule.remove_prerequisite("nonexistent").unwrap());
5035 }
5036
5037 #[test]
5038 fn test_rule_set_prerequisites() {
5039 let mut rule: Rule = "target: old_dep\n".parse().unwrap();
5040 rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
5041 .unwrap();
5042 assert_eq!(
5043 rule.prerequisites().collect::<Vec<_>>(),
5044 vec!["new_dep1", "new_dep2"]
5045 );
5046 }
5047
5048 #[test]
5049 fn test_rule_set_prerequisites_empty() {
5050 let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
5051 rule.set_prerequisites(vec![]).unwrap();
5052 assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
5053 }
5054
5055 #[test]
5056 fn test_rule_add_target() {
5057 let mut rule: Rule = "target1: dep1\n".parse().unwrap();
5058 rule.add_target("target2").unwrap();
5059 assert_eq!(
5060 rule.targets().collect::<Vec<_>>(),
5061 vec!["target1", "target2"]
5062 );
5063 }
5064
5065 #[test]
5066 fn test_rule_set_targets() {
5067 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5068 rule.set_targets(vec!["new_target1", "new_target2"])
5069 .unwrap();
5070 assert_eq!(
5071 rule.targets().collect::<Vec<_>>(),
5072 vec!["new_target1", "new_target2"]
5073 );
5074 }
5075
5076 #[test]
5077 fn test_rule_set_targets_empty() {
5078 let mut rule: Rule = "target: dep1\n".parse().unwrap();
5079 let result = rule.set_targets(vec![]);
5080 assert!(result.is_err());
5081 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
5083 }
5084
5085 #[test]
5086 fn test_rule_has_target() {
5087 let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
5088 assert!(rule.has_target("target1"));
5089 assert!(rule.has_target("target2"));
5090 assert!(!rule.has_target("target3"));
5091 assert!(!rule.has_target("nonexistent"));
5092 }
5093
5094 #[test]
5095 fn test_rule_rename_target() {
5096 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5097 assert!(rule.rename_target("old_target", "new_target").unwrap());
5098 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
5099 assert!(!rule.rename_target("nonexistent", "something").unwrap());
5101 }
5102
5103 #[test]
5104 fn test_rule_rename_target_multiple() {
5105 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5106 assert!(rule.rename_target("target2", "renamed_target").unwrap());
5107 assert_eq!(
5108 rule.targets().collect::<Vec<_>>(),
5109 vec!["target1", "renamed_target", "target3"]
5110 );
5111 }
5112
5113 #[test]
5114 fn test_rule_remove_target() {
5115 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5116 assert!(rule.remove_target("target2").unwrap());
5117 assert_eq!(
5118 rule.targets().collect::<Vec<_>>(),
5119 vec!["target1", "target3"]
5120 );
5121 assert!(!rule.remove_target("nonexistent").unwrap());
5123 }
5124
5125 #[test]
5126 fn test_rule_remove_target_last() {
5127 let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
5128 let result = rule.remove_target("single_target");
5129 assert!(result.is_err());
5130 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
5132 }
5133
5134 #[test]
5135 fn test_rule_target_manipulation_preserves_prerequisites() {
5136 let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
5137
5138 rule.remove_target("target1").unwrap();
5140 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
5141 assert_eq!(
5142 rule.prerequisites().collect::<Vec<_>>(),
5143 vec!["dep1", "dep2"]
5144 );
5145 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5146
5147 rule.add_target("target3").unwrap();
5149 assert_eq!(
5150 rule.targets().collect::<Vec<_>>(),
5151 vec!["target2", "target3"]
5152 );
5153 assert_eq!(
5154 rule.prerequisites().collect::<Vec<_>>(),
5155 vec!["dep1", "dep2"]
5156 );
5157 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5158
5159 rule.rename_target("target2", "renamed").unwrap();
5161 assert_eq!(
5162 rule.targets().collect::<Vec<_>>(),
5163 vec!["renamed", "target3"]
5164 );
5165 assert_eq!(
5166 rule.prerequisites().collect::<Vec<_>>(),
5167 vec!["dep1", "dep2"]
5168 );
5169 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5170 }
5171
5172 #[test]
5173 fn test_rule_remove() {
5174 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5175 let rule = makefile.find_rule_by_target("rule1").unwrap();
5176 rule.remove().unwrap();
5177 assert_eq!(makefile.rules().count(), 1);
5178 assert!(makefile.find_rule_by_target("rule1").is_none());
5179 assert!(makefile.find_rule_by_target("rule2").is_some());
5180 }
5181
5182 #[test]
5183 fn test_rule_remove_last_trims_blank_lines() {
5184 let makefile: Makefile =
5186 "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
5187 .parse()
5188 .unwrap();
5189
5190 let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
5192 rule.remove().unwrap();
5193
5194 assert_eq!(makefile.code(), "%:\n\tdh $@\n");
5196 assert_eq!(makefile.rules().count(), 1);
5197 }
5198
5199 #[test]
5200 fn test_makefile_find_rule_by_target() {
5201 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5202 let rule = makefile.find_rule_by_target("rule2");
5203 assert!(rule.is_some());
5204 assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
5205 assert!(makefile.find_rule_by_target("nonexistent").is_none());
5206 }
5207
5208 #[test]
5209 fn test_makefile_find_rules_by_target() {
5210 let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
5211 .parse()
5212 .unwrap();
5213 assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
5214 assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
5215 assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
5216 }
5217
5218 #[test]
5219 fn test_makefile_find_rule_by_target_pattern_simple() {
5220 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5221 let rule = makefile.find_rule_by_target_pattern("foo.o");
5222 assert!(rule.is_some());
5223 assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
5224 }
5225
5226 #[test]
5227 fn test_makefile_find_rule_by_target_pattern_no_match() {
5228 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5229 let rule = makefile.find_rule_by_target_pattern("foo.c");
5230 assert!(rule.is_none());
5231 }
5232
5233 #[test]
5234 fn test_makefile_find_rule_by_target_pattern_exact() {
5235 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5236 let rule = makefile.find_rule_by_target_pattern("foo.o");
5237 assert!(rule.is_some());
5238 assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
5239 }
5240
5241 #[test]
5242 fn test_makefile_find_rule_by_target_pattern_prefix() {
5243 let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5244 let rule = makefile.find_rule_by_target_pattern("libfoo.a");
5245 assert!(rule.is_some());
5246 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
5247 }
5248
5249 #[test]
5250 fn test_makefile_find_rule_by_target_pattern_suffix() {
5251 let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5252 let rule = makefile.find_rule_by_target_pattern("foo_test.o");
5253 assert!(rule.is_some());
5254 assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
5255 }
5256
5257 #[test]
5258 fn test_makefile_find_rule_by_target_pattern_middle() {
5259 let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5260 let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
5261 assert!(rule.is_some());
5262 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
5263 }
5264
5265 #[test]
5266 fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
5267 let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
5268 let rule = makefile.find_rule_by_target_pattern("anything");
5269 assert!(rule.is_some());
5270 assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
5271 }
5272
5273 #[test]
5274 fn test_makefile_find_rules_by_target_pattern_multiple() {
5275 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
5276 .parse()
5277 .unwrap();
5278 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5279 assert_eq!(rules.len(), 2);
5280 }
5281
5282 #[test]
5283 fn test_makefile_find_rules_by_target_pattern_mixed() {
5284 let makefile: Makefile =
5285 "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
5286 .parse()
5287 .unwrap();
5288 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5289 assert_eq!(rules.len(), 2); let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
5291 assert_eq!(rules.len(), 1); }
5293
5294 #[test]
5295 fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
5296 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5297 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5298 assert_eq!(rules.len(), 1);
5299 let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
5300 assert_eq!(rules.len(), 0);
5301 }
5302
5303 #[test]
5304 fn test_matches_pattern_exact() {
5305 assert!(matches_pattern("foo.o", "foo.o"));
5306 assert!(!matches_pattern("foo.o", "bar.o"));
5307 }
5308
5309 #[test]
5310 fn test_matches_pattern_suffix() {
5311 assert!(matches_pattern("%.o", "foo.o"));
5312 assert!(matches_pattern("%.o", "bar.o"));
5313 assert!(matches_pattern("%.o", "baz/qux.o"));
5314 assert!(!matches_pattern("%.o", "foo.c"));
5315 }
5316
5317 #[test]
5318 fn test_matches_pattern_prefix() {
5319 assert!(matches_pattern("lib%.a", "libfoo.a"));
5320 assert!(matches_pattern("lib%.a", "libbar.a"));
5321 assert!(!matches_pattern("lib%.a", "foo.a"));
5322 assert!(!matches_pattern("lib%.a", "lib.a"));
5323 }
5324
5325 #[test]
5326 fn test_matches_pattern_middle() {
5327 assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
5328 assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
5329 assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
5330 assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
5331 }
5332
5333 #[test]
5334 fn test_matches_pattern_wildcard_only() {
5335 assert!(matches_pattern("%", "anything"));
5336 assert!(matches_pattern("%", "foo.o"));
5337 assert!(!matches_pattern("%", ""));
5339 }
5340
5341 #[test]
5342 fn test_matches_pattern_empty_stem() {
5343 assert!(!matches_pattern("%.o", ".o")); assert!(!matches_pattern("lib%", "lib")); assert!(!matches_pattern("lib%.a", "lib.a")); }
5348
5349 #[test]
5350 fn test_matches_pattern_multiple_wildcards_not_supported() {
5351 assert!(!matches_pattern("%foo%bar", "xfooybarz"));
5354 assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
5355 }
5356
5357 #[test]
5358 fn test_makefile_add_phony_target() {
5359 let mut makefile = Makefile::new();
5360 makefile.add_phony_target("clean").unwrap();
5361 assert!(makefile.is_phony("clean"));
5362 assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
5363 }
5364
5365 #[test]
5366 fn test_makefile_add_phony_target_existing() {
5367 let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
5368 makefile.add_phony_target("clean").unwrap();
5369 assert!(makefile.is_phony("test"));
5370 assert!(makefile.is_phony("clean"));
5371 let targets: Vec<_> = makefile.phony_targets().collect();
5372 assert!(targets.contains(&"test".to_string()));
5373 assert!(targets.contains(&"clean".to_string()));
5374 }
5375
5376 #[test]
5377 fn test_makefile_remove_phony_target() {
5378 let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5379 assert!(makefile.remove_phony_target("clean").unwrap());
5380 assert!(!makefile.is_phony("clean"));
5381 assert!(makefile.is_phony("test"));
5382 assert!(!makefile.remove_phony_target("nonexistent").unwrap());
5383 }
5384
5385 #[test]
5386 fn test_makefile_remove_phony_target_last() {
5387 let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
5388 assert!(makefile.remove_phony_target("clean").unwrap());
5389 assert!(!makefile.is_phony("clean"));
5390 assert!(makefile.find_rule_by_target(".PHONY").is_none());
5392 }
5393
5394 #[test]
5395 fn test_makefile_is_phony() {
5396 let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5397 assert!(makefile.is_phony("clean"));
5398 assert!(makefile.is_phony("test"));
5399 assert!(!makefile.is_phony("build"));
5400 }
5401
5402 #[test]
5403 fn test_makefile_phony_targets() {
5404 let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5405 let phony_targets: Vec<_> = makefile.phony_targets().collect();
5406 assert_eq!(phony_targets, vec!["clean", "test", "build"]);
5407 }
5408
5409 #[test]
5410 fn test_makefile_phony_targets_empty() {
5411 let makefile = Makefile::new();
5412 assert_eq!(makefile.phony_targets().count(), 0);
5413 }
5414
5415 #[test]
5416 fn test_makefile_remove_first_phony_target_no_extra_space() {
5417 let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5418 assert!(makefile.remove_phony_target("clean").unwrap());
5419 let result = makefile.to_string();
5420 assert_eq!(result, ".PHONY: test build\n");
5421 }
5422
5423 #[test]
5424 fn test_recipe_with_leading_comments_and_blank_lines() {
5425 let makefile_text = r#"#!/usr/bin/make
5429
5430%:
5431 dh $@
5432
5433override_dh_build:
5434 # The next line is empty
5435
5436 dh_python3
5437"#;
5438 let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
5439
5440 let rules: Vec<_> = makefile.rules().collect();
5441 assert_eq!(rules.len(), 2, "Expected 2 rules");
5442
5443 let rule0 = &rules[0];
5445 assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
5446 assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
5447
5448 let rule1 = &rules[1];
5450 assert_eq!(
5451 rule1.targets().collect::<Vec<_>>(),
5452 vec!["override_dh_build"]
5453 );
5454
5455 let recipes: Vec<_> = rule1.recipes().collect();
5457 assert!(
5458 !recipes.is_empty(),
5459 "Expected at least one recipe for override_dh_build, got none"
5460 );
5461 assert!(
5462 recipes.contains(&"dh_python3".to_string()),
5463 "Expected 'dh_python3' in recipes, got: {:?}",
5464 recipes
5465 );
5466 }
5467
5468 #[test]
5469 fn test_rule_parse_preserves_trailing_blank_lines() {
5470 let input = r#"override_dh_systemd_enable:
5473 dh_systemd_enable -pracoon
5474
5475override_dh_install:
5476 dh_install
5477"#;
5478
5479 let mut mf: Makefile = input.parse().unwrap();
5480
5481 let rule = mf.rules().next().unwrap();
5483 let rule_text = rule.to_string();
5484
5485 assert_eq!(
5487 rule_text,
5488 "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
5489 );
5490
5491 let modified =
5493 rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
5494
5495 let new_rule: Rule = modified.parse().unwrap();
5497 assert_eq!(
5498 new_rule.to_string(),
5499 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
5500 );
5501
5502 mf.replace_rule(0, new_rule).unwrap();
5504
5505 let output = mf.to_string();
5507 assert!(
5508 output.contains(
5509 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
5510 ),
5511 "Blank line between rules should be preserved. Got: {:?}",
5512 output
5513 );
5514 }
5515
5516 #[test]
5517 fn test_rule_parse_round_trip_with_trailing_newlines() {
5518 let test_cases = vec![
5520 "rule:\n\tcommand\n", "rule:\n\tcommand\n\n", "rule:\n\tcommand\n\n\n", ];
5524
5525 for rule_text in test_cases {
5526 let rule: Rule = rule_text.parse().unwrap();
5527 let result = rule.to_string();
5528 assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
5529 }
5530 }
5531
5532 #[test]
5533 fn test_rule_clone() {
5534 let rule_text = "rule:\n\tcommand\n\n";
5536 let rule: Rule = rule_text.parse().unwrap();
5537 let cloned = rule.clone();
5538
5539 assert_eq!(rule.to_string(), cloned.to_string());
5541 assert_eq!(rule.to_string(), rule_text);
5542 assert_eq!(cloned.to_string(), rule_text);
5543
5544 assert_eq!(
5546 rule.targets().collect::<Vec<_>>(),
5547 cloned.targets().collect::<Vec<_>>()
5548 );
5549 assert_eq!(
5550 rule.recipes().collect::<Vec<_>>(),
5551 cloned.recipes().collect::<Vec<_>>()
5552 );
5553 }
5554
5555 #[test]
5556 fn test_makefile_clone() {
5557 let input = "VAR = value\n\nrule:\n\tcommand\n";
5559 let makefile: Makefile = input.parse().unwrap();
5560 let cloned = makefile.clone();
5561
5562 assert_eq!(makefile.to_string(), cloned.to_string());
5564 assert_eq!(makefile.to_string(), input);
5565
5566 assert_eq!(makefile.rules().count(), cloned.rules().count());
5568
5569 assert_eq!(
5571 makefile.variable_definitions().count(),
5572 cloned.variable_definitions().count()
5573 );
5574 }
5575
5576 #[test]
5577 fn test_conditional_with_recipe_line() {
5578 let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
5580 let parsed = parse(input, None);
5581
5582 assert!(
5584 parsed.errors.is_empty(),
5585 "Expected no parse errors, but got: {:?}",
5586 parsed.errors
5587 );
5588
5589 let mf = parsed.root();
5591 assert_eq!(mf.code(), input);
5592 }
5593
5594 #[test]
5595 fn test_conditional_in_rule_recipe() {
5596 let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
5598 let parsed = parse(input, None);
5599
5600 assert!(
5602 parsed.errors.is_empty(),
5603 "Expected no parse errors, but got: {:?}",
5604 parsed.errors
5605 );
5606
5607 let mf = parsed.root();
5609 assert_eq!(mf.code(), input);
5610
5611 assert_eq!(mf.rules().count(), 1);
5613 }
5614
5615 #[test]
5616 fn test_rule_items() {
5617 use crate::RuleItem;
5618
5619 let input = r#"test:
5621 echo "before"
5622ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
5623 ./run-tests
5624endif
5625 echo "after"
5626"#;
5627 let rule: Rule = input.parse().unwrap();
5628
5629 let items: Vec<_> = rule.items().collect();
5630 assert_eq!(
5631 items.len(),
5632 3,
5633 "Expected 3 items: recipe, conditional, recipe"
5634 );
5635
5636 match &items[0] {
5638 RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
5639 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5640 }
5641
5642 match &items[1] {
5644 RuleItem::Conditional(c) => {
5645 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5646 }
5647 RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
5648 }
5649
5650 match &items[2] {
5652 RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5653 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5654 }
5655
5656 let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5658 let simple_items: Vec<_> = simple_rule.items().collect();
5659 assert_eq!(simple_items.len(), 2);
5660
5661 match &simple_items[0] {
5662 RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5663 _ => panic!("Expected recipe"),
5664 }
5665
5666 match &simple_items[1] {
5667 RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5668 _ => panic!("Expected recipe"),
5669 }
5670
5671 let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5673 .parse()
5674 .unwrap();
5675 let cond_items: Vec<_> = cond_only.items().collect();
5676 assert_eq!(cond_items.len(), 1);
5677
5678 match &cond_items[0] {
5679 RuleItem::Conditional(c) => {
5680 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5681 }
5682 _ => panic!("Expected conditional"),
5683 }
5684 }
5685
5686 #[test]
5687 fn test_conditionals_iterator() {
5688 let makefile: Makefile = r#"ifdef DEBUG
5689VAR = debug
5690endif
5691
5692ifndef RELEASE
5693OTHER = dev
5694endif
5695"#
5696 .parse()
5697 .unwrap();
5698
5699 let conditionals: Vec<_> = makefile.conditionals().collect();
5700 assert_eq!(conditionals.len(), 2);
5701
5702 assert_eq!(
5703 conditionals[0].conditional_type(),
5704 Some("ifdef".to_string())
5705 );
5706 assert_eq!(
5707 conditionals[1].conditional_type(),
5708 Some("ifndef".to_string())
5709 );
5710 }
5711
5712 #[test]
5713 fn test_conditional_type_and_condition() {
5714 let makefile: Makefile = r#"ifdef DEBUG
5715VAR = debug
5716endif
5717"#
5718 .parse()
5719 .unwrap();
5720
5721 let conditional = makefile.conditionals().next().unwrap();
5722 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5723 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5724 }
5725
5726 #[test]
5727 fn test_conditional_has_else() {
5728 let makefile_with_else: Makefile = r#"ifdef DEBUG
5729VAR = debug
5730else
5731VAR = release
5732endif
5733"#
5734 .parse()
5735 .unwrap();
5736
5737 let conditional = makefile_with_else.conditionals().next().unwrap();
5738 assert!(conditional.has_else());
5739
5740 let makefile_without_else: Makefile = r#"ifdef DEBUG
5741VAR = debug
5742endif
5743"#
5744 .parse()
5745 .unwrap();
5746
5747 let conditional = makefile_without_else.conditionals().next().unwrap();
5748 assert!(!conditional.has_else());
5749 }
5750
5751 #[test]
5752 fn test_conditional_if_body() {
5753 let makefile: Makefile = r#"ifdef DEBUG
5754VAR = debug
5755endif
5756"#
5757 .parse()
5758 .unwrap();
5759
5760 let conditional = makefile.conditionals().next().unwrap();
5761 let if_body = conditional.if_body();
5762 assert!(if_body.is_some());
5763 assert!(if_body.unwrap().contains("VAR = debug"));
5764 }
5765
5766 #[test]
5767 fn test_conditional_else_body() {
5768 let makefile: Makefile = r#"ifdef DEBUG
5769VAR = debug
5770else
5771VAR = release
5772endif
5773"#
5774 .parse()
5775 .unwrap();
5776
5777 let conditional = makefile.conditionals().next().unwrap();
5778 let else_body = conditional.else_body();
5779 assert!(else_body.is_some());
5780 assert!(else_body.unwrap().contains("VAR = release"));
5781 }
5782
5783 #[test]
5784 fn test_add_conditional_ifdef() {
5785 let mut makefile = Makefile::new();
5786 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5787 assert!(result.is_ok());
5788
5789 let code = makefile.to_string();
5790 assert!(code.contains("ifdef DEBUG"));
5791 assert!(code.contains("VAR = debug"));
5792 assert!(code.contains("endif"));
5793 }
5794
5795 #[test]
5796 fn test_add_conditional_with_else() {
5797 let mut makefile = Makefile::new();
5798 let result =
5799 makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5800 assert!(result.is_ok());
5801
5802 let code = makefile.to_string();
5803 assert!(code.contains("ifdef DEBUG"));
5804 assert!(code.contains("VAR = debug"));
5805 assert!(code.contains("else"));
5806 assert!(code.contains("VAR = release"));
5807 assert!(code.contains("endif"));
5808 }
5809
5810 #[test]
5811 fn test_add_conditional_invalid_type() {
5812 let mut makefile = Makefile::new();
5813 let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5814 assert!(result.is_err());
5815 }
5816
5817 #[test]
5818 fn test_add_conditional_formatting() {
5819 let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5820 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5821 assert!(result.is_ok());
5822
5823 let code = makefile.to_string();
5824 assert!(code.contains("\n\nifdef DEBUG"));
5826 }
5827
5828 #[test]
5829 fn test_conditional_remove() {
5830 let makefile: Makefile = r#"ifdef DEBUG
5831VAR = debug
5832endif
5833
5834VAR2 = value2
5835"#
5836 .parse()
5837 .unwrap();
5838
5839 let mut conditional = makefile.conditionals().next().unwrap();
5840 let result = conditional.remove();
5841 assert!(result.is_ok());
5842
5843 let code = makefile.to_string();
5844 assert!(!code.contains("ifdef DEBUG"));
5845 assert!(!code.contains("VAR = debug"));
5846 assert!(code.contains("VAR2 = value2"));
5847 }
5848
5849 #[test]
5850 fn test_add_conditional_ifndef() {
5851 let mut makefile = Makefile::new();
5852 let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5853 assert!(result.is_ok());
5854
5855 let code = makefile.to_string();
5856 assert!(code.contains("ifndef NDEBUG"));
5857 assert!(code.contains("VAR = enabled"));
5858 assert!(code.contains("endif"));
5859 }
5860
5861 #[test]
5862 fn test_add_conditional_ifeq() {
5863 let mut makefile = Makefile::new();
5864 let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5865 assert!(result.is_ok());
5866
5867 let code = makefile.to_string();
5868 assert!(code.contains("ifeq ($(OS),Linux)"));
5869 assert!(code.contains("VAR = linux"));
5870 assert!(code.contains("endif"));
5871 }
5872
5873 #[test]
5874 fn test_add_conditional_ifneq() {
5875 let mut makefile = Makefile::new();
5876 let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5877 assert!(result.is_ok());
5878
5879 let code = makefile.to_string();
5880 assert!(code.contains("ifneq ($(OS),Windows)"));
5881 assert!(code.contains("VAR = unix"));
5882 assert!(code.contains("endif"));
5883 }
5884
5885 #[test]
5886 fn test_conditional_api_integration() {
5887 let mut makefile: Makefile = r#"VAR1 = value1
5889
5890rule1:
5891 command1
5892"#
5893 .parse()
5894 .unwrap();
5895
5896 makefile
5898 .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5899 .unwrap();
5900
5901 assert_eq!(makefile.conditionals().count(), 1);
5903 let conditional = makefile.conditionals().next().unwrap();
5904 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5905 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5906 assert!(conditional.has_else());
5907
5908 assert_eq!(makefile.variable_definitions().count(), 1);
5910 assert_eq!(makefile.rules().count(), 1);
5911 }
5912
5913 #[test]
5914 fn test_conditional_if_items() {
5915 let makefile: Makefile = r#"ifdef DEBUG
5916VAR = debug
5917rule:
5918 command
5919endif
5920"#
5921 .parse()
5922 .unwrap();
5923
5924 let cond = makefile.conditionals().next().unwrap();
5925 let items: Vec<_> = cond.if_items().collect();
5926 assert_eq!(items.len(), 2); match &items[0] {
5929 MakefileItem::Variable(v) => {
5930 assert_eq!(v.name(), Some("VAR".to_string()));
5931 }
5932 _ => panic!("Expected variable"),
5933 }
5934
5935 match &items[1] {
5936 MakefileItem::Rule(r) => {
5937 assert!(r.targets().any(|t| t == "rule"));
5938 }
5939 _ => panic!("Expected rule"),
5940 }
5941 }
5942
5943 #[test]
5944 fn test_conditional_else_items() {
5945 let makefile: Makefile = r#"ifdef DEBUG
5946VAR = debug
5947else
5948VAR2 = release
5949rule2:
5950 command
5951endif
5952"#
5953 .parse()
5954 .unwrap();
5955
5956 let cond = makefile.conditionals().next().unwrap();
5957 let items: Vec<_> = cond.else_items().collect();
5958 assert_eq!(items.len(), 2); match &items[0] {
5961 MakefileItem::Variable(v) => {
5962 assert_eq!(v.name(), Some("VAR2".to_string()));
5963 }
5964 _ => panic!("Expected variable"),
5965 }
5966
5967 match &items[1] {
5968 MakefileItem::Rule(r) => {
5969 assert!(r.targets().any(|t| t == "rule2"));
5970 }
5971 _ => panic!("Expected rule"),
5972 }
5973 }
5974
5975 #[test]
5976 fn test_conditional_add_if_item() {
5977 let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5978 let mut cond = makefile.conditionals().next().unwrap();
5979
5980 let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5982 let var = temp.variable_definitions().next().unwrap();
5983 cond.add_if_item(MakefileItem::Variable(var));
5984
5985 let code = makefile.to_string();
5986 assert!(code.contains("CFLAGS = -g"));
5987
5988 let cond = makefile.conditionals().next().unwrap();
5990 assert_eq!(cond.if_items().count(), 1);
5991 }
5992
5993 #[test]
5994 fn test_conditional_add_else_item() {
5995 let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
5996 let mut cond = makefile.conditionals().next().unwrap();
5997
5998 let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
6000 let var = temp.variable_definitions().next().unwrap();
6001 cond.add_else_item(MakefileItem::Variable(var));
6002
6003 let code = makefile.to_string();
6004 assert!(code.contains("else"));
6005 assert!(code.contains("CFLAGS = -O2"));
6006
6007 let cond = makefile.conditionals().next().unwrap();
6009 assert_eq!(cond.else_items().count(), 1);
6010 }
6011
6012 #[test]
6013 fn test_add_conditional_with_items() {
6014 let mut makefile = Makefile::new();
6015
6016 let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
6018 let var1 = temp1.variable_definitions().next().unwrap();
6019
6020 let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
6021 let var2 = temp2.variable_definitions().next().unwrap();
6022
6023 let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
6024 let rule1 = temp3.rules().next().unwrap();
6025
6026 let result = makefile.add_conditional_with_items(
6027 "ifdef",
6028 "DEBUG",
6029 vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
6030 Some(vec![MakefileItem::Variable(var2)]),
6031 );
6032
6033 assert!(result.is_ok());
6034
6035 let code = makefile.to_string();
6036 assert!(code.contains("ifdef DEBUG"));
6037 assert!(code.contains("CFLAGS = -g"));
6038 assert!(code.contains("debug:"));
6039 assert!(code.contains("else"));
6040 assert!(code.contains("CFLAGS = -O2"));
6041 }
6042
6043 #[test]
6044 fn test_conditional_items_with_nested_conditional() {
6045 let makefile: Makefile = r#"ifdef DEBUG
6046VAR = debug
6047ifdef VERBOSE
6048 VAR2 = verbose
6049endif
6050endif
6051"#
6052 .parse()
6053 .unwrap();
6054
6055 let cond = makefile.conditionals().next().unwrap();
6056 let items: Vec<_> = cond.if_items().collect();
6057 assert_eq!(items.len(), 2); match &items[0] {
6060 MakefileItem::Variable(v) => {
6061 assert_eq!(v.name(), Some("VAR".to_string()));
6062 }
6063 _ => panic!("Expected variable"),
6064 }
6065
6066 match &items[1] {
6067 MakefileItem::Conditional(c) => {
6068 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6069 }
6070 _ => panic!("Expected conditional"),
6071 }
6072 }
6073
6074 #[test]
6075 fn test_conditional_items_with_include() {
6076 let makefile: Makefile = r#"ifdef DEBUG
6077include debug.mk
6078VAR = debug
6079endif
6080"#
6081 .parse()
6082 .unwrap();
6083
6084 let cond = makefile.conditionals().next().unwrap();
6085 let items: Vec<_> = cond.if_items().collect();
6086 assert_eq!(items.len(), 2); match &items[0] {
6089 MakefileItem::Include(i) => {
6090 assert_eq!(i.path(), Some("debug.mk".to_string()));
6091 }
6092 _ => panic!("Expected include"),
6093 }
6094
6095 match &items[1] {
6096 MakefileItem::Variable(v) => {
6097 assert_eq!(v.name(), Some("VAR".to_string()));
6098 }
6099 _ => panic!("Expected variable"),
6100 }
6101 }
6102
6103 #[test]
6104 fn test_makefile_items_iterator() {
6105 let makefile: Makefile = r#"VAR = value
6106ifdef DEBUG
6107CFLAGS = -g
6108endif
6109rule:
6110 command
6111include common.mk
6112"#
6113 .parse()
6114 .unwrap();
6115
6116 assert_eq!(makefile.variable_definitions().count(), 2);
6119 assert_eq!(makefile.conditionals().count(), 1);
6120 assert_eq!(makefile.rules().count(), 1);
6121
6122 let items: Vec<_> = makefile.items().collect();
6123 assert!(
6125 items.len() >= 3,
6126 "Expected at least 3 items, got {}",
6127 items.len()
6128 );
6129
6130 match &items[0] {
6131 MakefileItem::Variable(v) => {
6132 assert_eq!(v.name(), Some("VAR".to_string()));
6133 }
6134 _ => panic!("Expected variable at position 0"),
6135 }
6136
6137 match &items[1] {
6138 MakefileItem::Conditional(c) => {
6139 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6140 }
6141 _ => panic!("Expected conditional at position 1"),
6142 }
6143
6144 match &items[2] {
6145 MakefileItem::Rule(r) => {
6146 let targets: Vec<_> = r.targets().collect();
6147 assert_eq!(targets, vec!["rule"]);
6148 }
6149 _ => panic!("Expected rule at position 2"),
6150 }
6151 }
6152
6153 #[test]
6154 fn test_conditional_unwrap() {
6155 let makefile: Makefile = r#"ifdef DEBUG
6156VAR = debug
6157rule:
6158 command
6159endif
6160"#
6161 .parse()
6162 .unwrap();
6163
6164 let mut cond = makefile.conditionals().next().unwrap();
6165 cond.unwrap().unwrap();
6166
6167 let code = makefile.to_string();
6168 let expected = "VAR = debug\nrule:\n\tcommand\n";
6169 assert_eq!(code, expected);
6170
6171 assert_eq!(makefile.conditionals().count(), 0);
6173
6174 assert_eq!(makefile.variable_definitions().count(), 1);
6176 assert_eq!(makefile.rules().count(), 1);
6177 }
6178
6179 #[test]
6180 fn test_conditional_unwrap_with_else_fails() {
6181 let makefile: Makefile = r#"ifdef DEBUG
6182VAR = debug
6183else
6184VAR = release
6185endif
6186"#
6187 .parse()
6188 .unwrap();
6189
6190 let mut cond = makefile.conditionals().next().unwrap();
6191 let result = cond.unwrap();
6192
6193 assert!(result.is_err());
6194 assert!(result
6195 .unwrap_err()
6196 .to_string()
6197 .contains("Cannot unwrap conditional with else clause"));
6198 }
6199
6200 #[test]
6201 fn test_conditional_unwrap_nested() {
6202 let makefile: Makefile = r#"ifdef OUTER
6203VAR = outer
6204ifdef INNER
6205VAR2 = inner
6206endif
6207endif
6208"#
6209 .parse()
6210 .unwrap();
6211
6212 let mut outer_cond = makefile.conditionals().next().unwrap();
6214 outer_cond.unwrap().unwrap();
6215
6216 let code = makefile.to_string();
6217 let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
6218 assert_eq!(code, expected);
6219 }
6220
6221 #[test]
6222 fn test_conditional_unwrap_empty() {
6223 let makefile: Makefile = r#"ifdef DEBUG
6224endif
6225"#
6226 .parse()
6227 .unwrap();
6228
6229 let mut cond = makefile.conditionals().next().unwrap();
6230 cond.unwrap().unwrap();
6231
6232 let code = makefile.to_string();
6233 assert_eq!(code, "");
6234 }
6235
6236 #[test]
6237 fn test_rule_parent() {
6238 let makefile: Makefile = r#"all:
6239 echo "test"
6240"#
6241 .parse()
6242 .unwrap();
6243
6244 let rule = makefile.rules().next().unwrap();
6245 let parent = rule.parent();
6246 assert!(parent.is_none());
6248 }
6249
6250 #[test]
6251 fn test_item_parent_in_conditional() {
6252 let makefile: Makefile = r#"ifdef DEBUG
6253VAR = debug
6254rule:
6255 command
6256endif
6257"#
6258 .parse()
6259 .unwrap();
6260
6261 let cond = makefile.conditionals().next().unwrap();
6262
6263 let items: Vec<_> = cond.if_items().collect();
6265 assert_eq!(items.len(), 2);
6266
6267 if let MakefileItem::Variable(var) = &items[0] {
6269 let parent = var.parent();
6270 assert!(parent.is_some());
6271 if let Some(MakefileItem::Conditional(_)) = parent {
6272 } else {
6274 panic!("Expected variable parent to be a Conditional");
6275 }
6276 } else {
6277 panic!("Expected first item to be a Variable");
6278 }
6279
6280 if let MakefileItem::Rule(rule) = &items[1] {
6282 let parent = rule.parent();
6283 assert!(parent.is_some());
6284 if let Some(MakefileItem::Conditional(_)) = parent {
6285 } else {
6287 panic!("Expected rule parent to be a Conditional");
6288 }
6289 } else {
6290 panic!("Expected second item to be a Rule");
6291 }
6292 }
6293
6294 #[test]
6295 fn test_nested_conditional_parent() {
6296 let makefile: Makefile = r#"ifdef OUTER
6297VAR = outer
6298ifdef INNER
6299VAR2 = inner
6300endif
6301endif
6302"#
6303 .parse()
6304 .unwrap();
6305
6306 let outer_cond = makefile.conditionals().next().unwrap();
6307
6308 let items: Vec<_> = outer_cond.if_items().collect();
6310
6311 let inner_cond = items
6313 .iter()
6314 .find_map(|item| {
6315 if let MakefileItem::Conditional(c) = item {
6316 Some(c)
6317 } else {
6318 None
6319 }
6320 })
6321 .unwrap();
6322
6323 let parent = inner_cond.parent();
6325 assert!(parent.is_some());
6326 if let Some(MakefileItem::Conditional(_)) = parent {
6327 } else {
6329 panic!("Expected inner conditional's parent to be a Conditional");
6330 }
6331 }
6332
6333 #[test]
6334 fn test_line_col() {
6335 let text = r#"# Comment at line 0
6336VAR1 = value1
6337VAR2 = value2
6338
6339rule1: dep1 dep2
6340 command1
6341 command2
6342
6343rule2:
6344 command3
6345
6346ifdef DEBUG
6347CFLAGS = -g
6348endif
6349"#;
6350 let makefile: Makefile = text.parse().unwrap();
6351
6352 let vars: Vec<_> = makefile.variable_definitions().collect();
6355 assert_eq!(vars.len(), 3);
6356
6357 assert_eq!(vars[0].line(), 1);
6359 assert_eq!(vars[0].column(), 0);
6360 assert_eq!(vars[0].line_col(), (1, 0));
6361
6362 assert_eq!(vars[1].line(), 2);
6364 assert_eq!(vars[1].column(), 0);
6365
6366 assert_eq!(vars[2].line(), 12);
6368 assert_eq!(vars[2].column(), 0);
6369
6370 let rules: Vec<_> = makefile.rules().collect();
6372 assert_eq!(rules.len(), 2);
6373
6374 assert_eq!(rules[0].line(), 4);
6376 assert_eq!(rules[0].column(), 0);
6377 assert_eq!(rules[0].line_col(), (4, 0));
6378
6379 assert_eq!(rules[1].line(), 8);
6381 assert_eq!(rules[1].column(), 0);
6382
6383 let conditionals: Vec<_> = makefile.conditionals().collect();
6385 assert_eq!(conditionals.len(), 1);
6386
6387 assert_eq!(conditionals[0].line(), 11);
6389 assert_eq!(conditionals[0].column(), 0);
6390 assert_eq!(conditionals[0].line_col(), (11, 0));
6391 }
6392
6393 #[test]
6394 fn test_line_col_multiline() {
6395 let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
6396 let makefile: Makefile = text.parse().unwrap();
6397
6398 let vars: Vec<_> = makefile.variable_definitions().collect();
6400 assert_eq!(vars.len(), 1);
6401 assert_eq!(vars[0].line(), 0);
6402 assert_eq!(vars[0].column(), 0);
6403
6404 let rules: Vec<_> = makefile.rules().collect();
6406 assert_eq!(rules.len(), 1);
6407 assert_eq!(rules[0].line(), 4);
6408 assert_eq!(rules[0].column(), 0);
6409 }
6410
6411 #[test]
6412 fn test_line_col_includes() {
6413 let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
6414 let makefile: Makefile = text.parse().unwrap();
6415
6416 let vars: Vec<_> = makefile.variable_definitions().collect();
6418 assert_eq!(vars[0].line(), 0);
6419
6420 let includes: Vec<_> = makefile.includes().collect();
6422 assert_eq!(includes.len(), 2);
6423 assert_eq!(includes[0].line(), 2);
6424 assert_eq!(includes[0].column(), 0);
6425 assert_eq!(includes[1].line(), 3);
6426 assert_eq!(includes[1].column(), 0);
6427 }
6428
6429 #[test]
6430 fn test_conditional_in_rule_vs_toplevel() {
6431 let text1 = r#"rule:
6433 command
6434ifeq (,$(X))
6435 test
6436endif
6437"#;
6438 let makefile: Makefile = text1.parse().unwrap();
6439 let rules: Vec<_> = makefile.rules().collect();
6440 let conditionals: Vec<_> = makefile.conditionals().collect();
6441
6442 assert_eq!(rules.len(), 1);
6443 assert_eq!(
6444 conditionals.len(),
6445 0,
6446 "Conditional should be part of rule, not top-level"
6447 );
6448
6449 let text2 = r#"rule:
6451 command
6452
6453ifeq (,$(X))
6454 test
6455endif
6456"#;
6457 let makefile: Makefile = text2.parse().unwrap();
6458 let rules: Vec<_> = makefile.rules().collect();
6459 let conditionals: Vec<_> = makefile.conditionals().collect();
6460
6461 assert_eq!(rules.len(), 1);
6462 assert_eq!(
6463 conditionals.len(),
6464 1,
6465 "Conditional after blank line should be top-level"
6466 );
6467 assert_eq!(conditionals[0].line(), 3);
6468 }
6469
6470 #[test]
6471 fn test_nested_conditionals_line_tracking() {
6472 let text = r#"ifdef OUTER
6473VAR1 = value1
6474ifdef INNER
6475VAR2 = value2
6476endif
6477VAR3 = value3
6478endif
6479"#;
6480 let makefile: Makefile = text.parse().unwrap();
6481
6482 let conditionals: Vec<_> = makefile.conditionals().collect();
6483 assert_eq!(
6484 conditionals.len(),
6485 1,
6486 "Only outer conditional should be top-level"
6487 );
6488 assert_eq!(conditionals[0].line(), 0);
6489 assert_eq!(conditionals[0].column(), 0);
6490 }
6491
6492 #[test]
6493 fn test_conditional_else_line_tracking() {
6494 let text = r#"VAR1 = before
6495
6496ifdef DEBUG
6497DEBUG_FLAGS = -g
6498else
6499DEBUG_FLAGS = -O2
6500endif
6501
6502VAR2 = after
6503"#;
6504 let makefile: Makefile = text.parse().unwrap();
6505
6506 let conditionals: Vec<_> = makefile.conditionals().collect();
6507 assert_eq!(conditionals.len(), 1);
6508 assert_eq!(conditionals[0].line(), 2);
6509 assert_eq!(conditionals[0].column(), 0);
6510 }
6511
6512 #[test]
6513 fn test_broken_conditional_endif_without_if() {
6514 let text = "VAR = value\nendif\n";
6516 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6517
6518 let vars: Vec<_> = makefile.variable_definitions().collect();
6520 assert_eq!(vars.len(), 1);
6521 assert_eq!(vars[0].line(), 0);
6522 }
6523
6524 #[test]
6525 fn test_broken_conditional_else_without_if() {
6526 let text = "VAR = value\nelse\nVAR2 = other\n";
6528 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6529
6530 let vars: Vec<_> = makefile.variable_definitions().collect();
6532 assert!(!vars.is_empty(), "Should parse at least the first variable");
6533 assert_eq!(vars[0].line(), 0);
6534 }
6535
6536 #[test]
6537 fn test_broken_conditional_missing_endif() {
6538 let text = r#"ifdef DEBUG
6540DEBUG_FLAGS = -g
6541VAR = value
6542"#;
6543 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6544
6545 assert!(makefile.code().contains("ifdef DEBUG"));
6547 }
6548
6549 #[test]
6550 fn test_multiple_conditionals_line_tracking() {
6551 let text = r#"ifdef A
6552VAR_A = a
6553endif
6554
6555ifdef B
6556VAR_B = b
6557endif
6558
6559ifdef C
6560VAR_C = c
6561endif
6562"#;
6563 let makefile: Makefile = text.parse().unwrap();
6564
6565 let conditionals: Vec<_> = makefile.conditionals().collect();
6566 assert_eq!(conditionals.len(), 3);
6567 assert_eq!(conditionals[0].line(), 0);
6568 assert_eq!(conditionals[1].line(), 4);
6569 assert_eq!(conditionals[2].line(), 8);
6570 }
6571
6572 #[test]
6573 fn test_conditional_with_multiple_else_ifeq() {
6574 let text = r#"ifeq ($(OS),Windows)
6575EXT = .exe
6576else ifeq ($(OS),Linux)
6577EXT = .bin
6578else
6579EXT = .out
6580endif
6581"#;
6582 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6583
6584 let conditionals: Vec<_> = makefile.conditionals().collect();
6585 assert_eq!(conditionals.len(), 1);
6586 assert_eq!(conditionals[0].line(), 0);
6587 assert_eq!(conditionals[0].column(), 0);
6588 }
6589
6590 #[test]
6591 fn test_conditional_types_line_tracking() {
6592 let text = r#"ifdef VAR1
6593A = 1
6594endif
6595
6596ifndef VAR2
6597B = 2
6598endif
6599
6600ifeq ($(X),y)
6601C = 3
6602endif
6603
6604ifneq ($(Y),n)
6605D = 4
6606endif
6607"#;
6608 let makefile: Makefile = text.parse().unwrap();
6609
6610 let conditionals: Vec<_> = makefile.conditionals().collect();
6611 assert_eq!(conditionals.len(), 4);
6612
6613 assert_eq!(conditionals[0].line(), 0); assert_eq!(
6615 conditionals[0].conditional_type(),
6616 Some("ifdef".to_string())
6617 );
6618
6619 assert_eq!(conditionals[1].line(), 4); assert_eq!(
6621 conditionals[1].conditional_type(),
6622 Some("ifndef".to_string())
6623 );
6624
6625 assert_eq!(conditionals[2].line(), 8); assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
6627
6628 assert_eq!(conditionals[3].line(), 12); assert_eq!(
6630 conditionals[3].conditional_type(),
6631 Some("ifneq".to_string())
6632 );
6633 }
6634
6635 #[test]
6636 fn test_conditional_in_rule_with_recipes() {
6637 let text = r#"test:
6638 echo "start"
6639ifdef VERBOSE
6640 echo "verbose mode"
6641endif
6642 echo "end"
6643"#;
6644 let makefile: Makefile = text.parse().unwrap();
6645
6646 let rules: Vec<_> = makefile.rules().collect();
6647 let conditionals: Vec<_> = makefile.conditionals().collect();
6648
6649 assert_eq!(rules.len(), 1);
6650 assert_eq!(rules[0].line(), 0);
6651 assert_eq!(conditionals.len(), 0);
6653 }
6654
6655 #[test]
6656 fn test_broken_conditional_double_else() {
6657 let text = r#"ifdef DEBUG
6659A = 1
6660else
6661B = 2
6662else
6663C = 3
6664endif
6665"#;
6666 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6667
6668 assert!(makefile.code().contains("ifdef DEBUG"));
6670 }
6671
6672 #[test]
6673 fn test_broken_conditional_mismatched_nesting() {
6674 let text = r#"ifdef A
6676VAR = value
6677endif
6678endif
6679"#;
6680 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6681
6682 let conditionals: Vec<_> = makefile.conditionals().collect();
6685 assert!(
6686 !conditionals.is_empty(),
6687 "Should parse at least the first conditional"
6688 );
6689 }
6690
6691 #[test]
6692 fn test_conditional_with_comment_line_tracking() {
6693 let text = r#"# This is a comment
6694ifdef DEBUG
6695# Another comment
6696CFLAGS = -g
6697endif
6698# Final comment
6699"#;
6700 let makefile: Makefile = text.parse().unwrap();
6701
6702 let conditionals: Vec<_> = makefile.conditionals().collect();
6703 assert_eq!(conditionals.len(), 1);
6704 assert_eq!(conditionals[0].line(), 1);
6705 assert_eq!(conditionals[0].column(), 0);
6706 }
6707
6708 #[test]
6709 fn test_conditional_after_variable_with_blank_lines() {
6710 let text = r#"VAR1 = value1
6711
6712
6713ifdef DEBUG
6714VAR2 = value2
6715endif
6716"#;
6717 let makefile: Makefile = text.parse().unwrap();
6718
6719 let vars: Vec<_> = makefile.variable_definitions().collect();
6720 let conditionals: Vec<_> = makefile.conditionals().collect();
6721
6722 assert_eq!(vars.len(), 2);
6724 assert_eq!(vars[0].line(), 0); assert_eq!(vars[1].line(), 4); assert_eq!(conditionals.len(), 1);
6728 assert_eq!(conditionals[0].line(), 3);
6729 }
6730
6731 #[test]
6732 fn test_empty_conditional_line_tracking() {
6733 let text = r#"ifdef DEBUG
6734endif
6735
6736ifndef RELEASE
6737endif
6738"#;
6739 let makefile: Makefile = text.parse().unwrap();
6740
6741 let conditionals: Vec<_> = makefile.conditionals().collect();
6742 assert_eq!(conditionals.len(), 2);
6743 assert_eq!(conditionals[0].line(), 0);
6744 assert_eq!(conditionals[1].line(), 3);
6745 }
6746
6747 #[test]
6748 fn test_recipe_line_tracking() {
6749 let text = r#"build:
6750 echo "Building..."
6751 gcc -o app main.c
6752 echo "Done"
6753
6754test:
6755 ./run-tests
6756"#;
6757 let makefile: Makefile = text.parse().unwrap();
6758
6759 let rule1 = makefile.rules().next().expect("Should have first rule");
6761 let recipes: Vec<_> = rule1.recipe_nodes().collect();
6762 assert_eq!(recipes.len(), 3);
6763
6764 assert_eq!(recipes[0].text(), "echo \"Building...\"");
6765 assert_eq!(recipes[0].line(), 1);
6766 assert_eq!(recipes[0].column(), 0);
6767
6768 assert_eq!(recipes[1].text(), "gcc -o app main.c");
6769 assert_eq!(recipes[1].line(), 2);
6770 assert_eq!(recipes[1].column(), 0);
6771
6772 assert_eq!(recipes[2].text(), "echo \"Done\"");
6773 assert_eq!(recipes[2].line(), 3);
6774 assert_eq!(recipes[2].column(), 0);
6775
6776 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6778 let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6779 assert_eq!(recipes2.len(), 1);
6780
6781 assert_eq!(recipes2[0].text(), "./run-tests");
6782 assert_eq!(recipes2[0].line(), 6);
6783 assert_eq!(recipes2[0].column(), 0);
6784 }
6785
6786 #[test]
6787 fn test_recipe_with_variables_line_tracking() {
6788 let text = r#"install:
6789 mkdir -p $(DESTDIR)
6790 cp $(BINARY) $(DESTDIR)/
6791"#;
6792 let makefile: Makefile = text.parse().unwrap();
6793 let rule = makefile.rules().next().expect("Should have rule");
6794 let recipes: Vec<_> = rule.recipe_nodes().collect();
6795
6796 assert_eq!(recipes.len(), 2);
6797 assert_eq!(recipes[0].line(), 1);
6798 assert_eq!(recipes[1].line(), 2);
6799 }
6800
6801 #[test]
6802 fn test_recipe_text_no_leading_tab() {
6803 let text = "test:\n\techo hello\n\t\techo nested\n\t echo with spaces\n";
6805 let makefile: Makefile = text.parse().unwrap();
6806 let rule = makefile.rules().next().expect("Should have rule");
6807 let recipes: Vec<_> = rule.recipe_nodes().collect();
6808
6809 assert_eq!(recipes.len(), 3);
6810
6811 eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6813
6814 assert_eq!(recipes[0].text(), "echo hello");
6816
6817 eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6819 assert_eq!(recipes[1].text(), "\techo nested");
6820
6821 eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6823 assert_eq!(recipes[2].text(), " echo with spaces");
6824 }
6825
6826 #[test]
6827 fn test_recipe_parent() {
6828 let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6829 let rule = makefile.rules().next().unwrap();
6830 let recipe = rule.recipe_nodes().next().unwrap();
6831
6832 let parent = recipe.parent().expect("Recipe should have parent");
6833 assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6834 assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6835 }
6836
6837 #[test]
6838 fn test_recipe_is_silent_various_prefixes() {
6839 let makefile: Makefile = r#"test:
6840 @echo silent
6841 -echo ignore
6842 +echo always
6843 @-echo silent_ignore
6844 -@echo ignore_silent
6845 +@echo always_silent
6846 echo normal
6847"#
6848 .parse()
6849 .unwrap();
6850
6851 let rule = makefile.rules().next().unwrap();
6852 let recipes: Vec<_> = rule.recipe_nodes().collect();
6853
6854 assert_eq!(recipes.len(), 7);
6855 assert!(recipes[0].is_silent(), "@echo should be silent");
6856 assert!(!recipes[1].is_silent(), "-echo should not be silent");
6857 assert!(!recipes[2].is_silent(), "+echo should not be silent");
6858 assert!(recipes[3].is_silent(), "@-echo should be silent");
6859 assert!(recipes[4].is_silent(), "-@echo should be silent");
6860 assert!(recipes[5].is_silent(), "+@echo should be silent");
6861 assert!(!recipes[6].is_silent(), "echo should not be silent");
6862 }
6863
6864 #[test]
6865 fn test_recipe_is_ignore_errors_various_prefixes() {
6866 let makefile: Makefile = r#"test:
6867 @echo silent
6868 -echo ignore
6869 +echo always
6870 @-echo silent_ignore
6871 -@echo ignore_silent
6872 +-echo always_ignore
6873 echo normal
6874"#
6875 .parse()
6876 .unwrap();
6877
6878 let rule = makefile.rules().next().unwrap();
6879 let recipes: Vec<_> = rule.recipe_nodes().collect();
6880
6881 assert_eq!(recipes.len(), 7);
6882 assert!(
6883 !recipes[0].is_ignore_errors(),
6884 "@echo should not ignore errors"
6885 );
6886 assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6887 assert!(
6888 !recipes[2].is_ignore_errors(),
6889 "+echo should not ignore errors"
6890 );
6891 assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6892 assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6893 assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6894 assert!(
6895 !recipes[6].is_ignore_errors(),
6896 "echo should not ignore errors"
6897 );
6898 }
6899
6900 #[test]
6901 fn test_recipe_set_prefix_add() {
6902 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6903 let rule = makefile.rules().next().unwrap();
6904 let mut recipe = rule.recipe_nodes().next().unwrap();
6905
6906 recipe.set_prefix("@");
6907 assert_eq!(recipe.text(), "@echo hello");
6908 assert!(recipe.is_silent());
6909 }
6910
6911 #[test]
6912 fn test_recipe_set_prefix_change() {
6913 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6914 let rule = makefile.rules().next().unwrap();
6915 let mut recipe = rule.recipe_nodes().next().unwrap();
6916
6917 recipe.set_prefix("-");
6918 assert_eq!(recipe.text(), "-echo hello");
6919 assert!(!recipe.is_silent());
6920 assert!(recipe.is_ignore_errors());
6921 }
6922
6923 #[test]
6924 fn test_recipe_set_prefix_remove() {
6925 let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
6926 let rule = makefile.rules().next().unwrap();
6927 let mut recipe = rule.recipe_nodes().next().unwrap();
6928
6929 recipe.set_prefix("");
6930 assert_eq!(recipe.text(), "echo hello");
6931 assert!(!recipe.is_silent());
6932 assert!(!recipe.is_ignore_errors());
6933 }
6934
6935 #[test]
6936 fn test_recipe_set_prefix_combinations() {
6937 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6938 let rule = makefile.rules().next().unwrap();
6939 let mut recipe = rule.recipe_nodes().next().unwrap();
6940
6941 recipe.set_prefix("@-");
6942 assert_eq!(recipe.text(), "@-echo hello");
6943 assert!(recipe.is_silent());
6944 assert!(recipe.is_ignore_errors());
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
6952 #[test]
6953 fn test_recipe_replace_text_basic() {
6954 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6955 let rule = makefile.rules().next().unwrap();
6956 let mut recipe = rule.recipe_nodes().next().unwrap();
6957
6958 recipe.replace_text("echo world");
6959 assert_eq!(recipe.text(), "echo world");
6960
6961 let rule = makefile.rules().next().unwrap();
6963 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6964 }
6965
6966 #[test]
6967 fn test_recipe_replace_text_with_prefix() {
6968 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6969 let rule = makefile.rules().next().unwrap();
6970 let mut recipe = rule.recipe_nodes().next().unwrap();
6971
6972 recipe.replace_text("@echo goodbye");
6973 assert_eq!(recipe.text(), "@echo goodbye");
6974 assert!(recipe.is_silent());
6975 }
6976
6977 #[test]
6978 fn test_recipe_insert_before_single() {
6979 let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6980 let rule = makefile.rules().next().unwrap();
6981 let recipe = rule.recipe_nodes().next().unwrap();
6982
6983 recipe.insert_before("echo hello");
6984
6985 let rule = makefile.rules().next().unwrap();
6986 let recipes: Vec<_> = rule.recipes().collect();
6987 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6988 }
6989
6990 #[test]
6991 fn test_recipe_insert_before_multiple() {
6992 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6993 .parse()
6994 .unwrap();
6995 let rule = makefile.rules().next().unwrap();
6996 let recipes: Vec<_> = rule.recipe_nodes().collect();
6997
6998 recipes[1].insert_before("echo middle");
7000
7001 let rule = makefile.rules().next().unwrap();
7002 let new_recipes: Vec<_> = rule.recipes().collect();
7003 assert_eq!(
7004 new_recipes,
7005 vec!["echo one", "echo middle", "echo two", "echo three"]
7006 );
7007 }
7008
7009 #[test]
7010 fn test_recipe_insert_before_first() {
7011 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7012 let rule = makefile.rules().next().unwrap();
7013 let recipes: Vec<_> = rule.recipe_nodes().collect();
7014
7015 recipes[0].insert_before("echo zero");
7016
7017 let rule = makefile.rules().next().unwrap();
7018 let new_recipes: Vec<_> = rule.recipes().collect();
7019 assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
7020 }
7021
7022 #[test]
7023 fn test_recipe_insert_after_single() {
7024 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7025 let rule = makefile.rules().next().unwrap();
7026 let recipe = rule.recipe_nodes().next().unwrap();
7027
7028 recipe.insert_after("echo world");
7029
7030 let rule = makefile.rules().next().unwrap();
7031 let recipes: Vec<_> = rule.recipes().collect();
7032 assert_eq!(recipes, vec!["echo hello", "echo world"]);
7033 }
7034
7035 #[test]
7036 fn test_recipe_insert_after_multiple() {
7037 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7038 .parse()
7039 .unwrap();
7040 let rule = makefile.rules().next().unwrap();
7041 let recipes: Vec<_> = rule.recipe_nodes().collect();
7042
7043 recipes[1].insert_after("echo middle");
7045
7046 let rule = makefile.rules().next().unwrap();
7047 let new_recipes: Vec<_> = rule.recipes().collect();
7048 assert_eq!(
7049 new_recipes,
7050 vec!["echo one", "echo two", "echo middle", "echo three"]
7051 );
7052 }
7053
7054 #[test]
7055 fn test_recipe_insert_after_last() {
7056 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7057 let rule = makefile.rules().next().unwrap();
7058 let recipes: Vec<_> = rule.recipe_nodes().collect();
7059
7060 recipes[1].insert_after("echo three");
7061
7062 let rule = makefile.rules().next().unwrap();
7063 let new_recipes: Vec<_> = rule.recipes().collect();
7064 assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
7065 }
7066
7067 #[test]
7068 fn test_recipe_remove_single() {
7069 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7070 let rule = makefile.rules().next().unwrap();
7071 let recipe = rule.recipe_nodes().next().unwrap();
7072
7073 recipe.remove();
7074
7075 let rule = makefile.rules().next().unwrap();
7076 assert_eq!(rule.recipes().count(), 0);
7077 }
7078
7079 #[test]
7080 fn test_recipe_remove_first() {
7081 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7082 .parse()
7083 .unwrap();
7084 let rule = makefile.rules().next().unwrap();
7085 let recipes: Vec<_> = rule.recipe_nodes().collect();
7086
7087 recipes[0].remove();
7088
7089 let rule = makefile.rules().next().unwrap();
7090 let new_recipes: Vec<_> = rule.recipes().collect();
7091 assert_eq!(new_recipes, vec!["echo two", "echo three"]);
7092 }
7093
7094 #[test]
7095 fn test_recipe_remove_middle() {
7096 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7097 .parse()
7098 .unwrap();
7099 let rule = makefile.rules().next().unwrap();
7100 let recipes: Vec<_> = rule.recipe_nodes().collect();
7101
7102 recipes[1].remove();
7103
7104 let rule = makefile.rules().next().unwrap();
7105 let new_recipes: Vec<_> = rule.recipes().collect();
7106 assert_eq!(new_recipes, vec!["echo one", "echo three"]);
7107 }
7108
7109 #[test]
7110 fn test_recipe_remove_last() {
7111 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7112 .parse()
7113 .unwrap();
7114 let rule = makefile.rules().next().unwrap();
7115 let recipes: Vec<_> = rule.recipe_nodes().collect();
7116
7117 recipes[2].remove();
7118
7119 let rule = makefile.rules().next().unwrap();
7120 let new_recipes: Vec<_> = rule.recipes().collect();
7121 assert_eq!(new_recipes, vec!["echo one", "echo two"]);
7122 }
7123
7124 #[test]
7125 fn test_recipe_multiple_operations() {
7126 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7127 let rule = makefile.rules().next().unwrap();
7128 let mut recipe = rule.recipe_nodes().next().unwrap();
7129
7130 recipe.replace_text("echo modified");
7132 assert_eq!(recipe.text(), "echo modified");
7133
7134 recipe.set_prefix("@");
7136 assert_eq!(recipe.text(), "@echo modified");
7137
7138 recipe.insert_after("echo three");
7140
7141 let rule = makefile.rules().next().unwrap();
7143 let recipes: Vec<_> = rule.recipes().collect();
7144 assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
7145 }
7146
7147 #[test]
7148 fn test_from_str_relaxed_valid() {
7149 let input = "all: foo\n\tfoo bar\n";
7150 let (makefile, errors) = Makefile::from_str_relaxed(input);
7151 assert!(errors.is_empty());
7152 assert_eq!(makefile.rules().count(), 1);
7153 assert_eq!(makefile.to_string(), input);
7154 }
7155
7156 #[test]
7157 fn test_from_str_relaxed_with_errors() {
7158 let input = "rule target\n\tcommand\n";
7160 let (makefile, errors) = Makefile::from_str_relaxed(input);
7161 assert!(!errors.is_empty());
7162 assert_eq!(makefile.to_string(), input);
7164 }
7165
7166 #[test]
7167 fn test_positioned_errors_have_valid_ranges() {
7168 let input = "rule target\n\tcommand\n";
7169 let parsed = Makefile::parse(input);
7170 assert!(!parsed.ok());
7171
7172 let positioned = parsed.positioned_errors();
7173 assert!(!positioned.is_empty());
7174
7175 for err in positioned {
7176 let start: u32 = err.range.start().into();
7178 let end: u32 = err.range.end().into();
7179 assert!(start <= end);
7180 assert!((end as usize) <= input.len());
7181 }
7182 }
7183
7184 #[test]
7185 fn test_positioned_errors_point_to_error_location() {
7186 let input = "rule target\n\tcommand\n";
7187 let parsed = Makefile::parse(input);
7188 assert!(!parsed.ok());
7189
7190 let positioned = parsed.positioned_errors();
7191 assert!(!positioned.is_empty());
7192
7193 let err = &positioned[0];
7194 let start: usize = err.range.start().into();
7195 let end: usize = err.range.end().into();
7196 let error_text = &input[start..end];
7198 assert!(!error_text.is_empty());
7199
7200 let tree = parsed.tree();
7202 assert_eq!(tree.to_string(), input);
7203 }
7204
7205 #[test]
7206 fn test_tree_with_errors_preserves_text() {
7207 let input = "rule target\n\tcommand\nVAR = value\n";
7208 let parsed = Makefile::parse(input);
7209 assert!(!parsed.ok());
7210
7211 let tree = parsed.tree();
7212 assert_eq!(tree.to_string(), input);
7213
7214 assert_eq!(tree.variable_definitions().count(), 1);
7216 }
7217}
7218
7219#[cfg(test)]
7220mod test_continuation {
7221 use super::*;
7222
7223 #[test]
7224 fn test_recipe_continuation_lines() {
7225 let makefile_content = r#"override_dh_autoreconf:
7226 set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \
7227 dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \
7228 sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs
7229 dh_autoreconf
7230"#;
7231
7232 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7233 let rule = makefile.rules().next().unwrap();
7234
7235 let recipes: Vec<_> = rule.recipe_nodes().collect();
7236
7237 assert_eq!(recipes.len(), 2);
7239
7240 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";
7243 assert_eq!(recipes[0].text(), expected_first);
7244
7245 assert_eq!(recipes[1].text(), "dh_autoreconf");
7247 }
7248
7249 #[test]
7250 fn test_simple_continuation() {
7251 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n";
7252
7253 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7254 let rule = makefile.rules().next().unwrap();
7255 let recipes: Vec<_> = rule.recipe_nodes().collect();
7256
7257 assert_eq!(recipes.len(), 1);
7258 assert_eq!(recipes[0].text(), "echo hello && \\\n echo world");
7259 }
7260
7261 #[test]
7262 fn test_multiple_continuations() {
7263 let makefile_content = "test:\n\techo line1 && \\\n\t echo line2 && \\\n\t echo line3 && \\\n\t echo line4\n";
7264
7265 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7266 let rule = makefile.rules().next().unwrap();
7267 let recipes: Vec<_> = rule.recipe_nodes().collect();
7268
7269 assert_eq!(recipes.len(), 1);
7270 assert_eq!(
7271 recipes[0].text(),
7272 "echo line1 && \\\n echo line2 && \\\n echo line3 && \\\n echo line4"
7273 );
7274 }
7275
7276 #[test]
7277 fn test_continuation_round_trip() {
7278 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7279
7280 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7281 let output = makefile.to_string();
7282
7283 assert_eq!(output, makefile_content);
7285 }
7286
7287 #[test]
7288 fn test_continuation_with_silent_prefix() {
7289 let makefile_content = "test:\n\t@echo hello && \\\n\t echo world\n";
7290
7291 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7292 let rule = makefile.rules().next().unwrap();
7293 let recipes: Vec<_> = rule.recipe_nodes().collect();
7294
7295 assert_eq!(recipes.len(), 1);
7296 assert_eq!(recipes[0].text(), "@echo hello && \\\n echo world");
7297 assert!(recipes[0].is_silent());
7298 }
7299
7300 #[test]
7301 fn test_mixed_continued_and_non_continued() {
7302 let makefile_content = r#"test:
7303 echo first
7304 echo second && \
7305 echo third
7306 echo fourth
7307"#;
7308
7309 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7310 let rule = makefile.rules().next().unwrap();
7311 let recipes: Vec<_> = rule.recipe_nodes().collect();
7312
7313 assert_eq!(recipes.len(), 3);
7314 assert_eq!(recipes[0].text(), "echo first");
7315 assert_eq!(recipes[1].text(), "echo second && \\\n echo third");
7316 assert_eq!(recipes[2].text(), "echo fourth");
7317 }
7318
7319 #[test]
7320 fn test_continuation_replace_command() {
7321 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7322
7323 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7324 let mut rule = makefile.rules().next().unwrap();
7325
7326 rule.replace_command(0, "echo replaced");
7328
7329 let recipes: Vec<_> = rule.recipe_nodes().collect();
7330 assert_eq!(recipes.len(), 2);
7331 assert_eq!(recipes[0].text(), "echo replaced");
7332 assert_eq!(recipes[1].text(), "echo done");
7333 }
7334
7335 #[test]
7336 fn test_continuation_count() {
7337 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7338
7339 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7340 let rule = makefile.rules().next().unwrap();
7341
7342 assert_eq!(rule.recipe_count(), 2);
7344 assert_eq!(rule.recipe_nodes().count(), 2);
7345
7346 let recipes_list: Vec<_> = rule.recipes().collect();
7348 assert_eq!(
7349 recipes_list,
7350 vec!["echo hello && \\\n echo world", "echo done"]
7351 );
7352 }
7353
7354 #[test]
7355 fn test_backslash_in_middle_of_line() {
7356 let makefile_content = "test:\n\techo hello\\nworld\n\techo done\n";
7358
7359 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7360 let rule = makefile.rules().next().unwrap();
7361 let recipes: Vec<_> = rule.recipe_nodes().collect();
7362
7363 assert_eq!(recipes.len(), 2);
7364 assert_eq!(recipes[0].text(), "echo hello\\nworld");
7365 assert_eq!(recipes[1].text(), "echo done");
7366 }
7367
7368 #[test]
7369 fn test_shell_for_loop_with_continuation() {
7370 let makefile_content = r#"override_dh_installman:
7374 for i in foo bar; do \
7375 pod2man --section=1 $$i ; \
7376 done
7377"#;
7378
7379 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7380 let rule = makefile.rules().next().unwrap();
7381
7382 let recipes: Vec<_> = rule.recipe_nodes().collect();
7384 assert_eq!(recipes.len(), 1);
7385
7386 let recipe_text = recipes[0].text();
7388 let expected_recipe = "for i in foo bar; do \\\n\tpod2man --section=1 $$i ; \\\ndone";
7389 assert_eq!(recipe_text, expected_recipe);
7390
7391 let output = makefile.to_string();
7393 assert_eq!(output, makefile_content);
7394 }
7395
7396 #[test]
7397 fn test_shell_for_loop_remove_command() {
7398 let makefile_content = r#"override_dh_installman:
7401 for i in foo bar; do \
7402 pod2man --section=1 $$i ; \
7403 done
7404 echo "Done with man pages"
7405"#;
7406
7407 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7408 let mut rule = makefile.rules().next().unwrap();
7409
7410 assert_eq!(rule.recipe_count(), 2);
7412
7413 rule.remove_command(1);
7415
7416 let recipes: Vec<_> = rule.recipe_nodes().collect();
7418 assert_eq!(recipes.len(), 1);
7419
7420 let output = makefile.to_string();
7422 let expected_output = r#"override_dh_installman:
7423 for i in foo bar; do \
7424 pod2man --section=1 $$i ; \
7425 done
7426"#;
7427 assert_eq!(output, expected_output);
7428 }
7429
7430 #[test]
7431 fn test_variable_reference_paren() {
7432 let makefile: Makefile = "CFLAGS = $(BASE_FLAGS) -Wall\n".parse().unwrap();
7433 let refs: Vec<_> = makefile.variable_references().collect();
7434 assert_eq!(refs.len(), 1);
7435 assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7436 assert_eq!(refs[0].to_string(), "$(BASE_FLAGS)");
7437 }
7438
7439 #[test]
7440 fn test_variable_reference_brace() {
7441 let makefile: Makefile = "CFLAGS = ${BASE_FLAGS} -Wall\n".parse().unwrap();
7442 let refs: Vec<_> = makefile.variable_references().collect();
7443 assert_eq!(refs.len(), 1);
7444 assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7445 assert_eq!(refs[0].to_string(), "${BASE_FLAGS}");
7446 }
7447
7448 #[test]
7449 fn test_variable_reference_in_prerequisites() {
7450 let makefile: Makefile = "all: $(TARGETS)\n".parse().unwrap();
7451 let refs: Vec<_> = makefile.variable_references().collect();
7452 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7453 assert!(names.contains(&"TARGETS".to_string()));
7454 }
7455
7456 #[test]
7457 fn test_variable_reference_multiple() {
7458 let makefile: Makefile =
7459 "CFLAGS = $(BASE_FLAGS) -Wall\nLDFLAGS = $(BASE_LDFLAGS) -lm\nall: $(TARGETS)\n"
7460 .parse()
7461 .unwrap();
7462 let refs: Vec<_> = makefile.variable_references().collect();
7463 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7464 assert!(names.contains(&"BASE_FLAGS".to_string()));
7465 assert!(names.contains(&"BASE_LDFLAGS".to_string()));
7466 assert!(names.contains(&"TARGETS".to_string()));
7467 }
7468
7469 #[test]
7470 fn test_variable_reference_nested() {
7471 let makefile: Makefile = "FOO = $($(INNER))\n".parse().unwrap();
7472 let refs: Vec<_> = makefile.variable_references().collect();
7473 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7474 assert!(names.contains(&"INNER".to_string()));
7475 }
7476
7477 #[test]
7478 fn test_variable_reference_line_col() {
7479 let makefile: Makefile = "A = 1\nB = $(FOO)\n".parse().unwrap();
7480 let refs: Vec<_> = makefile.variable_references().collect();
7481 assert_eq!(refs.len(), 1);
7482 assert_eq!(refs[0].name(), Some("FOO".to_string()));
7483 assert_eq!(refs[0].line(), 1);
7484 assert_eq!(refs[0].column(), 4);
7485 assert_eq!(refs[0].line_col(), (1, 4));
7486 }
7487
7488 #[test]
7489 fn test_variable_reference_no_refs() {
7490 let makefile: Makefile = "A = hello\nall:\n\techo done\n".parse().unwrap();
7491 let refs: Vec<_> = makefile.variable_references().collect();
7492 assert_eq!(refs.len(), 0);
7493 }
7494
7495 #[test]
7496 fn test_variable_reference_mixed_styles() {
7497 let makefile: Makefile = "A = $(FOO) ${BAR}\n".parse().unwrap();
7498 let refs: Vec<_> = makefile.variable_references().collect();
7499 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7500 assert_eq!(names.len(), 2);
7501 assert!(names.contains(&"FOO".to_string()));
7502 assert!(names.contains(&"BAR".to_string()));
7503 }
7504
7505 #[test]
7506 fn test_brace_variable_in_prerequisites() {
7507 let makefile: Makefile = "all: ${OBJS}\n".parse().unwrap();
7508 let refs: Vec<_> = makefile.variable_references().collect();
7509 assert_eq!(refs.len(), 1);
7510 assert_eq!(refs[0].name(), Some("OBJS".to_string()));
7511 }
7512
7513 #[test]
7514 fn test_parse_brace_variable_roundtrip() {
7515 let input = "CFLAGS = ${BASE_FLAGS} -Wall\n";
7516 let makefile: Makefile = input.parse().unwrap();
7517 assert_eq!(makefile.to_string(), input);
7518 }
7519
7520 #[test]
7521 fn test_parse_nested_variable_in_value_roundtrip() {
7522 let input = "FOO = $(BAR) baz $(QUUX)\n";
7523 let makefile: Makefile = input.parse().unwrap();
7524 assert_eq!(makefile.to_string(), input);
7525 }
7526
7527 #[test]
7528 fn test_is_function_call() {
7529 let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7530 let refs: Vec<_> = makefile.variable_references().collect();
7531 assert_eq!(refs.len(), 1);
7532 assert!(refs[0].is_function_call());
7533 }
7534
7535 #[test]
7536 fn test_is_function_call_simple_variable() {
7537 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7538 let refs: Vec<_> = makefile.variable_references().collect();
7539 assert_eq!(refs.len(), 1);
7540 assert!(!refs[0].is_function_call());
7541 }
7542
7543 #[test]
7544 fn test_is_function_call_with_commas() {
7545 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7546 let refs: Vec<_> = makefile.variable_references().collect();
7547 assert_eq!(refs.len(), 1);
7548 assert!(refs[0].is_function_call());
7549 }
7550
7551 #[test]
7552 fn test_is_function_call_braces() {
7553 let makefile: Makefile = "FILES = ${wildcard *.c}\n".parse().unwrap();
7554 let refs: Vec<_> = makefile.variable_references().collect();
7555 assert_eq!(refs.len(), 1);
7556 assert!(refs[0].is_function_call());
7557 }
7558
7559 #[test]
7560 fn test_argument_count_simple_variable() {
7561 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7562 let refs: Vec<_> = makefile.variable_references().collect();
7563 assert_eq!(refs[0].argument_count(), 0);
7564 }
7565
7566 #[test]
7567 fn test_argument_count_one_arg() {
7568 let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7569 let refs: Vec<_> = makefile.variable_references().collect();
7570 assert_eq!(refs[0].argument_count(), 1);
7571 }
7572
7573 #[test]
7574 fn test_argument_count_three_args() {
7575 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7576 let refs: Vec<_> = makefile.variable_references().collect();
7577 assert_eq!(refs[0].argument_count(), 3);
7578 }
7579
7580 #[test]
7581 fn test_argument_index_at_offset_subst() {
7582 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7583 let refs: Vec<_> = makefile.variable_references().collect();
7584 assert_eq!(refs[0].argument_index_at_offset(12), Some(0));
7590 assert_eq!(refs[0].argument_index_at_offset(14), Some(1));
7591 assert_eq!(refs[0].argument_index_at_offset(16), Some(2));
7592 }
7593
7594 #[test]
7595 fn test_argument_index_at_offset_outside() {
7596 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7597 let refs: Vec<_> = makefile.variable_references().collect();
7598 assert_eq!(refs[0].argument_index_at_offset(0), None);
7600 assert_eq!(refs[0].argument_index_at_offset(22), None);
7602 }
7603
7604 #[test]
7605 fn test_argument_index_at_offset_simple_variable() {
7606 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7607 let refs: Vec<_> = makefile.variable_references().collect();
7608 assert_eq!(refs[0].argument_index_at_offset(11), None);
7609 }
7610
7611 #[test]
7612 fn test_lex_braces() {
7613 use crate::lex::lex;
7614 let tokens = lex("${FOO}");
7615 let kinds: Vec<_> = tokens.iter().map(|(k, _)| *k).collect();
7616 assert!(kinds.contains(&DOLLAR));
7617 assert!(kinds.contains(&LBRACE));
7618 assert!(kinds.contains(&RBRACE));
7619 }
7620
7621 #[test]
7622 fn test_parse_quoted_string_inside_function_call() {
7623 let cases = [
7628 "X = $(if a,'foo')\n",
7629 "X = $(if a,'foo (bar)')\n",
7630 "X = $(if a,'(')\n",
7631 "X = $(if a,')')\n",
7632 "X = $(if $(SKIP),-k 'not ($(call f,$(s),$(SKIP)))')\n",
7633 "X = foo'bar\nY = baz\n",
7634 "X = it's fine\n",
7635 "X = $(if a,it's)\n",
7636 "X = '\nY = bar\n",
7637 ];
7638 for src in cases {
7639 let parsed: Makefile = src.parse().unwrap_or_else(|e| {
7640 panic!("failed to parse {src:?}: {e:?}");
7641 });
7642 assert_eq!(parsed.to_string(), src, "round-trip mismatch for {src:?}");
7643 }
7644 }
7645}