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) {
822 self.bump(); while !self.is_at_eof() && self.current() != Some(QUOTE) {
824 self.bump();
825 }
826 if self.current() == Some(QUOTE) {
827 self.bump();
828 }
829 }
830
831 fn parse_conditional_keyword(&mut self) -> Option<String> {
832 if self.current() != Some(IDENTIFIER) {
833 self.error(
834 "expected conditional keyword (ifdef, ifndef, ifeq, or ifneq)".to_string(),
835 );
836 return None;
837 }
838
839 let token = self.tokens.last().unwrap().1.clone();
840 if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&token.as_str()) {
841 self.error(format!("unknown conditional directive: {}", token));
842 return None;
843 }
844
845 self.bump();
846 Some(token)
847 }
848
849 fn parse_simple_condition(&mut self) {
850 self.builder.start_node(EXPR.into());
851
852 self.skip_ws();
854
855 let mut found_var = false;
857
858 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
859 match self.current() {
860 Some(WHITESPACE) => self.skip_ws(),
861 Some(DOLLAR) => {
862 found_var = true;
863 self.parse_variable_reference();
864 }
865 Some(_) => {
866 found_var = true;
868 self.bump();
869 }
870 None => break,
871 }
872 }
873
874 if !found_var {
875 self.error("expected condition after conditional directive".to_string());
877 }
878
879 self.builder.finish_node();
880
881 if self.current() == Some(NEWLINE) {
883 self.bump();
884 } else if !self.is_at_eof() {
885 self.skip_until_newline();
886 }
887 }
888
889 fn is_conditional_directive(&self, token: &str) -> bool {
891 token == "ifdef"
892 || token == "ifndef"
893 || token == "ifeq"
894 || token == "ifneq"
895 || token == "else"
896 || token == "endif"
897 }
898
899 fn handle_conditional_token(&mut self, token: &str, depth: &mut usize) -> bool {
901 match token {
902 "ifdef" | "ifndef" | "ifeq" | "ifneq"
903 if matches!(self.variant, None | Some(MakefileVariant::GNUMake)) =>
904 {
905 self.parse_conditional();
908 true
909 }
910 "else" => {
911 if *depth == 0 {
913 self.error("else without matching if".to_string());
914 self.bump();
916 false
917 } else {
918 self.builder.start_node(CONDITIONAL_ELSE.into());
920
921 self.bump();
923 self.skip_ws();
924
925 if self.current() == Some(IDENTIFIER) {
927 let next_token = &self.tokens.last().unwrap().1;
928 if next_token == "ifdef"
929 || next_token == "ifndef"
930 || next_token == "ifeq"
931 || next_token == "ifneq"
932 {
933 match next_token.as_str() {
936 "ifdef" | "ifndef" => {
937 self.bump(); self.skip_ws();
939 self.parse_simple_condition();
940 }
941 "ifeq" | "ifneq" => {
942 self.bump(); self.skip_ws();
944 self.parse_parenthesized_expr();
945 }
946 _ => unreachable!(),
947 }
948 } else {
950 }
953 } else {
954 }
956
957 self.builder.finish_node(); true
959 }
960 }
961 "endif" => {
962 if *depth == 0 {
964 self.error("endif without matching if".to_string());
965 self.bump();
967 false
968 } else {
969 *depth -= 1;
970
971 self.builder.start_node(CONDITIONAL_ENDIF.into());
973
974 self.bump();
976
977 self.skip_ws();
979
980 if self.current() == Some(COMMENT) {
985 self.parse_comment();
986 } else if self.current() == Some(NEWLINE) {
987 self.bump();
988 } else if self.current() == Some(WHITESPACE) {
989 self.skip_ws();
991 if self.current() == Some(NEWLINE) {
992 self.bump();
993 }
994 } else if !self.is_at_eof() {
996 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
999 self.bump();
1000 }
1001 if self.current() == Some(NEWLINE) {
1002 self.bump();
1003 }
1004 }
1005 self.builder.finish_node(); true
1009 }
1010 }
1011 _ => false,
1012 }
1013 }
1014
1015 fn parse_conditional(&mut self) {
1016 self.builder.start_node(CONDITIONAL.into());
1017
1018 self.builder.start_node(CONDITIONAL_IF.into());
1020
1021 let Some(token) = self.parse_conditional_keyword() else {
1023 self.skip_until_newline();
1024 self.builder.finish_node(); self.builder.finish_node(); return;
1027 };
1028
1029 self.skip_ws();
1031
1032 match token.as_str() {
1034 "ifdef" | "ifndef" => {
1035 self.parse_simple_condition();
1036 }
1037 "ifeq" | "ifneq" => {
1038 self.parse_parenthesized_expr();
1039 }
1040 _ => unreachable!("Invalid conditional token"),
1041 }
1042
1043 self.skip_ws();
1045 if self.current() == Some(COMMENT) {
1046 self.parse_comment();
1047 }
1048 self.builder.finish_node(); let mut depth = 1;
1054
1055 let mut position_count = std::collections::HashMap::<usize, usize>::new();
1057 let max_repetitions = 15; while depth > 0 && !self.is_at_eof() {
1060 let current_pos = self.tokens.len();
1062 *position_count.entry(current_pos).or_insert(0) += 1;
1063
1064 if position_count.get(¤t_pos).unwrap() > &max_repetitions {
1067 break;
1070 }
1071
1072 match self.current() {
1073 None => {
1074 self.error("unterminated conditional (missing endif)".to_string());
1075 break;
1076 }
1077 Some(IDENTIFIER) => {
1078 let token = self.tokens.last().unwrap().1.clone();
1079 if !self.handle_conditional_token(&token, &mut depth) {
1080 if token == "include" || token == "-include" || token == "sinclude" {
1081 self.parse_include();
1082 } else {
1083 self.parse_normal_content();
1084 }
1085 }
1086 }
1087 Some(INDENT) => self.parse_recipe_line(),
1088 Some(WHITESPACE) => self.bump(),
1089 Some(COMMENT) => self.parse_comment(),
1090 Some(NEWLINE) => self.bump(),
1091 Some(DOLLAR) => self.parse_normal_content(),
1092 Some(QUOTE) => self.parse_quoted_string(),
1093 Some(_) => {
1094 self.bump();
1096 }
1097 }
1098 }
1099
1100 self.builder.finish_node();
1101 }
1102
1103 fn parse_normal_content(&mut self) {
1105 self.skip_ws();
1107
1108 if self.is_assignment_line() {
1110 self.parse_assignment();
1111 } else {
1112 self.parse_rule();
1114 }
1115 }
1116
1117 fn parse_include(&mut self) {
1118 self.builder.start_node(INCLUDE.into());
1119
1120 if self.current() != Some(IDENTIFIER)
1122 || (!["include", "-include", "sinclude"]
1123 .contains(&self.tokens.last().unwrap().1.as_str()))
1124 {
1125 self.error("expected include directive".to_string());
1126 self.builder.finish_node();
1127 return;
1128 }
1129 self.bump();
1130 self.skip_ws();
1131
1132 self.builder.start_node(EXPR.into());
1134 let mut found_path = false;
1135
1136 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1137 match self.current() {
1138 Some(WHITESPACE) => self.skip_ws(),
1139 Some(DOLLAR) => {
1140 found_path = true;
1141 self.parse_variable_reference();
1142 }
1143 Some(_) => {
1144 found_path = true;
1146 self.bump();
1147 }
1148 None => break,
1149 }
1150 }
1151
1152 if !found_path {
1153 self.error("expected file path after include".to_string());
1154 }
1155
1156 self.builder.finish_node();
1157
1158 if self.current() == Some(NEWLINE) {
1160 self.bump();
1161 } else if !self.is_at_eof() {
1162 self.error("expected newline after include".to_string());
1163 self.skip_until_newline();
1164 }
1165
1166 self.builder.finish_node();
1167 }
1168
1169 fn parse_identifier_token(&mut self) -> bool {
1170 let token = &self.tokens.last().unwrap().1;
1171
1172 if token.starts_with("%") {
1174 self.parse_rule();
1175 return true;
1176 }
1177
1178 if token.starts_with("if")
1179 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1180 {
1181 self.parse_conditional();
1182 return true;
1183 }
1184
1185 if token == "include" || token == "-include" || token == "sinclude" {
1186 self.parse_include();
1187 return true;
1188 }
1189
1190 self.parse_normal_content();
1192 true
1193 }
1194
1195 fn parse_token(&mut self) -> bool {
1196 match self.current() {
1197 None => false,
1198 Some(IDENTIFIER) => {
1199 let token = &self.tokens.last().unwrap().1;
1200 if self.is_conditional_directive(token)
1201 && matches!(self.variant, None | Some(MakefileVariant::GNUMake))
1202 {
1203 self.parse_conditional();
1204 true
1205 } else {
1206 self.parse_identifier_token()
1207 }
1208 }
1209 Some(DOLLAR) => {
1210 self.parse_normal_content();
1211 true
1212 }
1213 Some(NEWLINE) => {
1214 self.builder.start_node(BLANK_LINE.into());
1215 self.bump();
1216 self.builder.finish_node();
1217 true
1218 }
1219 Some(COMMENT) => {
1220 self.parse_comment();
1221 true
1222 }
1223 Some(WHITESPACE) => {
1224 if self.is_end_of_file_or_newline_after_whitespace() {
1226 self.skip_ws();
1229 return true;
1230 }
1231
1232 let look_ahead_pos = self.tokens.len().saturating_sub(1);
1235 let mut is_documentation_or_help = false;
1236
1237 if look_ahead_pos > 0 {
1238 let next_token = &self.tokens[look_ahead_pos - 1];
1239 if next_token.0 == IDENTIFIER
1242 || next_token.0 == COMMENT
1243 || next_token.0 == TEXT
1244 {
1245 is_documentation_or_help = true;
1246 }
1247 }
1248
1249 if is_documentation_or_help {
1250 self.skip_ws();
1253 while self.current().is_some() && self.current() != Some(NEWLINE) {
1254 self.bump();
1255 }
1256 if self.current() == Some(NEWLINE) {
1257 self.bump();
1258 }
1259 } else {
1260 self.skip_ws();
1261 }
1262 true
1263 }
1264 Some(INDENT) => {
1265 self.bump();
1267
1268 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1270 self.bump();
1271 }
1272 if self.current() == Some(NEWLINE) {
1273 self.bump();
1274 }
1275 true
1276 }
1277 Some(kind) => {
1278 self.error(format!("unexpected token {:?}", kind));
1279 self.bump();
1280 true
1281 }
1282 }
1283 }
1284
1285 fn parse(mut self) -> Parse {
1286 self.builder.start_node(ROOT.into());
1287
1288 while self.parse_token() {}
1289
1290 self.builder.finish_node();
1291
1292 Parse {
1293 green_node: self.builder.finish(),
1294 errors: self.errors,
1295 positioned_errors: self.positioned_errors,
1296 }
1297 }
1298
1299 fn is_assignment_line(&mut self) -> bool {
1301 let assignment_ops = ["=", ":=", "::=", ":::=", "+=", "?=", "!="];
1302 let mut pos = self.tokens.len().saturating_sub(1);
1303 let mut seen_identifier = false;
1304 let mut seen_export = false;
1305
1306 while pos > 0 {
1307 let (kind, text) = &self.tokens[pos];
1308
1309 match kind {
1310 NEWLINE => break,
1311 IDENTIFIER if text == "export" => seen_export = true,
1312 IDENTIFIER if !seen_identifier => seen_identifier = true,
1313 OPERATOR if assignment_ops.contains(&text.as_str()) => {
1314 return seen_identifier || seen_export
1315 }
1316 OPERATOR if text == ":" || text == "::" => return false, WHITESPACE => (),
1318 _ if seen_export => return true, _ => return false,
1320 }
1321 pos = pos.saturating_sub(1);
1322 }
1323 seen_export
1325 }
1326
1327 fn bump(&mut self) {
1329 let (kind, text) = self.tokens.pop().unwrap();
1330 self.builder.token(kind.into(), text.as_str());
1331 if self.current_token_index > 0 {
1332 self.current_token_index -= 1;
1333 }
1334 }
1335 fn current(&self) -> Option<SyntaxKind> {
1337 self.tokens.last().map(|(kind, _)| *kind)
1338 }
1339
1340 fn expect_eol(&mut self) {
1341 self.skip_ws();
1343
1344 match self.current() {
1345 Some(NEWLINE) => {
1346 self.bump();
1347 }
1348 None => {
1349 }
1351 n => {
1352 self.error(format!("expected newline, got {:?}", n));
1353 self.skip_until_newline();
1355 }
1356 }
1357 }
1358
1359 fn is_at_eof(&self) -> bool {
1361 self.current().is_none()
1362 }
1363
1364 fn is_at_eof_or_only_whitespace(&self) -> bool {
1366 if self.is_at_eof() {
1367 return true;
1368 }
1369
1370 self.tokens
1372 .iter()
1373 .rev()
1374 .all(|(kind, _)| matches!(*kind, WHITESPACE | NEWLINE))
1375 }
1376
1377 fn skip_ws(&mut self) {
1378 while self.current() == Some(WHITESPACE) {
1379 self.bump()
1380 }
1381 }
1382
1383 fn skip_until_newline(&mut self) {
1384 while !self.is_at_eof() && self.current() != Some(NEWLINE) {
1385 self.bump();
1386 }
1387 if self.current() == Some(NEWLINE) {
1388 self.bump();
1389 }
1390 }
1391
1392 fn consume_balanced_parens(&mut self, start_paren_count: usize) -> usize {
1394 let mut paren_count = start_paren_count;
1395
1396 while paren_count > 0 && self.current().is_some() {
1397 match self.current() {
1398 Some(LPAREN) => {
1399 paren_count += 1;
1400 self.bump();
1401 }
1402 Some(RPAREN) => {
1403 paren_count -= 1;
1404 self.bump();
1405 if paren_count == 0 {
1406 break;
1407 }
1408 }
1409 Some(DOLLAR) => {
1410 self.parse_variable_reference();
1412 }
1413 Some(_) => self.bump(),
1414 None => {
1415 self.error("unclosed parenthesis".to_string());
1416 break;
1417 }
1418 }
1419 }
1420
1421 paren_count
1422 }
1423
1424 fn is_end_of_file_or_newline_after_whitespace(&self) -> bool {
1426 if self.is_at_eof_or_only_whitespace() {
1428 return true;
1429 }
1430
1431 if self.tokens.len() <= 1 {
1433 return true;
1434 }
1435
1436 false
1437 }
1438 }
1439
1440 let mut tokens = lex(text);
1441
1442 let mut token_positions = Vec::with_capacity(tokens.len());
1444 let mut position = rowan::TextSize::from(0);
1445 for (_kind, text) in &tokens {
1446 let start = position;
1447 let end = start + rowan::TextSize::of(text.as_str());
1448 token_positions.push((start, end));
1449 position = end;
1450 }
1451
1452 let current_token_index = tokens.len().saturating_sub(1);
1453 tokens.reverse();
1454 Parser {
1455 tokens,
1456 builder: GreenNodeBuilder::new(),
1457 errors: Vec::new(),
1458 positioned_errors: Vec::new(),
1459 token_positions,
1460 current_token_index,
1461 original_text: text.to_string(),
1462 variant,
1463 }
1464 .parse()
1465}
1466
1467pub(crate) type SyntaxNode = rowan::SyntaxNode<Lang>;
1473#[allow(unused)]
1474type SyntaxToken = rowan::SyntaxToken<Lang>;
1475#[allow(unused)]
1476pub(crate) type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
1477
1478impl Parse {
1479 fn syntax(&self) -> SyntaxNode {
1480 SyntaxNode::new_root_mut(self.green_node.clone())
1481 }
1482
1483 pub(crate) fn root(&self) -> Makefile {
1484 Makefile::cast(self.syntax()).unwrap()
1485 }
1486}
1487
1488fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
1491 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
1492 let mut line = 0;
1493 let mut last_newline_offset = rowan::TextSize::from(0);
1494
1495 for element in root.preorder_with_tokens() {
1496 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
1497 if token.text_range().start() >= offset {
1498 break;
1499 }
1500
1501 for (idx, _) in token.text().match_indices('\n') {
1503 line += 1;
1504 last_newline_offset =
1505 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
1506 }
1507 }
1508 }
1509
1510 let column: usize = (offset - last_newline_offset).into();
1511 (line, column)
1512}
1513
1514macro_rules! ast_node {
1515 ($ast:ident, $kind:ident) => {
1516 #[derive(Clone, PartialEq, Eq, Hash)]
1517 #[repr(transparent)]
1518 pub struct $ast(SyntaxNode);
1520
1521 impl AstNode for $ast {
1522 type Language = Lang;
1523
1524 fn can_cast(kind: SyntaxKind) -> bool {
1525 kind == $kind
1526 }
1527
1528 fn cast(syntax: SyntaxNode) -> Option<Self> {
1529 if Self::can_cast(syntax.kind()) {
1530 Some(Self(syntax))
1531 } else {
1532 None
1533 }
1534 }
1535
1536 fn syntax(&self) -> &SyntaxNode {
1537 &self.0
1538 }
1539 }
1540
1541 impl $ast {
1542 pub fn line(&self) -> usize {
1544 line_col_at_offset(&self.0, self.0.text_range().start()).0
1545 }
1546
1547 pub fn column(&self) -> usize {
1549 line_col_at_offset(&self.0, self.0.text_range().start()).1
1550 }
1551
1552 pub fn line_col(&self) -> (usize, usize) {
1555 line_col_at_offset(&self.0, self.0.text_range().start())
1556 }
1557 }
1558
1559 impl core::fmt::Display for $ast {
1560 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1561 write!(f, "{}", self.0.text())
1562 }
1563 }
1564 };
1565}
1566
1567ast_node!(Makefile, ROOT);
1568ast_node!(Rule, RULE);
1569ast_node!(Recipe, RECIPE);
1570ast_node!(Identifier, IDENTIFIER);
1571ast_node!(VariableDefinition, VARIABLE);
1572ast_node!(Include, INCLUDE);
1573ast_node!(ArchiveMembers, ARCHIVE_MEMBERS);
1574ast_node!(ArchiveMember, ARCHIVE_MEMBER);
1575ast_node!(Conditional, CONDITIONAL);
1576
1577#[derive(Clone, PartialEq, Eq, Hash)]
1581pub struct VariableReference(SyntaxNode);
1582
1583impl VariableReference {
1584 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
1589 if syntax.kind() != EXPR {
1590 return None;
1591 }
1592 let mut tokens = syntax
1593 .children_with_tokens()
1594 .filter_map(|it| it.into_token());
1595 let first = tokens.next()?;
1596 if first.kind() != DOLLAR {
1597 return None;
1598 }
1599 tokens.next()?;
1601 Some(Self(syntax))
1602 }
1603
1604 pub fn syntax(&self) -> &SyntaxNode {
1606 &self.0
1607 }
1608
1609 pub fn name(&self) -> Option<String> {
1626 self.0
1628 .children_with_tokens()
1629 .filter_map(|it| it.into_token())
1630 .find(|t| t.kind() == IDENTIFIER)
1631 .map(|t| t.text().to_string())
1632 }
1633
1634 pub fn is_function_call(&self) -> bool {
1647 let mut tokens = self
1648 .0
1649 .children_with_tokens()
1650 .filter_map(|it| it.into_token());
1651
1652 let Some(dollar) = tokens.next() else {
1654 return false;
1655 };
1656 if dollar.kind() != DOLLAR {
1657 return false;
1658 }
1659 let Some(open) = tokens.next() else {
1660 return false;
1661 };
1662 if open.kind() != LPAREN && open.kind() != LBRACE {
1663 return false;
1664 }
1665
1666 let Some(ident) = tokens.next() else {
1668 return false;
1669 };
1670 if ident.kind() != IDENTIFIER {
1671 return false;
1672 }
1673
1674 match tokens.next() {
1676 Some(t) => t.kind() == WHITESPACE || t.kind() == COMMA,
1677 None => false,
1678 }
1679 }
1680
1681 pub fn argument_count(&self) -> usize {
1694 if !self.is_function_call() {
1695 return 0;
1696 }
1697
1698 let mut commas = 0;
1699 let mut depth = 0;
1700 let mut past_name = false;
1701
1702 for element in self.0.children_with_tokens() {
1703 let Some(token) = element.as_token() else {
1704 continue;
1706 };
1707 match token.kind() {
1708 IDENTIFIER if !past_name => {
1709 past_name = true;
1710 }
1711 DOLLAR | LPAREN | LBRACE if !past_name => {}
1712 LPAREN => depth += 1,
1713 RPAREN if depth > 0 => depth -= 1,
1714 COMMA if depth == 0 && past_name => commas += 1,
1715 _ => {}
1716 }
1717 }
1718
1719 if past_name {
1720 commas + 1
1721 } else {
1722 0
1723 }
1724 }
1725
1726 pub fn argument_index_at_offset(&self, offset: usize) -> Option<usize> {
1742 if !self.is_function_call() {
1743 return None;
1744 }
1745
1746 let ref_start: usize = self.0.text_range().start().into();
1747 let ref_end: usize = self.0.text_range().end().into();
1748 if offset < ref_start || offset > ref_end {
1749 return None;
1750 }
1751
1752 let mut arg_index = 0;
1753 let mut depth = 0;
1754 let mut past_name = false;
1755
1756 for element in self.0.children_with_tokens() {
1757 let Some(token) = element.as_token() else {
1758 continue;
1759 };
1760 let token_end: usize = token.text_range().end().into();
1761
1762 match token.kind() {
1763 IDENTIFIER if !past_name => {
1764 past_name = true;
1765 }
1766 DOLLAR | LPAREN | LBRACE if !past_name => {}
1767 LPAREN => depth += 1,
1768 RPAREN if depth > 0 => depth -= 1,
1769 COMMA if depth == 0 && past_name => {
1770 if offset < token_end {
1771 return Some(arg_index);
1772 }
1773 arg_index += 1;
1774 }
1775 _ => {}
1776 }
1777 }
1778
1779 if past_name {
1780 Some(arg_index)
1781 } else {
1782 None
1783 }
1784 }
1785
1786 pub fn line(&self) -> usize {
1788 line_col_at_offset(&self.0, self.0.text_range().start()).0
1789 }
1790
1791 pub fn column(&self) -> usize {
1793 line_col_at_offset(&self.0, self.0.text_range().start()).1
1794 }
1795
1796 pub fn line_col(&self) -> (usize, usize) {
1798 line_col_at_offset(&self.0, self.0.text_range().start())
1799 }
1800
1801 pub fn text_range(&self) -> rowan::TextRange {
1803 self.0.text_range()
1804 }
1805}
1806
1807impl core::fmt::Display for VariableReference {
1808 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
1809 write!(f, "{}", self.0.text())
1810 }
1811}
1812
1813impl Recipe {
1814 pub fn text(&self) -> String {
1826 let tokens: Vec<_> = self
1827 .syntax()
1828 .children_with_tokens()
1829 .filter_map(|it| it.as_token().cloned())
1830 .collect();
1831
1832 if tokens.is_empty() {
1833 return String::new();
1834 }
1835
1836 let start = if tokens.first().map(|t| t.kind()) == Some(INDENT) {
1838 1
1839 } else {
1840 0
1841 };
1842
1843 let end = if tokens.last().map(|t| t.kind()) == Some(NEWLINE) {
1845 tokens.len() - 1
1846 } else {
1847 tokens.len()
1848 };
1849
1850 let mut after_newline = false;
1854 tokens[start..end]
1855 .iter()
1856 .filter_map(|t| match t.kind() {
1857 TEXT => {
1858 after_newline = false;
1859 Some(t.text().to_string())
1860 }
1861 NEWLINE => {
1862 after_newline = true;
1863 Some(t.text().to_string())
1864 }
1865 INDENT if after_newline => {
1866 after_newline = false;
1867 let text = t.text();
1869 Some(text.strip_prefix('\t').unwrap_or(text).to_string())
1870 }
1871 _ => None,
1872 })
1873 .collect()
1874 }
1875
1876 pub fn comment(&self) -> Option<String> {
1892 self.syntax()
1893 .children_with_tokens()
1894 .filter_map(|it| {
1895 if let Some(token) = it.as_token() {
1896 if token.kind() == COMMENT {
1897 return Some(token.text().to_string());
1898 }
1899 }
1900 None
1901 })
1902 .next()
1903 }
1904
1905 pub fn full(&self) -> String {
1921 self.syntax()
1922 .children_with_tokens()
1923 .filter_map(|it| {
1924 if let Some(token) = it.as_token() {
1925 if token.kind() == TEXT || token.kind() == COMMENT {
1927 return Some(token.text().to_string());
1928 }
1929 }
1930 None
1931 })
1932 .collect::<Vec<_>>()
1933 .join("")
1934 }
1935
1936 pub fn parent(&self) -> Option<Rule> {
1949 self.syntax().parent().and_then(Rule::cast)
1950 }
1951
1952 pub fn is_silent(&self) -> bool {
1965 let text = self.text();
1966 text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
1967 }
1968
1969 pub fn is_ignore_errors(&self) -> bool {
1982 let text = self.text();
1983 text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
1984 }
1985
1986 pub fn set_prefix(&mut self, prefix: &str) {
2003 let text = self.text();
2004
2005 let stripped = text.trim_start_matches(['@', '-', '+']);
2007
2008 let new_text = format!("{}{}", prefix, stripped);
2010
2011 self.replace_text(&new_text);
2012 }
2013
2014 pub fn replace_text(&mut self, new_text: &str) {
2027 let node = self.syntax();
2028 let parent = node.parent().expect("Recipe node must have a parent");
2029 let node_index = node.index();
2030
2031 let mut builder = GreenNodeBuilder::new();
2033 builder.start_node(RECIPE.into());
2034
2035 if let Some(indent_token) = node
2037 .children_with_tokens()
2038 .find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
2039 {
2040 builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
2041 } else {
2042 builder.token(INDENT.into(), "\t");
2043 }
2044
2045 builder.token(TEXT.into(), new_text);
2046
2047 if let Some(newline_token) = node
2049 .children_with_tokens()
2050 .find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
2051 {
2052 builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
2053 } else {
2054 builder.token(NEWLINE.into(), "\n");
2055 }
2056
2057 builder.finish_node();
2058 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2059
2060 parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
2062
2063 *self = parent
2067 .children_with_tokens()
2068 .nth(node_index)
2069 .and_then(|element| element.into_node())
2070 .and_then(Recipe::cast)
2071 .expect("New recipe node should exist at the same index");
2072 }
2073
2074 pub fn insert_before(&self, text: &str) {
2087 let node = self.syntax();
2088 let parent = node.parent().expect("Recipe node must have a parent");
2089 let node_index = node.index();
2090
2091 let mut builder = GreenNodeBuilder::new();
2093 builder.start_node(RECIPE.into());
2094 builder.token(INDENT.into(), "\t");
2095 builder.token(TEXT.into(), text);
2096 builder.token(NEWLINE.into(), "\n");
2097 builder.finish_node();
2098 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2099
2100 parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
2102 }
2103
2104 pub fn insert_after(&self, text: &str) {
2117 let node = self.syntax();
2118 let parent = node.parent().expect("Recipe node must have a parent");
2119 let node_index = node.index();
2120
2121 let mut builder = GreenNodeBuilder::new();
2123 builder.start_node(RECIPE.into());
2124 builder.token(INDENT.into(), "\t");
2125 builder.token(TEXT.into(), text);
2126 builder.token(NEWLINE.into(), "\n");
2127 builder.finish_node();
2128 let new_syntax = SyntaxNode::new_root_mut(builder.finish());
2129
2130 parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
2132 }
2133
2134 pub fn remove(&self) {
2147 let node = self.syntax();
2148 let parent = node.parent().expect("Recipe node must have a parent");
2149 let node_index = node.index();
2150
2151 parent.splice_children(node_index..node_index + 1, vec![]);
2153 }
2154}
2155
2156pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
2160 let mut newlines_to_remove = vec![];
2162 let mut current = node.last_child_or_token();
2163
2164 while let Some(element) = current {
2165 match &element {
2166 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2167 newlines_to_remove.push(token.clone());
2168 current = token.prev_sibling_or_token();
2169 }
2170 rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
2171 let mut recipe_current = n.last_child_or_token();
2173 while let Some(recipe_element) = recipe_current {
2174 match &recipe_element {
2175 rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
2176 newlines_to_remove.push(token.clone());
2177 recipe_current = token.prev_sibling_or_token();
2178 }
2179 _ => break,
2180 }
2181 }
2182 break; }
2184 _ => break,
2185 }
2186 }
2187
2188 if newlines_to_remove.len() > 1 {
2191 newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
2193
2194 for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
2195 let parent = token.parent().unwrap();
2196 let idx = token.index();
2197 parent.splice_children(idx..idx + 1, vec![]);
2198 }
2199 }
2200}
2201
2202pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
2210 let mut collected_elements = vec![];
2211 let mut found_comment = false;
2212
2213 let mut current = node.prev_sibling_or_token();
2215 while let Some(element) = current {
2216 match &element {
2217 rowan::NodeOrToken::Token(token) => match token.kind() {
2218 COMMENT => {
2219 if token.text().starts_with("#!") {
2220 break; }
2222 found_comment = true;
2223 collected_elements.push(element.clone());
2224 }
2225 NEWLINE | WHITESPACE => {
2226 collected_elements.push(element.clone());
2227 }
2228 _ => break, },
2230 rowan::NodeOrToken::Node(n) => {
2231 if n.kind() == BLANK_LINE {
2233 collected_elements.push(element.clone());
2234 } else {
2235 break; }
2237 }
2238 }
2239 current = element.prev_sibling_or_token();
2240 }
2241
2242 let mut elements_to_remove = vec![];
2245 let mut consecutive_newlines = 0;
2246 for element in collected_elements.iter().rev() {
2247 let should_remove = match element {
2248 rowan::NodeOrToken::Token(token) => match token.kind() {
2249 COMMENT => {
2250 consecutive_newlines = 0;
2251 found_comment
2252 }
2253 NEWLINE => {
2254 consecutive_newlines += 1;
2255 found_comment && consecutive_newlines <= 1
2256 }
2257 WHITESPACE => found_comment,
2258 _ => false,
2259 },
2260 rowan::NodeOrToken::Node(n) => {
2261 if n.kind() == BLANK_LINE {
2263 consecutive_newlines += 1;
2264 found_comment && consecutive_newlines <= 1
2265 } else {
2266 false
2267 }
2268 }
2269 };
2270
2271 if should_remove {
2272 elements_to_remove.push(element.clone());
2273 }
2274 }
2275
2276 let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
2279 all_to_remove.extend(elements_to_remove.into_iter().rev());
2280
2281 all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
2283
2284 for element in all_to_remove {
2285 let idx = element.index();
2286 parent.splice_children(idx..idx + 1, vec![]);
2287 }
2288}
2289
2290impl FromStr for Rule {
2291 type Err = crate::Error;
2292
2293 fn from_str(s: &str) -> Result<Self, Self::Err> {
2294 Rule::parse(s).to_rule_result()
2295 }
2296}
2297
2298impl FromStr for Makefile {
2299 type Err = crate::Error;
2300
2301 fn from_str(s: &str) -> Result<Self, Self::Err> {
2302 Makefile::parse(s).to_result()
2303 }
2304}
2305
2306#[cfg(test)]
2307mod tests {
2308 use super::*;
2309 use crate::ast::makefile::MakefileItem;
2310 use crate::pattern::matches_pattern;
2311
2312 #[test]
2313 fn test_conditionals() {
2314 let code = "ifdef DEBUG\n DEBUG_FLAG := 1\nendif\n";
2318 let mut buf = code.as_bytes();
2319 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
2320 assert!(makefile.code().contains("DEBUG_FLAG"));
2321
2322 let code =
2324 "ifeq ($(OS),Windows_NT)\n RESULT := windows\nelse\n RESULT := unix\nendif\n";
2325 let mut buf = code.as_bytes();
2326 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
2327 assert!(makefile.code().contains("RESULT"));
2328 assert!(makefile.code().contains("windows"));
2329
2330 let code = "ifdef DEBUG\n CFLAGS += -g\n ifdef VERBOSE\n CFLAGS += -v\n endif\nelse\n CFLAGS += -O2\nendif\n";
2332 let mut buf = code.as_bytes();
2333 let makefile = Makefile::read_relaxed(&mut buf)
2334 .expect("Failed to parse nested conditionals with else");
2335 assert!(makefile.code().contains("CFLAGS"));
2336 assert!(makefile.code().contains("VERBOSE"));
2337
2338 let code = "ifdef DEBUG\nendif\n";
2340 let mut buf = code.as_bytes();
2341 let makefile =
2342 Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
2343 assert!(makefile.code().contains("ifdef DEBUG"));
2344
2345 let code = "ifeq ($(OS),Windows)\n EXT := .exe\nelse ifeq ($(OS),Linux)\n EXT := .bin\nelse\n EXT := .out\nendif\n";
2347 let mut buf = code.as_bytes();
2348 let makefile =
2349 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
2350 assert!(makefile.code().contains("EXT"));
2351
2352 let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
2354 let mut buf = code.as_bytes();
2355 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
2356 assert!(makefile.code().contains("DEBUG"));
2357
2358 let code = "ifdef \nDEBUG := 1\nendif\n";
2360 let mut buf = code.as_bytes();
2361 let makefile = Makefile::read_relaxed(&mut buf)
2362 .expect("Failed to parse with recovery - missing condition");
2363 assert!(makefile.code().contains("DEBUG"));
2364 }
2365
2366 #[test]
2367 fn test_parse_simple() {
2368 const SIMPLE: &str = r#"VARIABLE = value
2369
2370rule: dependency
2371 command
2372"#;
2373 let parsed = parse(SIMPLE, None);
2374 assert!(parsed.errors.is_empty());
2375 let node = parsed.syntax();
2376 assert_eq!(
2377 format!("{:#?}", node),
2378 r#"ROOT@0..44
2379 VARIABLE@0..17
2380 IDENTIFIER@0..8 "VARIABLE"
2381 WHITESPACE@8..9 " "
2382 OPERATOR@9..10 "="
2383 WHITESPACE@10..11 " "
2384 EXPR@11..16
2385 IDENTIFIER@11..16 "value"
2386 NEWLINE@16..17 "\n"
2387 BLANK_LINE@17..18
2388 NEWLINE@17..18 "\n"
2389 RULE@18..44
2390 TARGETS@18..22
2391 IDENTIFIER@18..22 "rule"
2392 OPERATOR@22..23 ":"
2393 WHITESPACE@23..24 " "
2394 PREREQUISITES@24..34
2395 PREREQUISITE@24..34
2396 IDENTIFIER@24..34 "dependency"
2397 NEWLINE@34..35 "\n"
2398 RECIPE@35..44
2399 INDENT@35..36 "\t"
2400 TEXT@36..43 "command"
2401 NEWLINE@43..44 "\n"
2402"#
2403 );
2404
2405 let root = parsed.root();
2406
2407 let mut rules = root.rules().collect::<Vec<_>>();
2408 assert_eq!(rules.len(), 1);
2409 let rule = rules.pop().unwrap();
2410 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2411 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
2412 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2413
2414 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2415 assert_eq!(variables.len(), 1);
2416 let variable = variables.pop().unwrap();
2417 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2418 assert_eq!(variable.raw_value(), Some("value".to_string()));
2419 }
2420
2421 #[test]
2422 fn test_parse_export_assign() {
2423 const EXPORT: &str = r#"export VARIABLE := value
2424"#;
2425 let parsed = parse(EXPORT, None);
2426 assert!(parsed.errors.is_empty());
2427 let node = parsed.syntax();
2428 assert_eq!(
2429 format!("{:#?}", node),
2430 r#"ROOT@0..25
2431 VARIABLE@0..25
2432 IDENTIFIER@0..6 "export"
2433 WHITESPACE@6..7 " "
2434 IDENTIFIER@7..15 "VARIABLE"
2435 WHITESPACE@15..16 " "
2436 OPERATOR@16..18 ":="
2437 WHITESPACE@18..19 " "
2438 EXPR@19..24
2439 IDENTIFIER@19..24 "value"
2440 NEWLINE@24..25 "\n"
2441"#
2442 );
2443
2444 let root = parsed.root();
2445
2446 let mut variables = root.variable_definitions().collect::<Vec<_>>();
2447 assert_eq!(variables.len(), 1);
2448 let variable = variables.pop().unwrap();
2449 assert_eq!(variable.name(), Some("VARIABLE".to_string()));
2450 assert_eq!(variable.raw_value(), Some("value".to_string()));
2451 }
2452
2453 #[test]
2454 fn test_parse_multiple_prerequisites() {
2455 const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
2456 command
2457
2458"#;
2459 let parsed = parse(MULTIPLE_PREREQUISITES, None);
2460 assert!(parsed.errors.is_empty());
2461 let node = parsed.syntax();
2462 assert_eq!(
2463 format!("{:#?}", node),
2464 r#"ROOT@0..40
2465 RULE@0..40
2466 TARGETS@0..4
2467 IDENTIFIER@0..4 "rule"
2468 OPERATOR@4..5 ":"
2469 WHITESPACE@5..6 " "
2470 PREREQUISITES@6..29
2471 PREREQUISITE@6..17
2472 IDENTIFIER@6..17 "dependency1"
2473 WHITESPACE@17..18 " "
2474 PREREQUISITE@18..29
2475 IDENTIFIER@18..29 "dependency2"
2476 NEWLINE@29..30 "\n"
2477 RECIPE@30..39
2478 INDENT@30..31 "\t"
2479 TEXT@31..38 "command"
2480 NEWLINE@38..39 "\n"
2481 NEWLINE@39..40 "\n"
2482"#
2483 );
2484 let root = parsed.root();
2485
2486 let rule = root.rules().next().unwrap();
2487 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2488 assert_eq!(
2489 rule.prerequisites().collect::<Vec<_>>(),
2490 vec!["dependency1", "dependency2"]
2491 );
2492 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2493 }
2494
2495 #[test]
2496 fn test_add_rule() {
2497 let mut makefile = Makefile::new();
2498 let rule = makefile.add_rule("rule");
2499 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2500 assert_eq!(
2501 rule.prerequisites().collect::<Vec<_>>(),
2502 Vec::<String>::new()
2503 );
2504
2505 assert_eq!(makefile.to_string(), "rule:\n");
2506 }
2507
2508 #[test]
2509 fn test_add_rule_with_shebang() {
2510 let content = r#"#!/usr/bin/make -f
2512
2513build: blah
2514 $(MAKE) install
2515
2516clean:
2517 dh_clean
2518"#;
2519
2520 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2521 let initial_count = makefile.rules().count();
2522 assert_eq!(initial_count, 2);
2523
2524 let rule = makefile.add_rule("build-indep");
2526 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
2527
2528 assert_eq!(makefile.rules().count(), initial_count + 1);
2530 }
2531
2532 #[test]
2533 fn test_add_rule_formatting() {
2534 let content = r#"build: blah
2536 $(MAKE) install
2537
2538clean:
2539 dh_clean
2540"#;
2541
2542 let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
2543 let mut rule = makefile.add_rule("build-indep");
2544 rule.add_prerequisite("build").unwrap();
2545
2546 let expected = r#"build: blah
2547 $(MAKE) install
2548
2549clean:
2550 dh_clean
2551
2552build-indep: build
2553"#;
2554
2555 assert_eq!(makefile.to_string(), expected);
2556 }
2557
2558 #[test]
2559 fn test_push_command() {
2560 let mut makefile = Makefile::new();
2561 let mut rule = makefile.add_rule("rule");
2562
2563 rule.push_command("command");
2565 rule.push_command("command2");
2566
2567 assert_eq!(
2569 rule.recipes().collect::<Vec<_>>(),
2570 vec!["command", "command2"]
2571 );
2572
2573 rule.push_command("command3");
2575 assert_eq!(
2576 rule.recipes().collect::<Vec<_>>(),
2577 vec!["command", "command2", "command3"]
2578 );
2579
2580 assert_eq!(
2582 makefile.to_string(),
2583 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2584 );
2585
2586 assert_eq!(
2588 rule.to_string(),
2589 "rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
2590 );
2591 }
2592
2593 #[test]
2594 fn test_replace_command() {
2595 let mut makefile = Makefile::new();
2596 let mut rule = makefile.add_rule("rule");
2597
2598 rule.push_command("command");
2600 rule.push_command("command2");
2601
2602 assert_eq!(
2604 rule.recipes().collect::<Vec<_>>(),
2605 vec!["command", "command2"]
2606 );
2607
2608 rule.replace_command(0, "new command");
2610 assert_eq!(
2611 rule.recipes().collect::<Vec<_>>(),
2612 vec!["new command", "command2"]
2613 );
2614
2615 assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2617
2618 assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
2620 }
2621
2622 #[test]
2623 fn test_replace_command_with_comments() {
2624 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";
2627
2628 let makefile = Makefile::read_relaxed(&content[..]).unwrap();
2629
2630 let mut rule = makefile.rules().next().unwrap();
2631
2632 assert_eq!(rule.recipe_nodes().count(), 2);
2634 let recipes: Vec<_> = rule.recipe_nodes().collect();
2635 assert_eq!(recipes[0].text(), ""); assert_eq!(
2637 recipes[1].text(),
2638 "dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"
2639 );
2640
2641 assert!(rule.replace_command(1, "dh_strip"));
2643
2644 assert_eq!(rule.recipe_nodes().count(), 2);
2646 let recipes: Vec<_> = rule.recipe_nodes().collect();
2647 assert_eq!(recipes[0].text(), ""); assert_eq!(recipes[1].text(), "dh_strip");
2649 }
2650
2651 #[test]
2652 fn test_parse_rule_without_newline() {
2653 let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
2654 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2655 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
2656 let rule = "rule: dependency".parse::<Rule>().unwrap();
2657 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
2658 assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
2659 }
2660
2661 #[test]
2662 fn test_parse_makefile_without_newline() {
2663 let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
2664 assert_eq!(makefile.rules().count(), 1);
2665 }
2666
2667 #[test]
2668 fn test_from_reader() {
2669 let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
2670 assert_eq!(makefile.rules().count(), 1);
2671 }
2672
2673 #[test]
2674 fn test_parse_with_tab_after_last_newline() {
2675 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
2676 assert_eq!(makefile.rules().count(), 1);
2677 }
2678
2679 #[test]
2680 fn test_parse_with_space_after_last_newline() {
2681 let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
2682 assert_eq!(makefile.rules().count(), 1);
2683 }
2684
2685 #[test]
2686 fn test_parse_with_comment_after_last_newline() {
2687 let makefile =
2688 Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
2689 assert_eq!(makefile.rules().count(), 1);
2690 }
2691
2692 #[test]
2693 fn test_parse_with_variable_rule() {
2694 let makefile =
2695 Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
2696 .unwrap();
2697
2698 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2700 assert_eq!(vars.len(), 1);
2701 assert_eq!(vars[0].name(), Some("RULE".to_string()));
2702 assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
2703
2704 let rules = makefile.rules().collect::<Vec<_>>();
2706 assert_eq!(rules.len(), 1);
2707 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
2708 assert_eq!(
2709 rules[0].prerequisites().collect::<Vec<_>>(),
2710 vec!["dependency"]
2711 );
2712 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2713 }
2714
2715 #[test]
2716 fn test_parse_with_variable_dependency() {
2717 let makefile =
2718 Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
2719
2720 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2722 assert_eq!(vars.len(), 1);
2723 assert_eq!(vars[0].name(), Some("DEP".to_string()));
2724 assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
2725
2726 let rules = makefile.rules().collect::<Vec<_>>();
2728 assert_eq!(rules.len(), 1);
2729 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2730 assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
2731 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
2732 }
2733
2734 #[test]
2735 fn test_parse_with_variable_command() {
2736 let makefile =
2737 Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
2738
2739 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2741 assert_eq!(vars.len(), 1);
2742 assert_eq!(vars[0].name(), Some("COM".to_string()));
2743 assert_eq!(vars[0].raw_value(), Some("command".to_string()));
2744
2745 let rules = makefile.rules().collect::<Vec<_>>();
2747 assert_eq!(rules.len(), 1);
2748 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
2749 assert_eq!(
2750 rules[0].prerequisites().collect::<Vec<_>>(),
2751 vec!["dependency"]
2752 );
2753 assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
2754 }
2755
2756 #[test]
2757 fn test_regular_line_error_reporting() {
2758 let input = "rule target\n\tcommand";
2759
2760 let parsed = parse(input, None);
2762 let direct_error = &parsed.errors[0];
2763
2764 assert_eq!(direct_error.line, 2);
2766 assert!(
2767 direct_error.message.contains("expected"),
2768 "Error message should contain 'expected': {}",
2769 direct_error.message
2770 );
2771 assert_eq!(direct_error.context, "\tcommand");
2772
2773 let reader_result = Makefile::from_reader(input.as_bytes());
2775 let parse_error = match reader_result {
2776 Ok(_) => panic!("Expected Parse error from from_reader"),
2777 Err(err) => match err {
2778 self::Error::Parse(parse_err) => parse_err,
2779 _ => panic!("Expected Parse error"),
2780 },
2781 };
2782
2783 let error_text = parse_error.to_string();
2785 assert!(error_text.contains("Error at line 2:"));
2786 assert!(error_text.contains("2| \tcommand"));
2787 }
2788
2789 #[test]
2790 fn test_parsing_error_context_with_bad_syntax() {
2791 let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
2793
2794 match Makefile::from_reader(input.as_bytes()) {
2796 Ok(makefile) => {
2797 assert_eq!(
2799 makefile.rules().count(),
2800 0,
2801 "Should not have found any rules"
2802 );
2803 }
2804 Err(err) => match err {
2805 self::Error::Parse(error) => {
2806 assert!(error.errors[0].line >= 2, "Error line should be at least 2");
2808 assert!(
2809 !error.errors[0].context.is_empty(),
2810 "Error context should not be empty"
2811 );
2812 }
2813 _ => panic!("Unexpected error type"),
2814 },
2815 };
2816 }
2817
2818 #[test]
2819 fn test_error_message_format() {
2820 let parse_error = ParseError {
2822 errors: vec![ErrorInfo {
2823 message: "test error".to_string(),
2824 line: 42,
2825 context: "some problematic code".to_string(),
2826 }],
2827 };
2828
2829 let error_text = parse_error.to_string();
2830 assert!(error_text.contains("Error at line 42: test error"));
2831 assert!(error_text.contains("42| some problematic code"));
2832 }
2833
2834 #[test]
2835 fn test_line_number_calculation() {
2836 let test_cases = [
2838 ("rule dependency\n\tcommand", 2), ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2), ("var = value\n#comment\n\tindented line", 3), ];
2842
2843 for (input, expected_line) in test_cases {
2844 match input.parse::<Makefile>() {
2846 Ok(_) => {
2847 continue;
2850 }
2851 Err(err) => {
2852 if let Error::Parse(parse_err) = err {
2853 assert_eq!(
2855 parse_err.errors[0].line, expected_line,
2856 "Line number should match the expected line"
2857 );
2858
2859 if parse_err.errors[0].message.contains("indented") {
2861 assert!(
2862 parse_err.errors[0].context.starts_with('\t'),
2863 "Context for indentation errors should include the tab character"
2864 );
2865 }
2866 } else {
2867 panic!("Expected parse error, got: {:?}", err);
2868 }
2869 }
2870 }
2871 }
2872 }
2873
2874 #[test]
2875 fn test_conditional_features() {
2876 let code = r#"
2878# Set variables based on DEBUG flag
2879ifdef DEBUG
2880 CFLAGS += -g -DDEBUG
2881else
2882 CFLAGS = -O2
2883endif
2884
2885# Define a build rule
2886all: $(OBJS)
2887 $(CC) $(CFLAGS) -o $@ $^
2888"#;
2889
2890 let mut buf = code.as_bytes();
2891 let makefile =
2892 Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
2893
2894 assert!(!makefile.code().is_empty(), "Makefile has content");
2897
2898 let rules = makefile.rules().collect::<Vec<_>>();
2900 assert!(!rules.is_empty(), "Should have found rules");
2901
2902 assert!(code.contains("ifdef DEBUG"));
2904 assert!(code.contains("endif"));
2905
2906 let code_with_var = r#"
2908# Define a variable first
2909CC = gcc
2910
2911ifdef DEBUG
2912 CFLAGS += -g -DDEBUG
2913else
2914 CFLAGS = -O2
2915endif
2916
2917all: $(OBJS)
2918 $(CC) $(CFLAGS) -o $@ $^
2919"#;
2920
2921 let mut buf = code_with_var.as_bytes();
2922 let makefile =
2923 Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
2924
2925 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2927 assert!(
2928 !vars.is_empty(),
2929 "Should have found at least the CC variable definition"
2930 );
2931 }
2932
2933 #[test]
2934 fn test_include_directive() {
2935 let parsed = parse(
2936 "include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
2937 None,
2938 );
2939 assert!(parsed.errors.is_empty());
2940 let node = parsed.syntax();
2941 assert!(format!("{:#?}", node).contains("INCLUDE@"));
2942 }
2943
2944 #[test]
2945 fn test_export_variables() {
2946 let parsed = parse("export SHELL := /bin/bash\n", None);
2947 assert!(parsed.errors.is_empty());
2948 let makefile = parsed.root();
2949 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2950 assert_eq!(vars.len(), 1);
2951 let shell_var = vars
2952 .iter()
2953 .find(|v| v.name() == Some("SHELL".to_string()))
2954 .unwrap();
2955 assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
2956 }
2957
2958 #[test]
2959 fn test_bare_export_variable() {
2960 let parsed = parse(
2963 "DEB_CFLAGS_MAINT_APPEND = -Wno-error\nexport DEB_CFLAGS_MAINT_APPEND\n\n%:\n\tdh $@\n",
2964 None,
2965 );
2966 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2967 let makefile = parsed.root();
2968 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2970 assert_eq!(vars.len(), 2);
2971 let rules = makefile.rules().collect::<Vec<_>>();
2973 assert_eq!(rules.len(), 1);
2974 assert!(rules[0].targets().any(|t| t == "%"));
2975 assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
2977 }
2978
2979 #[test]
2980 fn test_bare_export_at_eof() {
2981 let parsed = parse("VAR = value\nexport VAR", None);
2983 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2984 let makefile = parsed.root();
2985 let vars = makefile.variable_definitions().collect::<Vec<_>>();
2986 assert_eq!(vars.len(), 2);
2987 assert_eq!(makefile.rules().count(), 0);
2988 }
2989
2990 #[test]
2991 fn test_bare_export_does_not_eat_include() {
2992 let parsed = parse("VAR = value\nexport VAR\ninclude other.mk\n", None);
2994 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
2995 let makefile = parsed.root();
2996 assert_eq!(makefile.includes().count(), 1);
2997 assert_eq!(
2998 makefile.included_files().collect::<Vec<_>>(),
2999 vec!["other.mk"]
3000 );
3001 }
3002
3003 #[test]
3004 fn test_bare_export_multiple() {
3005 let parsed = parse(
3007 "A = 1\nB = 2\nexport A\nexport B\n\nall:\n\techo done\n",
3008 None,
3009 );
3010 assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
3011 let makefile = parsed.root();
3012 assert_eq!(makefile.variable_definitions().count(), 4);
3013 let rules = makefile.rules().collect::<Vec<_>>();
3014 assert_eq!(rules.len(), 1);
3015 assert!(rules[0].targets().any(|t| t == "all"));
3016 }
3017
3018 #[test]
3019 fn test_parse_error_does_not_cross_lines() {
3020 let parsed = parse("notarule\n\nbuild-arch:\n\techo arch\n", None);
3023 let makefile = parsed.root();
3024 let rules = makefile.rules().collect::<Vec<_>>();
3025 assert!(
3027 rules.iter().any(|r| r.targets().any(|t| t == "build-arch")),
3028 "build-arch rule should be parsed despite earlier error; rules: {:?}",
3029 rules
3030 .iter()
3031 .map(|r| r.targets().collect::<Vec<_>>())
3032 .collect::<Vec<_>>()
3033 );
3034 }
3035
3036 #[test]
3037 fn test_pyfai_rules_full() {
3038 let input = "\
3040#!/usr/bin/make -f
3041
3042export DH_VERBOSE=1
3043export PYBUILD_NAME=pyfai
3044
3045DEB_CFLAGS_MAINT_APPEND = -Wno-error=incompatible-pointer-types
3046export DEB_CFLAGS_MAINT_APPEND
3047
3048PY3VER := $(shell py3versions -dv)
3049
3050include /usr/share/dpkg/pkg-info.mk # sets SOURCE_DATE_EPOCH
3051
3052%:
3053\tdh $@ --buildsystem=pybuild
3054
3055override_dh_auto_build-arch:
3056\tPYBUILD_BUILD_ARGS=\"-Ccompile-args=--verbose\" dh_auto_build
3057
3058override_dh_auto_build-indep: override_dh_auto_build-arch
3059\tsphinx-build -N -bhtml doc/source build/html
3060
3061override_dh_auto_test:
3062
3063execute_after_dh_auto_install:
3064\tdh_install -p pyfai debian/python3-pyfai/usr/bin /usr
3065";
3066 let parsed = parse(input, None);
3067 let makefile = parsed.root();
3068
3069 assert_eq!(makefile.includes().count(), 1);
3071
3072 assert!(
3074 makefile.find_rule_by_target_pattern("build-arch").is_some(),
3075 "build-arch should match via %: pattern rule"
3076 );
3077 assert!(
3078 makefile
3079 .find_rule_by_target_pattern("build-indep")
3080 .is_some(),
3081 "build-indep should match via %: pattern rule"
3082 );
3083
3084 let rule_targets: Vec<Vec<String>> =
3086 makefile.rules().map(|r| r.targets().collect()).collect();
3087 assert!(
3088 rule_targets.iter().any(|t| t.contains(&"%".to_string())),
3089 "missing %: rule; got: {:?}",
3090 rule_targets
3091 );
3092 assert!(
3093 rule_targets
3094 .iter()
3095 .any(|t| t.contains(&"override_dh_auto_build-arch".to_string())),
3096 "missing override_dh_auto_build-arch; got: {:?}",
3097 rule_targets
3098 );
3099 assert!(
3100 rule_targets
3101 .iter()
3102 .any(|t| t.contains(&"override_dh_auto_test".to_string())),
3103 "missing override_dh_auto_test; got: {:?}",
3104 rule_targets
3105 );
3106 assert!(
3107 rule_targets
3108 .iter()
3109 .any(|t| t.contains(&"execute_after_dh_auto_install".to_string())),
3110 "missing execute_after_dh_auto_install; got: {:?}",
3111 rule_targets
3112 );
3113 }
3114
3115 #[test]
3116 fn test_variable_scopes() {
3117 let parsed = parse(
3118 "SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
3119 None,
3120 );
3121 assert!(parsed.errors.is_empty());
3122 let makefile = parsed.root();
3123 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3124 assert_eq!(vars.len(), 4);
3125 let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
3126 assert!(var_names.contains(&"SIMPLE".to_string()));
3127 assert!(var_names.contains(&"IMMEDIATE".to_string()));
3128 assert!(var_names.contains(&"CONDITIONAL".to_string()));
3129 assert!(var_names.contains(&"APPEND".to_string()));
3130 }
3131
3132 #[test]
3133 fn test_pattern_rule_parsing() {
3134 let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
3135 assert!(parsed.errors.is_empty());
3136 let makefile = parsed.root();
3137 let rules = makefile.rules().collect::<Vec<_>>();
3138 assert_eq!(rules.len(), 1);
3139 assert_eq!(rules[0].targets().next().unwrap(), "%.o");
3140 assert!(rules[0].recipes().next().unwrap().contains("$@"));
3141 }
3142
3143 #[test]
3144 fn test_include_variants() {
3145 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
3147 let parsed = parse(makefile_str, None);
3148 assert!(parsed.errors.is_empty());
3149
3150 let node = parsed.syntax();
3152 let debug_str = format!("{:#?}", node);
3153
3154 assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
3156
3157 let makefile = parsed.root();
3159
3160 let include_count = makefile
3162 .syntax()
3163 .children()
3164 .filter(|child| child.kind() == INCLUDE)
3165 .count();
3166 assert_eq!(include_count, 4);
3167
3168 assert!(makefile
3170 .included_files()
3171 .any(|path| path.contains("$(VAR)")));
3172 }
3173
3174 #[test]
3175 fn test_include_api() {
3176 let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
3178 let makefile: Makefile = makefile_str.parse().unwrap();
3179
3180 let includes: Vec<_> = makefile.includes().collect();
3182 assert_eq!(includes.len(), 3);
3183
3184 assert!(!includes[0].is_optional()); assert!(includes[1].is_optional()); assert!(includes[2].is_optional()); let files: Vec<_> = makefile.included_files().collect();
3191 assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
3192
3193 assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
3195 assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
3196 assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
3197 }
3198
3199 #[test]
3200 fn test_include_integration() {
3201 let phony_makefile = Makefile::from_reader(
3205 ".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3206 .as_bytes()
3207 ).unwrap();
3208
3209 assert_eq!(phony_makefile.rules().count(), 2);
3211
3212 let normal_rules_count = phony_makefile
3214 .rules()
3215 .filter(|r| !r.targets().any(|t| t.starts_with('.')))
3216 .count();
3217 assert_eq!(normal_rules_count, 1);
3218
3219 assert_eq!(phony_makefile.includes().count(), 1);
3221 assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
3222
3223 let simple_makefile = Makefile::from_reader(
3225 "\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
3226 .as_bytes(),
3227 )
3228 .unwrap();
3229 assert_eq!(simple_makefile.rules().count(), 1);
3230 assert_eq!(simple_makefile.includes().count(), 1);
3231 }
3232
3233 #[test]
3234 fn test_real_conditional_directives() {
3235 let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
3237 let mut buf = conditional.as_bytes();
3238 let makefile =
3239 Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
3240 let code = makefile.code();
3241 assert!(code.contains("ifdef DEBUG"));
3242 assert!(code.contains("else"));
3243 assert!(code.contains("endif"));
3244
3245 let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
3247 let mut buf = nested.as_bytes();
3248 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
3249 let code = makefile.code();
3250 assert!(code.contains("ifdef DEBUG"));
3251 assert!(code.contains("ifdef VERBOSE"));
3252
3253 let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
3255 let mut buf = ifeq.as_bytes();
3256 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
3257 let code = makefile.code();
3258 assert!(code.contains("ifeq"));
3259 assert!(code.contains("Windows_NT"));
3260 }
3261
3262 #[test]
3263 fn test_indented_text_outside_rules() {
3264 let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \" help show help\"\n";
3266 let parsed = parse(help_text, None);
3267 assert!(parsed.errors.is_empty());
3268
3269 let root = parsed.root();
3271 let rules = root.rules().collect::<Vec<_>>();
3272 assert_eq!(rules.len(), 1);
3273
3274 let help_rule = &rules[0];
3275 let recipes = help_rule.recipes().collect::<Vec<_>>();
3276 assert_eq!(recipes.len(), 2);
3277 assert!(recipes[0].contains("Available targets"));
3278 assert!(recipes[1].contains("help"));
3279 }
3280
3281 #[test]
3282 fn test_comment_handling_in_recipes() {
3283 let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
3285
3286 let parsed = parse(recipe_comment, None);
3288
3289 assert!(
3291 parsed.errors.is_empty(),
3292 "Should parse recipe with comments without errors"
3293 );
3294
3295 let root = parsed.root();
3297 let rules = root.rules().collect::<Vec<_>>();
3298 assert_eq!(rules.len(), 1, "Should find exactly one rule");
3299
3300 let build_rule = &rules[0];
3302 assert_eq!(
3303 build_rule.targets().collect::<Vec<_>>(),
3304 vec!["build"],
3305 "Rule should have 'build' as target"
3306 );
3307
3308 let recipes = build_rule.recipe_nodes().collect::<Vec<_>>();
3311 assert_eq!(recipes.len(), 2, "Should find two recipe nodes");
3312
3313 assert_eq!(recipes[0].text(), "");
3315 assert_eq!(
3316 recipes[0].comment(),
3317 Some("# This is a comment".to_string())
3318 );
3319
3320 assert_eq!(recipes[1].text(), "gcc -o app main.c");
3322 assert_eq!(recipes[1].comment(), None);
3323 }
3324
3325 #[test]
3326 fn test_multiline_variables() {
3327 let multiline = "SOURCES = main.c \\\n util.c\n";
3329
3330 let parsed = parse(multiline, None);
3332
3333 let root = parsed.root();
3335 let vars = root.variable_definitions().collect::<Vec<_>>();
3336 assert!(!vars.is_empty(), "Should find at least one variable");
3337
3338 let operators = "CFLAGS := -Wall \\\n -Werror\n";
3342 let parsed_operators = parse(operators, None);
3343
3344 let root = parsed_operators.root();
3346 let vars = root.variable_definitions().collect::<Vec<_>>();
3347 assert!(
3348 !vars.is_empty(),
3349 "Should find at least one variable with := operator"
3350 );
3351
3352 let append = "LDFLAGS += -L/usr/lib \\\n -lm\n";
3354 let parsed_append = parse(append, None);
3355
3356 let root = parsed_append.root();
3358 let vars = root.variable_definitions().collect::<Vec<_>>();
3359 assert!(
3360 !vars.is_empty(),
3361 "Should find at least one variable with += operator"
3362 );
3363 }
3364
3365 #[test]
3366 fn test_whitespace_and_eof_handling() {
3367 let blank_lines = "VAR = value\n\n\n";
3369
3370 let parsed_blank = parse(blank_lines, None);
3371
3372 let root = parsed_blank.root();
3374 let vars = root.variable_definitions().collect::<Vec<_>>();
3375 assert_eq!(
3376 vars.len(),
3377 1,
3378 "Should find one variable in blank lines test"
3379 );
3380
3381 let trailing_space = "VAR = value \n";
3383
3384 let parsed_space = parse(trailing_space, None);
3385
3386 let root = parsed_space.root();
3388 let vars = root.variable_definitions().collect::<Vec<_>>();
3389 assert_eq!(
3390 vars.len(),
3391 1,
3392 "Should find one variable in trailing space test"
3393 );
3394
3395 let no_newline = "VAR = value";
3397
3398 let parsed_no_newline = parse(no_newline, None);
3399
3400 let root = parsed_no_newline.root();
3402 let vars = root.variable_definitions().collect::<Vec<_>>();
3403 assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
3404 assert_eq!(
3405 vars[0].name(),
3406 Some("VAR".to_string()),
3407 "Variable name should be VAR"
3408 );
3409 }
3410
3411 #[test]
3412 fn test_complex_variable_references() {
3413 let wildcard = "SOURCES = $(wildcard *.c)\n";
3415 let parsed = parse(wildcard, None);
3416 assert!(parsed.errors.is_empty());
3417
3418 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3420 let parsed = parse(nested, None);
3421 assert!(parsed.errors.is_empty());
3422
3423 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3425 let parsed = parse(patsubst, None);
3426 assert!(parsed.errors.is_empty());
3427 }
3428
3429 #[test]
3430 fn test_complex_variable_references_minimal() {
3431 let wildcard = "SOURCES = $(wildcard *.c)\n";
3433 let parsed = parse(wildcard, None);
3434 assert!(parsed.errors.is_empty());
3435
3436 let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
3438 let parsed = parse(nested, None);
3439 assert!(parsed.errors.is_empty());
3440
3441 let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
3443 let parsed = parse(patsubst, None);
3444 assert!(parsed.errors.is_empty());
3445 }
3446
3447 #[test]
3448 fn test_multiline_variable_with_backslash() {
3449 let content = r#"
3450LONG_VAR = This is a long variable \
3451 that continues on the next line \
3452 and even one more line
3453"#;
3454
3455 let mut buf = content.as_bytes();
3457 let makefile =
3458 Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
3459
3460 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3462 assert_eq!(
3463 vars.len(),
3464 1,
3465 "Expected 1 variable but found {}",
3466 vars.len()
3467 );
3468 let var_value = vars[0].raw_value();
3469 assert!(var_value.is_some(), "Variable value is None");
3470
3471 let value_str = var_value.unwrap();
3473 assert!(
3474 value_str.contains("long variable"),
3475 "Value doesn't contain expected content"
3476 );
3477 }
3478
3479 #[test]
3480 fn test_multiline_variable_with_mixed_operators() {
3481 let content = r#"
3482PREFIX ?= /usr/local
3483CFLAGS := -Wall -O2 \
3484 -I$(PREFIX)/include \
3485 -DDEBUG
3486"#;
3487 let mut buf = content.as_bytes();
3489 let makefile = Makefile::read_relaxed(&mut buf)
3490 .expect("Failed to parse multiline variable with operators");
3491
3492 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3494 assert!(
3495 !vars.is_empty(),
3496 "Expected at least 1 variable, found {}",
3497 vars.len()
3498 );
3499
3500 let prefix_var = vars
3502 .iter()
3503 .find(|v| v.name().unwrap_or_default() == "PREFIX");
3504 assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
3505 assert!(
3506 prefix_var.unwrap().raw_value().is_some(),
3507 "PREFIX variable has no value"
3508 );
3509
3510 let cflags_var = vars
3512 .iter()
3513 .find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
3514 assert!(
3515 cflags_var.is_some(),
3516 "Expected to find CFLAGS variable (or part of it)"
3517 );
3518 }
3519
3520 #[test]
3521 fn test_indented_help_text() {
3522 let content = r#"
3523.PHONY: help
3524help:
3525 @echo "Available targets:"
3526 @echo " build - Build the project"
3527 @echo " test - Run tests"
3528 @echo " clean - Remove build artifacts"
3529"#;
3530 let mut buf = content.as_bytes();
3532 let makefile =
3533 Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
3534
3535 let rules = makefile.rules().collect::<Vec<_>>();
3537 assert!(!rules.is_empty(), "Expected at least one rule");
3538
3539 let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
3541 assert!(help_rule.is_some(), "Expected to find help rule");
3542
3543 let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
3545 assert!(
3546 !recipes.is_empty(),
3547 "Expected at least one recipe line in help rule"
3548 );
3549 assert!(
3550 recipes.iter().any(|r| r.contains("Available targets")),
3551 "Expected to find 'Available targets' in recipes"
3552 );
3553 }
3554
3555 #[test]
3556 fn test_indented_lines_in_conditionals() {
3557 let content = r#"
3558ifdef DEBUG
3559 CFLAGS += -g -DDEBUG
3560 # This is a comment inside conditional
3561 ifdef VERBOSE
3562 CFLAGS += -v
3563 endif
3564endif
3565"#;
3566 let mut buf = content.as_bytes();
3568 let makefile = Makefile::read_relaxed(&mut buf)
3569 .expect("Failed to parse indented lines in conditionals");
3570
3571 let code = makefile.code();
3573 assert!(code.contains("ifdef DEBUG"));
3574 assert!(code.contains("ifdef VERBOSE"));
3575 assert!(code.contains("endif"));
3576 }
3577
3578 #[test]
3579 fn test_recipe_with_colon() {
3580 let content = r#"
3581build:
3582 @echo "Building at: $(shell date)"
3583 gcc -o program main.c
3584"#;
3585 let parsed = parse(content, None);
3586 assert!(
3587 parsed.errors.is_empty(),
3588 "Failed to parse recipe with colon: {:?}",
3589 parsed.errors
3590 );
3591 }
3592
3593 #[test]
3594 fn test_double_colon_rules() {
3595 let content = r#"
3596%.o :: %.c
3597 $(CC) -c $< -o $@
3598
3599# Double colon allows multiple rules for same target
3600all:: prerequisite1
3601 @echo "First rule for all"
3602
3603all:: prerequisite2
3604 @echo "Second rule for all"
3605"#;
3606 let parsed = parse(content, None);
3607 assert!(
3608 parsed.errors.is_empty(),
3609 "Failed to parse double colon rules: {:?}",
3610 parsed.errors
3611 );
3612
3613 let makefile = parsed.root();
3614 let rules: Vec<_> = makefile.rules().collect();
3615 assert_eq!(rules.len(), 3);
3616
3617 for rule in &rules {
3619 assert!(rule.is_double_colon());
3620 }
3621
3622 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["%.o"]);
3624 assert_eq!(rules[1].targets().collect::<Vec<_>>(), vec!["all"]);
3625 assert_eq!(rules[2].targets().collect::<Vec<_>>(), vec!["all"]);
3626
3627 assert_eq!(
3629 rules[1].prerequisites().collect::<Vec<_>>(),
3630 vec!["prerequisite1"]
3631 );
3632 assert_eq!(
3633 rules[2].prerequisites().collect::<Vec<_>>(),
3634 vec!["prerequisite2"]
3635 );
3636 }
3637
3638 #[test]
3639 fn test_else_conditional_directives() {
3640 let content = r#"
3642ifeq ($(OS),Windows_NT)
3643 TARGET = windows
3644else ifeq ($(OS),Darwin)
3645 TARGET = macos
3646else ifeq ($(OS),Linux)
3647 TARGET = linux
3648else
3649 TARGET = unknown
3650endif
3651"#;
3652 let mut buf = content.as_bytes();
3653 let makefile =
3654 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
3655 assert!(makefile.code().contains("else ifeq"));
3656 assert!(makefile.code().contains("TARGET"));
3657
3658 let content = r#"
3660ifdef WINDOWS
3661 TARGET = windows
3662else ifdef DARWIN
3663 TARGET = macos
3664else ifdef LINUX
3665 TARGET = linux
3666else
3667 TARGET = unknown
3668endif
3669"#;
3670 let mut buf = content.as_bytes();
3671 let makefile =
3672 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
3673 assert!(makefile.code().contains("else ifdef"));
3674
3675 let content = r#"
3677ifndef NOWINDOWS
3678 TARGET = windows
3679else ifndef NODARWIN
3680 TARGET = macos
3681else
3682 TARGET = linux
3683endif
3684"#;
3685 let mut buf = content.as_bytes();
3686 let makefile =
3687 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
3688 assert!(makefile.code().contains("else ifndef"));
3689
3690 let content = r#"
3692ifneq ($(OS),Windows_NT)
3693 TARGET = not_windows
3694else ifneq ($(OS),Darwin)
3695 TARGET = not_macos
3696else
3697 TARGET = darwin
3698endif
3699"#;
3700 let mut buf = content.as_bytes();
3701 let makefile =
3702 Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
3703 assert!(makefile.code().contains("else ifneq"));
3704 }
3705
3706 #[test]
3707 fn test_complex_else_conditionals() {
3708 let content = r#"VAR1 := foo
3710VAR2 := bar
3711
3712ifeq ($(VAR1),foo)
3713 RESULT := foo_matched
3714else ifdef VAR2
3715 RESULT := var2_defined
3716else ifndef VAR3
3717 RESULT := var3_not_defined
3718else
3719 RESULT := final_else
3720endif
3721
3722all:
3723 @echo $(RESULT)
3724"#;
3725 let mut buf = content.as_bytes();
3726 let makefile =
3727 Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
3728
3729 let code = makefile.code();
3731 assert!(code.contains("ifeq ($(VAR1),foo)"));
3732 assert!(code.contains("else ifdef VAR2"));
3733 assert!(code.contains("else ifndef VAR3"));
3734 assert!(code.contains("else"));
3735 assert!(code.contains("endif"));
3736 assert!(code.contains("RESULT"));
3737
3738 let rules: Vec<_> = makefile.rules().collect();
3740 assert_eq!(rules.len(), 1);
3741 assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
3742 }
3743
3744 #[test]
3745 fn test_conditional_token_structure() {
3746 let content = r#"ifdef VAR1
3748X := 1
3749else ifdef VAR2
3750X := 2
3751else
3752X := 3
3753endif
3754"#;
3755 let mut buf = content.as_bytes();
3756 let makefile = Makefile::read_relaxed(&mut buf).unwrap();
3757
3758 let syntax = makefile.syntax();
3760
3761 let mut found_conditional = false;
3763 let mut found_conditional_if = false;
3764 let mut found_conditional_else = false;
3765 let mut found_conditional_endif = false;
3766
3767 fn check_node(
3768 node: &SyntaxNode,
3769 found_cond: &mut bool,
3770 found_if: &mut bool,
3771 found_else: &mut bool,
3772 found_endif: &mut bool,
3773 ) {
3774 match node.kind() {
3775 SyntaxKind::CONDITIONAL => *found_cond = true,
3776 SyntaxKind::CONDITIONAL_IF => *found_if = true,
3777 SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
3778 SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
3779 _ => {}
3780 }
3781
3782 for child in node.children() {
3783 check_node(&child, found_cond, found_if, found_else, found_endif);
3784 }
3785 }
3786
3787 check_node(
3788 syntax,
3789 &mut found_conditional,
3790 &mut found_conditional_if,
3791 &mut found_conditional_else,
3792 &mut found_conditional_endif,
3793 );
3794
3795 assert!(found_conditional, "Should have CONDITIONAL node");
3796 assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
3797 assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
3798 assert!(
3799 found_conditional_endif,
3800 "Should have CONDITIONAL_ENDIF node"
3801 );
3802 }
3803
3804 #[test]
3805 fn test_ambiguous_assignment_vs_rule() {
3806 const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
3808
3809 let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
3810 let makefile =
3811 Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
3812
3813 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3814 let rules = makefile.rules().collect::<Vec<_>>();
3815
3816 assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
3817 assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
3818
3819 assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
3820
3821 const SIMPLE_RULE: &str = "target: dependency\n";
3823
3824 let mut buf = std::io::Cursor::new(SIMPLE_RULE);
3825 let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
3826
3827 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3828 let rules = makefile.rules().collect::<Vec<_>>();
3829
3830 assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
3831 assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
3832
3833 let rule = &rules[0];
3834 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
3835 }
3836
3837 #[test]
3838 fn test_nested_conditionals() {
3839 let content = r#"
3840ifdef RELEASE
3841 CFLAGS += -O3
3842 ifndef DEBUG
3843 ifneq ($(ARCH),arm)
3844 CFLAGS += -march=native
3845 else
3846 CFLAGS += -mcpu=cortex-a72
3847 endif
3848 endif
3849endif
3850"#;
3851 let mut buf = content.as_bytes();
3853 let makefile =
3854 Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
3855
3856 let code = makefile.code();
3858 assert!(code.contains("ifdef RELEASE"));
3859 assert!(code.contains("ifndef DEBUG"));
3860 assert!(code.contains("ifneq"));
3861 }
3862
3863 #[test]
3864 fn test_space_indented_recipes() {
3865 let content = r#"
3868build:
3869 @echo "Building with spaces instead of tabs"
3870 gcc -o program main.c
3871"#;
3872 let mut buf = content.as_bytes();
3874 let makefile =
3875 Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
3876
3877 let rules = makefile.rules().collect::<Vec<_>>();
3879 assert!(!rules.is_empty(), "Expected at least one rule");
3880
3881 let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
3883 assert!(build_rule.is_some(), "Expected to find build rule");
3884 }
3885
3886 #[test]
3887 fn test_complex_variable_functions() {
3888 let content = r#"
3889FILES := $(shell find . -name "*.c")
3890OBJS := $(patsubst %.c,%.o,$(FILES))
3891NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
3892HEADERS := ${wildcard *.h}
3893"#;
3894 let parsed = parse(content, None);
3895 assert!(
3896 parsed.errors.is_empty(),
3897 "Failed to parse complex variable functions: {:?}",
3898 parsed.errors
3899 );
3900 }
3901
3902 #[test]
3903 fn test_nested_variable_expansions() {
3904 let content = r#"
3905VERSION = 1.0
3906PACKAGE = myapp
3907TARBALL = $(PACKAGE)-$(VERSION).tar.gz
3908INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
3909"#;
3910 let parsed = parse(content, None);
3911 assert!(
3912 parsed.errors.is_empty(),
3913 "Failed to parse nested variable expansions: {:?}",
3914 parsed.errors
3915 );
3916 }
3917
3918 #[test]
3919 fn test_special_directives() {
3920 let content = r#"
3921# Special makefile directives
3922.PHONY: all clean
3923.SUFFIXES: .c .o
3924.DEFAULT: all
3925
3926# Variable definition and export directive
3927export PATH := /usr/bin:/bin
3928"#;
3929 let mut buf = content.as_bytes();
3931 let makefile =
3932 Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
3933
3934 let rules = makefile.rules().collect::<Vec<_>>();
3936
3937 let phony_rule = rules
3939 .iter()
3940 .find(|r| r.targets().any(|t| t.contains(".PHONY")));
3941 assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
3942
3943 let vars = makefile.variable_definitions().collect::<Vec<_>>();
3945 assert!(!vars.is_empty(), "Expected to find at least one variable");
3946 }
3947
3948 #[test]
3951 fn test_comprehensive_real_world_makefile() {
3952 let content = r#"
3954# Basic variable assignment
3955VERSION = 1.0.0
3956
3957# Phony target
3958.PHONY: all clean
3959
3960# Simple rule
3961all:
3962 echo "Building version $(VERSION)"
3963
3964# Another rule with dependencies
3965clean:
3966 rm -f *.o
3967"#;
3968
3969 let parsed = parse(content, None);
3971
3972 assert!(parsed.errors.is_empty(), "Expected no parsing errors");
3974
3975 let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
3977 assert!(!variables.is_empty(), "Expected at least one variable");
3978 assert_eq!(
3979 variables[0].name(),
3980 Some("VERSION".to_string()),
3981 "Expected VERSION variable"
3982 );
3983
3984 let rules = parsed.root().rules().collect::<Vec<_>>();
3986 assert!(!rules.is_empty(), "Expected at least one rule");
3987
3988 let rule_targets: Vec<String> = rules
3990 .iter()
3991 .flat_map(|r| r.targets().collect::<Vec<_>>())
3992 .collect();
3993 assert!(
3994 rule_targets.contains(&".PHONY".to_string()),
3995 "Expected .PHONY rule"
3996 );
3997 assert!(
3998 rule_targets.contains(&"all".to_string()),
3999 "Expected 'all' rule"
4000 );
4001 assert!(
4002 rule_targets.contains(&"clean".to_string()),
4003 "Expected 'clean' rule"
4004 );
4005 }
4006
4007 #[test]
4008 fn test_indented_help_text_outside_rules() {
4009 let content = r#"
4011# Targets with help text
4012help:
4013 @echo "Available targets:"
4014 @echo " build build the project"
4015 @echo " test run tests"
4016 @echo " clean clean build artifacts"
4017
4018# Another target
4019clean:
4020 rm -rf build/
4021"#;
4022
4023 let parsed = parse(content, None);
4025
4026 assert!(
4028 parsed.errors.is_empty(),
4029 "Failed to parse indented help text"
4030 );
4031
4032 let rules = parsed.root().rules().collect::<Vec<_>>();
4034 assert_eq!(rules.len(), 2, "Expected to find two rules");
4035
4036 let help_rule = rules
4038 .iter()
4039 .find(|r| r.targets().any(|t| t == "help"))
4040 .expect("Expected to find help rule");
4041
4042 let clean_rule = rules
4043 .iter()
4044 .find(|r| r.targets().any(|t| t == "clean"))
4045 .expect("Expected to find clean rule");
4046
4047 let help_recipes = help_rule.recipes().collect::<Vec<_>>();
4049 assert!(
4050 !help_recipes.is_empty(),
4051 "Help rule should have recipe lines"
4052 );
4053 assert!(
4054 help_recipes
4055 .iter()
4056 .any(|line| line.contains("Available targets")),
4057 "Help recipes should include 'Available targets' line"
4058 );
4059
4060 let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
4062 assert!(
4063 !clean_recipes.is_empty(),
4064 "Clean rule should have recipe lines"
4065 );
4066 assert!(
4067 clean_recipes.iter().any(|line| line.contains("rm -rf")),
4068 "Clean recipes should include 'rm -rf' command"
4069 );
4070 }
4071
4072 #[test]
4073 fn test_makefile1_phony_pattern() {
4074 let content = "#line 2145\n.PHONY: $(PHONY)\n";
4076
4077 let result = parse(content, None);
4079
4080 assert!(
4082 result.errors.is_empty(),
4083 "Failed to parse .PHONY: $(PHONY) pattern"
4084 );
4085
4086 let rules = result.root().rules().collect::<Vec<_>>();
4088 assert_eq!(rules.len(), 1, "Expected 1 rule");
4089 assert_eq!(
4090 rules[0].targets().next().unwrap(),
4091 ".PHONY",
4092 "Expected .PHONY rule"
4093 );
4094
4095 let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
4097 assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
4098 assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
4099 }
4100
4101 #[test]
4102 fn test_skip_until_newline_behavior() {
4103 let input = "text without newline";
4105 let parsed = parse(input, None);
4106 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4108
4109 let input_with_newline = "text\nafter newline";
4110 let parsed2 = parse(input_with_newline, None);
4111 assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
4112 }
4113
4114 #[test]
4115 #[ignore] fn test_error_with_indent_token() {
4117 let input = "\tinvalid indented line";
4119 let parsed = parse(input, None);
4120 assert!(!parsed.errors.is_empty());
4122
4123 let error_msg = &parsed.errors[0].message;
4124 assert!(error_msg.contains("recipe commences before first target"));
4125 }
4126
4127 #[test]
4128 fn test_conditional_token_handling() {
4129 let input = r#"
4131ifndef VAR
4132 CFLAGS = -DTEST
4133endif
4134"#;
4135 let parsed = parse(input, None);
4136 let makefile = parsed.root();
4138 let _vars = makefile.variable_definitions().collect::<Vec<_>>();
4139 let nested = r#"
4143ifdef DEBUG
4144 ifndef RELEASE
4145 CFLAGS = -g
4146 endif
4147endif
4148"#;
4149 let parsed_nested = parse(nested, None);
4150 let _makefile = parsed_nested.root();
4152 }
4153
4154 #[test]
4155 fn test_include_vs_conditional_logic() {
4156 let input = r#"
4158include file.mk
4159ifdef VAR
4160 VALUE = 1
4161endif
4162"#;
4163 let parsed = parse(input, None);
4164 let makefile = parsed.root();
4166 let includes = makefile.includes().collect::<Vec<_>>();
4167 assert!(!includes.is_empty() || !parsed.errors.is_empty());
4169
4170 let optional_include = r#"
4172-include optional.mk
4173ifndef VAR
4174 VALUE = default
4175endif
4176"#;
4177 let parsed2 = parse(optional_include, None);
4178 let _makefile = parsed2.root();
4180 }
4181
4182 #[test]
4183 fn test_balanced_parens_counting() {
4184 let input = r#"
4186VAR = $(call func,$(nested,arg),extra)
4187COMPLEX = $(if $(condition),$(then_val),$(else_val))
4188"#;
4189 let parsed = parse(input, None);
4190 assert!(parsed.errors.is_empty());
4191
4192 let makefile = parsed.root();
4193 let vars = makefile.variable_definitions().collect::<Vec<_>>();
4194 assert_eq!(vars.len(), 2);
4195 }
4196
4197 #[test]
4198 fn test_documentation_lookahead() {
4199 let input = r#"
4201# Documentation comment
4202help:
4203 @echo "Usage instructions"
4204 @echo "More help text"
4205"#;
4206 let parsed = parse(input, None);
4207 assert!(parsed.errors.is_empty());
4208
4209 let makefile = parsed.root();
4210 let rules = makefile.rules().collect::<Vec<_>>();
4211 assert_eq!(rules.len(), 1);
4212 assert_eq!(rules[0].targets().next().unwrap(), "help");
4213 }
4214
4215 #[test]
4216 fn test_edge_case_empty_input() {
4217 let parsed = parse("", None);
4219 assert!(parsed.errors.is_empty());
4220
4221 let parsed2 = parse(" \n \n", None);
4223 let _makefile = parsed2.root();
4226 }
4227
4228 #[test]
4229 fn test_malformed_conditional_recovery() {
4230 let input = r#"
4232ifdef
4233 # Missing condition variable
4234endif
4235"#;
4236 let parsed = parse(input, None);
4237 assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
4240 }
4241
4242 #[test]
4243 fn test_replace_rule() {
4244 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4245 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4246
4247 makefile.replace_rule(0, new_rule).unwrap();
4248
4249 let targets: Vec<_> = makefile
4250 .rules()
4251 .flat_map(|r| r.targets().collect::<Vec<_>>())
4252 .collect();
4253 assert_eq!(targets, vec!["new_rule", "rule2"]);
4254
4255 let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
4256 assert_eq!(recipes, vec!["new_command"]);
4257 }
4258
4259 #[test]
4260 fn test_replace_rule_out_of_bounds() {
4261 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4262 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4263
4264 let result = makefile.replace_rule(5, new_rule);
4265 assert!(result.is_err());
4266 }
4267
4268 #[test]
4269 fn test_remove_rule() {
4270 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
4271 .parse()
4272 .unwrap();
4273
4274 let removed = makefile.remove_rule(1).unwrap();
4275 assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
4276
4277 let remaining_targets: Vec<_> = makefile
4278 .rules()
4279 .flat_map(|r| r.targets().collect::<Vec<_>>())
4280 .collect();
4281 assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
4282 assert_eq!(makefile.rules().count(), 2);
4283 }
4284
4285 #[test]
4286 fn test_remove_rule_out_of_bounds() {
4287 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4288
4289 let result = makefile.remove_rule(5);
4290 assert!(result.is_err());
4291 }
4292
4293 #[test]
4294 fn test_insert_rule() {
4295 let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
4296 let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
4297
4298 makefile.insert_rule(1, new_rule).unwrap();
4299
4300 let targets: Vec<_> = makefile
4301 .rules()
4302 .flat_map(|r| r.targets().collect::<Vec<_>>())
4303 .collect();
4304 assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
4305 assert_eq!(makefile.rules().count(), 3);
4306 }
4307
4308 #[test]
4309 fn test_insert_rule_at_end() {
4310 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4311 let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
4312
4313 makefile.insert_rule(1, new_rule).unwrap();
4314
4315 let targets: Vec<_> = makefile
4316 .rules()
4317 .flat_map(|r| r.targets().collect::<Vec<_>>())
4318 .collect();
4319 assert_eq!(targets, vec!["rule1", "end_rule"]);
4320 }
4321
4322 #[test]
4323 fn test_insert_rule_out_of_bounds() {
4324 let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
4325 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4326
4327 let result = makefile.insert_rule(5, new_rule);
4328 assert!(result.is_err());
4329 }
4330
4331 #[test]
4332 fn test_insert_rule_preserves_blank_line_spacing_at_end() {
4333 let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
4335 let mut makefile: Makefile = input.parse().unwrap();
4336 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4337
4338 makefile.insert_rule(2, new_rule).unwrap();
4339
4340 let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4341 assert_eq!(makefile.to_string(), expected);
4342 }
4343
4344 #[test]
4345 fn test_insert_rule_adds_blank_lines_when_missing() {
4346 let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
4348 let mut makefile: Makefile = input.parse().unwrap();
4349 let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
4350
4351 makefile.insert_rule(2, new_rule).unwrap();
4352
4353 let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
4354 assert_eq!(makefile.to_string(), expected);
4355 }
4356
4357 #[test]
4358 fn test_remove_command() {
4359 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4360 .parse()
4361 .unwrap();
4362
4363 rule.remove_command(1);
4364 let recipes: Vec<_> = rule.recipes().collect();
4365 assert_eq!(recipes, vec!["command1", "command3"]);
4366 assert_eq!(rule.recipe_count(), 2);
4367 }
4368
4369 #[test]
4370 fn test_remove_command_out_of_bounds() {
4371 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4372
4373 let result = rule.remove_command(5);
4374 assert!(!result);
4375 }
4376
4377 #[test]
4378 fn test_insert_command() {
4379 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
4380
4381 rule.insert_command(1, "command2");
4382 let recipes: Vec<_> = rule.recipes().collect();
4383 assert_eq!(recipes, vec!["command1", "command2", "command3"]);
4384 }
4385
4386 #[test]
4387 fn test_insert_command_at_end() {
4388 let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
4389
4390 rule.insert_command(1, "command2");
4391 let recipes: Vec<_> = rule.recipes().collect();
4392 assert_eq!(recipes, vec!["command1", "command2"]);
4393 }
4394
4395 #[test]
4396 fn test_insert_command_in_empty_rule() {
4397 let mut rule: Rule = "rule:\n".parse().unwrap();
4398
4399 rule.insert_command(0, "new_command");
4400 let recipes: Vec<_> = rule.recipes().collect();
4401 assert_eq!(recipes, vec!["new_command"]);
4402 }
4403
4404 #[test]
4405 fn test_recipe_count() {
4406 let rule1: Rule = "rule:\n".parse().unwrap();
4407 assert_eq!(rule1.recipe_count(), 0);
4408
4409 let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
4410 assert_eq!(rule2.recipe_count(), 2);
4411 }
4412
4413 #[test]
4414 fn test_clear_commands() {
4415 let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
4416 .parse()
4417 .unwrap();
4418
4419 rule.clear_commands();
4420 assert_eq!(rule.recipe_count(), 0);
4421
4422 let recipes: Vec<_> = rule.recipes().collect();
4423 assert_eq!(recipes, Vec::<String>::new());
4424
4425 let targets: Vec<_> = rule.targets().collect();
4427 assert_eq!(targets, vec!["rule"]);
4428 }
4429
4430 #[test]
4431 fn test_clear_commands_empty_rule() {
4432 let mut rule: Rule = "rule:\n".parse().unwrap();
4433
4434 rule.clear_commands();
4435 assert_eq!(rule.recipe_count(), 0);
4436
4437 let targets: Vec<_> = rule.targets().collect();
4438 assert_eq!(targets, vec!["rule"]);
4439 }
4440
4441 #[test]
4442 fn test_rule_manipulation_preserves_structure() {
4443 let input = r#"# Comment
4445VAR = value
4446
4447rule1:
4448 command1
4449
4450# Another comment
4451rule2:
4452 command2
4453
4454VAR2 = value2
4455"#;
4456
4457 let mut makefile: Makefile = input.parse().unwrap();
4458 let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
4459
4460 makefile.insert_rule(1, new_rule).unwrap();
4462
4463 let targets: Vec<_> = makefile
4465 .rules()
4466 .flat_map(|r| r.targets().collect::<Vec<_>>())
4467 .collect();
4468 assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
4469
4470 let vars: Vec<_> = makefile.variable_definitions().collect();
4472 assert_eq!(vars.len(), 2);
4473
4474 let output = makefile.code();
4476 assert!(output.contains("# Comment"));
4477 assert!(output.contains("VAR = value"));
4478 assert!(output.contains("# Another comment"));
4479 assert!(output.contains("VAR2 = value2"));
4480 }
4481
4482 #[test]
4483 fn test_replace_rule_with_multiple_targets() {
4484 let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
4485 let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
4486
4487 makefile.replace_rule(0, new_rule).unwrap();
4488
4489 let targets: Vec<_> = makefile
4490 .rules()
4491 .flat_map(|r| r.targets().collect::<Vec<_>>())
4492 .collect();
4493 assert_eq!(targets, vec!["new_target"]);
4494 }
4495
4496 #[test]
4497 fn test_empty_makefile_operations() {
4498 let mut makefile = Makefile::new();
4499
4500 assert!(makefile
4502 .replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
4503 .is_err());
4504 assert!(makefile.remove_rule(0).is_err());
4505
4506 let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
4508 makefile.insert_rule(0, new_rule).unwrap();
4509 assert_eq!(makefile.rules().count(), 1);
4510 }
4511
4512 #[test]
4513 fn test_command_operations_preserve_indentation() {
4514 let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
4515 .parse()
4516 .unwrap();
4517
4518 rule.insert_command(1, "middle_command");
4519 let recipes: Vec<_> = rule.recipes().collect();
4520 assert_eq!(
4521 recipes,
4522 vec!["\tdeep_indent", "middle_command", "shallow_indent"]
4523 );
4524 }
4525
4526 #[test]
4527 fn test_rule_operations_with_variables_and_includes() {
4528 let input = r#"VAR1 = value1
4529include common.mk
4530
4531rule1:
4532 command1
4533
4534VAR2 = value2
4535include other.mk
4536
4537rule2:
4538 command2
4539"#;
4540
4541 let mut makefile: Makefile = input.parse().unwrap();
4542
4543 makefile.remove_rule(0).unwrap();
4545
4546 let output = makefile.code();
4548 assert!(output.contains("VAR1 = value1"));
4549 assert!(output.contains("include common.mk"));
4550 assert!(output.contains("VAR2 = value2"));
4551 assert!(output.contains("include other.mk"));
4552
4553 assert_eq!(makefile.rules().count(), 1);
4555 let remaining_targets: Vec<_> = makefile
4556 .rules()
4557 .flat_map(|r| r.targets().collect::<Vec<_>>())
4558 .collect();
4559 assert_eq!(remaining_targets, vec!["rule2"]);
4560 }
4561
4562 #[test]
4563 fn test_command_manipulation_edge_cases() {
4564 let mut empty_rule: Rule = "empty:\n".parse().unwrap();
4566 assert_eq!(empty_rule.recipe_count(), 0);
4567
4568 empty_rule.insert_command(0, "first_command");
4569 assert_eq!(empty_rule.recipe_count(), 1);
4570
4571 let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
4573 empty_rule2.clear_commands();
4574 assert_eq!(empty_rule2.recipe_count(), 0);
4575 }
4576
4577 #[test]
4578 fn test_large_makefile_performance() {
4579 let mut makefile = Makefile::new();
4581
4582 for i in 0..100 {
4584 let rule_name = format!("rule{}", i);
4585 makefile
4586 .add_rule(&rule_name)
4587 .push_command(&format!("command{}", i));
4588 }
4589
4590 assert_eq!(makefile.rules().count(), 100);
4591
4592 let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
4594 makefile.replace_rule(50, new_rule).unwrap();
4595
4596 let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
4598 assert_eq!(rule_50_targets, vec!["middle_rule"]);
4599
4600 assert_eq!(makefile.rules().count(), 100); }
4602
4603 #[test]
4604 fn test_complex_recipe_manipulation() {
4605 let mut complex_rule: Rule = r#"complex:
4606 @echo "Starting build"
4607 $(CC) $(CFLAGS) -o $@ $<
4608 @echo "Build complete"
4609 chmod +x $@
4610"#
4611 .parse()
4612 .unwrap();
4613
4614 assert_eq!(complex_rule.recipe_count(), 4);
4615
4616 complex_rule.remove_command(0); complex_rule.remove_command(1); let final_recipes: Vec<_> = complex_rule.recipes().collect();
4621 assert_eq!(final_recipes.len(), 2);
4622 assert!(final_recipes[0].contains("$(CC)"));
4623 assert!(final_recipes[1].contains("chmod"));
4624 }
4625
4626 #[test]
4627 fn test_variable_definition_remove() {
4628 let makefile: Makefile = r#"VAR1 = value1
4629VAR2 = value2
4630VAR3 = value3
4631"#
4632 .parse()
4633 .unwrap();
4634
4635 assert_eq!(makefile.variable_definitions().count(), 3);
4637
4638 let mut var2 = makefile
4640 .variable_definitions()
4641 .nth(1)
4642 .expect("Should have second variable");
4643 assert_eq!(var2.name(), Some("VAR2".to_string()));
4644 var2.remove();
4645
4646 assert_eq!(makefile.variable_definitions().count(), 2);
4648 let var_names: Vec<_> = makefile
4649 .variable_definitions()
4650 .filter_map(|v| v.name())
4651 .collect();
4652 assert_eq!(var_names, vec!["VAR1", "VAR3"]);
4653 }
4654
4655 #[test]
4656 fn test_variable_definition_set_value() {
4657 let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
4658
4659 let mut var = makefile
4660 .variable_definitions()
4661 .next()
4662 .expect("Should have variable");
4663 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4664
4665 var.set_value("new_value");
4667
4668 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4670 assert!(makefile.code().contains("VAR = new_value"));
4671 }
4672
4673 #[test]
4674 fn test_variable_definition_set_value_preserves_format() {
4675 let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
4676
4677 let mut var = makefile
4678 .variable_definitions()
4679 .next()
4680 .expect("Should have variable");
4681 assert_eq!(var.raw_value(), Some("old_value".to_string()));
4682
4683 var.set_value("new_value");
4685
4686 assert_eq!(var.raw_value(), Some("new_value".to_string()));
4688 let code = makefile.code();
4689 assert!(code.contains("export"), "Should preserve export prefix");
4690 assert!(code.contains(":="), "Should preserve := operator");
4691 assert!(code.contains("new_value"), "Should have new value");
4692 }
4693
4694 #[test]
4695 fn test_makefile_find_variable() {
4696 let makefile: Makefile = r#"VAR1 = value1
4697VAR2 = value2
4698VAR3 = value3
4699"#
4700 .parse()
4701 .unwrap();
4702
4703 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4705 assert_eq!(vars.len(), 1);
4706 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4707 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4708
4709 assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
4711 }
4712
4713 #[test]
4714 fn test_makefile_find_variable_with_export() {
4715 let makefile: Makefile = r#"VAR1 = value1
4716export VAR2 := value2
4717VAR3 = value3
4718"#
4719 .parse()
4720 .unwrap();
4721
4722 let vars: Vec<_> = makefile.find_variable("VAR2").collect();
4724 assert_eq!(vars.len(), 1);
4725 assert_eq!(vars[0].name(), Some("VAR2".to_string()));
4726 assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
4727 }
4728
4729 #[test]
4730 fn test_variable_definition_is_export() {
4731 let makefile: Makefile = r#"VAR1 = value1
4732export VAR2 := value2
4733export VAR3 = value3
4734VAR4 := value4
4735"#
4736 .parse()
4737 .unwrap();
4738
4739 let vars: Vec<_> = makefile.variable_definitions().collect();
4740 assert_eq!(vars.len(), 4);
4741
4742 assert!(!vars[0].is_export());
4743 assert!(vars[1].is_export());
4744 assert!(vars[2].is_export());
4745 assert!(!vars[3].is_export());
4746 }
4747
4748 #[test]
4749 fn test_makefile_find_variable_multiple() {
4750 let makefile: Makefile = r#"VAR1 = value1
4751VAR1 = value2
4752VAR2 = other
4753VAR1 = value3
4754"#
4755 .parse()
4756 .unwrap();
4757
4758 let vars: Vec<_> = makefile.find_variable("VAR1").collect();
4760 assert_eq!(vars.len(), 3);
4761 assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
4762 assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
4763 assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
4764
4765 let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
4767 assert_eq!(var2s.len(), 1);
4768 assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
4769 }
4770
4771 #[test]
4772 fn test_variable_remove_and_find() {
4773 let makefile: Makefile = r#"VAR1 = value1
4774VAR2 = value2
4775VAR3 = value3
4776"#
4777 .parse()
4778 .unwrap();
4779
4780 let mut var2 = makefile
4782 .find_variable("VAR2")
4783 .next()
4784 .expect("Should find VAR2");
4785 var2.remove();
4786
4787 assert_eq!(makefile.find_variable("VAR2").count(), 0);
4789
4790 assert_eq!(makefile.find_variable("VAR1").count(), 1);
4792 assert_eq!(makefile.find_variable("VAR3").count(), 1);
4793 }
4794
4795 #[test]
4796 fn test_variable_remove_with_comment() {
4797 let makefile: Makefile = r#"VAR1 = value1
4798# This is a comment about VAR2
4799VAR2 = value2
4800VAR3 = value3
4801"#
4802 .parse()
4803 .unwrap();
4804
4805 let mut var2 = makefile
4807 .variable_definitions()
4808 .nth(1)
4809 .expect("Should have second variable");
4810 assert_eq!(var2.name(), Some("VAR2".to_string()));
4811 var2.remove();
4812
4813 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4815 }
4816
4817 #[test]
4818 fn test_variable_remove_with_multiple_comments() {
4819 let makefile: Makefile = r#"VAR1 = value1
4820# Comment line 1
4821# Comment line 2
4822# Comment line 3
4823VAR2 = value2
4824VAR3 = value3
4825"#
4826 .parse()
4827 .unwrap();
4828
4829 let mut var2 = makefile
4831 .variable_definitions()
4832 .nth(1)
4833 .expect("Should have second variable");
4834 var2.remove();
4835
4836 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4838 }
4839
4840 #[test]
4841 fn test_variable_remove_with_empty_line() {
4842 let makefile: Makefile = r#"VAR1 = value1
4843
4844# Comment about VAR2
4845VAR2 = value2
4846VAR3 = value3
4847"#
4848 .parse()
4849 .unwrap();
4850
4851 let mut var2 = makefile
4853 .variable_definitions()
4854 .nth(1)
4855 .expect("Should have second variable");
4856 var2.remove();
4857
4858 assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
4861 }
4862
4863 #[test]
4864 fn test_variable_remove_with_multiple_empty_lines() {
4865 let makefile: Makefile = r#"VAR1 = value1
4866
4867
4868# Comment about VAR2
4869VAR2 = value2
4870VAR3 = value3
4871"#
4872 .parse()
4873 .unwrap();
4874
4875 let mut var2 = makefile
4877 .variable_definitions()
4878 .nth(1)
4879 .expect("Should have second variable");
4880 var2.remove();
4881
4882 assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
4885 }
4886
4887 #[test]
4888 fn test_rule_remove_with_comment() {
4889 let makefile: Makefile = r#"rule1:
4890 command1
4891
4892# Comment about rule2
4893rule2:
4894 command2
4895rule3:
4896 command3
4897"#
4898 .parse()
4899 .unwrap();
4900
4901 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
4903 rule2.remove().unwrap();
4904
4905 assert_eq!(
4908 makefile.code(),
4909 "rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
4910 );
4911 }
4912
4913 #[test]
4914 fn test_variable_remove_preserves_shebang() {
4915 let makefile: Makefile = r#"#!/usr/bin/make -f
4916# This is a regular comment
4917VAR1 = value1
4918VAR2 = value2
4919"#
4920 .parse()
4921 .unwrap();
4922
4923 let mut var1 = makefile.variable_definitions().next().unwrap();
4925 var1.remove();
4926
4927 let code = makefile.code();
4929 assert!(code.starts_with("#!/usr/bin/make -f"));
4930 assert!(!code.contains("regular comment"));
4931 assert!(!code.contains("VAR1"));
4932 assert!(code.contains("VAR2"));
4933 }
4934
4935 #[test]
4936 fn test_variable_remove_preserves_subsequent_comments() {
4937 let makefile: Makefile = r#"VAR1 = value1
4938# Comment about VAR2
4939VAR2 = value2
4940
4941# Comment about VAR3
4942VAR3 = value3
4943"#
4944 .parse()
4945 .unwrap();
4946
4947 let mut var2 = makefile
4949 .variable_definitions()
4950 .nth(1)
4951 .expect("Should have second variable");
4952 var2.remove();
4953
4954 let code = makefile.code();
4956 assert_eq!(
4957 code,
4958 "VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
4959 );
4960 }
4961
4962 #[test]
4963 fn test_variable_remove_after_shebang_preserves_empty_line() {
4964 let makefile: Makefile = r#"#!/usr/bin/make -f
4965export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
4966
4967%:
4968 dh $@
4969"#
4970 .parse()
4971 .unwrap();
4972
4973 let mut var = makefile.variable_definitions().next().unwrap();
4975 var.remove();
4976
4977 assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
4979 }
4980
4981 #[test]
4982 fn test_rule_add_prerequisite() {
4983 let mut rule: Rule = "target: dep1\n".parse().unwrap();
4984 rule.add_prerequisite("dep2").unwrap();
4985 assert_eq!(
4986 rule.prerequisites().collect::<Vec<_>>(),
4987 vec!["dep1", "dep2"]
4988 );
4989 assert_eq!(rule.to_string(), "target: dep1 dep2\n");
4991 }
4992
4993 #[test]
4994 fn test_rule_add_prerequisite_to_rule_without_prereqs() {
4995 let mut rule: Rule = "target:\n".parse().unwrap();
4997 rule.add_prerequisite("dep1").unwrap();
4998 assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
4999 assert_eq!(rule.to_string(), "target: dep1\n");
5001 }
5002
5003 #[test]
5004 fn test_rule_remove_prerequisite() {
5005 let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
5006 assert!(rule.remove_prerequisite("dep2").unwrap());
5007 assert_eq!(
5008 rule.prerequisites().collect::<Vec<_>>(),
5009 vec!["dep1", "dep3"]
5010 );
5011 assert!(!rule.remove_prerequisite("nonexistent").unwrap());
5012 }
5013
5014 #[test]
5015 fn test_rule_set_prerequisites() {
5016 let mut rule: Rule = "target: old_dep\n".parse().unwrap();
5017 rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
5018 .unwrap();
5019 assert_eq!(
5020 rule.prerequisites().collect::<Vec<_>>(),
5021 vec!["new_dep1", "new_dep2"]
5022 );
5023 }
5024
5025 #[test]
5026 fn test_rule_set_prerequisites_empty() {
5027 let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
5028 rule.set_prerequisites(vec![]).unwrap();
5029 assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
5030 }
5031
5032 #[test]
5033 fn test_rule_add_target() {
5034 let mut rule: Rule = "target1: dep1\n".parse().unwrap();
5035 rule.add_target("target2").unwrap();
5036 assert_eq!(
5037 rule.targets().collect::<Vec<_>>(),
5038 vec!["target1", "target2"]
5039 );
5040 }
5041
5042 #[test]
5043 fn test_rule_set_targets() {
5044 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5045 rule.set_targets(vec!["new_target1", "new_target2"])
5046 .unwrap();
5047 assert_eq!(
5048 rule.targets().collect::<Vec<_>>(),
5049 vec!["new_target1", "new_target2"]
5050 );
5051 }
5052
5053 #[test]
5054 fn test_rule_set_targets_empty() {
5055 let mut rule: Rule = "target: dep1\n".parse().unwrap();
5056 let result = rule.set_targets(vec![]);
5057 assert!(result.is_err());
5058 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
5060 }
5061
5062 #[test]
5063 fn test_rule_has_target() {
5064 let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
5065 assert!(rule.has_target("target1"));
5066 assert!(rule.has_target("target2"));
5067 assert!(!rule.has_target("target3"));
5068 assert!(!rule.has_target("nonexistent"));
5069 }
5070
5071 #[test]
5072 fn test_rule_rename_target() {
5073 let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
5074 assert!(rule.rename_target("old_target", "new_target").unwrap());
5075 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
5076 assert!(!rule.rename_target("nonexistent", "something").unwrap());
5078 }
5079
5080 #[test]
5081 fn test_rule_rename_target_multiple() {
5082 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5083 assert!(rule.rename_target("target2", "renamed_target").unwrap());
5084 assert_eq!(
5085 rule.targets().collect::<Vec<_>>(),
5086 vec!["target1", "renamed_target", "target3"]
5087 );
5088 }
5089
5090 #[test]
5091 fn test_rule_remove_target() {
5092 let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
5093 assert!(rule.remove_target("target2").unwrap());
5094 assert_eq!(
5095 rule.targets().collect::<Vec<_>>(),
5096 vec!["target1", "target3"]
5097 );
5098 assert!(!rule.remove_target("nonexistent").unwrap());
5100 }
5101
5102 #[test]
5103 fn test_rule_remove_target_last() {
5104 let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
5105 let result = rule.remove_target("single_target");
5106 assert!(result.is_err());
5107 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
5109 }
5110
5111 #[test]
5112 fn test_rule_target_manipulation_preserves_prerequisites() {
5113 let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
5114
5115 rule.remove_target("target1").unwrap();
5117 assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
5118 assert_eq!(
5119 rule.prerequisites().collect::<Vec<_>>(),
5120 vec!["dep1", "dep2"]
5121 );
5122 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5123
5124 rule.add_target("target3").unwrap();
5126 assert_eq!(
5127 rule.targets().collect::<Vec<_>>(),
5128 vec!["target2", "target3"]
5129 );
5130 assert_eq!(
5131 rule.prerequisites().collect::<Vec<_>>(),
5132 vec!["dep1", "dep2"]
5133 );
5134 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5135
5136 rule.rename_target("target2", "renamed").unwrap();
5138 assert_eq!(
5139 rule.targets().collect::<Vec<_>>(),
5140 vec!["renamed", "target3"]
5141 );
5142 assert_eq!(
5143 rule.prerequisites().collect::<Vec<_>>(),
5144 vec!["dep1", "dep2"]
5145 );
5146 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
5147 }
5148
5149 #[test]
5150 fn test_rule_remove() {
5151 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5152 let rule = makefile.find_rule_by_target("rule1").unwrap();
5153 rule.remove().unwrap();
5154 assert_eq!(makefile.rules().count(), 1);
5155 assert!(makefile.find_rule_by_target("rule1").is_none());
5156 assert!(makefile.find_rule_by_target("rule2").is_some());
5157 }
5158
5159 #[test]
5160 fn test_rule_remove_last_trims_blank_lines() {
5161 let makefile: Makefile =
5163 "%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
5164 .parse()
5165 .unwrap();
5166
5167 let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
5169 rule.remove().unwrap();
5170
5171 assert_eq!(makefile.code(), "%:\n\tdh $@\n");
5173 assert_eq!(makefile.rules().count(), 1);
5174 }
5175
5176 #[test]
5177 fn test_makefile_find_rule_by_target() {
5178 let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
5179 let rule = makefile.find_rule_by_target("rule2");
5180 assert!(rule.is_some());
5181 assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
5182 assert!(makefile.find_rule_by_target("nonexistent").is_none());
5183 }
5184
5185 #[test]
5186 fn test_makefile_find_rules_by_target() {
5187 let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
5188 .parse()
5189 .unwrap();
5190 assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
5191 assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
5192 assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
5193 }
5194
5195 #[test]
5196 fn test_makefile_find_rule_by_target_pattern_simple() {
5197 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5198 let rule = makefile.find_rule_by_target_pattern("foo.o");
5199 assert!(rule.is_some());
5200 assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
5201 }
5202
5203 #[test]
5204 fn test_makefile_find_rule_by_target_pattern_no_match() {
5205 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5206 let rule = makefile.find_rule_by_target_pattern("foo.c");
5207 assert!(rule.is_none());
5208 }
5209
5210 #[test]
5211 fn test_makefile_find_rule_by_target_pattern_exact() {
5212 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5213 let rule = makefile.find_rule_by_target_pattern("foo.o");
5214 assert!(rule.is_some());
5215 assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
5216 }
5217
5218 #[test]
5219 fn test_makefile_find_rule_by_target_pattern_prefix() {
5220 let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5221 let rule = makefile.find_rule_by_target_pattern("libfoo.a");
5222 assert!(rule.is_some());
5223 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
5224 }
5225
5226 #[test]
5227 fn test_makefile_find_rule_by_target_pattern_suffix() {
5228 let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
5229 let rule = makefile.find_rule_by_target_pattern("foo_test.o");
5230 assert!(rule.is_some());
5231 assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
5232 }
5233
5234 #[test]
5235 fn test_makefile_find_rule_by_target_pattern_middle() {
5236 let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
5237 let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
5238 assert!(rule.is_some());
5239 assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
5240 }
5241
5242 #[test]
5243 fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
5244 let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
5245 let rule = makefile.find_rule_by_target_pattern("anything");
5246 assert!(rule.is_some());
5247 assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
5248 }
5249
5250 #[test]
5251 fn test_makefile_find_rules_by_target_pattern_multiple() {
5252 let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
5253 .parse()
5254 .unwrap();
5255 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5256 assert_eq!(rules.len(), 2);
5257 }
5258
5259 #[test]
5260 fn test_makefile_find_rules_by_target_pattern_mixed() {
5261 let makefile: Makefile =
5262 "%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
5263 .parse()
5264 .unwrap();
5265 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5266 assert_eq!(rules.len(), 2); let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
5268 assert_eq!(rules.len(), 1); }
5270
5271 #[test]
5272 fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
5273 let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
5274 let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
5275 assert_eq!(rules.len(), 1);
5276 let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
5277 assert_eq!(rules.len(), 0);
5278 }
5279
5280 #[test]
5281 fn test_matches_pattern_exact() {
5282 assert!(matches_pattern("foo.o", "foo.o"));
5283 assert!(!matches_pattern("foo.o", "bar.o"));
5284 }
5285
5286 #[test]
5287 fn test_matches_pattern_suffix() {
5288 assert!(matches_pattern("%.o", "foo.o"));
5289 assert!(matches_pattern("%.o", "bar.o"));
5290 assert!(matches_pattern("%.o", "baz/qux.o"));
5291 assert!(!matches_pattern("%.o", "foo.c"));
5292 }
5293
5294 #[test]
5295 fn test_matches_pattern_prefix() {
5296 assert!(matches_pattern("lib%.a", "libfoo.a"));
5297 assert!(matches_pattern("lib%.a", "libbar.a"));
5298 assert!(!matches_pattern("lib%.a", "foo.a"));
5299 assert!(!matches_pattern("lib%.a", "lib.a"));
5300 }
5301
5302 #[test]
5303 fn test_matches_pattern_middle() {
5304 assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
5305 assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
5306 assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
5307 assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
5308 }
5309
5310 #[test]
5311 fn test_matches_pattern_wildcard_only() {
5312 assert!(matches_pattern("%", "anything"));
5313 assert!(matches_pattern("%", "foo.o"));
5314 assert!(!matches_pattern("%", ""));
5316 }
5317
5318 #[test]
5319 fn test_matches_pattern_empty_stem() {
5320 assert!(!matches_pattern("%.o", ".o")); assert!(!matches_pattern("lib%", "lib")); assert!(!matches_pattern("lib%.a", "lib.a")); }
5325
5326 #[test]
5327 fn test_matches_pattern_multiple_wildcards_not_supported() {
5328 assert!(!matches_pattern("%foo%bar", "xfooybarz"));
5331 assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
5332 }
5333
5334 #[test]
5335 fn test_makefile_add_phony_target() {
5336 let mut makefile = Makefile::new();
5337 makefile.add_phony_target("clean").unwrap();
5338 assert!(makefile.is_phony("clean"));
5339 assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
5340 }
5341
5342 #[test]
5343 fn test_makefile_add_phony_target_existing() {
5344 let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
5345 makefile.add_phony_target("clean").unwrap();
5346 assert!(makefile.is_phony("test"));
5347 assert!(makefile.is_phony("clean"));
5348 let targets: Vec<_> = makefile.phony_targets().collect();
5349 assert!(targets.contains(&"test".to_string()));
5350 assert!(targets.contains(&"clean".to_string()));
5351 }
5352
5353 #[test]
5354 fn test_makefile_remove_phony_target() {
5355 let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5356 assert!(makefile.remove_phony_target("clean").unwrap());
5357 assert!(!makefile.is_phony("clean"));
5358 assert!(makefile.is_phony("test"));
5359 assert!(!makefile.remove_phony_target("nonexistent").unwrap());
5360 }
5361
5362 #[test]
5363 fn test_makefile_remove_phony_target_last() {
5364 let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
5365 assert!(makefile.remove_phony_target("clean").unwrap());
5366 assert!(!makefile.is_phony("clean"));
5367 assert!(makefile.find_rule_by_target(".PHONY").is_none());
5369 }
5370
5371 #[test]
5372 fn test_makefile_is_phony() {
5373 let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
5374 assert!(makefile.is_phony("clean"));
5375 assert!(makefile.is_phony("test"));
5376 assert!(!makefile.is_phony("build"));
5377 }
5378
5379 #[test]
5380 fn test_makefile_phony_targets() {
5381 let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5382 let phony_targets: Vec<_> = makefile.phony_targets().collect();
5383 assert_eq!(phony_targets, vec!["clean", "test", "build"]);
5384 }
5385
5386 #[test]
5387 fn test_makefile_phony_targets_empty() {
5388 let makefile = Makefile::new();
5389 assert_eq!(makefile.phony_targets().count(), 0);
5390 }
5391
5392 #[test]
5393 fn test_makefile_remove_first_phony_target_no_extra_space() {
5394 let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
5395 assert!(makefile.remove_phony_target("clean").unwrap());
5396 let result = makefile.to_string();
5397 assert_eq!(result, ".PHONY: test build\n");
5398 }
5399
5400 #[test]
5401 fn test_recipe_with_leading_comments_and_blank_lines() {
5402 let makefile_text = r#"#!/usr/bin/make
5406
5407%:
5408 dh $@
5409
5410override_dh_build:
5411 # The next line is empty
5412
5413 dh_python3
5414"#;
5415 let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
5416
5417 let rules: Vec<_> = makefile.rules().collect();
5418 assert_eq!(rules.len(), 2, "Expected 2 rules");
5419
5420 let rule0 = &rules[0];
5422 assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
5423 assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
5424
5425 let rule1 = &rules[1];
5427 assert_eq!(
5428 rule1.targets().collect::<Vec<_>>(),
5429 vec!["override_dh_build"]
5430 );
5431
5432 let recipes: Vec<_> = rule1.recipes().collect();
5434 assert!(
5435 !recipes.is_empty(),
5436 "Expected at least one recipe for override_dh_build, got none"
5437 );
5438 assert!(
5439 recipes.contains(&"dh_python3".to_string()),
5440 "Expected 'dh_python3' in recipes, got: {:?}",
5441 recipes
5442 );
5443 }
5444
5445 #[test]
5446 fn test_rule_parse_preserves_trailing_blank_lines() {
5447 let input = r#"override_dh_systemd_enable:
5450 dh_systemd_enable -pracoon
5451
5452override_dh_install:
5453 dh_install
5454"#;
5455
5456 let mut mf: Makefile = input.parse().unwrap();
5457
5458 let rule = mf.rules().next().unwrap();
5460 let rule_text = rule.to_string();
5461
5462 assert_eq!(
5464 rule_text,
5465 "override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
5466 );
5467
5468 let modified =
5470 rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
5471
5472 let new_rule: Rule = modified.parse().unwrap();
5474 assert_eq!(
5475 new_rule.to_string(),
5476 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
5477 );
5478
5479 mf.replace_rule(0, new_rule).unwrap();
5481
5482 let output = mf.to_string();
5484 assert!(
5485 output.contains(
5486 "override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
5487 ),
5488 "Blank line between rules should be preserved. Got: {:?}",
5489 output
5490 );
5491 }
5492
5493 #[test]
5494 fn test_rule_parse_round_trip_with_trailing_newlines() {
5495 let test_cases = vec![
5497 "rule:\n\tcommand\n", "rule:\n\tcommand\n\n", "rule:\n\tcommand\n\n\n", ];
5501
5502 for rule_text in test_cases {
5503 let rule: Rule = rule_text.parse().unwrap();
5504 let result = rule.to_string();
5505 assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
5506 }
5507 }
5508
5509 #[test]
5510 fn test_rule_clone() {
5511 let rule_text = "rule:\n\tcommand\n\n";
5513 let rule: Rule = rule_text.parse().unwrap();
5514 let cloned = rule.clone();
5515
5516 assert_eq!(rule.to_string(), cloned.to_string());
5518 assert_eq!(rule.to_string(), rule_text);
5519 assert_eq!(cloned.to_string(), rule_text);
5520
5521 assert_eq!(
5523 rule.targets().collect::<Vec<_>>(),
5524 cloned.targets().collect::<Vec<_>>()
5525 );
5526 assert_eq!(
5527 rule.recipes().collect::<Vec<_>>(),
5528 cloned.recipes().collect::<Vec<_>>()
5529 );
5530 }
5531
5532 #[test]
5533 fn test_makefile_clone() {
5534 let input = "VAR = value\n\nrule:\n\tcommand\n";
5536 let makefile: Makefile = input.parse().unwrap();
5537 let cloned = makefile.clone();
5538
5539 assert_eq!(makefile.to_string(), cloned.to_string());
5541 assert_eq!(makefile.to_string(), input);
5542
5543 assert_eq!(makefile.rules().count(), cloned.rules().count());
5545
5546 assert_eq!(
5548 makefile.variable_definitions().count(),
5549 cloned.variable_definitions().count()
5550 );
5551 }
5552
5553 #[test]
5554 fn test_conditional_with_recipe_line() {
5555 let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
5557 let parsed = parse(input, None);
5558
5559 assert!(
5561 parsed.errors.is_empty(),
5562 "Expected no parse errors, but got: {:?}",
5563 parsed.errors
5564 );
5565
5566 let mf = parsed.root();
5568 assert_eq!(mf.code(), input);
5569 }
5570
5571 #[test]
5572 fn test_conditional_in_rule_recipe() {
5573 let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
5575 let parsed = parse(input, None);
5576
5577 assert!(
5579 parsed.errors.is_empty(),
5580 "Expected no parse errors, but got: {:?}",
5581 parsed.errors
5582 );
5583
5584 let mf = parsed.root();
5586 assert_eq!(mf.code(), input);
5587
5588 assert_eq!(mf.rules().count(), 1);
5590 }
5591
5592 #[test]
5593 fn test_rule_items() {
5594 use crate::RuleItem;
5595
5596 let input = r#"test:
5598 echo "before"
5599ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
5600 ./run-tests
5601endif
5602 echo "after"
5603"#;
5604 let rule: Rule = input.parse().unwrap();
5605
5606 let items: Vec<_> = rule.items().collect();
5607 assert_eq!(
5608 items.len(),
5609 3,
5610 "Expected 3 items: recipe, conditional, recipe"
5611 );
5612
5613 match &items[0] {
5615 RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
5616 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5617 }
5618
5619 match &items[1] {
5621 RuleItem::Conditional(c) => {
5622 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5623 }
5624 RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
5625 }
5626
5627 match &items[2] {
5629 RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
5630 RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
5631 }
5632
5633 let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
5635 let simple_items: Vec<_> = simple_rule.items().collect();
5636 assert_eq!(simple_items.len(), 2);
5637
5638 match &simple_items[0] {
5639 RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
5640 _ => panic!("Expected recipe"),
5641 }
5642
5643 match &simple_items[1] {
5644 RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
5645 _ => panic!("Expected recipe"),
5646 }
5647
5648 let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
5650 .parse()
5651 .unwrap();
5652 let cond_items: Vec<_> = cond_only.items().collect();
5653 assert_eq!(cond_items.len(), 1);
5654
5655 match &cond_items[0] {
5656 RuleItem::Conditional(c) => {
5657 assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
5658 }
5659 _ => panic!("Expected conditional"),
5660 }
5661 }
5662
5663 #[test]
5664 fn test_conditionals_iterator() {
5665 let makefile: Makefile = r#"ifdef DEBUG
5666VAR = debug
5667endif
5668
5669ifndef RELEASE
5670OTHER = dev
5671endif
5672"#
5673 .parse()
5674 .unwrap();
5675
5676 let conditionals: Vec<_> = makefile.conditionals().collect();
5677 assert_eq!(conditionals.len(), 2);
5678
5679 assert_eq!(
5680 conditionals[0].conditional_type(),
5681 Some("ifdef".to_string())
5682 );
5683 assert_eq!(
5684 conditionals[1].conditional_type(),
5685 Some("ifndef".to_string())
5686 );
5687 }
5688
5689 #[test]
5690 fn test_conditional_type_and_condition() {
5691 let makefile: Makefile = r#"ifdef DEBUG
5692VAR = debug
5693endif
5694"#
5695 .parse()
5696 .unwrap();
5697
5698 let conditional = makefile.conditionals().next().unwrap();
5699 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5700 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5701 }
5702
5703 #[test]
5704 fn test_conditional_has_else() {
5705 let makefile_with_else: Makefile = r#"ifdef DEBUG
5706VAR = debug
5707else
5708VAR = release
5709endif
5710"#
5711 .parse()
5712 .unwrap();
5713
5714 let conditional = makefile_with_else.conditionals().next().unwrap();
5715 assert!(conditional.has_else());
5716
5717 let makefile_without_else: Makefile = r#"ifdef DEBUG
5718VAR = debug
5719endif
5720"#
5721 .parse()
5722 .unwrap();
5723
5724 let conditional = makefile_without_else.conditionals().next().unwrap();
5725 assert!(!conditional.has_else());
5726 }
5727
5728 #[test]
5729 fn test_conditional_if_body() {
5730 let makefile: Makefile = r#"ifdef DEBUG
5731VAR = debug
5732endif
5733"#
5734 .parse()
5735 .unwrap();
5736
5737 let conditional = makefile.conditionals().next().unwrap();
5738 let if_body = conditional.if_body();
5739 assert!(if_body.is_some());
5740 assert!(if_body.unwrap().contains("VAR = debug"));
5741 }
5742
5743 #[test]
5744 fn test_conditional_else_body() {
5745 let makefile: Makefile = r#"ifdef DEBUG
5746VAR = debug
5747else
5748VAR = release
5749endif
5750"#
5751 .parse()
5752 .unwrap();
5753
5754 let conditional = makefile.conditionals().next().unwrap();
5755 let else_body = conditional.else_body();
5756 assert!(else_body.is_some());
5757 assert!(else_body.unwrap().contains("VAR = release"));
5758 }
5759
5760 #[test]
5761 fn test_add_conditional_ifdef() {
5762 let mut makefile = Makefile::new();
5763 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5764 assert!(result.is_ok());
5765
5766 let code = makefile.to_string();
5767 assert!(code.contains("ifdef DEBUG"));
5768 assert!(code.contains("VAR = debug"));
5769 assert!(code.contains("endif"));
5770 }
5771
5772 #[test]
5773 fn test_add_conditional_with_else() {
5774 let mut makefile = Makefile::new();
5775 let result =
5776 makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
5777 assert!(result.is_ok());
5778
5779 let code = makefile.to_string();
5780 assert!(code.contains("ifdef DEBUG"));
5781 assert!(code.contains("VAR = debug"));
5782 assert!(code.contains("else"));
5783 assert!(code.contains("VAR = release"));
5784 assert!(code.contains("endif"));
5785 }
5786
5787 #[test]
5788 fn test_add_conditional_invalid_type() {
5789 let mut makefile = Makefile::new();
5790 let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
5791 assert!(result.is_err());
5792 }
5793
5794 #[test]
5795 fn test_add_conditional_formatting() {
5796 let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
5797 let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
5798 assert!(result.is_ok());
5799
5800 let code = makefile.to_string();
5801 assert!(code.contains("\n\nifdef DEBUG"));
5803 }
5804
5805 #[test]
5806 fn test_conditional_remove() {
5807 let makefile: Makefile = r#"ifdef DEBUG
5808VAR = debug
5809endif
5810
5811VAR2 = value2
5812"#
5813 .parse()
5814 .unwrap();
5815
5816 let mut conditional = makefile.conditionals().next().unwrap();
5817 let result = conditional.remove();
5818 assert!(result.is_ok());
5819
5820 let code = makefile.to_string();
5821 assert!(!code.contains("ifdef DEBUG"));
5822 assert!(!code.contains("VAR = debug"));
5823 assert!(code.contains("VAR2 = value2"));
5824 }
5825
5826 #[test]
5827 fn test_add_conditional_ifndef() {
5828 let mut makefile = Makefile::new();
5829 let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
5830 assert!(result.is_ok());
5831
5832 let code = makefile.to_string();
5833 assert!(code.contains("ifndef NDEBUG"));
5834 assert!(code.contains("VAR = enabled"));
5835 assert!(code.contains("endif"));
5836 }
5837
5838 #[test]
5839 fn test_add_conditional_ifeq() {
5840 let mut makefile = Makefile::new();
5841 let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
5842 assert!(result.is_ok());
5843
5844 let code = makefile.to_string();
5845 assert!(code.contains("ifeq ($(OS),Linux)"));
5846 assert!(code.contains("VAR = linux"));
5847 assert!(code.contains("endif"));
5848 }
5849
5850 #[test]
5851 fn test_add_conditional_ifneq() {
5852 let mut makefile = Makefile::new();
5853 let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
5854 assert!(result.is_ok());
5855
5856 let code = makefile.to_string();
5857 assert!(code.contains("ifneq ($(OS),Windows)"));
5858 assert!(code.contains("VAR = unix"));
5859 assert!(code.contains("endif"));
5860 }
5861
5862 #[test]
5863 fn test_conditional_api_integration() {
5864 let mut makefile: Makefile = r#"VAR1 = value1
5866
5867rule1:
5868 command1
5869"#
5870 .parse()
5871 .unwrap();
5872
5873 makefile
5875 .add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
5876 .unwrap();
5877
5878 assert_eq!(makefile.conditionals().count(), 1);
5880 let conditional = makefile.conditionals().next().unwrap();
5881 assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
5882 assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
5883 assert!(conditional.has_else());
5884
5885 assert_eq!(makefile.variable_definitions().count(), 1);
5887 assert_eq!(makefile.rules().count(), 1);
5888 }
5889
5890 #[test]
5891 fn test_conditional_if_items() {
5892 let makefile: Makefile = r#"ifdef DEBUG
5893VAR = debug
5894rule:
5895 command
5896endif
5897"#
5898 .parse()
5899 .unwrap();
5900
5901 let cond = makefile.conditionals().next().unwrap();
5902 let items: Vec<_> = cond.if_items().collect();
5903 assert_eq!(items.len(), 2); match &items[0] {
5906 MakefileItem::Variable(v) => {
5907 assert_eq!(v.name(), Some("VAR".to_string()));
5908 }
5909 _ => panic!("Expected variable"),
5910 }
5911
5912 match &items[1] {
5913 MakefileItem::Rule(r) => {
5914 assert!(r.targets().any(|t| t == "rule"));
5915 }
5916 _ => panic!("Expected rule"),
5917 }
5918 }
5919
5920 #[test]
5921 fn test_conditional_else_items() {
5922 let makefile: Makefile = r#"ifdef DEBUG
5923VAR = debug
5924else
5925VAR2 = release
5926rule2:
5927 command
5928endif
5929"#
5930 .parse()
5931 .unwrap();
5932
5933 let cond = makefile.conditionals().next().unwrap();
5934 let items: Vec<_> = cond.else_items().collect();
5935 assert_eq!(items.len(), 2); match &items[0] {
5938 MakefileItem::Variable(v) => {
5939 assert_eq!(v.name(), Some("VAR2".to_string()));
5940 }
5941 _ => panic!("Expected variable"),
5942 }
5943
5944 match &items[1] {
5945 MakefileItem::Rule(r) => {
5946 assert!(r.targets().any(|t| t == "rule2"));
5947 }
5948 _ => panic!("Expected rule"),
5949 }
5950 }
5951
5952 #[test]
5953 fn test_conditional_add_if_item() {
5954 let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
5955 let mut cond = makefile.conditionals().next().unwrap();
5956
5957 let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
5959 let var = temp.variable_definitions().next().unwrap();
5960 cond.add_if_item(MakefileItem::Variable(var));
5961
5962 let code = makefile.to_string();
5963 assert!(code.contains("CFLAGS = -g"));
5964
5965 let cond = makefile.conditionals().next().unwrap();
5967 assert_eq!(cond.if_items().count(), 1);
5968 }
5969
5970 #[test]
5971 fn test_conditional_add_else_item() {
5972 let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
5973 let mut cond = makefile.conditionals().next().unwrap();
5974
5975 let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5977 let var = temp.variable_definitions().next().unwrap();
5978 cond.add_else_item(MakefileItem::Variable(var));
5979
5980 let code = makefile.to_string();
5981 assert!(code.contains("else"));
5982 assert!(code.contains("CFLAGS = -O2"));
5983
5984 let cond = makefile.conditionals().next().unwrap();
5986 assert_eq!(cond.else_items().count(), 1);
5987 }
5988
5989 #[test]
5990 fn test_add_conditional_with_items() {
5991 let mut makefile = Makefile::new();
5992
5993 let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
5995 let var1 = temp1.variable_definitions().next().unwrap();
5996
5997 let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
5998 let var2 = temp2.variable_definitions().next().unwrap();
5999
6000 let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
6001 let rule1 = temp3.rules().next().unwrap();
6002
6003 let result = makefile.add_conditional_with_items(
6004 "ifdef",
6005 "DEBUG",
6006 vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
6007 Some(vec![MakefileItem::Variable(var2)]),
6008 );
6009
6010 assert!(result.is_ok());
6011
6012 let code = makefile.to_string();
6013 assert!(code.contains("ifdef DEBUG"));
6014 assert!(code.contains("CFLAGS = -g"));
6015 assert!(code.contains("debug:"));
6016 assert!(code.contains("else"));
6017 assert!(code.contains("CFLAGS = -O2"));
6018 }
6019
6020 #[test]
6021 fn test_conditional_items_with_nested_conditional() {
6022 let makefile: Makefile = r#"ifdef DEBUG
6023VAR = debug
6024ifdef VERBOSE
6025 VAR2 = verbose
6026endif
6027endif
6028"#
6029 .parse()
6030 .unwrap();
6031
6032 let cond = makefile.conditionals().next().unwrap();
6033 let items: Vec<_> = cond.if_items().collect();
6034 assert_eq!(items.len(), 2); match &items[0] {
6037 MakefileItem::Variable(v) => {
6038 assert_eq!(v.name(), Some("VAR".to_string()));
6039 }
6040 _ => panic!("Expected variable"),
6041 }
6042
6043 match &items[1] {
6044 MakefileItem::Conditional(c) => {
6045 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6046 }
6047 _ => panic!("Expected conditional"),
6048 }
6049 }
6050
6051 #[test]
6052 fn test_conditional_items_with_include() {
6053 let makefile: Makefile = r#"ifdef DEBUG
6054include debug.mk
6055VAR = debug
6056endif
6057"#
6058 .parse()
6059 .unwrap();
6060
6061 let cond = makefile.conditionals().next().unwrap();
6062 let items: Vec<_> = cond.if_items().collect();
6063 assert_eq!(items.len(), 2); match &items[0] {
6066 MakefileItem::Include(i) => {
6067 assert_eq!(i.path(), Some("debug.mk".to_string()));
6068 }
6069 _ => panic!("Expected include"),
6070 }
6071
6072 match &items[1] {
6073 MakefileItem::Variable(v) => {
6074 assert_eq!(v.name(), Some("VAR".to_string()));
6075 }
6076 _ => panic!("Expected variable"),
6077 }
6078 }
6079
6080 #[test]
6081 fn test_makefile_items_iterator() {
6082 let makefile: Makefile = r#"VAR = value
6083ifdef DEBUG
6084CFLAGS = -g
6085endif
6086rule:
6087 command
6088include common.mk
6089"#
6090 .parse()
6091 .unwrap();
6092
6093 assert_eq!(makefile.variable_definitions().count(), 2);
6096 assert_eq!(makefile.conditionals().count(), 1);
6097 assert_eq!(makefile.rules().count(), 1);
6098
6099 let items: Vec<_> = makefile.items().collect();
6100 assert!(
6102 items.len() >= 3,
6103 "Expected at least 3 items, got {}",
6104 items.len()
6105 );
6106
6107 match &items[0] {
6108 MakefileItem::Variable(v) => {
6109 assert_eq!(v.name(), Some("VAR".to_string()));
6110 }
6111 _ => panic!("Expected variable at position 0"),
6112 }
6113
6114 match &items[1] {
6115 MakefileItem::Conditional(c) => {
6116 assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
6117 }
6118 _ => panic!("Expected conditional at position 1"),
6119 }
6120
6121 match &items[2] {
6122 MakefileItem::Rule(r) => {
6123 let targets: Vec<_> = r.targets().collect();
6124 assert_eq!(targets, vec!["rule"]);
6125 }
6126 _ => panic!("Expected rule at position 2"),
6127 }
6128 }
6129
6130 #[test]
6131 fn test_conditional_unwrap() {
6132 let makefile: Makefile = r#"ifdef DEBUG
6133VAR = debug
6134rule:
6135 command
6136endif
6137"#
6138 .parse()
6139 .unwrap();
6140
6141 let mut cond = makefile.conditionals().next().unwrap();
6142 cond.unwrap().unwrap();
6143
6144 let code = makefile.to_string();
6145 let expected = "VAR = debug\nrule:\n\tcommand\n";
6146 assert_eq!(code, expected);
6147
6148 assert_eq!(makefile.conditionals().count(), 0);
6150
6151 assert_eq!(makefile.variable_definitions().count(), 1);
6153 assert_eq!(makefile.rules().count(), 1);
6154 }
6155
6156 #[test]
6157 fn test_conditional_unwrap_with_else_fails() {
6158 let makefile: Makefile = r#"ifdef DEBUG
6159VAR = debug
6160else
6161VAR = release
6162endif
6163"#
6164 .parse()
6165 .unwrap();
6166
6167 let mut cond = makefile.conditionals().next().unwrap();
6168 let result = cond.unwrap();
6169
6170 assert!(result.is_err());
6171 assert!(result
6172 .unwrap_err()
6173 .to_string()
6174 .contains("Cannot unwrap conditional with else clause"));
6175 }
6176
6177 #[test]
6178 fn test_conditional_unwrap_nested() {
6179 let makefile: Makefile = r#"ifdef OUTER
6180VAR = outer
6181ifdef INNER
6182VAR2 = inner
6183endif
6184endif
6185"#
6186 .parse()
6187 .unwrap();
6188
6189 let mut outer_cond = makefile.conditionals().next().unwrap();
6191 outer_cond.unwrap().unwrap();
6192
6193 let code = makefile.to_string();
6194 let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
6195 assert_eq!(code, expected);
6196 }
6197
6198 #[test]
6199 fn test_conditional_unwrap_empty() {
6200 let makefile: Makefile = r#"ifdef DEBUG
6201endif
6202"#
6203 .parse()
6204 .unwrap();
6205
6206 let mut cond = makefile.conditionals().next().unwrap();
6207 cond.unwrap().unwrap();
6208
6209 let code = makefile.to_string();
6210 assert_eq!(code, "");
6211 }
6212
6213 #[test]
6214 fn test_rule_parent() {
6215 let makefile: Makefile = r#"all:
6216 echo "test"
6217"#
6218 .parse()
6219 .unwrap();
6220
6221 let rule = makefile.rules().next().unwrap();
6222 let parent = rule.parent();
6223 assert!(parent.is_none());
6225 }
6226
6227 #[test]
6228 fn test_item_parent_in_conditional() {
6229 let makefile: Makefile = r#"ifdef DEBUG
6230VAR = debug
6231rule:
6232 command
6233endif
6234"#
6235 .parse()
6236 .unwrap();
6237
6238 let cond = makefile.conditionals().next().unwrap();
6239
6240 let items: Vec<_> = cond.if_items().collect();
6242 assert_eq!(items.len(), 2);
6243
6244 if let MakefileItem::Variable(var) = &items[0] {
6246 let parent = var.parent();
6247 assert!(parent.is_some());
6248 if let Some(MakefileItem::Conditional(_)) = parent {
6249 } else {
6251 panic!("Expected variable parent to be a Conditional");
6252 }
6253 } else {
6254 panic!("Expected first item to be a Variable");
6255 }
6256
6257 if let MakefileItem::Rule(rule) = &items[1] {
6259 let parent = rule.parent();
6260 assert!(parent.is_some());
6261 if let Some(MakefileItem::Conditional(_)) = parent {
6262 } else {
6264 panic!("Expected rule parent to be a Conditional");
6265 }
6266 } else {
6267 panic!("Expected second item to be a Rule");
6268 }
6269 }
6270
6271 #[test]
6272 fn test_nested_conditional_parent() {
6273 let makefile: Makefile = r#"ifdef OUTER
6274VAR = outer
6275ifdef INNER
6276VAR2 = inner
6277endif
6278endif
6279"#
6280 .parse()
6281 .unwrap();
6282
6283 let outer_cond = makefile.conditionals().next().unwrap();
6284
6285 let items: Vec<_> = outer_cond.if_items().collect();
6287
6288 let inner_cond = items
6290 .iter()
6291 .find_map(|item| {
6292 if let MakefileItem::Conditional(c) = item {
6293 Some(c)
6294 } else {
6295 None
6296 }
6297 })
6298 .unwrap();
6299
6300 let parent = inner_cond.parent();
6302 assert!(parent.is_some());
6303 if let Some(MakefileItem::Conditional(_)) = parent {
6304 } else {
6306 panic!("Expected inner conditional's parent to be a Conditional");
6307 }
6308 }
6309
6310 #[test]
6311 fn test_line_col() {
6312 let text = r#"# Comment at line 0
6313VAR1 = value1
6314VAR2 = value2
6315
6316rule1: dep1 dep2
6317 command1
6318 command2
6319
6320rule2:
6321 command3
6322
6323ifdef DEBUG
6324CFLAGS = -g
6325endif
6326"#;
6327 let makefile: Makefile = text.parse().unwrap();
6328
6329 let vars: Vec<_> = makefile.variable_definitions().collect();
6332 assert_eq!(vars.len(), 3);
6333
6334 assert_eq!(vars[0].line(), 1);
6336 assert_eq!(vars[0].column(), 0);
6337 assert_eq!(vars[0].line_col(), (1, 0));
6338
6339 assert_eq!(vars[1].line(), 2);
6341 assert_eq!(vars[1].column(), 0);
6342
6343 assert_eq!(vars[2].line(), 12);
6345 assert_eq!(vars[2].column(), 0);
6346
6347 let rules: Vec<_> = makefile.rules().collect();
6349 assert_eq!(rules.len(), 2);
6350
6351 assert_eq!(rules[0].line(), 4);
6353 assert_eq!(rules[0].column(), 0);
6354 assert_eq!(rules[0].line_col(), (4, 0));
6355
6356 assert_eq!(rules[1].line(), 8);
6358 assert_eq!(rules[1].column(), 0);
6359
6360 let conditionals: Vec<_> = makefile.conditionals().collect();
6362 assert_eq!(conditionals.len(), 1);
6363
6364 assert_eq!(conditionals[0].line(), 11);
6366 assert_eq!(conditionals[0].column(), 0);
6367 assert_eq!(conditionals[0].line_col(), (11, 0));
6368 }
6369
6370 #[test]
6371 fn test_line_col_multiline() {
6372 let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
6373 let makefile: Makefile = text.parse().unwrap();
6374
6375 let vars: Vec<_> = makefile.variable_definitions().collect();
6377 assert_eq!(vars.len(), 1);
6378 assert_eq!(vars[0].line(), 0);
6379 assert_eq!(vars[0].column(), 0);
6380
6381 let rules: Vec<_> = makefile.rules().collect();
6383 assert_eq!(rules.len(), 1);
6384 assert_eq!(rules[0].line(), 4);
6385 assert_eq!(rules[0].column(), 0);
6386 }
6387
6388 #[test]
6389 fn test_line_col_includes() {
6390 let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
6391 let makefile: Makefile = text.parse().unwrap();
6392
6393 let vars: Vec<_> = makefile.variable_definitions().collect();
6395 assert_eq!(vars[0].line(), 0);
6396
6397 let includes: Vec<_> = makefile.includes().collect();
6399 assert_eq!(includes.len(), 2);
6400 assert_eq!(includes[0].line(), 2);
6401 assert_eq!(includes[0].column(), 0);
6402 assert_eq!(includes[1].line(), 3);
6403 assert_eq!(includes[1].column(), 0);
6404 }
6405
6406 #[test]
6407 fn test_conditional_in_rule_vs_toplevel() {
6408 let text1 = r#"rule:
6410 command
6411ifeq (,$(X))
6412 test
6413endif
6414"#;
6415 let makefile: Makefile = text1.parse().unwrap();
6416 let rules: Vec<_> = makefile.rules().collect();
6417 let conditionals: Vec<_> = makefile.conditionals().collect();
6418
6419 assert_eq!(rules.len(), 1);
6420 assert_eq!(
6421 conditionals.len(),
6422 0,
6423 "Conditional should be part of rule, not top-level"
6424 );
6425
6426 let text2 = r#"rule:
6428 command
6429
6430ifeq (,$(X))
6431 test
6432endif
6433"#;
6434 let makefile: Makefile = text2.parse().unwrap();
6435 let rules: Vec<_> = makefile.rules().collect();
6436 let conditionals: Vec<_> = makefile.conditionals().collect();
6437
6438 assert_eq!(rules.len(), 1);
6439 assert_eq!(
6440 conditionals.len(),
6441 1,
6442 "Conditional after blank line should be top-level"
6443 );
6444 assert_eq!(conditionals[0].line(), 3);
6445 }
6446
6447 #[test]
6448 fn test_nested_conditionals_line_tracking() {
6449 let text = r#"ifdef OUTER
6450VAR1 = value1
6451ifdef INNER
6452VAR2 = value2
6453endif
6454VAR3 = value3
6455endif
6456"#;
6457 let makefile: Makefile = text.parse().unwrap();
6458
6459 let conditionals: Vec<_> = makefile.conditionals().collect();
6460 assert_eq!(
6461 conditionals.len(),
6462 1,
6463 "Only outer conditional should be top-level"
6464 );
6465 assert_eq!(conditionals[0].line(), 0);
6466 assert_eq!(conditionals[0].column(), 0);
6467 }
6468
6469 #[test]
6470 fn test_conditional_else_line_tracking() {
6471 let text = r#"VAR1 = before
6472
6473ifdef DEBUG
6474DEBUG_FLAGS = -g
6475else
6476DEBUG_FLAGS = -O2
6477endif
6478
6479VAR2 = after
6480"#;
6481 let makefile: Makefile = text.parse().unwrap();
6482
6483 let conditionals: Vec<_> = makefile.conditionals().collect();
6484 assert_eq!(conditionals.len(), 1);
6485 assert_eq!(conditionals[0].line(), 2);
6486 assert_eq!(conditionals[0].column(), 0);
6487 }
6488
6489 #[test]
6490 fn test_broken_conditional_endif_without_if() {
6491 let text = "VAR = value\nendif\n";
6493 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6494
6495 let vars: Vec<_> = makefile.variable_definitions().collect();
6497 assert_eq!(vars.len(), 1);
6498 assert_eq!(vars[0].line(), 0);
6499 }
6500
6501 #[test]
6502 fn test_broken_conditional_else_without_if() {
6503 let text = "VAR = value\nelse\nVAR2 = other\n";
6505 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6506
6507 let vars: Vec<_> = makefile.variable_definitions().collect();
6509 assert!(!vars.is_empty(), "Should parse at least the first variable");
6510 assert_eq!(vars[0].line(), 0);
6511 }
6512
6513 #[test]
6514 fn test_broken_conditional_missing_endif() {
6515 let text = r#"ifdef DEBUG
6517DEBUG_FLAGS = -g
6518VAR = value
6519"#;
6520 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6521
6522 assert!(makefile.code().contains("ifdef DEBUG"));
6524 }
6525
6526 #[test]
6527 fn test_multiple_conditionals_line_tracking() {
6528 let text = r#"ifdef A
6529VAR_A = a
6530endif
6531
6532ifdef B
6533VAR_B = b
6534endif
6535
6536ifdef C
6537VAR_C = c
6538endif
6539"#;
6540 let makefile: Makefile = text.parse().unwrap();
6541
6542 let conditionals: Vec<_> = makefile.conditionals().collect();
6543 assert_eq!(conditionals.len(), 3);
6544 assert_eq!(conditionals[0].line(), 0);
6545 assert_eq!(conditionals[1].line(), 4);
6546 assert_eq!(conditionals[2].line(), 8);
6547 }
6548
6549 #[test]
6550 fn test_conditional_with_multiple_else_ifeq() {
6551 let text = r#"ifeq ($(OS),Windows)
6552EXT = .exe
6553else ifeq ($(OS),Linux)
6554EXT = .bin
6555else
6556EXT = .out
6557endif
6558"#;
6559 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6560
6561 let conditionals: Vec<_> = makefile.conditionals().collect();
6562 assert_eq!(conditionals.len(), 1);
6563 assert_eq!(conditionals[0].line(), 0);
6564 assert_eq!(conditionals[0].column(), 0);
6565 }
6566
6567 #[test]
6568 fn test_conditional_types_line_tracking() {
6569 let text = r#"ifdef VAR1
6570A = 1
6571endif
6572
6573ifndef VAR2
6574B = 2
6575endif
6576
6577ifeq ($(X),y)
6578C = 3
6579endif
6580
6581ifneq ($(Y),n)
6582D = 4
6583endif
6584"#;
6585 let makefile: Makefile = text.parse().unwrap();
6586
6587 let conditionals: Vec<_> = makefile.conditionals().collect();
6588 assert_eq!(conditionals.len(), 4);
6589
6590 assert_eq!(conditionals[0].line(), 0); assert_eq!(
6592 conditionals[0].conditional_type(),
6593 Some("ifdef".to_string())
6594 );
6595
6596 assert_eq!(conditionals[1].line(), 4); assert_eq!(
6598 conditionals[1].conditional_type(),
6599 Some("ifndef".to_string())
6600 );
6601
6602 assert_eq!(conditionals[2].line(), 8); assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
6604
6605 assert_eq!(conditionals[3].line(), 12); assert_eq!(
6607 conditionals[3].conditional_type(),
6608 Some("ifneq".to_string())
6609 );
6610 }
6611
6612 #[test]
6613 fn test_conditional_in_rule_with_recipes() {
6614 let text = r#"test:
6615 echo "start"
6616ifdef VERBOSE
6617 echo "verbose mode"
6618endif
6619 echo "end"
6620"#;
6621 let makefile: Makefile = text.parse().unwrap();
6622
6623 let rules: Vec<_> = makefile.rules().collect();
6624 let conditionals: Vec<_> = makefile.conditionals().collect();
6625
6626 assert_eq!(rules.len(), 1);
6627 assert_eq!(rules[0].line(), 0);
6628 assert_eq!(conditionals.len(), 0);
6630 }
6631
6632 #[test]
6633 fn test_broken_conditional_double_else() {
6634 let text = r#"ifdef DEBUG
6636A = 1
6637else
6638B = 2
6639else
6640C = 3
6641endif
6642"#;
6643 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6644
6645 assert!(makefile.code().contains("ifdef DEBUG"));
6647 }
6648
6649 #[test]
6650 fn test_broken_conditional_mismatched_nesting() {
6651 let text = r#"ifdef A
6653VAR = value
6654endif
6655endif
6656"#;
6657 let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
6658
6659 let conditionals: Vec<_> = makefile.conditionals().collect();
6662 assert!(
6663 !conditionals.is_empty(),
6664 "Should parse at least the first conditional"
6665 );
6666 }
6667
6668 #[test]
6669 fn test_conditional_with_comment_line_tracking() {
6670 let text = r#"# This is a comment
6671ifdef DEBUG
6672# Another comment
6673CFLAGS = -g
6674endif
6675# Final comment
6676"#;
6677 let makefile: Makefile = text.parse().unwrap();
6678
6679 let conditionals: Vec<_> = makefile.conditionals().collect();
6680 assert_eq!(conditionals.len(), 1);
6681 assert_eq!(conditionals[0].line(), 1);
6682 assert_eq!(conditionals[0].column(), 0);
6683 }
6684
6685 #[test]
6686 fn test_conditional_after_variable_with_blank_lines() {
6687 let text = r#"VAR1 = value1
6688
6689
6690ifdef DEBUG
6691VAR2 = value2
6692endif
6693"#;
6694 let makefile: Makefile = text.parse().unwrap();
6695
6696 let vars: Vec<_> = makefile.variable_definitions().collect();
6697 let conditionals: Vec<_> = makefile.conditionals().collect();
6698
6699 assert_eq!(vars.len(), 2);
6701 assert_eq!(vars[0].line(), 0); assert_eq!(vars[1].line(), 4); assert_eq!(conditionals.len(), 1);
6705 assert_eq!(conditionals[0].line(), 3);
6706 }
6707
6708 #[test]
6709 fn test_empty_conditional_line_tracking() {
6710 let text = r#"ifdef DEBUG
6711endif
6712
6713ifndef RELEASE
6714endif
6715"#;
6716 let makefile: Makefile = text.parse().unwrap();
6717
6718 let conditionals: Vec<_> = makefile.conditionals().collect();
6719 assert_eq!(conditionals.len(), 2);
6720 assert_eq!(conditionals[0].line(), 0);
6721 assert_eq!(conditionals[1].line(), 3);
6722 }
6723
6724 #[test]
6725 fn test_recipe_line_tracking() {
6726 let text = r#"build:
6727 echo "Building..."
6728 gcc -o app main.c
6729 echo "Done"
6730
6731test:
6732 ./run-tests
6733"#;
6734 let makefile: Makefile = text.parse().unwrap();
6735
6736 let rule1 = makefile.rules().next().expect("Should have first rule");
6738 let recipes: Vec<_> = rule1.recipe_nodes().collect();
6739 assert_eq!(recipes.len(), 3);
6740
6741 assert_eq!(recipes[0].text(), "echo \"Building...\"");
6742 assert_eq!(recipes[0].line(), 1);
6743 assert_eq!(recipes[0].column(), 0);
6744
6745 assert_eq!(recipes[1].text(), "gcc -o app main.c");
6746 assert_eq!(recipes[1].line(), 2);
6747 assert_eq!(recipes[1].column(), 0);
6748
6749 assert_eq!(recipes[2].text(), "echo \"Done\"");
6750 assert_eq!(recipes[2].line(), 3);
6751 assert_eq!(recipes[2].column(), 0);
6752
6753 let rule2 = makefile.rules().nth(1).expect("Should have second rule");
6755 let recipes2: Vec<_> = rule2.recipe_nodes().collect();
6756 assert_eq!(recipes2.len(), 1);
6757
6758 assert_eq!(recipes2[0].text(), "./run-tests");
6759 assert_eq!(recipes2[0].line(), 6);
6760 assert_eq!(recipes2[0].column(), 0);
6761 }
6762
6763 #[test]
6764 fn test_recipe_with_variables_line_tracking() {
6765 let text = r#"install:
6766 mkdir -p $(DESTDIR)
6767 cp $(BINARY) $(DESTDIR)/
6768"#;
6769 let makefile: Makefile = text.parse().unwrap();
6770 let rule = makefile.rules().next().expect("Should have rule");
6771 let recipes: Vec<_> = rule.recipe_nodes().collect();
6772
6773 assert_eq!(recipes.len(), 2);
6774 assert_eq!(recipes[0].line(), 1);
6775 assert_eq!(recipes[1].line(), 2);
6776 }
6777
6778 #[test]
6779 fn test_recipe_text_no_leading_tab() {
6780 let text = "test:\n\techo hello\n\t\techo nested\n\t echo with spaces\n";
6782 let makefile: Makefile = text.parse().unwrap();
6783 let rule = makefile.rules().next().expect("Should have rule");
6784 let recipes: Vec<_> = rule.recipe_nodes().collect();
6785
6786 assert_eq!(recipes.len(), 3);
6787
6788 eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
6790
6791 assert_eq!(recipes[0].text(), "echo hello");
6793
6794 eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
6796 assert_eq!(recipes[1].text(), "\techo nested");
6797
6798 eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
6800 assert_eq!(recipes[2].text(), " echo with spaces");
6801 }
6802
6803 #[test]
6804 fn test_recipe_parent() {
6805 let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
6806 let rule = makefile.rules().next().unwrap();
6807 let recipe = rule.recipe_nodes().next().unwrap();
6808
6809 let parent = recipe.parent().expect("Recipe should have parent");
6810 assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
6811 assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
6812 }
6813
6814 #[test]
6815 fn test_recipe_is_silent_various_prefixes() {
6816 let makefile: Makefile = r#"test:
6817 @echo silent
6818 -echo ignore
6819 +echo always
6820 @-echo silent_ignore
6821 -@echo ignore_silent
6822 +@echo always_silent
6823 echo normal
6824"#
6825 .parse()
6826 .unwrap();
6827
6828 let rule = makefile.rules().next().unwrap();
6829 let recipes: Vec<_> = rule.recipe_nodes().collect();
6830
6831 assert_eq!(recipes.len(), 7);
6832 assert!(recipes[0].is_silent(), "@echo should be silent");
6833 assert!(!recipes[1].is_silent(), "-echo should not be silent");
6834 assert!(!recipes[2].is_silent(), "+echo should not be silent");
6835 assert!(recipes[3].is_silent(), "@-echo should be silent");
6836 assert!(recipes[4].is_silent(), "-@echo should be silent");
6837 assert!(recipes[5].is_silent(), "+@echo should be silent");
6838 assert!(!recipes[6].is_silent(), "echo should not be silent");
6839 }
6840
6841 #[test]
6842 fn test_recipe_is_ignore_errors_various_prefixes() {
6843 let makefile: Makefile = r#"test:
6844 @echo silent
6845 -echo ignore
6846 +echo always
6847 @-echo silent_ignore
6848 -@echo ignore_silent
6849 +-echo always_ignore
6850 echo normal
6851"#
6852 .parse()
6853 .unwrap();
6854
6855 let rule = makefile.rules().next().unwrap();
6856 let recipes: Vec<_> = rule.recipe_nodes().collect();
6857
6858 assert_eq!(recipes.len(), 7);
6859 assert!(
6860 !recipes[0].is_ignore_errors(),
6861 "@echo should not ignore errors"
6862 );
6863 assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
6864 assert!(
6865 !recipes[2].is_ignore_errors(),
6866 "+echo should not ignore errors"
6867 );
6868 assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
6869 assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
6870 assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
6871 assert!(
6872 !recipes[6].is_ignore_errors(),
6873 "echo should not ignore errors"
6874 );
6875 }
6876
6877 #[test]
6878 fn test_recipe_set_prefix_add() {
6879 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6880 let rule = makefile.rules().next().unwrap();
6881 let mut recipe = rule.recipe_nodes().next().unwrap();
6882
6883 recipe.set_prefix("@");
6884 assert_eq!(recipe.text(), "@echo hello");
6885 assert!(recipe.is_silent());
6886 }
6887
6888 #[test]
6889 fn test_recipe_set_prefix_change() {
6890 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6891 let rule = makefile.rules().next().unwrap();
6892 let mut recipe = rule.recipe_nodes().next().unwrap();
6893
6894 recipe.set_prefix("-");
6895 assert_eq!(recipe.text(), "-echo hello");
6896 assert!(!recipe.is_silent());
6897 assert!(recipe.is_ignore_errors());
6898 }
6899
6900 #[test]
6901 fn test_recipe_set_prefix_remove() {
6902 let makefile: Makefile = "all:\n\t@-echo 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 assert!(!recipe.is_ignore_errors());
6910 }
6911
6912 #[test]
6913 fn test_recipe_set_prefix_combinations() {
6914 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6915 let rule = makefile.rules().next().unwrap();
6916 let mut recipe = rule.recipe_nodes().next().unwrap();
6917
6918 recipe.set_prefix("@-");
6919 assert_eq!(recipe.text(), "@-echo hello");
6920 assert!(recipe.is_silent());
6921 assert!(recipe.is_ignore_errors());
6922
6923 recipe.set_prefix("-@");
6924 assert_eq!(recipe.text(), "-@echo hello");
6925 assert!(recipe.is_silent());
6926 assert!(recipe.is_ignore_errors());
6927 }
6928
6929 #[test]
6930 fn test_recipe_replace_text_basic() {
6931 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
6932 let rule = makefile.rules().next().unwrap();
6933 let mut recipe = rule.recipe_nodes().next().unwrap();
6934
6935 recipe.replace_text("echo world");
6936 assert_eq!(recipe.text(), "echo world");
6937
6938 let rule = makefile.rules().next().unwrap();
6940 assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
6941 }
6942
6943 #[test]
6944 fn test_recipe_replace_text_with_prefix() {
6945 let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
6946 let rule = makefile.rules().next().unwrap();
6947 let mut recipe = rule.recipe_nodes().next().unwrap();
6948
6949 recipe.replace_text("@echo goodbye");
6950 assert_eq!(recipe.text(), "@echo goodbye");
6951 assert!(recipe.is_silent());
6952 }
6953
6954 #[test]
6955 fn test_recipe_insert_before_single() {
6956 let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
6957 let rule = makefile.rules().next().unwrap();
6958 let recipe = rule.recipe_nodes().next().unwrap();
6959
6960 recipe.insert_before("echo hello");
6961
6962 let rule = makefile.rules().next().unwrap();
6963 let recipes: Vec<_> = rule.recipes().collect();
6964 assert_eq!(recipes, vec!["echo hello", "echo world"]);
6965 }
6966
6967 #[test]
6968 fn test_recipe_insert_before_multiple() {
6969 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
6970 .parse()
6971 .unwrap();
6972 let rule = makefile.rules().next().unwrap();
6973 let recipes: Vec<_> = rule.recipe_nodes().collect();
6974
6975 recipes[1].insert_before("echo middle");
6977
6978 let rule = makefile.rules().next().unwrap();
6979 let new_recipes: Vec<_> = rule.recipes().collect();
6980 assert_eq!(
6981 new_recipes,
6982 vec!["echo one", "echo middle", "echo two", "echo three"]
6983 );
6984 }
6985
6986 #[test]
6987 fn test_recipe_insert_before_first() {
6988 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
6989 let rule = makefile.rules().next().unwrap();
6990 let recipes: Vec<_> = rule.recipe_nodes().collect();
6991
6992 recipes[0].insert_before("echo zero");
6993
6994 let rule = makefile.rules().next().unwrap();
6995 let new_recipes: Vec<_> = rule.recipes().collect();
6996 assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
6997 }
6998
6999 #[test]
7000 fn test_recipe_insert_after_single() {
7001 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7002 let rule = makefile.rules().next().unwrap();
7003 let recipe = rule.recipe_nodes().next().unwrap();
7004
7005 recipe.insert_after("echo world");
7006
7007 let rule = makefile.rules().next().unwrap();
7008 let recipes: Vec<_> = rule.recipes().collect();
7009 assert_eq!(recipes, vec!["echo hello", "echo world"]);
7010 }
7011
7012 #[test]
7013 fn test_recipe_insert_after_multiple() {
7014 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7015 .parse()
7016 .unwrap();
7017 let rule = makefile.rules().next().unwrap();
7018 let recipes: Vec<_> = rule.recipe_nodes().collect();
7019
7020 recipes[1].insert_after("echo middle");
7022
7023 let rule = makefile.rules().next().unwrap();
7024 let new_recipes: Vec<_> = rule.recipes().collect();
7025 assert_eq!(
7026 new_recipes,
7027 vec!["echo one", "echo two", "echo middle", "echo three"]
7028 );
7029 }
7030
7031 #[test]
7032 fn test_recipe_insert_after_last() {
7033 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7034 let rule = makefile.rules().next().unwrap();
7035 let recipes: Vec<_> = rule.recipe_nodes().collect();
7036
7037 recipes[1].insert_after("echo three");
7038
7039 let rule = makefile.rules().next().unwrap();
7040 let new_recipes: Vec<_> = rule.recipes().collect();
7041 assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
7042 }
7043
7044 #[test]
7045 fn test_recipe_remove_single() {
7046 let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
7047 let rule = makefile.rules().next().unwrap();
7048 let recipe = rule.recipe_nodes().next().unwrap();
7049
7050 recipe.remove();
7051
7052 let rule = makefile.rules().next().unwrap();
7053 assert_eq!(rule.recipes().count(), 0);
7054 }
7055
7056 #[test]
7057 fn test_recipe_remove_first() {
7058 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7059 .parse()
7060 .unwrap();
7061 let rule = makefile.rules().next().unwrap();
7062 let recipes: Vec<_> = rule.recipe_nodes().collect();
7063
7064 recipes[0].remove();
7065
7066 let rule = makefile.rules().next().unwrap();
7067 let new_recipes: Vec<_> = rule.recipes().collect();
7068 assert_eq!(new_recipes, vec!["echo two", "echo three"]);
7069 }
7070
7071 #[test]
7072 fn test_recipe_remove_middle() {
7073 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7074 .parse()
7075 .unwrap();
7076 let rule = makefile.rules().next().unwrap();
7077 let recipes: Vec<_> = rule.recipe_nodes().collect();
7078
7079 recipes[1].remove();
7080
7081 let rule = makefile.rules().next().unwrap();
7082 let new_recipes: Vec<_> = rule.recipes().collect();
7083 assert_eq!(new_recipes, vec!["echo one", "echo three"]);
7084 }
7085
7086 #[test]
7087 fn test_recipe_remove_last() {
7088 let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
7089 .parse()
7090 .unwrap();
7091 let rule = makefile.rules().next().unwrap();
7092 let recipes: Vec<_> = rule.recipe_nodes().collect();
7093
7094 recipes[2].remove();
7095
7096 let rule = makefile.rules().next().unwrap();
7097 let new_recipes: Vec<_> = rule.recipes().collect();
7098 assert_eq!(new_recipes, vec!["echo one", "echo two"]);
7099 }
7100
7101 #[test]
7102 fn test_recipe_multiple_operations() {
7103 let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
7104 let rule = makefile.rules().next().unwrap();
7105 let mut recipe = rule.recipe_nodes().next().unwrap();
7106
7107 recipe.replace_text("echo modified");
7109 assert_eq!(recipe.text(), "echo modified");
7110
7111 recipe.set_prefix("@");
7113 assert_eq!(recipe.text(), "@echo modified");
7114
7115 recipe.insert_after("echo three");
7117
7118 let rule = makefile.rules().next().unwrap();
7120 let recipes: Vec<_> = rule.recipes().collect();
7121 assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
7122 }
7123
7124 #[test]
7125 fn test_from_str_relaxed_valid() {
7126 let input = "all: foo\n\tfoo bar\n";
7127 let (makefile, errors) = Makefile::from_str_relaxed(input);
7128 assert!(errors.is_empty());
7129 assert_eq!(makefile.rules().count(), 1);
7130 assert_eq!(makefile.to_string(), input);
7131 }
7132
7133 #[test]
7134 fn test_from_str_relaxed_with_errors() {
7135 let input = "rule target\n\tcommand\n";
7137 let (makefile, errors) = Makefile::from_str_relaxed(input);
7138 assert!(!errors.is_empty());
7139 assert_eq!(makefile.to_string(), input);
7141 }
7142
7143 #[test]
7144 fn test_positioned_errors_have_valid_ranges() {
7145 let input = "rule target\n\tcommand\n";
7146 let parsed = Makefile::parse(input);
7147 assert!(!parsed.ok());
7148
7149 let positioned = parsed.positioned_errors();
7150 assert!(!positioned.is_empty());
7151
7152 for err in positioned {
7153 let start: u32 = err.range.start().into();
7155 let end: u32 = err.range.end().into();
7156 assert!(start <= end);
7157 assert!((end as usize) <= input.len());
7158 }
7159 }
7160
7161 #[test]
7162 fn test_positioned_errors_point_to_error_location() {
7163 let input = "rule target\n\tcommand\n";
7164 let parsed = Makefile::parse(input);
7165 assert!(!parsed.ok());
7166
7167 let positioned = parsed.positioned_errors();
7168 assert!(!positioned.is_empty());
7169
7170 let err = &positioned[0];
7171 let start: usize = err.range.start().into();
7172 let end: usize = err.range.end().into();
7173 let error_text = &input[start..end];
7175 assert!(!error_text.is_empty());
7176
7177 let tree = parsed.tree();
7179 assert_eq!(tree.to_string(), input);
7180 }
7181
7182 #[test]
7183 fn test_tree_with_errors_preserves_text() {
7184 let input = "rule target\n\tcommand\nVAR = value\n";
7185 let parsed = Makefile::parse(input);
7186 assert!(!parsed.ok());
7187
7188 let tree = parsed.tree();
7189 assert_eq!(tree.to_string(), input);
7190
7191 assert_eq!(tree.variable_definitions().count(), 1);
7193 }
7194}
7195
7196#[cfg(test)]
7197mod test_continuation {
7198 use super::*;
7199
7200 #[test]
7201 fn test_recipe_continuation_lines() {
7202 let makefile_content = r#"override_dh_autoreconf:
7203 set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \
7204 dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \
7205 sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs
7206 dh_autoreconf
7207"#;
7208
7209 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7210 let rule = makefile.rules().next().unwrap();
7211
7212 let recipes: Vec<_> = rule.recipe_nodes().collect();
7213
7214 assert_eq!(recipes.len(), 2);
7216
7217 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";
7220 assert_eq!(recipes[0].text(), expected_first);
7221
7222 assert_eq!(recipes[1].text(), "dh_autoreconf");
7224 }
7225
7226 #[test]
7227 fn test_simple_continuation() {
7228 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n";
7229
7230 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7231 let rule = makefile.rules().next().unwrap();
7232 let recipes: Vec<_> = rule.recipe_nodes().collect();
7233
7234 assert_eq!(recipes.len(), 1);
7235 assert_eq!(recipes[0].text(), "echo hello && \\\n echo world");
7236 }
7237
7238 #[test]
7239 fn test_multiple_continuations() {
7240 let makefile_content = "test:\n\techo line1 && \\\n\t echo line2 && \\\n\t echo line3 && \\\n\t echo line4\n";
7241
7242 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7243 let rule = makefile.rules().next().unwrap();
7244 let recipes: Vec<_> = rule.recipe_nodes().collect();
7245
7246 assert_eq!(recipes.len(), 1);
7247 assert_eq!(
7248 recipes[0].text(),
7249 "echo line1 && \\\n echo line2 && \\\n echo line3 && \\\n echo line4"
7250 );
7251 }
7252
7253 #[test]
7254 fn test_continuation_round_trip() {
7255 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7256
7257 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7258 let output = makefile.to_string();
7259
7260 assert_eq!(output, makefile_content);
7262 }
7263
7264 #[test]
7265 fn test_continuation_with_silent_prefix() {
7266 let makefile_content = "test:\n\t@echo hello && \\\n\t echo world\n";
7267
7268 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7269 let rule = makefile.rules().next().unwrap();
7270 let recipes: Vec<_> = rule.recipe_nodes().collect();
7271
7272 assert_eq!(recipes.len(), 1);
7273 assert_eq!(recipes[0].text(), "@echo hello && \\\n echo world");
7274 assert!(recipes[0].is_silent());
7275 }
7276
7277 #[test]
7278 fn test_mixed_continued_and_non_continued() {
7279 let makefile_content = r#"test:
7280 echo first
7281 echo second && \
7282 echo third
7283 echo fourth
7284"#;
7285
7286 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7287 let rule = makefile.rules().next().unwrap();
7288 let recipes: Vec<_> = rule.recipe_nodes().collect();
7289
7290 assert_eq!(recipes.len(), 3);
7291 assert_eq!(recipes[0].text(), "echo first");
7292 assert_eq!(recipes[1].text(), "echo second && \\\n echo third");
7293 assert_eq!(recipes[2].text(), "echo fourth");
7294 }
7295
7296 #[test]
7297 fn test_continuation_replace_command() {
7298 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7299
7300 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7301 let mut rule = makefile.rules().next().unwrap();
7302
7303 rule.replace_command(0, "echo replaced");
7305
7306 let recipes: Vec<_> = rule.recipe_nodes().collect();
7307 assert_eq!(recipes.len(), 2);
7308 assert_eq!(recipes[0].text(), "echo replaced");
7309 assert_eq!(recipes[1].text(), "echo done");
7310 }
7311
7312 #[test]
7313 fn test_continuation_count() {
7314 let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
7315
7316 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7317 let rule = makefile.rules().next().unwrap();
7318
7319 assert_eq!(rule.recipe_count(), 2);
7321 assert_eq!(rule.recipe_nodes().count(), 2);
7322
7323 let recipes_list: Vec<_> = rule.recipes().collect();
7325 assert_eq!(
7326 recipes_list,
7327 vec!["echo hello && \\\n echo world", "echo done"]
7328 );
7329 }
7330
7331 #[test]
7332 fn test_backslash_in_middle_of_line() {
7333 let makefile_content = "test:\n\techo hello\\nworld\n\techo done\n";
7335
7336 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7337 let rule = makefile.rules().next().unwrap();
7338 let recipes: Vec<_> = rule.recipe_nodes().collect();
7339
7340 assert_eq!(recipes.len(), 2);
7341 assert_eq!(recipes[0].text(), "echo hello\\nworld");
7342 assert_eq!(recipes[1].text(), "echo done");
7343 }
7344
7345 #[test]
7346 fn test_shell_for_loop_with_continuation() {
7347 let makefile_content = r#"override_dh_installman:
7351 for i in foo bar; do \
7352 pod2man --section=1 $$i ; \
7353 done
7354"#;
7355
7356 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7357 let rule = makefile.rules().next().unwrap();
7358
7359 let recipes: Vec<_> = rule.recipe_nodes().collect();
7361 assert_eq!(recipes.len(), 1);
7362
7363 let recipe_text = recipes[0].text();
7365 let expected_recipe = "for i in foo bar; do \\\n\tpod2man --section=1 $$i ; \\\ndone";
7366 assert_eq!(recipe_text, expected_recipe);
7367
7368 let output = makefile.to_string();
7370 assert_eq!(output, makefile_content);
7371 }
7372
7373 #[test]
7374 fn test_shell_for_loop_remove_command() {
7375 let makefile_content = r#"override_dh_installman:
7378 for i in foo bar; do \
7379 pod2man --section=1 $$i ; \
7380 done
7381 echo "Done with man pages"
7382"#;
7383
7384 let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
7385 let mut rule = makefile.rules().next().unwrap();
7386
7387 assert_eq!(rule.recipe_count(), 2);
7389
7390 rule.remove_command(1);
7392
7393 let recipes: Vec<_> = rule.recipe_nodes().collect();
7395 assert_eq!(recipes.len(), 1);
7396
7397 let output = makefile.to_string();
7399 let expected_output = r#"override_dh_installman:
7400 for i in foo bar; do \
7401 pod2man --section=1 $$i ; \
7402 done
7403"#;
7404 assert_eq!(output, expected_output);
7405 }
7406
7407 #[test]
7408 fn test_variable_reference_paren() {
7409 let makefile: Makefile = "CFLAGS = $(BASE_FLAGS) -Wall\n".parse().unwrap();
7410 let refs: Vec<_> = makefile.variable_references().collect();
7411 assert_eq!(refs.len(), 1);
7412 assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7413 assert_eq!(refs[0].to_string(), "$(BASE_FLAGS)");
7414 }
7415
7416 #[test]
7417 fn test_variable_reference_brace() {
7418 let makefile: Makefile = "CFLAGS = ${BASE_FLAGS} -Wall\n".parse().unwrap();
7419 let refs: Vec<_> = makefile.variable_references().collect();
7420 assert_eq!(refs.len(), 1);
7421 assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
7422 assert_eq!(refs[0].to_string(), "${BASE_FLAGS}");
7423 }
7424
7425 #[test]
7426 fn test_variable_reference_in_prerequisites() {
7427 let makefile: Makefile = "all: $(TARGETS)\n".parse().unwrap();
7428 let refs: Vec<_> = makefile.variable_references().collect();
7429 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7430 assert!(names.contains(&"TARGETS".to_string()));
7431 }
7432
7433 #[test]
7434 fn test_variable_reference_multiple() {
7435 let makefile: Makefile =
7436 "CFLAGS = $(BASE_FLAGS) -Wall\nLDFLAGS = $(BASE_LDFLAGS) -lm\nall: $(TARGETS)\n"
7437 .parse()
7438 .unwrap();
7439 let refs: Vec<_> = makefile.variable_references().collect();
7440 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7441 assert!(names.contains(&"BASE_FLAGS".to_string()));
7442 assert!(names.contains(&"BASE_LDFLAGS".to_string()));
7443 assert!(names.contains(&"TARGETS".to_string()));
7444 }
7445
7446 #[test]
7447 fn test_variable_reference_nested() {
7448 let makefile: Makefile = "FOO = $($(INNER))\n".parse().unwrap();
7449 let refs: Vec<_> = makefile.variable_references().collect();
7450 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7451 assert!(names.contains(&"INNER".to_string()));
7452 }
7453
7454 #[test]
7455 fn test_variable_reference_line_col() {
7456 let makefile: Makefile = "A = 1\nB = $(FOO)\n".parse().unwrap();
7457 let refs: Vec<_> = makefile.variable_references().collect();
7458 assert_eq!(refs.len(), 1);
7459 assert_eq!(refs[0].name(), Some("FOO".to_string()));
7460 assert_eq!(refs[0].line(), 1);
7461 assert_eq!(refs[0].column(), 4);
7462 assert_eq!(refs[0].line_col(), (1, 4));
7463 }
7464
7465 #[test]
7466 fn test_variable_reference_no_refs() {
7467 let makefile: Makefile = "A = hello\nall:\n\techo done\n".parse().unwrap();
7468 let refs: Vec<_> = makefile.variable_references().collect();
7469 assert_eq!(refs.len(), 0);
7470 }
7471
7472 #[test]
7473 fn test_variable_reference_mixed_styles() {
7474 let makefile: Makefile = "A = $(FOO) ${BAR}\n".parse().unwrap();
7475 let refs: Vec<_> = makefile.variable_references().collect();
7476 let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
7477 assert_eq!(names.len(), 2);
7478 assert!(names.contains(&"FOO".to_string()));
7479 assert!(names.contains(&"BAR".to_string()));
7480 }
7481
7482 #[test]
7483 fn test_brace_variable_in_prerequisites() {
7484 let makefile: Makefile = "all: ${OBJS}\n".parse().unwrap();
7485 let refs: Vec<_> = makefile.variable_references().collect();
7486 assert_eq!(refs.len(), 1);
7487 assert_eq!(refs[0].name(), Some("OBJS".to_string()));
7488 }
7489
7490 #[test]
7491 fn test_parse_brace_variable_roundtrip() {
7492 let input = "CFLAGS = ${BASE_FLAGS} -Wall\n";
7493 let makefile: Makefile = input.parse().unwrap();
7494 assert_eq!(makefile.to_string(), input);
7495 }
7496
7497 #[test]
7498 fn test_parse_nested_variable_in_value_roundtrip() {
7499 let input = "FOO = $(BAR) baz $(QUUX)\n";
7500 let makefile: Makefile = input.parse().unwrap();
7501 assert_eq!(makefile.to_string(), input);
7502 }
7503
7504 #[test]
7505 fn test_is_function_call() {
7506 let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7507 let refs: Vec<_> = makefile.variable_references().collect();
7508 assert_eq!(refs.len(), 1);
7509 assert!(refs[0].is_function_call());
7510 }
7511
7512 #[test]
7513 fn test_is_function_call_simple_variable() {
7514 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7515 let refs: Vec<_> = makefile.variable_references().collect();
7516 assert_eq!(refs.len(), 1);
7517 assert!(!refs[0].is_function_call());
7518 }
7519
7520 #[test]
7521 fn test_is_function_call_with_commas() {
7522 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7523 let refs: Vec<_> = makefile.variable_references().collect();
7524 assert_eq!(refs.len(), 1);
7525 assert!(refs[0].is_function_call());
7526 }
7527
7528 #[test]
7529 fn test_is_function_call_braces() {
7530 let makefile: Makefile = "FILES = ${wildcard *.c}\n".parse().unwrap();
7531 let refs: Vec<_> = makefile.variable_references().collect();
7532 assert_eq!(refs.len(), 1);
7533 assert!(refs[0].is_function_call());
7534 }
7535
7536 #[test]
7537 fn test_argument_count_simple_variable() {
7538 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7539 let refs: Vec<_> = makefile.variable_references().collect();
7540 assert_eq!(refs[0].argument_count(), 0);
7541 }
7542
7543 #[test]
7544 fn test_argument_count_one_arg() {
7545 let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
7546 let refs: Vec<_> = makefile.variable_references().collect();
7547 assert_eq!(refs[0].argument_count(), 1);
7548 }
7549
7550 #[test]
7551 fn test_argument_count_three_args() {
7552 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7553 let refs: Vec<_> = makefile.variable_references().collect();
7554 assert_eq!(refs[0].argument_count(), 3);
7555 }
7556
7557 #[test]
7558 fn test_argument_index_at_offset_subst() {
7559 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7560 let refs: Vec<_> = makefile.variable_references().collect();
7561 assert_eq!(refs[0].argument_index_at_offset(12), Some(0));
7567 assert_eq!(refs[0].argument_index_at_offset(14), Some(1));
7568 assert_eq!(refs[0].argument_index_at_offset(16), Some(2));
7569 }
7570
7571 #[test]
7572 fn test_argument_index_at_offset_outside() {
7573 let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
7574 let refs: Vec<_> = makefile.variable_references().collect();
7575 assert_eq!(refs[0].argument_index_at_offset(0), None);
7577 assert_eq!(refs[0].argument_index_at_offset(22), None);
7579 }
7580
7581 #[test]
7582 fn test_argument_index_at_offset_simple_variable() {
7583 let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
7584 let refs: Vec<_> = makefile.variable_references().collect();
7585 assert_eq!(refs[0].argument_index_at_offset(11), None);
7586 }
7587
7588 #[test]
7589 fn test_lex_braces() {
7590 use crate::lex::lex;
7591 let tokens = lex("${FOO}");
7592 let kinds: Vec<_> = tokens.iter().map(|(k, _)| *k).collect();
7593 assert!(kinds.contains(&DOLLAR));
7594 assert!(kinds.contains(&LBRACE));
7595 assert!(kinds.contains(&RBRACE));
7596 }
7597}