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