use crate::lex::lex;
use crate::MakefileVariant;
use crate::SyntaxKind;
use crate::SyntaxKind::*;
use rowan::ast::AstNode;
use std::str::FromStr;
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Parse(ParseError),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self {
Error::Io(e) => write!(f, "IO error: {}", e),
Error::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ParseError {
pub errors: Vec<ErrorInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ErrorInfo {
pub message: String,
pub line: usize,
pub context: String,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
for err in &self.errors {
writeln!(f, "Error at line {}: {}", err.line, err.message)?;
writeln!(f, "{}| {}", err.line, err.context)?;
}
Ok(())
}
}
impl std::error::Error for ParseError {}
impl From<ParseError> for Error {
fn from(e: ParseError) -> Self {
Error::Parse(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PositionedParseError {
pub message: String,
pub range: rowan::TextRange,
pub code: Option<String>,
}
impl std::fmt::Display for PositionedParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for PositionedParseError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Lang {}
impl rowan::Language for Lang {
type Kind = SyntaxKind;
fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
}
fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
kind.into()
}
}
use rowan::GreenNode;
use rowan::GreenNodeBuilder;
#[derive(Debug)]
pub(crate) struct Parse {
pub(crate) green_node: GreenNode,
pub(crate) errors: Vec<ErrorInfo>,
pub(crate) positioned_errors: Vec<PositionedParseError>,
}
pub(crate) fn parse(text: &str, variant: Option<MakefileVariant>) -> Parse {
struct Parser {
tokens: Vec<(SyntaxKind, String)>,
builder: GreenNodeBuilder<'static>,
errors: Vec<ErrorInfo>,
positioned_errors: Vec<PositionedParseError>,
token_positions: Vec<(rowan::TextSize, rowan::TextSize)>,
current_token_index: usize,
original_text: String,
variant: Option<MakefileVariant>,
}
impl Parser {
fn error(&mut self, msg: String) {
self.builder.start_node(ERROR.into());
let (line, context) = if self.current() == Some(INDENT) {
let lines: Vec<&str> = self.original_text.lines().collect();
let tab_line = lines
.iter()
.enumerate()
.find(|(_, line)| line.starts_with('\t'))
.map(|(i, _)| i + 1)
.unwrap_or(1);
let next_line = tab_line + 1;
if next_line <= lines.len() {
(next_line, lines[next_line - 1].to_string())
} else {
(tab_line, lines[tab_line - 1].to_string())
}
} else {
let line = self.get_line_number_for_position(self.tokens.len());
(line, self.get_context_for_line(line))
};
let message = if self.current() == Some(INDENT) && !msg.contains("indented") {
if !self.tokens.is_empty() && self.tokens[self.tokens.len() - 1].0 == IDENTIFIER {
"expected ':'".to_string()
} else {
"indented line not part of a rule".to_string()
}
} else {
msg
};
self.errors.push(ErrorInfo {
message: message.clone(),
line,
context,
});
self.add_positioned_error(message, None);
if self.current().is_some() {
self.bump();
}
self.builder.finish_node();
}
fn add_positioned_error(&mut self, message: String, code: Option<String>) {
let range = if self.current_token_index < self.token_positions.len() {
let (start, end) = self.token_positions[self.current_token_index];
rowan::TextRange::new(start, end)
} else {
let end = self
.token_positions
.last()
.map(|(_, end)| *end)
.unwrap_or_else(|| rowan::TextSize::from(0));
rowan::TextRange::new(end, end)
};
self.positioned_errors.push(PositionedParseError {
message,
range,
code,
});
}
fn get_line_number_for_position(&self, position: usize) -> usize {
if position >= self.tokens.len() {
return self.original_text.matches('\n').count() + 1;
}
self.tokens[0..position]
.iter()
.filter(|(kind, _)| *kind == NEWLINE)
.count()
+ 1
}
fn get_context_for_line(&self, line_number: usize) -> String {
self.original_text
.lines()
.nth(line_number - 1)
.unwrap_or("")
.to_string()
}
fn parse_recipe_line(&mut self) {
self.builder.start_node(RECIPE.into());
if self.current() != Some(INDENT) {
self.error("recipe line must start with a tab".to_string());
self.builder.finish_node();
return;
}
self.bump();
loop {
let mut last_text_content: Option<String> = None;
while self.current().is_some() && self.current() != Some(NEWLINE) {
if self.current() == Some(TEXT) {
if let Some((_kind, text)) = self.tokens.last() {
last_text_content = Some(text.clone());
}
}
self.bump();
}
if self.current() == Some(NEWLINE) {
self.bump();
}
let is_continuation = last_text_content
.as_ref()
.map(|text| text.trim_end().ends_with('\\'))
.unwrap_or(false);
if is_continuation {
if self.current() == Some(INDENT) {
self.bump();
continue;
} else {
break;
}
} else {
break;
}
}
self.builder.finish_node();
}
fn parse_rule_target(&mut self) -> bool {
match self.current() {
Some(IDENTIFIER) => {
if self.is_archive_member() {
self.parse_archive_member();
} else {
self.bump();
}
true
}
Some(DOLLAR) => {
self.parse_variable_reference();
true
}
_ => {
self.error("expected rule target".to_string());
false
}
}
}
fn is_archive_member(&self) -> bool {
if self.tokens.len() < 2 {
return false;
}
let current_is_identifier = self.current() == Some(IDENTIFIER);
let next_is_lparen =
self.tokens.len() > 1 && self.tokens[self.tokens.len() - 2].0 == LPAREN;
current_is_identifier && next_is_lparen
}
fn parse_archive_member(&mut self) {
if self.current() == Some(IDENTIFIER) {
self.bump();
}
if self.current() == Some(LPAREN) {
self.bump();
self.builder.start_node(ARCHIVE_MEMBERS.into());
while self.current().is_some() && self.current() != Some(RPAREN) {
match self.current() {
Some(IDENTIFIER) | Some(TEXT) => {
self.builder.start_node(ARCHIVE_MEMBER.into());
self.bump();
self.builder.finish_node();
}
Some(WHITESPACE) => self.bump(),
Some(DOLLAR) => {
self.builder.start_node(ARCHIVE_MEMBER.into());
self.parse_variable_reference();
self.builder.finish_node();
}
_ => break,
}
}
self.builder.finish_node();
if self.current() == Some(RPAREN) {
self.bump();
} else {
self.error("expected ')' to close archive member".to_string());
}
}
}
fn parse_rule_dependencies(&mut self) {
self.builder.start_node(PREREQUISITES.into());
while self.current().is_some() && self.current() != Some(NEWLINE) {
match self.current() {
Some(WHITESPACE) => {
self.bump(); }
Some(IDENTIFIER) => {
self.builder.start_node(PREREQUISITE.into());
if self.is_archive_member() {
self.parse_archive_member();
} else {
self.bump(); }
self.builder.finish_node(); }
Some(DOLLAR) => {
self.builder.start_node(PREREQUISITE.into());
self.parse_variable_reference();
self.builder.finish_node(); }
_ => {
self.bump();
}
}
}
self.builder.finish_node(); }
fn parse_rule_recipes(&mut self) {
let mut conditional_depth = 0;
let mut newline_count = 0;
loop {
match self.current() {
Some(INDENT) => {
newline_count = 0;
self.parse_recipe_line();
}
Some(NEWLINE) => {
newline_count += 1;
self.bump();
}
Some(COMMENT) => {
if conditional_depth == 0 && newline_count >= 1 {
break;
}
newline_count = 0;
self.parse_comment();
}
Some(IDENTIFIER) => {
let token = &self.tokens.last().unwrap().1.clone();
if (token == "ifdef"
|| token == "ifndef"
|| token == "ifeq"
|| token == "ifneq")
&& matches!(self.variant, None | Some(MakefileVariant::GNUMake))
{
if conditional_depth == 0 && newline_count >= 1 {
break;
}
newline_count = 0;
conditional_depth += 1;
self.parse_conditional();
conditional_depth -= 1;
} else if token == "include" || token == "-include" || token == "sinclude" {
if conditional_depth == 0 && newline_count >= 1 {
break;
}
newline_count = 0;
self.parse_include();
} else if token == "else" || token == "endif" {
break;
} else {
if conditional_depth == 0 {
break;
}
break;
}
}
_ => break,
}
}
}
fn find_and_consume_colon(&mut self) -> bool {
self.skip_ws();
if self.current() == Some(OPERATOR)
&& matches!(self.tokens.last().unwrap().1.as_str(), ":" | "::")
{
self.bump();
return true;
}
let has_colon = self
.tokens
.iter()
.rev()
.take_while(|(kind, _)| *kind != NEWLINE)
.any(|(kind, text)| *kind == OPERATOR && (text == ":" || text == "::"));
if has_colon {
while self.current().is_some() && self.current() != Some(NEWLINE) {
if self.current() == Some(OPERATOR)
&& matches!(
self.tokens.last().map(|(_, text)| text.as_str()),
Some(":" | "::")
)
{
self.bump();
return true;
}
self.bump();
}
}
self.error("expected ':'".to_string());
false
}
fn parse_rule(&mut self) {
self.builder.start_node(RULE.into());
self.skip_ws();
self.builder.start_node(TARGETS.into());
let has_target = self.parse_rule_targets();
self.builder.finish_node();
let has_colon = if has_target {
self.find_and_consume_colon()
} else {
false
};
if has_target && has_colon {
self.skip_ws();
self.parse_rule_dependencies();
self.expect_eol();
self.parse_rule_recipes();
}
self.builder.finish_node();
}
fn parse_rule_targets(&mut self) -> bool {
let has_first_target = self.parse_rule_target();
if !has_first_target {
return false;
}
loop {
self.skip_ws();
if self.current() == Some(OPERATOR) && self.tokens.last().unwrap().1 == ":" {
break;
}
match self.current() {
Some(IDENTIFIER) | Some(DOLLAR) => {
if !self.parse_rule_target() {
break;
}
}
_ => break,
}
}
true
}
fn parse_comment(&mut self) {
if self.current() == Some(COMMENT) {
self.bump();
if self.current() == Some(NEWLINE) {
self.bump(); } else if self.current() == Some(WHITESPACE) {
self.skip_ws();
if self.current() == Some(NEWLINE) {
self.bump();
}
}
} else {
self.error("expected comment".to_string());
}
}
fn parse_assignment(&mut self) {
self.builder.start_node(VARIABLE.into());
self.skip_ws();
if self.current() == Some(IDENTIFIER) && self.tokens.last().unwrap().1 == "export" {
self.bump();
self.skip_ws();
}
match self.current() {
Some(IDENTIFIER) => self.bump(),
Some(DOLLAR) => self.parse_variable_reference(),
_ => {
self.error("expected variable name".to_string());
self.builder.finish_node();
return;
}
}
self.skip_ws();
match self.current() {
Some(OPERATOR) => {
let op = &self.tokens.last().unwrap().1;
if ["=", ":=", "::=", ":::=", "+=", "?=", "!="].contains(&op.as_str()) {
self.bump();
self.skip_ws();
self.builder.start_node(EXPR.into());
while self.current().is_some() && self.current() != Some(NEWLINE) {
if self.current() == Some(DOLLAR) {
self.parse_variable_reference();
} else {
self.bump();
}
}
self.builder.finish_node();
if self.current() == Some(NEWLINE) {
self.bump();
} else {
self.error("expected newline after variable value".to_string());
}
} else {
self.error(format!("invalid assignment operator: {}", op));
}
}
Some(NEWLINE) => {
self.bump();
}
None => {
}
_ => self.error("expected assignment operator".to_string()),
}
self.builder.finish_node();
}
fn parse_variable_reference(&mut self) {
self.builder.start_node(EXPR.into());
self.bump();
if self.current() == Some(LPAREN) || self.current() == Some(LBRACE) {
let is_brace = self.current() == Some(LBRACE);
self.bump();
if is_brace {
while self.current().is_some() && self.current() != Some(RBRACE) {
if self.current() == Some(DOLLAR) {
self.parse_variable_reference();
} else {
self.bump();
}
}
if self.current() == Some(RBRACE) {
self.bump(); }
} else {
let mut is_function = false;
if self.current() == Some(IDENTIFIER) {
let function_name = &self.tokens.last().unwrap().1;
let known_functions = [
"shell", "wildcard", "call", "eval", "file", "abspath", "dir",
];
if known_functions.contains(&function_name.as_str()) {
is_function = true;
}
}
if is_function {
self.bump();
self.consume_balanced_parens(1);
} else {
self.parse_parenthesized_expr_internal(true);
}
}
} else if self.current().is_some() && self.current() != Some(NEWLINE) {
self.bump();
} else {
self.error("expected variable name after $".to_string());
}
self.builder.finish_node();
}
fn parse_parenthesized_expr(&mut self) {
self.builder.start_node(EXPR.into());
if self.current() == Some(LPAREN) {
self.bump(); self.parse_parenthesized_expr_internal(false);
} else if self.current() == Some(QUOTE) {
self.parse_quoted_comparison();
} else {
self.error("expected opening parenthesis or quote".to_string());
}
self.builder.finish_node();
}
fn parse_parenthesized_expr_internal(&mut self, is_variable_ref: bool) {
let mut paren_count = 1;
while paren_count > 0 && self.current().is_some() {
match self.current() {
Some(LPAREN) => {
paren_count += 1;
self.bump();
self.builder.start_node(EXPR.into());
}
Some(RPAREN) => {
paren_count -= 1;
self.bump();
if paren_count > 0 {
self.builder.finish_node();
}
}
Some(QUOTE) => {
self.parse_quoted_string();
}
Some(DOLLAR) => {
self.parse_variable_reference();
}
Some(_) => self.bump(),
None => {
self.error(if is_variable_ref {
"unclosed variable reference".to_string()
} else {
"unclosed parenthesis".to_string()
});
break;
}
}
}
if !is_variable_ref {
self.skip_ws();
self.expect_eol();
}
}
fn parse_quoted_comparison(&mut self) {
if self.current() == Some(QUOTE) {
self.bump(); } else {
self.error("expected first quoted argument".to_string());
}
self.skip_ws();
if self.current() == Some(QUOTE) {
self.bump(); } else {
self.error("expected second quoted argument".to_string());
}
self.skip_ws();
self.expect_eol();
}
fn parse_quoted_string(&mut self) {
self.bump(); while !self.is_at_eof() && self.current() != Some(QUOTE) {
self.bump();
}
if self.current() == Some(QUOTE) {
self.bump();
}
}
fn parse_conditional_keyword(&mut self) -> Option<String> {
if self.current() != Some(IDENTIFIER) {
self.error(
"expected conditional keyword (ifdef, ifndef, ifeq, or ifneq)".to_string(),
);
return None;
}
let token = self.tokens.last().unwrap().1.clone();
if !["ifdef", "ifndef", "ifeq", "ifneq"].contains(&token.as_str()) {
self.error(format!("unknown conditional directive: {}", token));
return None;
}
self.bump();
Some(token)
}
fn parse_simple_condition(&mut self) {
self.builder.start_node(EXPR.into());
self.skip_ws();
let mut found_var = false;
while !self.is_at_eof() && self.current() != Some(NEWLINE) {
match self.current() {
Some(WHITESPACE) => self.skip_ws(),
Some(DOLLAR) => {
found_var = true;
self.parse_variable_reference();
}
Some(_) => {
found_var = true;
self.bump();
}
None => break,
}
}
if !found_var {
self.error("expected condition after conditional directive".to_string());
}
self.builder.finish_node();
if self.current() == Some(NEWLINE) {
self.bump();
} else if !self.is_at_eof() {
self.skip_until_newline();
}
}
fn is_conditional_directive(&self, token: &str) -> bool {
token == "ifdef"
|| token == "ifndef"
|| token == "ifeq"
|| token == "ifneq"
|| token == "else"
|| token == "endif"
}
fn handle_conditional_token(&mut self, token: &str, depth: &mut usize) -> bool {
match token {
"ifdef" | "ifndef" | "ifeq" | "ifneq"
if matches!(self.variant, None | Some(MakefileVariant::GNUMake)) =>
{
self.parse_conditional();
true
}
"else" => {
if *depth == 0 {
self.error("else without matching if".to_string());
self.bump();
false
} else {
self.builder.start_node(CONDITIONAL_ELSE.into());
self.bump();
self.skip_ws();
if self.current() == Some(IDENTIFIER) {
let next_token = &self.tokens.last().unwrap().1;
if next_token == "ifdef"
|| next_token == "ifndef"
|| next_token == "ifeq"
|| next_token == "ifneq"
{
match next_token.as_str() {
"ifdef" | "ifndef" => {
self.bump(); self.skip_ws();
self.parse_simple_condition();
}
"ifeq" | "ifneq" => {
self.bump(); self.skip_ws();
self.parse_parenthesized_expr();
}
_ => unreachable!(),
}
} else {
}
} else {
}
self.builder.finish_node(); true
}
}
"endif" => {
if *depth == 0 {
self.error("endif without matching if".to_string());
self.bump();
false
} else {
*depth -= 1;
self.builder.start_node(CONDITIONAL_ENDIF.into());
self.bump();
self.skip_ws();
if self.current() == Some(COMMENT) {
self.parse_comment();
} else if self.current() == Some(NEWLINE) {
self.bump();
} else if self.current() == Some(WHITESPACE) {
self.skip_ws();
if self.current() == Some(NEWLINE) {
self.bump();
}
} else if !self.is_at_eof() {
while !self.is_at_eof() && self.current() != Some(NEWLINE) {
self.bump();
}
if self.current() == Some(NEWLINE) {
self.bump();
}
}
self.builder.finish_node(); true
}
}
_ => false,
}
}
fn parse_conditional(&mut self) {
self.builder.start_node(CONDITIONAL.into());
self.builder.start_node(CONDITIONAL_IF.into());
let Some(token) = self.parse_conditional_keyword() else {
self.skip_until_newline();
self.builder.finish_node(); self.builder.finish_node(); return;
};
self.skip_ws();
match token.as_str() {
"ifdef" | "ifndef" => {
self.parse_simple_condition();
}
"ifeq" | "ifneq" => {
self.parse_parenthesized_expr();
}
_ => unreachable!("Invalid conditional token"),
}
self.skip_ws();
if self.current() == Some(COMMENT) {
self.parse_comment();
}
self.builder.finish_node();
let mut depth = 1;
let mut position_count = std::collections::HashMap::<usize, usize>::new();
let max_repetitions = 15;
while depth > 0 && !self.is_at_eof() {
let current_pos = self.tokens.len();
*position_count.entry(current_pos).or_insert(0) += 1;
if position_count.get(¤t_pos).unwrap() > &max_repetitions {
break;
}
match self.current() {
None => {
self.error("unterminated conditional (missing endif)".to_string());
break;
}
Some(IDENTIFIER) => {
let token = self.tokens.last().unwrap().1.clone();
if !self.handle_conditional_token(&token, &mut depth) {
if token == "include" || token == "-include" || token == "sinclude" {
self.parse_include();
} else {
self.parse_normal_content();
}
}
}
Some(INDENT) => self.parse_recipe_line(),
Some(WHITESPACE) => self.bump(),
Some(COMMENT) => self.parse_comment(),
Some(NEWLINE) => self.bump(),
Some(DOLLAR) => self.parse_normal_content(),
Some(QUOTE) => self.parse_quoted_string(),
Some(_) => {
self.bump();
}
}
}
self.builder.finish_node();
}
fn parse_normal_content(&mut self) {
self.skip_ws();
if self.is_assignment_line() {
self.parse_assignment();
} else {
self.parse_rule();
}
}
fn parse_include(&mut self) {
self.builder.start_node(INCLUDE.into());
if self.current() != Some(IDENTIFIER)
|| (!["include", "-include", "sinclude"]
.contains(&self.tokens.last().unwrap().1.as_str()))
{
self.error("expected include directive".to_string());
self.builder.finish_node();
return;
}
self.bump();
self.skip_ws();
self.builder.start_node(EXPR.into());
let mut found_path = false;
while !self.is_at_eof() && self.current() != Some(NEWLINE) {
match self.current() {
Some(WHITESPACE) => self.skip_ws(),
Some(DOLLAR) => {
found_path = true;
self.parse_variable_reference();
}
Some(_) => {
found_path = true;
self.bump();
}
None => break,
}
}
if !found_path {
self.error("expected file path after include".to_string());
}
self.builder.finish_node();
if self.current() == Some(NEWLINE) {
self.bump();
} else if !self.is_at_eof() {
self.error("expected newline after include".to_string());
self.skip_until_newline();
}
self.builder.finish_node();
}
fn parse_identifier_token(&mut self) -> bool {
let token = &self.tokens.last().unwrap().1;
if token.starts_with("%") {
self.parse_rule();
return true;
}
if token.starts_with("if")
&& matches!(self.variant, None | Some(MakefileVariant::GNUMake))
{
self.parse_conditional();
return true;
}
if token == "include" || token == "-include" || token == "sinclude" {
self.parse_include();
return true;
}
self.parse_normal_content();
true
}
fn parse_token(&mut self) -> bool {
match self.current() {
None => false,
Some(IDENTIFIER) => {
let token = &self.tokens.last().unwrap().1;
if self.is_conditional_directive(token)
&& matches!(self.variant, None | Some(MakefileVariant::GNUMake))
{
self.parse_conditional();
true
} else {
self.parse_identifier_token()
}
}
Some(DOLLAR) => {
self.parse_normal_content();
true
}
Some(NEWLINE) => {
self.builder.start_node(BLANK_LINE.into());
self.bump();
self.builder.finish_node();
true
}
Some(COMMENT) => {
self.parse_comment();
true
}
Some(WHITESPACE) => {
if self.is_end_of_file_or_newline_after_whitespace() {
self.skip_ws();
return true;
}
let look_ahead_pos = self.tokens.len().saturating_sub(1);
let mut is_documentation_or_help = false;
if look_ahead_pos > 0 {
let next_token = &self.tokens[look_ahead_pos - 1];
if next_token.0 == IDENTIFIER
|| next_token.0 == COMMENT
|| next_token.0 == TEXT
{
is_documentation_or_help = true;
}
}
if is_documentation_or_help {
self.skip_ws();
while self.current().is_some() && self.current() != Some(NEWLINE) {
self.bump();
}
if self.current() == Some(NEWLINE) {
self.bump();
}
} else {
self.skip_ws();
}
true
}
Some(INDENT) => {
self.bump();
while !self.is_at_eof() && self.current() != Some(NEWLINE) {
self.bump();
}
if self.current() == Some(NEWLINE) {
self.bump();
}
true
}
Some(kind) => {
self.error(format!("unexpected token {:?}", kind));
self.bump();
true
}
}
}
fn parse(mut self) -> Parse {
self.builder.start_node(ROOT.into());
while self.parse_token() {}
self.builder.finish_node();
Parse {
green_node: self.builder.finish(),
errors: self.errors,
positioned_errors: self.positioned_errors,
}
}
fn is_assignment_line(&mut self) -> bool {
let assignment_ops = ["=", ":=", "::=", ":::=", "+=", "?=", "!="];
let mut pos = self.tokens.len().saturating_sub(1);
let mut seen_identifier = false;
let mut seen_export = false;
while pos > 0 {
let (kind, text) = &self.tokens[pos];
match kind {
NEWLINE => break,
IDENTIFIER if text == "export" => seen_export = true,
IDENTIFIER if !seen_identifier => seen_identifier = true,
OPERATOR if assignment_ops.contains(&text.as_str()) => {
return seen_identifier || seen_export
}
OPERATOR if text == ":" || text == "::" => return false, WHITESPACE => (),
_ if seen_export => return true, _ => return false,
}
pos = pos.saturating_sub(1);
}
seen_export
}
fn bump(&mut self) {
let (kind, text) = self.tokens.pop().unwrap();
self.builder.token(kind.into(), text.as_str());
if self.current_token_index > 0 {
self.current_token_index -= 1;
}
}
fn current(&self) -> Option<SyntaxKind> {
self.tokens.last().map(|(kind, _)| *kind)
}
fn expect_eol(&mut self) {
self.skip_ws();
match self.current() {
Some(NEWLINE) => {
self.bump();
}
None => {
}
n => {
self.error(format!("expected newline, got {:?}", n));
self.skip_until_newline();
}
}
}
fn is_at_eof(&self) -> bool {
self.current().is_none()
}
fn is_at_eof_or_only_whitespace(&self) -> bool {
if self.is_at_eof() {
return true;
}
self.tokens
.iter()
.rev()
.all(|(kind, _)| matches!(*kind, WHITESPACE | NEWLINE))
}
fn skip_ws(&mut self) {
while self.current() == Some(WHITESPACE) {
self.bump()
}
}
fn skip_until_newline(&mut self) {
while !self.is_at_eof() && self.current() != Some(NEWLINE) {
self.bump();
}
if self.current() == Some(NEWLINE) {
self.bump();
}
}
fn consume_balanced_parens(&mut self, start_paren_count: usize) -> usize {
let mut paren_count = start_paren_count;
while paren_count > 0 && self.current().is_some() {
match self.current() {
Some(LPAREN) => {
paren_count += 1;
self.bump();
}
Some(RPAREN) => {
paren_count -= 1;
self.bump();
if paren_count == 0 {
break;
}
}
Some(DOLLAR) => {
self.parse_variable_reference();
}
Some(_) => self.bump(),
None => {
self.error("unclosed parenthesis".to_string());
break;
}
}
}
paren_count
}
fn is_end_of_file_or_newline_after_whitespace(&self) -> bool {
if self.is_at_eof_or_only_whitespace() {
return true;
}
if self.tokens.len() <= 1 {
return true;
}
false
}
}
let mut tokens = lex(text);
let mut token_positions = Vec::with_capacity(tokens.len());
let mut position = rowan::TextSize::from(0);
for (_kind, text) in &tokens {
let start = position;
let end = start + rowan::TextSize::of(text.as_str());
token_positions.push((start, end));
position = end;
}
let current_token_index = tokens.len().saturating_sub(1);
tokens.reverse();
Parser {
tokens,
builder: GreenNodeBuilder::new(),
errors: Vec::new(),
positioned_errors: Vec::new(),
token_positions,
current_token_index,
original_text: text.to_string(),
variant,
}
.parse()
}
pub(crate) type SyntaxNode = rowan::SyntaxNode<Lang>;
#[allow(unused)]
type SyntaxToken = rowan::SyntaxToken<Lang>;
#[allow(unused)]
pub(crate) type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
impl Parse {
fn syntax(&self) -> SyntaxNode {
SyntaxNode::new_root_mut(self.green_node.clone())
}
pub(crate) fn root(&self) -> Makefile {
Makefile::cast(self.syntax()).unwrap()
}
}
fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
let root = node.ancestors().last().unwrap_or_else(|| node.clone());
let mut line = 0;
let mut last_newline_offset = rowan::TextSize::from(0);
for element in root.preorder_with_tokens() {
if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
if token.text_range().start() >= offset {
break;
}
for (idx, _) in token.text().match_indices('\n') {
line += 1;
last_newline_offset =
token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
}
}
}
let column: usize = (offset - last_newline_offset).into();
(line, column)
}
macro_rules! ast_node {
($ast:ident, $kind:ident) => {
#[derive(Clone, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct $ast(SyntaxNode);
impl AstNode for $ast {
type Language = Lang;
fn can_cast(kind: SyntaxKind) -> bool {
kind == $kind
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self(syntax))
} else {
None
}
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
impl $ast {
pub fn line(&self) -> usize {
line_col_at_offset(&self.0, self.0.text_range().start()).0
}
pub fn column(&self) -> usize {
line_col_at_offset(&self.0, self.0.text_range().start()).1
}
pub fn line_col(&self) -> (usize, usize) {
line_col_at_offset(&self.0, self.0.text_range().start())
}
}
impl core::fmt::Display for $ast {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
write!(f, "{}", self.0.text())
}
}
};
}
ast_node!(Makefile, ROOT);
ast_node!(Rule, RULE);
ast_node!(Recipe, RECIPE);
ast_node!(Identifier, IDENTIFIER);
ast_node!(VariableDefinition, VARIABLE);
ast_node!(Include, INCLUDE);
ast_node!(ArchiveMembers, ARCHIVE_MEMBERS);
ast_node!(ArchiveMember, ARCHIVE_MEMBER);
ast_node!(Conditional, CONDITIONAL);
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct VariableReference(SyntaxNode);
impl VariableReference {
pub fn cast(syntax: SyntaxNode) -> Option<Self> {
if syntax.kind() != EXPR {
return None;
}
let mut tokens = syntax
.children_with_tokens()
.filter_map(|it| it.into_token());
let first = tokens.next()?;
if first.kind() != DOLLAR {
return None;
}
tokens.next()?;
Some(Self(syntax))
}
pub fn syntax(&self) -> &SyntaxNode {
&self.0
}
pub fn name(&self) -> Option<String> {
self.0
.children_with_tokens()
.filter_map(|it| it.into_token())
.find(|t| t.kind() == IDENTIFIER)
.map(|t| t.text().to_string())
}
pub fn is_function_call(&self) -> bool {
let mut tokens = self
.0
.children_with_tokens()
.filter_map(|it| it.into_token());
let Some(dollar) = tokens.next() else {
return false;
};
if dollar.kind() != DOLLAR {
return false;
}
let Some(open) = tokens.next() else {
return false;
};
if open.kind() != LPAREN && open.kind() != LBRACE {
return false;
}
let Some(ident) = tokens.next() else {
return false;
};
if ident.kind() != IDENTIFIER {
return false;
}
match tokens.next() {
Some(t) => t.kind() == WHITESPACE || t.kind() == COMMA,
None => false,
}
}
pub fn argument_count(&self) -> usize {
if !self.is_function_call() {
return 0;
}
let mut commas = 0;
let mut depth = 0;
let mut past_name = false;
for element in self.0.children_with_tokens() {
let Some(token) = element.as_token() else {
continue;
};
match token.kind() {
IDENTIFIER if !past_name => {
past_name = true;
}
DOLLAR | LPAREN | LBRACE if !past_name => {}
LPAREN => depth += 1,
RPAREN if depth > 0 => depth -= 1,
COMMA if depth == 0 && past_name => commas += 1,
_ => {}
}
}
if past_name {
commas + 1
} else {
0
}
}
pub fn argument_index_at_offset(&self, offset: usize) -> Option<usize> {
if !self.is_function_call() {
return None;
}
let ref_start: usize = self.0.text_range().start().into();
let ref_end: usize = self.0.text_range().end().into();
if offset < ref_start || offset > ref_end {
return None;
}
let mut arg_index = 0;
let mut depth = 0;
let mut past_name = false;
for element in self.0.children_with_tokens() {
let Some(token) = element.as_token() else {
continue;
};
let token_end: usize = token.text_range().end().into();
match token.kind() {
IDENTIFIER if !past_name => {
past_name = true;
}
DOLLAR | LPAREN | LBRACE if !past_name => {}
LPAREN => depth += 1,
RPAREN if depth > 0 => depth -= 1,
COMMA if depth == 0 && past_name => {
if offset < token_end {
return Some(arg_index);
}
arg_index += 1;
}
_ => {}
}
}
if past_name {
Some(arg_index)
} else {
None
}
}
pub fn line(&self) -> usize {
line_col_at_offset(&self.0, self.0.text_range().start()).0
}
pub fn column(&self) -> usize {
line_col_at_offset(&self.0, self.0.text_range().start()).1
}
pub fn line_col(&self) -> (usize, usize) {
line_col_at_offset(&self.0, self.0.text_range().start())
}
pub fn text_range(&self) -> rowan::TextRange {
self.0.text_range()
}
}
impl core::fmt::Display for VariableReference {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
write!(f, "{}", self.0.text())
}
}
impl Recipe {
pub fn text(&self) -> String {
let tokens: Vec<_> = self
.syntax()
.children_with_tokens()
.filter_map(|it| it.as_token().cloned())
.collect();
if tokens.is_empty() {
return String::new();
}
let start = if tokens.first().map(|t| t.kind()) == Some(INDENT) {
1
} else {
0
};
let end = if tokens.last().map(|t| t.kind()) == Some(NEWLINE) {
tokens.len() - 1
} else {
tokens.len()
};
let mut after_newline = false;
tokens[start..end]
.iter()
.filter_map(|t| match t.kind() {
TEXT => {
after_newline = false;
Some(t.text().to_string())
}
NEWLINE => {
after_newline = true;
Some(t.text().to_string())
}
INDENT if after_newline => {
after_newline = false;
let text = t.text();
Some(text.strip_prefix('\t').unwrap_or(text).to_string())
}
_ => None,
})
.collect()
}
pub fn comment(&self) -> Option<String> {
self.syntax()
.children_with_tokens()
.filter_map(|it| {
if let Some(token) = it.as_token() {
if token.kind() == COMMENT {
return Some(token.text().to_string());
}
}
None
})
.next()
}
pub fn full(&self) -> String {
self.syntax()
.children_with_tokens()
.filter_map(|it| {
if let Some(token) = it.as_token() {
if token.kind() == TEXT || token.kind() == COMMENT {
return Some(token.text().to_string());
}
}
None
})
.collect::<Vec<_>>()
.join("")
}
pub fn parent(&self) -> Option<Rule> {
self.syntax().parent().and_then(Rule::cast)
}
pub fn is_silent(&self) -> bool {
let text = self.text();
text.starts_with('@') || text.starts_with("-@") || text.starts_with("+@")
}
pub fn is_ignore_errors(&self) -> bool {
let text = self.text();
text.starts_with('-') || text.starts_with("@-") || text.starts_with("+-")
}
pub fn set_prefix(&mut self, prefix: &str) {
let text = self.text();
let stripped = text.trim_start_matches(['@', '-', '+']);
let new_text = format!("{}{}", prefix, stripped);
self.replace_text(&new_text);
}
pub fn replace_text(&mut self, new_text: &str) {
let node = self.syntax();
let parent = node.parent().expect("Recipe node must have a parent");
let node_index = node.index();
let mut builder = GreenNodeBuilder::new();
builder.start_node(RECIPE.into());
if let Some(indent_token) = node
.children_with_tokens()
.find(|it| it.as_token().map(|t| t.kind() == INDENT).unwrap_or(false))
{
builder.token(INDENT.into(), indent_token.as_token().unwrap().text());
} else {
builder.token(INDENT.into(), "\t");
}
builder.token(TEXT.into(), new_text);
if let Some(newline_token) = node
.children_with_tokens()
.find(|it| it.as_token().map(|t| t.kind() == NEWLINE).unwrap_or(false))
{
builder.token(NEWLINE.into(), newline_token.as_token().unwrap().text());
} else {
builder.token(NEWLINE.into(), "\n");
}
builder.finish_node();
let new_syntax = SyntaxNode::new_root_mut(builder.finish());
parent.splice_children(node_index..node_index + 1, vec![new_syntax.into()]);
*self = parent
.children_with_tokens()
.nth(node_index)
.and_then(|element| element.into_node())
.and_then(Recipe::cast)
.expect("New recipe node should exist at the same index");
}
pub fn insert_before(&self, text: &str) {
let node = self.syntax();
let parent = node.parent().expect("Recipe node must have a parent");
let node_index = node.index();
let mut builder = GreenNodeBuilder::new();
builder.start_node(RECIPE.into());
builder.token(INDENT.into(), "\t");
builder.token(TEXT.into(), text);
builder.token(NEWLINE.into(), "\n");
builder.finish_node();
let new_syntax = SyntaxNode::new_root_mut(builder.finish());
parent.splice_children(node_index..node_index, vec![new_syntax.into()]);
}
pub fn insert_after(&self, text: &str) {
let node = self.syntax();
let parent = node.parent().expect("Recipe node must have a parent");
let node_index = node.index();
let mut builder = GreenNodeBuilder::new();
builder.start_node(RECIPE.into());
builder.token(INDENT.into(), "\t");
builder.token(TEXT.into(), text);
builder.token(NEWLINE.into(), "\n");
builder.finish_node();
let new_syntax = SyntaxNode::new_root_mut(builder.finish());
parent.splice_children(node_index + 1..node_index + 1, vec![new_syntax.into()]);
}
pub fn remove(&self) {
let node = self.syntax();
let parent = node.parent().expect("Recipe node must have a parent");
let node_index = node.index();
parent.splice_children(node_index..node_index + 1, vec![]);
}
}
pub(crate) fn trim_trailing_newlines(node: &SyntaxNode) {
let mut newlines_to_remove = vec![];
let mut current = node.last_child_or_token();
while let Some(element) = current {
match &element {
rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
newlines_to_remove.push(token.clone());
current = token.prev_sibling_or_token();
}
rowan::NodeOrToken::Node(n) if n.kind() == RECIPE => {
let mut recipe_current = n.last_child_or_token();
while let Some(recipe_element) = recipe_current {
match &recipe_element {
rowan::NodeOrToken::Token(token) if token.kind() == NEWLINE => {
newlines_to_remove.push(token.clone());
recipe_current = token.prev_sibling_or_token();
}
_ => break,
}
}
break; }
_ => break,
}
}
if newlines_to_remove.len() > 1 {
newlines_to_remove.sort_by_key(|t| std::cmp::Reverse(t.index()));
for token in newlines_to_remove.iter().take(newlines_to_remove.len() - 1) {
let parent = token.parent().unwrap();
let idx = token.index();
parent.splice_children(idx..idx + 1, vec![]);
}
}
}
pub(crate) fn remove_with_preceding_comments(node: &SyntaxNode, parent: &SyntaxNode) {
let mut collected_elements = vec![];
let mut found_comment = false;
let mut current = node.prev_sibling_or_token();
while let Some(element) = current {
match &element {
rowan::NodeOrToken::Token(token) => match token.kind() {
COMMENT => {
if token.text().starts_with("#!") {
break; }
found_comment = true;
collected_elements.push(element.clone());
}
NEWLINE | WHITESPACE => {
collected_elements.push(element.clone());
}
_ => break, },
rowan::NodeOrToken::Node(n) => {
if n.kind() == BLANK_LINE {
collected_elements.push(element.clone());
} else {
break; }
}
}
current = element.prev_sibling_or_token();
}
let mut elements_to_remove = vec![];
let mut consecutive_newlines = 0;
for element in collected_elements.iter().rev() {
let should_remove = match element {
rowan::NodeOrToken::Token(token) => match token.kind() {
COMMENT => {
consecutive_newlines = 0;
found_comment
}
NEWLINE => {
consecutive_newlines += 1;
found_comment && consecutive_newlines <= 1
}
WHITESPACE => found_comment,
_ => false,
},
rowan::NodeOrToken::Node(n) => {
if n.kind() == BLANK_LINE {
consecutive_newlines += 1;
found_comment && consecutive_newlines <= 1
} else {
false
}
}
};
if should_remove {
elements_to_remove.push(element.clone());
}
}
let mut all_to_remove = vec![rowan::NodeOrToken::Node(node.clone())];
all_to_remove.extend(elements_to_remove.into_iter().rev());
all_to_remove.sort_by_key(|el| std::cmp::Reverse(el.index()));
for element in all_to_remove {
let idx = element.index();
parent.splice_children(idx..idx + 1, vec![]);
}
}
impl FromStr for Rule {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Rule::parse(s).to_rule_result()
}
}
impl FromStr for Makefile {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Makefile::parse(s).to_result()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::makefile::MakefileItem;
use crate::pattern::matches_pattern;
#[test]
fn test_conditionals() {
let code = "ifdef DEBUG\n DEBUG_FLAG := 1\nendif\n";
let mut buf = code.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse basic ifdef");
assert!(makefile.code().contains("DEBUG_FLAG"));
let code =
"ifeq ($(OS),Windows_NT)\n RESULT := windows\nelse\n RESULT := unix\nendif\n";
let mut buf = code.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq/ifneq");
assert!(makefile.code().contains("RESULT"));
assert!(makefile.code().contains("windows"));
let code = "ifdef DEBUG\n CFLAGS += -g\n ifdef VERBOSE\n CFLAGS += -v\n endif\nelse\n CFLAGS += -O2\nendif\n";
let mut buf = code.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf)
.expect("Failed to parse nested conditionals with else");
assert!(makefile.code().contains("CFLAGS"));
assert!(makefile.code().contains("VERBOSE"));
let code = "ifdef DEBUG\nendif\n";
let mut buf = code.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse empty conditionals");
assert!(makefile.code().contains("ifdef DEBUG"));
let code = "ifeq ($(OS),Windows)\n EXT := .exe\nelse ifeq ($(OS),Linux)\n EXT := .bin\nelse\n EXT := .out\nendif\n";
let mut buf = code.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse conditionals with else ifeq");
assert!(makefile.code().contains("EXT"));
let code = "ifXYZ DEBUG\nDEBUG := 1\nendif\n";
let mut buf = code.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse with recovery");
assert!(makefile.code().contains("DEBUG"));
let code = "ifdef \nDEBUG := 1\nendif\n";
let mut buf = code.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf)
.expect("Failed to parse with recovery - missing condition");
assert!(makefile.code().contains("DEBUG"));
}
#[test]
fn test_parse_simple() {
const SIMPLE: &str = r#"VARIABLE = value
rule: dependency
command
"#;
let parsed = parse(SIMPLE, None);
assert!(parsed.errors.is_empty());
let node = parsed.syntax();
assert_eq!(
format!("{:#?}", node),
r#"ROOT@0..44
VARIABLE@0..17
IDENTIFIER@0..8 "VARIABLE"
WHITESPACE@8..9 " "
OPERATOR@9..10 "="
WHITESPACE@10..11 " "
EXPR@11..16
IDENTIFIER@11..16 "value"
NEWLINE@16..17 "\n"
BLANK_LINE@17..18
NEWLINE@17..18 "\n"
RULE@18..44
TARGETS@18..22
IDENTIFIER@18..22 "rule"
OPERATOR@22..23 ":"
WHITESPACE@23..24 " "
PREREQUISITES@24..34
PREREQUISITE@24..34
IDENTIFIER@24..34 "dependency"
NEWLINE@34..35 "\n"
RECIPE@35..44
INDENT@35..36 "\t"
TEXT@36..43 "command"
NEWLINE@43..44 "\n"
"#
);
let root = parsed.root();
let mut rules = root.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
let rule = rules.pop().unwrap();
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dependency"]);
assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
let mut variables = root.variable_definitions().collect::<Vec<_>>();
assert_eq!(variables.len(), 1);
let variable = variables.pop().unwrap();
assert_eq!(variable.name(), Some("VARIABLE".to_string()));
assert_eq!(variable.raw_value(), Some("value".to_string()));
}
#[test]
fn test_parse_export_assign() {
const EXPORT: &str = r#"export VARIABLE := value
"#;
let parsed = parse(EXPORT, None);
assert!(parsed.errors.is_empty());
let node = parsed.syntax();
assert_eq!(
format!("{:#?}", node),
r#"ROOT@0..25
VARIABLE@0..25
IDENTIFIER@0..6 "export"
WHITESPACE@6..7 " "
IDENTIFIER@7..15 "VARIABLE"
WHITESPACE@15..16 " "
OPERATOR@16..18 ":="
WHITESPACE@18..19 " "
EXPR@19..24
IDENTIFIER@19..24 "value"
NEWLINE@24..25 "\n"
"#
);
let root = parsed.root();
let mut variables = root.variable_definitions().collect::<Vec<_>>();
assert_eq!(variables.len(), 1);
let variable = variables.pop().unwrap();
assert_eq!(variable.name(), Some("VARIABLE".to_string()));
assert_eq!(variable.raw_value(), Some("value".to_string()));
}
#[test]
fn test_parse_multiple_prerequisites() {
const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2
command
"#;
let parsed = parse(MULTIPLE_PREREQUISITES, None);
assert!(parsed.errors.is_empty());
let node = parsed.syntax();
assert_eq!(
format!("{:#?}", node),
r#"ROOT@0..40
RULE@0..40
TARGETS@0..4
IDENTIFIER@0..4 "rule"
OPERATOR@4..5 ":"
WHITESPACE@5..6 " "
PREREQUISITES@6..29
PREREQUISITE@6..17
IDENTIFIER@6..17 "dependency1"
WHITESPACE@17..18 " "
PREREQUISITE@18..29
IDENTIFIER@18..29 "dependency2"
NEWLINE@29..30 "\n"
RECIPE@30..39
INDENT@30..31 "\t"
TEXT@31..38 "command"
NEWLINE@38..39 "\n"
NEWLINE@39..40 "\n"
"#
);
let root = parsed.root();
let rule = root.rules().next().unwrap();
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
assert_eq!(
rule.prerequisites().collect::<Vec<_>>(),
vec!["dependency1", "dependency2"]
);
assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
}
#[test]
fn test_add_rule() {
let mut makefile = Makefile::new();
let rule = makefile.add_rule("rule");
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
assert_eq!(
rule.prerequisites().collect::<Vec<_>>(),
Vec::<String>::new()
);
assert_eq!(makefile.to_string(), "rule:\n");
}
#[test]
fn test_add_rule_with_shebang() {
let content = r#"#!/usr/bin/make -f
build: blah
$(MAKE) install
clean:
dh_clean
"#;
let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
let initial_count = makefile.rules().count();
assert_eq!(initial_count, 2);
let rule = makefile.add_rule("build-indep");
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["build-indep"]);
assert_eq!(makefile.rules().count(), initial_count + 1);
}
#[test]
fn test_add_rule_formatting() {
let content = r#"build: blah
$(MAKE) install
clean:
dh_clean
"#;
let mut makefile = Makefile::read_relaxed(content.as_bytes()).unwrap();
let mut rule = makefile.add_rule("build-indep");
rule.add_prerequisite("build").unwrap();
let expected = r#"build: blah
$(MAKE) install
clean:
dh_clean
build-indep: build
"#;
assert_eq!(makefile.to_string(), expected);
}
#[test]
fn test_push_command() {
let mut makefile = Makefile::new();
let mut rule = makefile.add_rule("rule");
rule.push_command("command");
rule.push_command("command2");
assert_eq!(
rule.recipes().collect::<Vec<_>>(),
vec!["command", "command2"]
);
rule.push_command("command3");
assert_eq!(
rule.recipes().collect::<Vec<_>>(),
vec!["command", "command2", "command3"]
);
assert_eq!(
makefile.to_string(),
"rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
);
assert_eq!(
rule.to_string(),
"rule:\n\tcommand\n\tcommand2\n\tcommand3\n"
);
}
#[test]
fn test_replace_command() {
let mut makefile = Makefile::new();
let mut rule = makefile.add_rule("rule");
rule.push_command("command");
rule.push_command("command2");
assert_eq!(
rule.recipes().collect::<Vec<_>>(),
vec!["command", "command2"]
);
rule.replace_command(0, "new command");
assert_eq!(
rule.recipes().collect::<Vec<_>>(),
vec!["new command", "command2"]
);
assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n");
assert_eq!(rule.to_string(), "rule:\n\tnew command\n\tcommand2\n");
}
#[test]
fn test_replace_command_with_comments() {
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";
let makefile = Makefile::read_relaxed(&content[..]).unwrap();
let mut rule = makefile.rules().next().unwrap();
assert_eq!(rule.recipe_nodes().count(), 2);
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes[0].text(), ""); assert_eq!(
recipes[1].text(),
"dh_strip --dbgsym-migration='amule-dbg (<< 1:2.3.2-2~)'"
);
assert!(rule.replace_command(1, "dh_strip"));
assert_eq!(rule.recipe_nodes().count(), 2);
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes[0].text(), ""); assert_eq!(recipes[1].text(), "dh_strip");
}
#[test]
fn test_parse_rule_without_newline() {
let rule = "rule: dependency\n\tcommand".parse::<Rule>().unwrap();
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
let rule = "rule: dependency".parse::<Rule>().unwrap();
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["rule"]);
assert_eq!(rule.recipes().collect::<Vec<_>>(), Vec::<String>::new());
}
#[test]
fn test_parse_makefile_without_newline() {
let makefile = "rule: dependency\n\tcommand".parse::<Makefile>().unwrap();
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_from_reader() {
let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap();
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_parse_with_tab_after_last_newline() {
let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n\t".as_bytes()).unwrap();
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_parse_with_space_after_last_newline() {
let makefile = Makefile::from_reader("rule: dependency\n\tcommand\n ".as_bytes()).unwrap();
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_parse_with_comment_after_last_newline() {
let makefile =
Makefile::from_reader("rule: dependency\n\tcommand\n#comment".as_bytes()).unwrap();
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_parse_with_variable_rule() {
let makefile =
Makefile::from_reader("RULE := rule\n$(RULE): dependency\n\tcommand".as_bytes())
.unwrap();
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name(), Some("RULE".to_string()));
assert_eq!(vars[0].raw_value(), Some("rule".to_string()));
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["$(RULE)"]);
assert_eq!(
rules[0].prerequisites().collect::<Vec<_>>(),
vec!["dependency"]
);
assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
}
#[test]
fn test_parse_with_variable_dependency() {
let makefile =
Makefile::from_reader("DEP := dependency\nrule: $(DEP)\n\tcommand".as_bytes()).unwrap();
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name(), Some("DEP".to_string()));
assert_eq!(vars[0].raw_value(), Some("dependency".to_string()));
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
assert_eq!(rules[0].prerequisites().collect::<Vec<_>>(), vec!["$(DEP)"]);
assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["command"]);
}
#[test]
fn test_parse_with_variable_command() {
let makefile =
Makefile::from_reader("COM := command\nrule: dependency\n\t$(COM)".as_bytes()).unwrap();
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name(), Some("COM".to_string()));
assert_eq!(vars[0].raw_value(), Some("command".to_string()));
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["rule"]);
assert_eq!(
rules[0].prerequisites().collect::<Vec<_>>(),
vec!["dependency"]
);
assert_eq!(rules[0].recipes().collect::<Vec<_>>(), vec!["$(COM)"]);
}
#[test]
fn test_regular_line_error_reporting() {
let input = "rule target\n\tcommand";
let parsed = parse(input, None);
let direct_error = &parsed.errors[0];
assert_eq!(direct_error.line, 2);
assert!(
direct_error.message.contains("expected"),
"Error message should contain 'expected': {}",
direct_error.message
);
assert_eq!(direct_error.context, "\tcommand");
let reader_result = Makefile::from_reader(input.as_bytes());
let parse_error = match reader_result {
Ok(_) => panic!("Expected Parse error from from_reader"),
Err(err) => match err {
self::Error::Parse(parse_err) => parse_err,
_ => panic!("Expected Parse error"),
},
};
let error_text = parse_error.to_string();
assert!(error_text.contains("Error at line 2:"));
assert!(error_text.contains("2| \tcommand"));
}
#[test]
fn test_parsing_error_context_with_bad_syntax() {
let input = "#begin comment\n\t(╯°□°)╯︵ ┻━┻\n#end comment";
match Makefile::from_reader(input.as_bytes()) {
Ok(makefile) => {
assert_eq!(
makefile.rules().count(),
0,
"Should not have found any rules"
);
}
Err(err) => match err {
self::Error::Parse(error) => {
assert!(error.errors[0].line >= 2, "Error line should be at least 2");
assert!(
!error.errors[0].context.is_empty(),
"Error context should not be empty"
);
}
_ => panic!("Unexpected error type"),
},
};
}
#[test]
fn test_error_message_format() {
let parse_error = ParseError {
errors: vec![ErrorInfo {
message: "test error".to_string(),
line: 42,
context: "some problematic code".to_string(),
}],
};
let error_text = parse_error.to_string();
assert!(error_text.contains("Error at line 42: test error"));
assert!(error_text.contains("42| some problematic code"));
}
#[test]
fn test_line_number_calculation() {
let test_cases = [
("rule dependency\n\tcommand", 2), ("#comment\n\t(╯°□°)╯︵ ┻━┻", 2), ("var = value\n#comment\n\tindented line", 3), ];
for (input, expected_line) in test_cases {
match input.parse::<Makefile>() {
Ok(_) => {
continue;
}
Err(err) => {
if let Error::Parse(parse_err) = err {
assert_eq!(
parse_err.errors[0].line, expected_line,
"Line number should match the expected line"
);
if parse_err.errors[0].message.contains("indented") {
assert!(
parse_err.errors[0].context.starts_with('\t'),
"Context for indentation errors should include the tab character"
);
}
} else {
panic!("Expected parse error, got: {:?}", err);
}
}
}
}
}
#[test]
fn test_conditional_features() {
let code = r#"
# Set variables based on DEBUG flag
ifdef DEBUG
CFLAGS += -g -DDEBUG
else
CFLAGS = -O2
endif
# Define a build rule
all: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
"#;
let mut buf = code.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse conditional features");
assert!(!makefile.code().is_empty(), "Makefile has content");
let rules = makefile.rules().collect::<Vec<_>>();
assert!(!rules.is_empty(), "Should have found rules");
assert!(code.contains("ifdef DEBUG"));
assert!(code.contains("endif"));
let code_with_var = r#"
# Define a variable first
CC = gcc
ifdef DEBUG
CFLAGS += -g -DDEBUG
else
CFLAGS = -O2
endif
all: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
"#;
let mut buf = code_with_var.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse with explicit variable");
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert!(
!vars.is_empty(),
"Should have found at least the CC variable definition"
);
}
#[test]
fn test_include_directive() {
let parsed = parse(
"include config.mk\ninclude $(TOPDIR)/rules.mk\ninclude *.mk\n",
None,
);
assert!(parsed.errors.is_empty());
let node = parsed.syntax();
assert!(format!("{:#?}", node).contains("INCLUDE@"));
}
#[test]
fn test_export_variables() {
let parsed = parse("export SHELL := /bin/bash\n", None);
assert!(parsed.errors.is_empty());
let makefile = parsed.root();
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 1);
let shell_var = vars
.iter()
.find(|v| v.name() == Some("SHELL".to_string()))
.unwrap();
assert!(shell_var.raw_value().unwrap().contains("bin/bash"));
}
#[test]
fn test_bare_export_variable() {
let parsed = parse(
"DEB_CFLAGS_MAINT_APPEND = -Wno-error\nexport DEB_CFLAGS_MAINT_APPEND\n\n%:\n\tdh $@\n",
None,
);
assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
let makefile = parsed.root();
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 2);
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
assert!(rules[0].targets().any(|t| t == "%"));
assert!(makefile.find_rule_by_target_pattern("build-arch").is_some());
}
#[test]
fn test_bare_export_at_eof() {
let parsed = parse("VAR = value\nexport VAR", None);
assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
let makefile = parsed.root();
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 2);
assert_eq!(makefile.rules().count(), 0);
}
#[test]
fn test_bare_export_does_not_eat_include() {
let parsed = parse("VAR = value\nexport VAR\ninclude other.mk\n", None);
assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
let makefile = parsed.root();
assert_eq!(makefile.includes().count(), 1);
assert_eq!(
makefile.included_files().collect::<Vec<_>>(),
vec!["other.mk"]
);
}
#[test]
fn test_bare_export_multiple() {
let parsed = parse(
"A = 1\nB = 2\nexport A\nexport B\n\nall:\n\techo done\n",
None,
);
assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors);
let makefile = parsed.root();
assert_eq!(makefile.variable_definitions().count(), 4);
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
assert!(rules[0].targets().any(|t| t == "all"));
}
#[test]
fn test_parse_error_does_not_cross_lines() {
let parsed = parse("notarule\n\nbuild-arch:\n\techo arch\n", None);
let makefile = parsed.root();
let rules = makefile.rules().collect::<Vec<_>>();
assert!(
rules.iter().any(|r| r.targets().any(|t| t == "build-arch")),
"build-arch rule should be parsed despite earlier error; rules: {:?}",
rules
.iter()
.map(|r| r.targets().collect::<Vec<_>>())
.collect::<Vec<_>>()
);
}
#[test]
fn test_pyfai_rules_full() {
let input = "\
#!/usr/bin/make -f
export DH_VERBOSE=1
export PYBUILD_NAME=pyfai
DEB_CFLAGS_MAINT_APPEND = -Wno-error=incompatible-pointer-types
export DEB_CFLAGS_MAINT_APPEND
PY3VER := $(shell py3versions -dv)
include /usr/share/dpkg/pkg-info.mk # sets SOURCE_DATE_EPOCH
%:
\tdh $@ --buildsystem=pybuild
override_dh_auto_build-arch:
\tPYBUILD_BUILD_ARGS=\"-Ccompile-args=--verbose\" dh_auto_build
override_dh_auto_build-indep: override_dh_auto_build-arch
\tsphinx-build -N -bhtml doc/source build/html
override_dh_auto_test:
execute_after_dh_auto_install:
\tdh_install -p pyfai debian/python3-pyfai/usr/bin /usr
";
let parsed = parse(input, None);
let makefile = parsed.root();
assert_eq!(makefile.includes().count(), 1);
assert!(
makefile.find_rule_by_target_pattern("build-arch").is_some(),
"build-arch should match via %: pattern rule"
);
assert!(
makefile
.find_rule_by_target_pattern("build-indep")
.is_some(),
"build-indep should match via %: pattern rule"
);
let rule_targets: Vec<Vec<String>> =
makefile.rules().map(|r| r.targets().collect()).collect();
assert!(
rule_targets.iter().any(|t| t.contains(&"%".to_string())),
"missing %: rule; got: {:?}",
rule_targets
);
assert!(
rule_targets
.iter()
.any(|t| t.contains(&"override_dh_auto_build-arch".to_string())),
"missing override_dh_auto_build-arch; got: {:?}",
rule_targets
);
assert!(
rule_targets
.iter()
.any(|t| t.contains(&"override_dh_auto_test".to_string())),
"missing override_dh_auto_test; got: {:?}",
rule_targets
);
assert!(
rule_targets
.iter()
.any(|t| t.contains(&"execute_after_dh_auto_install".to_string())),
"missing execute_after_dh_auto_install; got: {:?}",
rule_targets
);
}
#[test]
fn test_variable_scopes() {
let parsed = parse(
"SIMPLE = value\nIMMEDIATE := value\nCONDITIONAL ?= value\nAPPEND += value\n",
None,
);
assert!(parsed.errors.is_empty());
let makefile = parsed.root();
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 4);
let var_names: Vec<_> = vars.iter().filter_map(|v| v.name()).collect();
assert!(var_names.contains(&"SIMPLE".to_string()));
assert!(var_names.contains(&"IMMEDIATE".to_string()));
assert!(var_names.contains(&"CONDITIONAL".to_string()));
assert!(var_names.contains(&"APPEND".to_string()));
}
#[test]
fn test_pattern_rule_parsing() {
let parsed = parse("%.o: %.c\n\t$(CC) -c -o $@ $<\n", None);
assert!(parsed.errors.is_empty());
let makefile = parsed.root();
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].targets().next().unwrap(), "%.o");
assert!(rules[0].recipes().next().unwrap().contains("$@"));
}
#[test]
fn test_include_variants() {
let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\ninclude $(VAR)/generated.mk\n";
let parsed = parse(makefile_str, None);
assert!(parsed.errors.is_empty());
let node = parsed.syntax();
let debug_str = format!("{:#?}", node);
assert_eq!(debug_str.matches("INCLUDE@").count(), 4);
let makefile = parsed.root();
let include_count = makefile
.syntax()
.children()
.filter(|child| child.kind() == INCLUDE)
.count();
assert_eq!(include_count, 4);
assert!(makefile
.included_files()
.any(|path| path.contains("$(VAR)")));
}
#[test]
fn test_include_api() {
let makefile_str = "include simple.mk\n-include optional.mk\nsinclude synonym.mk\n";
let makefile: Makefile = makefile_str.parse().unwrap();
let includes: Vec<_> = makefile.includes().collect();
assert_eq!(includes.len(), 3);
assert!(!includes[0].is_optional()); assert!(includes[1].is_optional()); assert!(includes[2].is_optional());
let files: Vec<_> = makefile.included_files().collect();
assert_eq!(files, vec!["simple.mk", "optional.mk", "synonym.mk"]);
assert_eq!(includes[0].path(), Some("simple.mk".to_string()));
assert_eq!(includes[1].path(), Some("optional.mk".to_string()));
assert_eq!(includes[2].path(), Some("synonym.mk".to_string()));
}
#[test]
fn test_include_integration() {
let phony_makefile = Makefile::from_reader(
".PHONY: build\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
.as_bytes()
).unwrap();
assert_eq!(phony_makefile.rules().count(), 2);
let normal_rules_count = phony_makefile
.rules()
.filter(|r| !r.targets().any(|t| t.starts_with('.')))
.count();
assert_eq!(normal_rules_count, 1);
assert_eq!(phony_makefile.includes().count(), 1);
assert_eq!(phony_makefile.included_files().next().unwrap(), ".env");
let simple_makefile = Makefile::from_reader(
"\n\nVERBOSE ?= 0\n\n# comment\n-include .env\n\nrule: dependency\n\tcommand"
.as_bytes(),
)
.unwrap();
assert_eq!(simple_makefile.rules().count(), 1);
assert_eq!(simple_makefile.includes().count(), 1);
}
#[test]
fn test_real_conditional_directives() {
let conditional = "ifdef DEBUG\nCFLAGS = -g\nelse\nCFLAGS = -O2\nendif\n";
let mut buf = conditional.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse basic if/else conditional");
let code = makefile.code();
assert!(code.contains("ifdef DEBUG"));
assert!(code.contains("else"));
assert!(code.contains("endif"));
let nested = "ifdef DEBUG\nCFLAGS = -g\nifdef VERBOSE\nCFLAGS += -v\nendif\nendif\n";
let mut buf = nested.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse nested ifdef");
let code = makefile.code();
assert!(code.contains("ifdef DEBUG"));
assert!(code.contains("ifdef VERBOSE"));
let ifeq = "ifeq ($(OS),Windows_NT)\nTARGET = app.exe\nelse\nTARGET = app\nendif\n";
let mut buf = ifeq.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse ifeq form");
let code = makefile.code();
assert!(code.contains("ifeq"));
assert!(code.contains("Windows_NT"));
}
#[test]
fn test_indented_text_outside_rules() {
let help_text = "help:\n\t@echo \"Available targets:\"\n\t@echo \" help show help\"\n";
let parsed = parse(help_text, None);
assert!(parsed.errors.is_empty());
let root = parsed.root();
let rules = root.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
let help_rule = &rules[0];
let recipes = help_rule.recipes().collect::<Vec<_>>();
assert_eq!(recipes.len(), 2);
assert!(recipes[0].contains("Available targets"));
assert!(recipes[1].contains("help"));
}
#[test]
fn test_comment_handling_in_recipes() {
let recipe_comment = "build:\n\t# This is a comment\n\tgcc -o app main.c\n";
let parsed = parse(recipe_comment, None);
assert!(
parsed.errors.is_empty(),
"Should parse recipe with comments without errors"
);
let root = parsed.root();
let rules = root.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1, "Should find exactly one rule");
let build_rule = &rules[0];
assert_eq!(
build_rule.targets().collect::<Vec<_>>(),
vec!["build"],
"Rule should have 'build' as target"
);
let recipes = build_rule.recipe_nodes().collect::<Vec<_>>();
assert_eq!(recipes.len(), 2, "Should find two recipe nodes");
assert_eq!(recipes[0].text(), "");
assert_eq!(
recipes[0].comment(),
Some("# This is a comment".to_string())
);
assert_eq!(recipes[1].text(), "gcc -o app main.c");
assert_eq!(recipes[1].comment(), None);
}
#[test]
fn test_multiline_variables() {
let multiline = "SOURCES = main.c \\\n util.c\n";
let parsed = parse(multiline, None);
let root = parsed.root();
let vars = root.variable_definitions().collect::<Vec<_>>();
assert!(!vars.is_empty(), "Should find at least one variable");
let operators = "CFLAGS := -Wall \\\n -Werror\n";
let parsed_operators = parse(operators, None);
let root = parsed_operators.root();
let vars = root.variable_definitions().collect::<Vec<_>>();
assert!(
!vars.is_empty(),
"Should find at least one variable with := operator"
);
let append = "LDFLAGS += -L/usr/lib \\\n -lm\n";
let parsed_append = parse(append, None);
let root = parsed_append.root();
let vars = root.variable_definitions().collect::<Vec<_>>();
assert!(
!vars.is_empty(),
"Should find at least one variable with += operator"
);
}
#[test]
fn test_whitespace_and_eof_handling() {
let blank_lines = "VAR = value\n\n\n";
let parsed_blank = parse(blank_lines, None);
let root = parsed_blank.root();
let vars = root.variable_definitions().collect::<Vec<_>>();
assert_eq!(
vars.len(),
1,
"Should find one variable in blank lines test"
);
let trailing_space = "VAR = value \n";
let parsed_space = parse(trailing_space, None);
let root = parsed_space.root();
let vars = root.variable_definitions().collect::<Vec<_>>();
assert_eq!(
vars.len(),
1,
"Should find one variable in trailing space test"
);
let no_newline = "VAR = value";
let parsed_no_newline = parse(no_newline, None);
let root = parsed_no_newline.root();
let vars = root.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 1, "Should find one variable in no newline test");
assert_eq!(
vars[0].name(),
Some("VAR".to_string()),
"Variable name should be VAR"
);
}
#[test]
fn test_complex_variable_references() {
let wildcard = "SOURCES = $(wildcard *.c)\n";
let parsed = parse(wildcard, None);
assert!(parsed.errors.is_empty());
let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
let parsed = parse(nested, None);
assert!(parsed.errors.is_empty());
let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
let parsed = parse(patsubst, None);
assert!(parsed.errors.is_empty());
}
#[test]
fn test_complex_variable_references_minimal() {
let wildcard = "SOURCES = $(wildcard *.c)\n";
let parsed = parse(wildcard, None);
assert!(parsed.errors.is_empty());
let nested = "PREFIX = /usr\nBINDIR = $(PREFIX)/bin\n";
let parsed = parse(nested, None);
assert!(parsed.errors.is_empty());
let patsubst = "OBJECTS = $(patsubst %.c,%.o,$(SOURCES))\n";
let parsed = parse(patsubst, None);
assert!(parsed.errors.is_empty());
}
#[test]
fn test_multiline_variable_with_backslash() {
let content = r#"
LONG_VAR = This is a long variable \
that continues on the next line \
and even one more line
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse multiline variable");
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(
vars.len(),
1,
"Expected 1 variable but found {}",
vars.len()
);
let var_value = vars[0].raw_value();
assert!(var_value.is_some(), "Variable value is None");
let value_str = var_value.unwrap();
assert!(
value_str.contains("long variable"),
"Value doesn't contain expected content"
);
}
#[test]
fn test_multiline_variable_with_mixed_operators() {
let content = r#"
PREFIX ?= /usr/local
CFLAGS := -Wall -O2 \
-I$(PREFIX)/include \
-DDEBUG
"#;
let mut buf = content.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf)
.expect("Failed to parse multiline variable with operators");
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert!(
!vars.is_empty(),
"Expected at least 1 variable, found {}",
vars.len()
);
let prefix_var = vars
.iter()
.find(|v| v.name().unwrap_or_default() == "PREFIX");
assert!(prefix_var.is_some(), "Expected to find PREFIX variable");
assert!(
prefix_var.unwrap().raw_value().is_some(),
"PREFIX variable has no value"
);
let cflags_var = vars
.iter()
.find(|v| v.name().unwrap_or_default().contains("CFLAGS"));
assert!(
cflags_var.is_some(),
"Expected to find CFLAGS variable (or part of it)"
);
}
#[test]
fn test_indented_help_text() {
let content = r#"
.PHONY: help
help:
@echo "Available targets:"
@echo " build - Build the project"
@echo " test - Run tests"
@echo " clean - Remove build artifacts"
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse indented help text");
let rules = makefile.rules().collect::<Vec<_>>();
assert!(!rules.is_empty(), "Expected at least one rule");
let help_rule = rules.iter().find(|r| r.targets().any(|t| t == "help"));
assert!(help_rule.is_some(), "Expected to find help rule");
let recipes = help_rule.unwrap().recipes().collect::<Vec<_>>();
assert!(
!recipes.is_empty(),
"Expected at least one recipe line in help rule"
);
assert!(
recipes.iter().any(|r| r.contains("Available targets")),
"Expected to find 'Available targets' in recipes"
);
}
#[test]
fn test_indented_lines_in_conditionals() {
let content = r#"
ifdef DEBUG
CFLAGS += -g -DDEBUG
# This is a comment inside conditional
ifdef VERBOSE
CFLAGS += -v
endif
endif
"#;
let mut buf = content.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf)
.expect("Failed to parse indented lines in conditionals");
let code = makefile.code();
assert!(code.contains("ifdef DEBUG"));
assert!(code.contains("ifdef VERBOSE"));
assert!(code.contains("endif"));
}
#[test]
fn test_recipe_with_colon() {
let content = r#"
build:
@echo "Building at: $(shell date)"
gcc -o program main.c
"#;
let parsed = parse(content, None);
assert!(
parsed.errors.is_empty(),
"Failed to parse recipe with colon: {:?}",
parsed.errors
);
}
#[test]
fn test_double_colon_rules() {
let content = r#"
%.o :: %.c
$(CC) -c $< -o $@
# Double colon allows multiple rules for same target
all:: prerequisite1
@echo "First rule for all"
all:: prerequisite2
@echo "Second rule for all"
"#;
let parsed = parse(content, None);
assert!(
parsed.errors.is_empty(),
"Failed to parse double colon rules: {:?}",
parsed.errors
);
let makefile = parsed.root();
let rules: Vec<_> = makefile.rules().collect();
assert_eq!(rules.len(), 3);
for rule in &rules {
assert!(rule.is_double_colon());
}
assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["%.o"]);
assert_eq!(rules[1].targets().collect::<Vec<_>>(), vec!["all"]);
assert_eq!(rules[2].targets().collect::<Vec<_>>(), vec!["all"]);
assert_eq!(
rules[1].prerequisites().collect::<Vec<_>>(),
vec!["prerequisite1"]
);
assert_eq!(
rules[2].prerequisites().collect::<Vec<_>>(),
vec!["prerequisite2"]
);
}
#[test]
fn test_else_conditional_directives() {
let content = r#"
ifeq ($(OS),Windows_NT)
TARGET = windows
else ifeq ($(OS),Darwin)
TARGET = macos
else ifeq ($(OS),Linux)
TARGET = linux
else
TARGET = unknown
endif
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifeq directive");
assert!(makefile.code().contains("else ifeq"));
assert!(makefile.code().contains("TARGET"));
let content = r#"
ifdef WINDOWS
TARGET = windows
else ifdef DARWIN
TARGET = macos
else ifdef LINUX
TARGET = linux
else
TARGET = unknown
endif
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifdef directive");
assert!(makefile.code().contains("else ifdef"));
let content = r#"
ifndef NOWINDOWS
TARGET = windows
else ifndef NODARWIN
TARGET = macos
else
TARGET = linux
endif
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifndef directive");
assert!(makefile.code().contains("else ifndef"));
let content = r#"
ifneq ($(OS),Windows_NT)
TARGET = not_windows
else ifneq ($(OS),Darwin)
TARGET = not_macos
else
TARGET = darwin
endif
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse else ifneq directive");
assert!(makefile.code().contains("else ifneq"));
}
#[test]
fn test_complex_else_conditionals() {
let content = r#"VAR1 := foo
VAR2 := bar
ifeq ($(VAR1),foo)
RESULT := foo_matched
else ifdef VAR2
RESULT := var2_defined
else ifndef VAR3
RESULT := var3_not_defined
else
RESULT := final_else
endif
all:
@echo $(RESULT)
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse complex else conditionals");
let code = makefile.code();
assert!(code.contains("ifeq ($(VAR1),foo)"));
assert!(code.contains("else ifdef VAR2"));
assert!(code.contains("else ifndef VAR3"));
assert!(code.contains("else"));
assert!(code.contains("endif"));
assert!(code.contains("RESULT"));
let rules: Vec<_> = makefile.rules().collect();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].targets().collect::<Vec<_>>(), vec!["all"]);
}
#[test]
fn test_conditional_token_structure() {
let content = r#"ifdef VAR1
X := 1
else ifdef VAR2
X := 2
else
X := 3
endif
"#;
let mut buf = content.as_bytes();
let makefile = Makefile::read_relaxed(&mut buf).unwrap();
let syntax = makefile.syntax();
let mut found_conditional = false;
let mut found_conditional_if = false;
let mut found_conditional_else = false;
let mut found_conditional_endif = false;
fn check_node(
node: &SyntaxNode,
found_cond: &mut bool,
found_if: &mut bool,
found_else: &mut bool,
found_endif: &mut bool,
) {
match node.kind() {
SyntaxKind::CONDITIONAL => *found_cond = true,
SyntaxKind::CONDITIONAL_IF => *found_if = true,
SyntaxKind::CONDITIONAL_ELSE => *found_else = true,
SyntaxKind::CONDITIONAL_ENDIF => *found_endif = true,
_ => {}
}
for child in node.children() {
check_node(&child, found_cond, found_if, found_else, found_endif);
}
}
check_node(
syntax,
&mut found_conditional,
&mut found_conditional_if,
&mut found_conditional_else,
&mut found_conditional_endif,
);
assert!(found_conditional, "Should have CONDITIONAL node");
assert!(found_conditional_if, "Should have CONDITIONAL_IF node");
assert!(found_conditional_else, "Should have CONDITIONAL_ELSE node");
assert!(
found_conditional_endif,
"Should have CONDITIONAL_ENDIF node"
);
}
#[test]
fn test_ambiguous_assignment_vs_rule() {
const VAR_ASSIGNMENT: &str = "VARIABLE = value\n";
let mut buf = std::io::Cursor::new(VAR_ASSIGNMENT);
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse variable assignment");
let vars = makefile.variable_definitions().collect::<Vec<_>>();
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(vars.len(), 1, "Expected 1 variable, found {}", vars.len());
assert_eq!(rules.len(), 0, "Expected 0 rules, found {}", rules.len());
assert_eq!(vars[0].name(), Some("VARIABLE".to_string()));
const SIMPLE_RULE: &str = "target: dependency\n";
let mut buf = std::io::Cursor::new(SIMPLE_RULE);
let makefile = Makefile::read_relaxed(&mut buf).expect("Failed to parse simple rule");
let vars = makefile.variable_definitions().collect::<Vec<_>>();
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(vars.len(), 0, "Expected 0 variables, found {}", vars.len());
assert_eq!(rules.len(), 1, "Expected 1 rule, found {}", rules.len());
let rule = &rules[0];
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
}
#[test]
fn test_nested_conditionals() {
let content = r#"
ifdef RELEASE
CFLAGS += -O3
ifndef DEBUG
ifneq ($(ARCH),arm)
CFLAGS += -march=native
else
CFLAGS += -mcpu=cortex-a72
endif
endif
endif
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse nested conditionals");
let code = makefile.code();
assert!(code.contains("ifdef RELEASE"));
assert!(code.contains("ifndef DEBUG"));
assert!(code.contains("ifneq"));
}
#[test]
fn test_space_indented_recipes() {
let content = r#"
build:
@echo "Building with spaces instead of tabs"
gcc -o program main.c
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse space-indented recipes");
let rules = makefile.rules().collect::<Vec<_>>();
assert!(!rules.is_empty(), "Expected at least one rule");
let build_rule = rules.iter().find(|r| r.targets().any(|t| t == "build"));
assert!(build_rule.is_some(), "Expected to find build rule");
}
#[test]
fn test_complex_variable_functions() {
let content = r#"
FILES := $(shell find . -name "*.c")
OBJS := $(patsubst %.c,%.o,$(FILES))
NAME := $(if $(PROGRAM),$(PROGRAM),a.out)
HEADERS := ${wildcard *.h}
"#;
let parsed = parse(content, None);
assert!(
parsed.errors.is_empty(),
"Failed to parse complex variable functions: {:?}",
parsed.errors
);
}
#[test]
fn test_nested_variable_expansions() {
let content = r#"
VERSION = 1.0
PACKAGE = myapp
TARBALL = $(PACKAGE)-$(VERSION).tar.gz
INSTALL_PATH = $(shell echo $(PREFIX) | sed 's/\/$//')
"#;
let parsed = parse(content, None);
assert!(
parsed.errors.is_empty(),
"Failed to parse nested variable expansions: {:?}",
parsed.errors
);
}
#[test]
fn test_special_directives() {
let content = r#"
# Special makefile directives
.PHONY: all clean
.SUFFIXES: .c .o
.DEFAULT: all
# Variable definition and export directive
export PATH := /usr/bin:/bin
"#;
let mut buf = content.as_bytes();
let makefile =
Makefile::read_relaxed(&mut buf).expect("Failed to parse special directives");
let rules = makefile.rules().collect::<Vec<_>>();
let phony_rule = rules
.iter()
.find(|r| r.targets().any(|t| t.contains(".PHONY")));
assert!(phony_rule.is_some(), "Expected to find .PHONY rule");
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert!(!vars.is_empty(), "Expected to find at least one variable");
}
#[test]
fn test_comprehensive_real_world_makefile() {
let content = r#"
# Basic variable assignment
VERSION = 1.0.0
# Phony target
.PHONY: all clean
# Simple rule
all:
echo "Building version $(VERSION)"
# Another rule with dependencies
clean:
rm -f *.o
"#;
let parsed = parse(content, None);
assert!(parsed.errors.is_empty(), "Expected no parsing errors");
let variables = parsed.root().variable_definitions().collect::<Vec<_>>();
assert!(!variables.is_empty(), "Expected at least one variable");
assert_eq!(
variables[0].name(),
Some("VERSION".to_string()),
"Expected VERSION variable"
);
let rules = parsed.root().rules().collect::<Vec<_>>();
assert!(!rules.is_empty(), "Expected at least one rule");
let rule_targets: Vec<String> = rules
.iter()
.flat_map(|r| r.targets().collect::<Vec<_>>())
.collect();
assert!(
rule_targets.contains(&".PHONY".to_string()),
"Expected .PHONY rule"
);
assert!(
rule_targets.contains(&"all".to_string()),
"Expected 'all' rule"
);
assert!(
rule_targets.contains(&"clean".to_string()),
"Expected 'clean' rule"
);
}
#[test]
fn test_indented_help_text_outside_rules() {
let content = r#"
# Targets with help text
help:
@echo "Available targets:"
@echo " build build the project"
@echo " test run tests"
@echo " clean clean build artifacts"
# Another target
clean:
rm -rf build/
"#;
let parsed = parse(content, None);
assert!(
parsed.errors.is_empty(),
"Failed to parse indented help text"
);
let rules = parsed.root().rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 2, "Expected to find two rules");
let help_rule = rules
.iter()
.find(|r| r.targets().any(|t| t == "help"))
.expect("Expected to find help rule");
let clean_rule = rules
.iter()
.find(|r| r.targets().any(|t| t == "clean"))
.expect("Expected to find clean rule");
let help_recipes = help_rule.recipes().collect::<Vec<_>>();
assert!(
!help_recipes.is_empty(),
"Help rule should have recipe lines"
);
assert!(
help_recipes
.iter()
.any(|line| line.contains("Available targets")),
"Help recipes should include 'Available targets' line"
);
let clean_recipes = clean_rule.recipes().collect::<Vec<_>>();
assert!(
!clean_recipes.is_empty(),
"Clean rule should have recipe lines"
);
assert!(
clean_recipes.iter().any(|line| line.contains("rm -rf")),
"Clean recipes should include 'rm -rf' command"
);
}
#[test]
fn test_makefile1_phony_pattern() {
let content = "#line 2145\n.PHONY: $(PHONY)\n";
let result = parse(content, None);
assert!(
result.errors.is_empty(),
"Failed to parse .PHONY: $(PHONY) pattern"
);
let rules = result.root().rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1, "Expected 1 rule");
assert_eq!(
rules[0].targets().next().unwrap(),
".PHONY",
"Expected .PHONY rule"
);
let prereqs = rules[0].prerequisites().collect::<Vec<_>>();
assert_eq!(prereqs.len(), 1, "Expected 1 prerequisite");
assert_eq!(prereqs[0], "$(PHONY)", "Expected $(PHONY) prerequisite");
}
#[test]
fn test_skip_until_newline_behavior() {
let input = "text without newline";
let parsed = parse(input, None);
assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
let input_with_newline = "text\nafter newline";
let parsed2 = parse(input_with_newline, None);
assert!(parsed2.errors.is_empty() || !parsed2.errors.is_empty());
}
#[test]
#[ignore] fn test_error_with_indent_token() {
let input = "\tinvalid indented line";
let parsed = parse(input, None);
assert!(!parsed.errors.is_empty());
let error_msg = &parsed.errors[0].message;
assert!(error_msg.contains("recipe commences before first target"));
}
#[test]
fn test_conditional_token_handling() {
let input = r#"
ifndef VAR
CFLAGS = -DTEST
endif
"#;
let parsed = parse(input, None);
let makefile = parsed.root();
let _vars = makefile.variable_definitions().collect::<Vec<_>>();
let nested = r#"
ifdef DEBUG
ifndef RELEASE
CFLAGS = -g
endif
endif
"#;
let parsed_nested = parse(nested, None);
let _makefile = parsed_nested.root();
}
#[test]
fn test_include_vs_conditional_logic() {
let input = r#"
include file.mk
ifdef VAR
VALUE = 1
endif
"#;
let parsed = parse(input, None);
let makefile = parsed.root();
let includes = makefile.includes().collect::<Vec<_>>();
assert!(!includes.is_empty() || !parsed.errors.is_empty());
let optional_include = r#"
-include optional.mk
ifndef VAR
VALUE = default
endif
"#;
let parsed2 = parse(optional_include, None);
let _makefile = parsed2.root();
}
#[test]
fn test_balanced_parens_counting() {
let input = r#"
VAR = $(call func,$(nested,arg),extra)
COMPLEX = $(if $(condition),$(then_val),$(else_val))
"#;
let parsed = parse(input, None);
assert!(parsed.errors.is_empty());
let makefile = parsed.root();
let vars = makefile.variable_definitions().collect::<Vec<_>>();
assert_eq!(vars.len(), 2);
}
#[test]
fn test_documentation_lookahead() {
let input = r#"
# Documentation comment
help:
@echo "Usage instructions"
@echo "More help text"
"#;
let parsed = parse(input, None);
assert!(parsed.errors.is_empty());
let makefile = parsed.root();
let rules = makefile.rules().collect::<Vec<_>>();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].targets().next().unwrap(), "help");
}
#[test]
fn test_edge_case_empty_input() {
let parsed = parse("", None);
assert!(parsed.errors.is_empty());
let parsed2 = parse(" \n \n", None);
let _makefile = parsed2.root();
}
#[test]
fn test_malformed_conditional_recovery() {
let input = r#"
ifdef
# Missing condition variable
endif
"#;
let parsed = parse(input, None);
assert!(parsed.errors.is_empty() || !parsed.errors.is_empty());
}
#[test]
fn test_replace_rule() {
let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
makefile.replace_rule(0, new_rule).unwrap();
let targets: Vec<_> = makefile
.rules()
.flat_map(|r| r.targets().collect::<Vec<_>>())
.collect();
assert_eq!(targets, vec!["new_rule", "rule2"]);
let recipes: Vec<_> = makefile.rules().next().unwrap().recipes().collect();
assert_eq!(recipes, vec!["new_command"]);
}
#[test]
fn test_replace_rule_out_of_bounds() {
let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
let result = makefile.replace_rule(5, new_rule);
assert!(result.is_err());
}
#[test]
fn test_remove_rule() {
let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\nrule3:\n\tcommand3\n"
.parse()
.unwrap();
let removed = makefile.remove_rule(1).unwrap();
assert_eq!(removed.targets().collect::<Vec<_>>(), vec!["rule2"]);
let remaining_targets: Vec<_> = makefile
.rules()
.flat_map(|r| r.targets().collect::<Vec<_>>())
.collect();
assert_eq!(remaining_targets, vec!["rule1", "rule3"]);
assert_eq!(makefile.rules().count(), 2);
}
#[test]
fn test_remove_rule_out_of_bounds() {
let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
let result = makefile.remove_rule(5);
assert!(result.is_err());
}
#[test]
fn test_insert_rule() {
let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
let new_rule: Rule = "inserted_rule:\n\tinserted_command\n".parse().unwrap();
makefile.insert_rule(1, new_rule).unwrap();
let targets: Vec<_> = makefile
.rules()
.flat_map(|r| r.targets().collect::<Vec<_>>())
.collect();
assert_eq!(targets, vec!["rule1", "inserted_rule", "rule2"]);
assert_eq!(makefile.rules().count(), 3);
}
#[test]
fn test_insert_rule_at_end() {
let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
let new_rule: Rule = "end_rule:\n\tend_command\n".parse().unwrap();
makefile.insert_rule(1, new_rule).unwrap();
let targets: Vec<_> = makefile
.rules()
.flat_map(|r| r.targets().collect::<Vec<_>>())
.collect();
assert_eq!(targets, vec!["rule1", "end_rule"]);
}
#[test]
fn test_insert_rule_out_of_bounds() {
let mut makefile: Makefile = "rule1:\n\tcommand1\n".parse().unwrap();
let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
let result = makefile.insert_rule(5, new_rule);
assert!(result.is_err());
}
#[test]
fn test_insert_rule_preserves_blank_line_spacing_at_end() {
let input = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n";
let mut makefile: Makefile = input.parse().unwrap();
let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
makefile.insert_rule(2, new_rule).unwrap();
let expected = "rule1:\n\tcommand1\n\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
assert_eq!(makefile.to_string(), expected);
}
#[test]
fn test_insert_rule_adds_blank_lines_when_missing() {
let input = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n";
let mut makefile: Makefile = input.parse().unwrap();
let new_rule = Rule::new(&["rule3"], &[], &["command3"]);
makefile.insert_rule(2, new_rule).unwrap();
let expected = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n\nrule3:\n\tcommand3\n";
assert_eq!(makefile.to_string(), expected);
}
#[test]
fn test_remove_command() {
let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
.parse()
.unwrap();
rule.remove_command(1);
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(recipes, vec!["command1", "command3"]);
assert_eq!(rule.recipe_count(), 2);
}
#[test]
fn test_remove_command_out_of_bounds() {
let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
let result = rule.remove_command(5);
assert!(!result);
}
#[test]
fn test_insert_command() {
let mut rule: Rule = "rule:\n\tcommand1\n\tcommand3\n".parse().unwrap();
rule.insert_command(1, "command2");
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(recipes, vec!["command1", "command2", "command3"]);
}
#[test]
fn test_insert_command_at_end() {
let mut rule: Rule = "rule:\n\tcommand1\n".parse().unwrap();
rule.insert_command(1, "command2");
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(recipes, vec!["command1", "command2"]);
}
#[test]
fn test_insert_command_in_empty_rule() {
let mut rule: Rule = "rule:\n".parse().unwrap();
rule.insert_command(0, "new_command");
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(recipes, vec!["new_command"]);
}
#[test]
fn test_recipe_count() {
let rule1: Rule = "rule:\n".parse().unwrap();
assert_eq!(rule1.recipe_count(), 0);
let rule2: Rule = "rule:\n\tcommand1\n\tcommand2\n".parse().unwrap();
assert_eq!(rule2.recipe_count(), 2);
}
#[test]
fn test_clear_commands() {
let mut rule: Rule = "rule:\n\tcommand1\n\tcommand2\n\tcommand3\n"
.parse()
.unwrap();
rule.clear_commands();
assert_eq!(rule.recipe_count(), 0);
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(recipes, Vec::<String>::new());
let targets: Vec<_> = rule.targets().collect();
assert_eq!(targets, vec!["rule"]);
}
#[test]
fn test_clear_commands_empty_rule() {
let mut rule: Rule = "rule:\n".parse().unwrap();
rule.clear_commands();
assert_eq!(rule.recipe_count(), 0);
let targets: Vec<_> = rule.targets().collect();
assert_eq!(targets, vec!["rule"]);
}
#[test]
fn test_rule_manipulation_preserves_structure() {
let input = r#"# Comment
VAR = value
rule1:
command1
# Another comment
rule2:
command2
VAR2 = value2
"#;
let mut makefile: Makefile = input.parse().unwrap();
let new_rule: Rule = "new_rule:\n\tnew_command\n".parse().unwrap();
makefile.insert_rule(1, new_rule).unwrap();
let targets: Vec<_> = makefile
.rules()
.flat_map(|r| r.targets().collect::<Vec<_>>())
.collect();
assert_eq!(targets, vec!["rule1", "new_rule", "rule2"]);
let vars: Vec<_> = makefile.variable_definitions().collect();
assert_eq!(vars.len(), 2);
let output = makefile.code();
assert!(output.contains("# Comment"));
assert!(output.contains("VAR = value"));
assert!(output.contains("# Another comment"));
assert!(output.contains("VAR2 = value2"));
}
#[test]
fn test_replace_rule_with_multiple_targets() {
let mut makefile: Makefile = "target1 target2: dep\n\tcommand\n".parse().unwrap();
let new_rule: Rule = "new_target: new_dep\n\tnew_command\n".parse().unwrap();
makefile.replace_rule(0, new_rule).unwrap();
let targets: Vec<_> = makefile
.rules()
.flat_map(|r| r.targets().collect::<Vec<_>>())
.collect();
assert_eq!(targets, vec!["new_target"]);
}
#[test]
fn test_empty_makefile_operations() {
let mut makefile = Makefile::new();
assert!(makefile
.replace_rule(0, "rule:\n\tcommand\n".parse().unwrap())
.is_err());
assert!(makefile.remove_rule(0).is_err());
let new_rule: Rule = "first_rule:\n\tcommand\n".parse().unwrap();
makefile.insert_rule(0, new_rule).unwrap();
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_command_operations_preserve_indentation() {
let mut rule: Rule = "rule:\n\t\tdeep_indent\n\tshallow_indent\n"
.parse()
.unwrap();
rule.insert_command(1, "middle_command");
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(
recipes,
vec!["\tdeep_indent", "middle_command", "shallow_indent"]
);
}
#[test]
fn test_rule_operations_with_variables_and_includes() {
let input = r#"VAR1 = value1
include common.mk
rule1:
command1
VAR2 = value2
include other.mk
rule2:
command2
"#;
let mut makefile: Makefile = input.parse().unwrap();
makefile.remove_rule(0).unwrap();
let output = makefile.code();
assert!(output.contains("VAR1 = value1"));
assert!(output.contains("include common.mk"));
assert!(output.contains("VAR2 = value2"));
assert!(output.contains("include other.mk"));
assert_eq!(makefile.rules().count(), 1);
let remaining_targets: Vec<_> = makefile
.rules()
.flat_map(|r| r.targets().collect::<Vec<_>>())
.collect();
assert_eq!(remaining_targets, vec!["rule2"]);
}
#[test]
fn test_command_manipulation_edge_cases() {
let mut empty_rule: Rule = "empty:\n".parse().unwrap();
assert_eq!(empty_rule.recipe_count(), 0);
empty_rule.insert_command(0, "first_command");
assert_eq!(empty_rule.recipe_count(), 1);
let mut empty_rule2: Rule = "empty:\n".parse().unwrap();
empty_rule2.clear_commands();
assert_eq!(empty_rule2.recipe_count(), 0);
}
#[test]
fn test_large_makefile_performance() {
let mut makefile = Makefile::new();
for i in 0..100 {
let rule_name = format!("rule{}", i);
makefile
.add_rule(&rule_name)
.push_command(&format!("command{}", i));
}
assert_eq!(makefile.rules().count(), 100);
let new_rule: Rule = "middle_rule:\n\tmiddle_command\n".parse().unwrap();
makefile.replace_rule(50, new_rule).unwrap();
let rule_50_targets: Vec<_> = makefile.rules().nth(50).unwrap().targets().collect();
assert_eq!(rule_50_targets, vec!["middle_rule"]);
assert_eq!(makefile.rules().count(), 100); }
#[test]
fn test_complex_recipe_manipulation() {
let mut complex_rule: Rule = r#"complex:
@echo "Starting build"
$(CC) $(CFLAGS) -o $@ $<
@echo "Build complete"
chmod +x $@
"#
.parse()
.unwrap();
assert_eq!(complex_rule.recipe_count(), 4);
complex_rule.remove_command(0); complex_rule.remove_command(1);
let final_recipes: Vec<_> = complex_rule.recipes().collect();
assert_eq!(final_recipes.len(), 2);
assert!(final_recipes[0].contains("$(CC)"));
assert!(final_recipes[1].contains("chmod"));
}
#[test]
fn test_variable_definition_remove() {
let makefile: Makefile = r#"VAR1 = value1
VAR2 = value2
VAR3 = value3
"#
.parse()
.unwrap();
assert_eq!(makefile.variable_definitions().count(), 3);
let mut var2 = makefile
.variable_definitions()
.nth(1)
.expect("Should have second variable");
assert_eq!(var2.name(), Some("VAR2".to_string()));
var2.remove();
assert_eq!(makefile.variable_definitions().count(), 2);
let var_names: Vec<_> = makefile
.variable_definitions()
.filter_map(|v| v.name())
.collect();
assert_eq!(var_names, vec!["VAR1", "VAR3"]);
}
#[test]
fn test_variable_definition_set_value() {
let makefile: Makefile = "VAR = old_value\n".parse().unwrap();
let mut var = makefile
.variable_definitions()
.next()
.expect("Should have variable");
assert_eq!(var.raw_value(), Some("old_value".to_string()));
var.set_value("new_value");
assert_eq!(var.raw_value(), Some("new_value".to_string()));
assert!(makefile.code().contains("VAR = new_value"));
}
#[test]
fn test_variable_definition_set_value_preserves_format() {
let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
let mut var = makefile
.variable_definitions()
.next()
.expect("Should have variable");
assert_eq!(var.raw_value(), Some("old_value".to_string()));
var.set_value("new_value");
assert_eq!(var.raw_value(), Some("new_value".to_string()));
let code = makefile.code();
assert!(code.contains("export"), "Should preserve export prefix");
assert!(code.contains(":="), "Should preserve := operator");
assert!(code.contains("new_value"), "Should have new value");
}
#[test]
fn test_makefile_find_variable() {
let makefile: Makefile = r#"VAR1 = value1
VAR2 = value2
VAR3 = value3
"#
.parse()
.unwrap();
let vars: Vec<_> = makefile.find_variable("VAR2").collect();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name(), Some("VAR2".to_string()));
assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
assert_eq!(makefile.find_variable("NONEXISTENT").count(), 0);
}
#[test]
fn test_makefile_find_variable_with_export() {
let makefile: Makefile = r#"VAR1 = value1
export VAR2 := value2
VAR3 = value3
"#
.parse()
.unwrap();
let vars: Vec<_> = makefile.find_variable("VAR2").collect();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name(), Some("VAR2".to_string()));
assert_eq!(vars[0].raw_value(), Some("value2".to_string()));
}
#[test]
fn test_variable_definition_is_export() {
let makefile: Makefile = r#"VAR1 = value1
export VAR2 := value2
export VAR3 = value3
VAR4 := value4
"#
.parse()
.unwrap();
let vars: Vec<_> = makefile.variable_definitions().collect();
assert_eq!(vars.len(), 4);
assert!(!vars[0].is_export());
assert!(vars[1].is_export());
assert!(vars[2].is_export());
assert!(!vars[3].is_export());
}
#[test]
fn test_makefile_find_variable_multiple() {
let makefile: Makefile = r#"VAR1 = value1
VAR1 = value2
VAR2 = other
VAR1 = value3
"#
.parse()
.unwrap();
let vars: Vec<_> = makefile.find_variable("VAR1").collect();
assert_eq!(vars.len(), 3);
assert_eq!(vars[0].raw_value(), Some("value1".to_string()));
assert_eq!(vars[1].raw_value(), Some("value2".to_string()));
assert_eq!(vars[2].raw_value(), Some("value3".to_string()));
let var2s: Vec<_> = makefile.find_variable("VAR2").collect();
assert_eq!(var2s.len(), 1);
assert_eq!(var2s[0].raw_value(), Some("other".to_string()));
}
#[test]
fn test_variable_remove_and_find() {
let makefile: Makefile = r#"VAR1 = value1
VAR2 = value2
VAR3 = value3
"#
.parse()
.unwrap();
let mut var2 = makefile
.find_variable("VAR2")
.next()
.expect("Should find VAR2");
var2.remove();
assert_eq!(makefile.find_variable("VAR2").count(), 0);
assert_eq!(makefile.find_variable("VAR1").count(), 1);
assert_eq!(makefile.find_variable("VAR3").count(), 1);
}
#[test]
fn test_variable_remove_with_comment() {
let makefile: Makefile = r#"VAR1 = value1
# This is a comment about VAR2
VAR2 = value2
VAR3 = value3
"#
.parse()
.unwrap();
let mut var2 = makefile
.variable_definitions()
.nth(1)
.expect("Should have second variable");
assert_eq!(var2.name(), Some("VAR2".to_string()));
var2.remove();
assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
}
#[test]
fn test_variable_remove_with_multiple_comments() {
let makefile: Makefile = r#"VAR1 = value1
# Comment line 1
# Comment line 2
# Comment line 3
VAR2 = value2
VAR3 = value3
"#
.parse()
.unwrap();
let mut var2 = makefile
.variable_definitions()
.nth(1)
.expect("Should have second variable");
var2.remove();
assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
}
#[test]
fn test_variable_remove_with_empty_line() {
let makefile: Makefile = r#"VAR1 = value1
# Comment about VAR2
VAR2 = value2
VAR3 = value3
"#
.parse()
.unwrap();
let mut var2 = makefile
.variable_definitions()
.nth(1)
.expect("Should have second variable");
var2.remove();
assert_eq!(makefile.code(), "VAR1 = value1\nVAR3 = value3\n");
}
#[test]
fn test_variable_remove_with_multiple_empty_lines() {
let makefile: Makefile = r#"VAR1 = value1
# Comment about VAR2
VAR2 = value2
VAR3 = value3
"#
.parse()
.unwrap();
let mut var2 = makefile
.variable_definitions()
.nth(1)
.expect("Should have second variable");
var2.remove();
assert_eq!(makefile.code(), "VAR1 = value1\n\nVAR3 = value3\n");
}
#[test]
fn test_rule_remove_with_comment() {
let makefile: Makefile = r#"rule1:
command1
# Comment about rule2
rule2:
command2
rule3:
command3
"#
.parse()
.unwrap();
let rule2 = makefile.rules().nth(1).expect("Should have second rule");
rule2.remove().unwrap();
assert_eq!(
makefile.code(),
"rule1:\n\tcommand1\n\nrule3:\n\tcommand3\n"
);
}
#[test]
fn test_variable_remove_preserves_shebang() {
let makefile: Makefile = r#"#!/usr/bin/make -f
# This is a regular comment
VAR1 = value1
VAR2 = value2
"#
.parse()
.unwrap();
let mut var1 = makefile.variable_definitions().next().unwrap();
var1.remove();
let code = makefile.code();
assert!(code.starts_with("#!/usr/bin/make -f"));
assert!(!code.contains("regular comment"));
assert!(!code.contains("VAR1"));
assert!(code.contains("VAR2"));
}
#[test]
fn test_variable_remove_preserves_subsequent_comments() {
let makefile: Makefile = r#"VAR1 = value1
# Comment about VAR2
VAR2 = value2
# Comment about VAR3
VAR3 = value3
"#
.parse()
.unwrap();
let mut var2 = makefile
.variable_definitions()
.nth(1)
.expect("Should have second variable");
var2.remove();
let code = makefile.code();
assert_eq!(
code,
"VAR1 = value1\n\n# Comment about VAR3\nVAR3 = value3\n"
);
}
#[test]
fn test_variable_remove_after_shebang_preserves_empty_line() {
let makefile: Makefile = r#"#!/usr/bin/make -f
export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
%:
dh $@
"#
.parse()
.unwrap();
let mut var = makefile.variable_definitions().next().unwrap();
var.remove();
assert_eq!(makefile.code(), "#!/usr/bin/make -f\n\n%:\n\tdh $@\n");
}
#[test]
fn test_rule_add_prerequisite() {
let mut rule: Rule = "target: dep1\n".parse().unwrap();
rule.add_prerequisite("dep2").unwrap();
assert_eq!(
rule.prerequisites().collect::<Vec<_>>(),
vec!["dep1", "dep2"]
);
assert_eq!(rule.to_string(), "target: dep1 dep2\n");
}
#[test]
fn test_rule_add_prerequisite_to_rule_without_prereqs() {
let mut rule: Rule = "target:\n".parse().unwrap();
rule.add_prerequisite("dep1").unwrap();
assert_eq!(rule.prerequisites().collect::<Vec<_>>(), vec!["dep1"]);
assert_eq!(rule.to_string(), "target: dep1\n");
}
#[test]
fn test_rule_remove_prerequisite() {
let mut rule: Rule = "target: dep1 dep2 dep3\n".parse().unwrap();
assert!(rule.remove_prerequisite("dep2").unwrap());
assert_eq!(
rule.prerequisites().collect::<Vec<_>>(),
vec!["dep1", "dep3"]
);
assert!(!rule.remove_prerequisite("nonexistent").unwrap());
}
#[test]
fn test_rule_set_prerequisites() {
let mut rule: Rule = "target: old_dep\n".parse().unwrap();
rule.set_prerequisites(vec!["new_dep1", "new_dep2"])
.unwrap();
assert_eq!(
rule.prerequisites().collect::<Vec<_>>(),
vec!["new_dep1", "new_dep2"]
);
}
#[test]
fn test_rule_set_prerequisites_empty() {
let mut rule: Rule = "target: dep1 dep2\n".parse().unwrap();
rule.set_prerequisites(vec![]).unwrap();
assert_eq!(rule.prerequisites().collect::<Vec<_>>().len(), 0);
}
#[test]
fn test_rule_add_target() {
let mut rule: Rule = "target1: dep1\n".parse().unwrap();
rule.add_target("target2").unwrap();
assert_eq!(
rule.targets().collect::<Vec<_>>(),
vec!["target1", "target2"]
);
}
#[test]
fn test_rule_set_targets() {
let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
rule.set_targets(vec!["new_target1", "new_target2"])
.unwrap();
assert_eq!(
rule.targets().collect::<Vec<_>>(),
vec!["new_target1", "new_target2"]
);
}
#[test]
fn test_rule_set_targets_empty() {
let mut rule: Rule = "target: dep1\n".parse().unwrap();
let result = rule.set_targets(vec![]);
assert!(result.is_err());
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target"]);
}
#[test]
fn test_rule_has_target() {
let rule: Rule = "target1 target2: dependency\n".parse().unwrap();
assert!(rule.has_target("target1"));
assert!(rule.has_target("target2"));
assert!(!rule.has_target("target3"));
assert!(!rule.has_target("nonexistent"));
}
#[test]
fn test_rule_rename_target() {
let mut rule: Rule = "old_target: dependency\n".parse().unwrap();
assert!(rule.rename_target("old_target", "new_target").unwrap());
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["new_target"]);
assert!(!rule.rename_target("nonexistent", "something").unwrap());
}
#[test]
fn test_rule_rename_target_multiple() {
let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
assert!(rule.rename_target("target2", "renamed_target").unwrap());
assert_eq!(
rule.targets().collect::<Vec<_>>(),
vec!["target1", "renamed_target", "target3"]
);
}
#[test]
fn test_rule_remove_target() {
let mut rule: Rule = "target1 target2 target3: dependency\n".parse().unwrap();
assert!(rule.remove_target("target2").unwrap());
assert_eq!(
rule.targets().collect::<Vec<_>>(),
vec!["target1", "target3"]
);
assert!(!rule.remove_target("nonexistent").unwrap());
}
#[test]
fn test_rule_remove_target_last() {
let mut rule: Rule = "single_target: dependency\n".parse().unwrap();
let result = rule.remove_target("single_target");
assert!(result.is_err());
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["single_target"]);
}
#[test]
fn test_rule_target_manipulation_preserves_prerequisites() {
let mut rule: Rule = "target1 target2: dep1 dep2\n\tcommand".parse().unwrap();
rule.remove_target("target1").unwrap();
assert_eq!(rule.targets().collect::<Vec<_>>(), vec!["target2"]);
assert_eq!(
rule.prerequisites().collect::<Vec<_>>(),
vec!["dep1", "dep2"]
);
assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
rule.add_target("target3").unwrap();
assert_eq!(
rule.targets().collect::<Vec<_>>(),
vec!["target2", "target3"]
);
assert_eq!(
rule.prerequisites().collect::<Vec<_>>(),
vec!["dep1", "dep2"]
);
assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
rule.rename_target("target2", "renamed").unwrap();
assert_eq!(
rule.targets().collect::<Vec<_>>(),
vec!["renamed", "target3"]
);
assert_eq!(
rule.prerequisites().collect::<Vec<_>>(),
vec!["dep1", "dep2"]
);
assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["command"]);
}
#[test]
fn test_rule_remove() {
let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
let rule = makefile.find_rule_by_target("rule1").unwrap();
rule.remove().unwrap();
assert_eq!(makefile.rules().count(), 1);
assert!(makefile.find_rule_by_target("rule1").is_none());
assert!(makefile.find_rule_by_target("rule2").is_some());
}
#[test]
fn test_rule_remove_last_trims_blank_lines() {
let makefile: Makefile =
"%:\n\tdh $@\n\noverride_dh_missing:\n\tdh_missing --fail-missing\n"
.parse()
.unwrap();
let rule = makefile.find_rule_by_target("override_dh_missing").unwrap();
rule.remove().unwrap();
assert_eq!(makefile.code(), "%:\n\tdh $@\n");
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_makefile_find_rule_by_target() {
let makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
let rule = makefile.find_rule_by_target("rule2");
assert!(rule.is_some());
assert_eq!(rule.unwrap().targets().collect::<Vec<_>>(), vec!["rule2"]);
assert!(makefile.find_rule_by_target("nonexistent").is_none());
}
#[test]
fn test_makefile_find_rules_by_target() {
let makefile: Makefile = "rule1:\n\tcommand1\nrule1:\n\tcommand2\nrule2:\n\tcommand3\n"
.parse()
.unwrap();
assert_eq!(makefile.find_rules_by_target("rule1").count(), 2);
assert_eq!(makefile.find_rules_by_target("rule2").count(), 1);
assert_eq!(makefile.find_rules_by_target("nonexistent").count(), 0);
}
#[test]
fn test_makefile_find_rule_by_target_pattern_simple() {
let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
let rule = makefile.find_rule_by_target_pattern("foo.o");
assert!(rule.is_some());
assert_eq!(rule.unwrap().targets().next().unwrap(), "%.o");
}
#[test]
fn test_makefile_find_rule_by_target_pattern_no_match() {
let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
let rule = makefile.find_rule_by_target_pattern("foo.c");
assert!(rule.is_none());
}
#[test]
fn test_makefile_find_rule_by_target_pattern_exact() {
let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
let rule = makefile.find_rule_by_target_pattern("foo.o");
assert!(rule.is_some());
assert_eq!(rule.unwrap().targets().next().unwrap(), "foo.o");
}
#[test]
fn test_makefile_find_rule_by_target_pattern_prefix() {
let makefile: Makefile = "lib%.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
let rule = makefile.find_rule_by_target_pattern("libfoo.a");
assert!(rule.is_some());
assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%.a");
}
#[test]
fn test_makefile_find_rule_by_target_pattern_suffix() {
let makefile: Makefile = "%_test.o: %.c\n\t$(CC) -c $<\n".parse().unwrap();
let rule = makefile.find_rule_by_target_pattern("foo_test.o");
assert!(rule.is_some());
assert_eq!(rule.unwrap().targets().next().unwrap(), "%_test.o");
}
#[test]
fn test_makefile_find_rule_by_target_pattern_middle() {
let makefile: Makefile = "lib%_debug.a: %.o\n\tar rcs $@ $<\n".parse().unwrap();
let rule = makefile.find_rule_by_target_pattern("libfoo_debug.a");
assert!(rule.is_some());
assert_eq!(rule.unwrap().targets().next().unwrap(), "lib%_debug.a");
}
#[test]
fn test_makefile_find_rule_by_target_pattern_wildcard_only() {
let makefile: Makefile = "%: %.c\n\t$(CC) -o $@ $<\n".parse().unwrap();
let rule = makefile.find_rule_by_target_pattern("anything");
assert!(rule.is_some());
assert_eq!(rule.unwrap().targets().next().unwrap(), "%");
}
#[test]
fn test_makefile_find_rules_by_target_pattern_multiple() {
let makefile: Makefile = "%.o: %.c\n\t$(CC) -c $<\n%.o: %.s\n\t$(AS) -o $@ $<\n"
.parse()
.unwrap();
let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
assert_eq!(rules.len(), 2);
}
#[test]
fn test_makefile_find_rules_by_target_pattern_mixed() {
let makefile: Makefile =
"%.o: %.c\n\t$(CC) -c $<\nfoo.o: foo.h\n\t$(CC) -c foo.c\nbar.txt: baz.txt\n\tcp $< $@\n"
.parse()
.unwrap();
let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
assert_eq!(rules.len(), 2); let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.txt").collect();
assert_eq!(rules.len(), 1); }
#[test]
fn test_makefile_find_rules_by_target_pattern_no_wildcard() {
let makefile: Makefile = "foo.o: foo.c\n\t$(CC) -c $<\n".parse().unwrap();
let rules: Vec<_> = makefile.find_rules_by_target_pattern("foo.o").collect();
assert_eq!(rules.len(), 1);
let rules: Vec<_> = makefile.find_rules_by_target_pattern("bar.o").collect();
assert_eq!(rules.len(), 0);
}
#[test]
fn test_matches_pattern_exact() {
assert!(matches_pattern("foo.o", "foo.o"));
assert!(!matches_pattern("foo.o", "bar.o"));
}
#[test]
fn test_matches_pattern_suffix() {
assert!(matches_pattern("%.o", "foo.o"));
assert!(matches_pattern("%.o", "bar.o"));
assert!(matches_pattern("%.o", "baz/qux.o"));
assert!(!matches_pattern("%.o", "foo.c"));
}
#[test]
fn test_matches_pattern_prefix() {
assert!(matches_pattern("lib%.a", "libfoo.a"));
assert!(matches_pattern("lib%.a", "libbar.a"));
assert!(!matches_pattern("lib%.a", "foo.a"));
assert!(!matches_pattern("lib%.a", "lib.a"));
}
#[test]
fn test_matches_pattern_middle() {
assert!(matches_pattern("lib%_debug.a", "libfoo_debug.a"));
assert!(matches_pattern("lib%_debug.a", "libbar_debug.a"));
assert!(!matches_pattern("lib%_debug.a", "libfoo.a"));
assert!(!matches_pattern("lib%_debug.a", "foo_debug.a"));
}
#[test]
fn test_matches_pattern_wildcard_only() {
assert!(matches_pattern("%", "anything"));
assert!(matches_pattern("%", "foo.o"));
assert!(!matches_pattern("%", ""));
}
#[test]
fn test_matches_pattern_empty_stem() {
assert!(!matches_pattern("%.o", ".o")); assert!(!matches_pattern("lib%", "lib")); assert!(!matches_pattern("lib%.a", "lib.a")); }
#[test]
fn test_matches_pattern_multiple_wildcards_not_supported() {
assert!(!matches_pattern("%foo%bar", "xfooybarz"));
assert!(!matches_pattern("lib%.so.%", "libfoo.so.1"));
}
#[test]
fn test_makefile_add_phony_target() {
let mut makefile = Makefile::new();
makefile.add_phony_target("clean").unwrap();
assert!(makefile.is_phony("clean"));
assert_eq!(makefile.phony_targets().collect::<Vec<_>>(), vec!["clean"]);
}
#[test]
fn test_makefile_add_phony_target_existing() {
let mut makefile: Makefile = ".PHONY: test\n".parse().unwrap();
makefile.add_phony_target("clean").unwrap();
assert!(makefile.is_phony("test"));
assert!(makefile.is_phony("clean"));
let targets: Vec<_> = makefile.phony_targets().collect();
assert!(targets.contains(&"test".to_string()));
assert!(targets.contains(&"clean".to_string()));
}
#[test]
fn test_makefile_remove_phony_target() {
let mut makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
assert!(makefile.remove_phony_target("clean").unwrap());
assert!(!makefile.is_phony("clean"));
assert!(makefile.is_phony("test"));
assert!(!makefile.remove_phony_target("nonexistent").unwrap());
}
#[test]
fn test_makefile_remove_phony_target_last() {
let mut makefile: Makefile = ".PHONY: clean\n".parse().unwrap();
assert!(makefile.remove_phony_target("clean").unwrap());
assert!(!makefile.is_phony("clean"));
assert!(makefile.find_rule_by_target(".PHONY").is_none());
}
#[test]
fn test_makefile_is_phony() {
let makefile: Makefile = ".PHONY: clean test\n".parse().unwrap();
assert!(makefile.is_phony("clean"));
assert!(makefile.is_phony("test"));
assert!(!makefile.is_phony("build"));
}
#[test]
fn test_makefile_phony_targets() {
let makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
let phony_targets: Vec<_> = makefile.phony_targets().collect();
assert_eq!(phony_targets, vec!["clean", "test", "build"]);
}
#[test]
fn test_makefile_phony_targets_empty() {
let makefile = Makefile::new();
assert_eq!(makefile.phony_targets().count(), 0);
}
#[test]
fn test_makefile_remove_first_phony_target_no_extra_space() {
let mut makefile: Makefile = ".PHONY: clean test build\n".parse().unwrap();
assert!(makefile.remove_phony_target("clean").unwrap());
let result = makefile.to_string();
assert_eq!(result, ".PHONY: test build\n");
}
#[test]
fn test_recipe_with_leading_comments_and_blank_lines() {
let makefile_text = r#"#!/usr/bin/make
%:
dh $@
override_dh_build:
# The next line is empty
dh_python3
"#;
let makefile = Makefile::read_relaxed(makefile_text.as_bytes()).unwrap();
let rules: Vec<_> = makefile.rules().collect();
assert_eq!(rules.len(), 2, "Expected 2 rules");
let rule0 = &rules[0];
assert_eq!(rule0.targets().collect::<Vec<_>>(), vec!["%"]);
assert_eq!(rule0.recipes().collect::<Vec<_>>(), vec!["dh $@"]);
let rule1 = &rules[1];
assert_eq!(
rule1.targets().collect::<Vec<_>>(),
vec!["override_dh_build"]
);
let recipes: Vec<_> = rule1.recipes().collect();
assert!(
!recipes.is_empty(),
"Expected at least one recipe for override_dh_build, got none"
);
assert!(
recipes.contains(&"dh_python3".to_string()),
"Expected 'dh_python3' in recipes, got: {:?}",
recipes
);
}
#[test]
fn test_rule_parse_preserves_trailing_blank_lines() {
let input = r#"override_dh_systemd_enable:
dh_systemd_enable -pracoon
override_dh_install:
dh_install
"#;
let mut mf: Makefile = input.parse().unwrap();
let rule = mf.rules().next().unwrap();
let rule_text = rule.to_string();
assert_eq!(
rule_text,
"override_dh_systemd_enable:\n\tdh_systemd_enable -pracoon\n\n"
);
let modified =
rule_text.replace("override_dh_systemd_enable:", "override_dh_installsystemd:");
let new_rule: Rule = modified.parse().unwrap();
assert_eq!(
new_rule.to_string(),
"override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\n"
);
mf.replace_rule(0, new_rule).unwrap();
let output = mf.to_string();
assert!(
output.contains(
"override_dh_installsystemd:\n\tdh_systemd_enable -pracoon\n\noverride_dh_install:"
),
"Blank line between rules should be preserved. Got: {:?}",
output
);
}
#[test]
fn test_rule_parse_round_trip_with_trailing_newlines() {
let test_cases = vec![
"rule:\n\tcommand\n", "rule:\n\tcommand\n\n", "rule:\n\tcommand\n\n\n", ];
for rule_text in test_cases {
let rule: Rule = rule_text.parse().unwrap();
let result = rule.to_string();
assert_eq!(rule_text, result, "Round-trip failed for {:?}", rule_text);
}
}
#[test]
fn test_rule_clone() {
let rule_text = "rule:\n\tcommand\n\n";
let rule: Rule = rule_text.parse().unwrap();
let cloned = rule.clone();
assert_eq!(rule.to_string(), cloned.to_string());
assert_eq!(rule.to_string(), rule_text);
assert_eq!(cloned.to_string(), rule_text);
assert_eq!(
rule.targets().collect::<Vec<_>>(),
cloned.targets().collect::<Vec<_>>()
);
assert_eq!(
rule.recipes().collect::<Vec<_>>(),
cloned.recipes().collect::<Vec<_>>()
);
}
#[test]
fn test_makefile_clone() {
let input = "VAR = value\n\nrule:\n\tcommand\n";
let makefile: Makefile = input.parse().unwrap();
let cloned = makefile.clone();
assert_eq!(makefile.to_string(), cloned.to_string());
assert_eq!(makefile.to_string(), input);
assert_eq!(makefile.rules().count(), cloned.rules().count());
assert_eq!(
makefile.variable_definitions().count(),
cloned.variable_definitions().count()
);
}
#[test]
fn test_conditional_with_recipe_line() {
let input = "ifeq (,$(X))\n\t./run-tests\nendif\n";
let parsed = parse(input, None);
assert!(
parsed.errors.is_empty(),
"Expected no parse errors, but got: {:?}",
parsed.errors
);
let mf = parsed.root();
assert_eq!(mf.code(), input);
}
#[test]
fn test_conditional_in_rule_recipe() {
let input = "override_dh_auto_test:\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t./run-tests\nendif\n";
let parsed = parse(input, None);
assert!(
parsed.errors.is_empty(),
"Expected no parse errors, but got: {:?}",
parsed.errors
);
let mf = parsed.root();
assert_eq!(mf.code(), input);
assert_eq!(mf.rules().count(), 1);
}
#[test]
fn test_rule_items() {
use crate::RuleItem;
let input = r#"test:
echo "before"
ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
./run-tests
endif
echo "after"
"#;
let rule: Rule = input.parse().unwrap();
let items: Vec<_> = rule.items().collect();
assert_eq!(
items.len(),
3,
"Expected 3 items: recipe, conditional, recipe"
);
match &items[0] {
RuleItem::Recipe(r) => assert_eq!(r, "echo \"before\""),
RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
}
match &items[1] {
RuleItem::Conditional(c) => {
assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
}
RuleItem::Recipe(_) => panic!("Expected conditional, got recipe"),
}
match &items[2] {
RuleItem::Recipe(r) => assert_eq!(r, "echo \"after\""),
RuleItem::Conditional(_) => panic!("Expected recipe, got conditional"),
}
let simple_rule: Rule = "simple:\n\techo one\n\techo two\n".parse().unwrap();
let simple_items: Vec<_> = simple_rule.items().collect();
assert_eq!(simple_items.len(), 2);
match &simple_items[0] {
RuleItem::Recipe(r) => assert_eq!(r, "echo one"),
_ => panic!("Expected recipe"),
}
match &simple_items[1] {
RuleItem::Recipe(r) => assert_eq!(r, "echo two"),
_ => panic!("Expected recipe"),
}
let cond_only: Rule = "condtest:\nifeq (a,b)\n\techo yes\nendif\n"
.parse()
.unwrap();
let cond_items: Vec<_> = cond_only.items().collect();
assert_eq!(cond_items.len(), 1);
match &cond_items[0] {
RuleItem::Conditional(c) => {
assert_eq!(c.conditional_type(), Some("ifeq".to_string()));
}
_ => panic!("Expected conditional"),
}
}
#[test]
fn test_conditionals_iterator() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
endif
ifndef RELEASE
OTHER = dev
endif
"#
.parse()
.unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(conditionals.len(), 2);
assert_eq!(
conditionals[0].conditional_type(),
Some("ifdef".to_string())
);
assert_eq!(
conditionals[1].conditional_type(),
Some("ifndef".to_string())
);
}
#[test]
fn test_conditional_type_and_condition() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
endif
"#
.parse()
.unwrap();
let conditional = makefile.conditionals().next().unwrap();
assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
}
#[test]
fn test_conditional_has_else() {
let makefile_with_else: Makefile = r#"ifdef DEBUG
VAR = debug
else
VAR = release
endif
"#
.parse()
.unwrap();
let conditional = makefile_with_else.conditionals().next().unwrap();
assert!(conditional.has_else());
let makefile_without_else: Makefile = r#"ifdef DEBUG
VAR = debug
endif
"#
.parse()
.unwrap();
let conditional = makefile_without_else.conditionals().next().unwrap();
assert!(!conditional.has_else());
}
#[test]
fn test_conditional_if_body() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
endif
"#
.parse()
.unwrap();
let conditional = makefile.conditionals().next().unwrap();
let if_body = conditional.if_body();
assert!(if_body.is_some());
assert!(if_body.unwrap().contains("VAR = debug"));
}
#[test]
fn test_conditional_else_body() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
else
VAR = release
endif
"#
.parse()
.unwrap();
let conditional = makefile.conditionals().next().unwrap();
let else_body = conditional.else_body();
assert!(else_body.is_some());
assert!(else_body.unwrap().contains("VAR = release"));
}
#[test]
fn test_add_conditional_ifdef() {
let mut makefile = Makefile::new();
let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
assert!(result.is_ok());
let code = makefile.to_string();
assert!(code.contains("ifdef DEBUG"));
assert!(code.contains("VAR = debug"));
assert!(code.contains("endif"));
}
#[test]
fn test_add_conditional_with_else() {
let mut makefile = Makefile::new();
let result =
makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", Some("VAR = release\n"));
assert!(result.is_ok());
let code = makefile.to_string();
assert!(code.contains("ifdef DEBUG"));
assert!(code.contains("VAR = debug"));
assert!(code.contains("else"));
assert!(code.contains("VAR = release"));
assert!(code.contains("endif"));
}
#[test]
fn test_add_conditional_invalid_type() {
let mut makefile = Makefile::new();
let result = makefile.add_conditional("invalid", "DEBUG", "VAR = debug\n", None);
assert!(result.is_err());
}
#[test]
fn test_add_conditional_formatting() {
let mut makefile: Makefile = "VAR1 = value1\n".parse().unwrap();
let result = makefile.add_conditional("ifdef", "DEBUG", "VAR = debug\n", None);
assert!(result.is_ok());
let code = makefile.to_string();
assert!(code.contains("\n\nifdef DEBUG"));
}
#[test]
fn test_conditional_remove() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
endif
VAR2 = value2
"#
.parse()
.unwrap();
let mut conditional = makefile.conditionals().next().unwrap();
let result = conditional.remove();
assert!(result.is_ok());
let code = makefile.to_string();
assert!(!code.contains("ifdef DEBUG"));
assert!(!code.contains("VAR = debug"));
assert!(code.contains("VAR2 = value2"));
}
#[test]
fn test_add_conditional_ifndef() {
let mut makefile = Makefile::new();
let result = makefile.add_conditional("ifndef", "NDEBUG", "VAR = enabled\n", None);
assert!(result.is_ok());
let code = makefile.to_string();
assert!(code.contains("ifndef NDEBUG"));
assert!(code.contains("VAR = enabled"));
assert!(code.contains("endif"));
}
#[test]
fn test_add_conditional_ifeq() {
let mut makefile = Makefile::new();
let result = makefile.add_conditional("ifeq", "($(OS),Linux)", "VAR = linux\n", None);
assert!(result.is_ok());
let code = makefile.to_string();
assert!(code.contains("ifeq ($(OS),Linux)"));
assert!(code.contains("VAR = linux"));
assert!(code.contains("endif"));
}
#[test]
fn test_add_conditional_ifneq() {
let mut makefile = Makefile::new();
let result = makefile.add_conditional("ifneq", "($(OS),Windows)", "VAR = unix\n", None);
assert!(result.is_ok());
let code = makefile.to_string();
assert!(code.contains("ifneq ($(OS),Windows)"));
assert!(code.contains("VAR = unix"));
assert!(code.contains("endif"));
}
#[test]
fn test_conditional_api_integration() {
let mut makefile: Makefile = r#"VAR1 = value1
rule1:
command1
"#
.parse()
.unwrap();
makefile
.add_conditional("ifdef", "DEBUG", "CFLAGS += -g\n", Some("CFLAGS += -O2\n"))
.unwrap();
assert_eq!(makefile.conditionals().count(), 1);
let conditional = makefile.conditionals().next().unwrap();
assert_eq!(conditional.conditional_type(), Some("ifdef".to_string()));
assert_eq!(conditional.condition(), Some("DEBUG".to_string()));
assert!(conditional.has_else());
assert_eq!(makefile.variable_definitions().count(), 1);
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_conditional_if_items() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
rule:
command
endif
"#
.parse()
.unwrap();
let cond = makefile.conditionals().next().unwrap();
let items: Vec<_> = cond.if_items().collect();
assert_eq!(items.len(), 2);
match &items[0] {
MakefileItem::Variable(v) => {
assert_eq!(v.name(), Some("VAR".to_string()));
}
_ => panic!("Expected variable"),
}
match &items[1] {
MakefileItem::Rule(r) => {
assert!(r.targets().any(|t| t == "rule"));
}
_ => panic!("Expected rule"),
}
}
#[test]
fn test_conditional_else_items() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
else
VAR2 = release
rule2:
command
endif
"#
.parse()
.unwrap();
let cond = makefile.conditionals().next().unwrap();
let items: Vec<_> = cond.else_items().collect();
assert_eq!(items.len(), 2);
match &items[0] {
MakefileItem::Variable(v) => {
assert_eq!(v.name(), Some("VAR2".to_string()));
}
_ => panic!("Expected variable"),
}
match &items[1] {
MakefileItem::Rule(r) => {
assert!(r.targets().any(|t| t == "rule2"));
}
_ => panic!("Expected rule"),
}
}
#[test]
fn test_conditional_add_if_item() {
let makefile: Makefile = "ifdef DEBUG\nendif\n".parse().unwrap();
let mut cond = makefile.conditionals().next().unwrap();
let temp: Makefile = "CFLAGS = -g\n".parse().unwrap();
let var = temp.variable_definitions().next().unwrap();
cond.add_if_item(MakefileItem::Variable(var));
let code = makefile.to_string();
assert!(code.contains("CFLAGS = -g"));
let cond = makefile.conditionals().next().unwrap();
assert_eq!(cond.if_items().count(), 1);
}
#[test]
fn test_conditional_add_else_item() {
let makefile: Makefile = "ifdef DEBUG\nVAR=1\nendif\n".parse().unwrap();
let mut cond = makefile.conditionals().next().unwrap();
let temp: Makefile = "CFLAGS = -O2\n".parse().unwrap();
let var = temp.variable_definitions().next().unwrap();
cond.add_else_item(MakefileItem::Variable(var));
let code = makefile.to_string();
assert!(code.contains("else"));
assert!(code.contains("CFLAGS = -O2"));
let cond = makefile.conditionals().next().unwrap();
assert_eq!(cond.else_items().count(), 1);
}
#[test]
fn test_add_conditional_with_items() {
let mut makefile = Makefile::new();
let temp1: Makefile = "CFLAGS = -g\n".parse().unwrap();
let var1 = temp1.variable_definitions().next().unwrap();
let temp2: Makefile = "CFLAGS = -O2\n".parse().unwrap();
let var2 = temp2.variable_definitions().next().unwrap();
let temp3: Makefile = "debug:\n\techo debug\n".parse().unwrap();
let rule1 = temp3.rules().next().unwrap();
let result = makefile.add_conditional_with_items(
"ifdef",
"DEBUG",
vec![MakefileItem::Variable(var1), MakefileItem::Rule(rule1)],
Some(vec![MakefileItem::Variable(var2)]),
);
assert!(result.is_ok());
let code = makefile.to_string();
assert!(code.contains("ifdef DEBUG"));
assert!(code.contains("CFLAGS = -g"));
assert!(code.contains("debug:"));
assert!(code.contains("else"));
assert!(code.contains("CFLAGS = -O2"));
}
#[test]
fn test_conditional_items_with_nested_conditional() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
ifdef VERBOSE
VAR2 = verbose
endif
endif
"#
.parse()
.unwrap();
let cond = makefile.conditionals().next().unwrap();
let items: Vec<_> = cond.if_items().collect();
assert_eq!(items.len(), 2);
match &items[0] {
MakefileItem::Variable(v) => {
assert_eq!(v.name(), Some("VAR".to_string()));
}
_ => panic!("Expected variable"),
}
match &items[1] {
MakefileItem::Conditional(c) => {
assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
}
_ => panic!("Expected conditional"),
}
}
#[test]
fn test_conditional_items_with_include() {
let makefile: Makefile = r#"ifdef DEBUG
include debug.mk
VAR = debug
endif
"#
.parse()
.unwrap();
let cond = makefile.conditionals().next().unwrap();
let items: Vec<_> = cond.if_items().collect();
assert_eq!(items.len(), 2);
match &items[0] {
MakefileItem::Include(i) => {
assert_eq!(i.path(), Some("debug.mk".to_string()));
}
_ => panic!("Expected include"),
}
match &items[1] {
MakefileItem::Variable(v) => {
assert_eq!(v.name(), Some("VAR".to_string()));
}
_ => panic!("Expected variable"),
}
}
#[test]
fn test_makefile_items_iterator() {
let makefile: Makefile = r#"VAR = value
ifdef DEBUG
CFLAGS = -g
endif
rule:
command
include common.mk
"#
.parse()
.unwrap();
assert_eq!(makefile.variable_definitions().count(), 2);
assert_eq!(makefile.conditionals().count(), 1);
assert_eq!(makefile.rules().count(), 1);
let items: Vec<_> = makefile.items().collect();
assert!(
items.len() >= 3,
"Expected at least 3 items, got {}",
items.len()
);
match &items[0] {
MakefileItem::Variable(v) => {
assert_eq!(v.name(), Some("VAR".to_string()));
}
_ => panic!("Expected variable at position 0"),
}
match &items[1] {
MakefileItem::Conditional(c) => {
assert_eq!(c.conditional_type(), Some("ifdef".to_string()));
}
_ => panic!("Expected conditional at position 1"),
}
match &items[2] {
MakefileItem::Rule(r) => {
let targets: Vec<_> = r.targets().collect();
assert_eq!(targets, vec!["rule"]);
}
_ => panic!("Expected rule at position 2"),
}
}
#[test]
fn test_conditional_unwrap() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
rule:
command
endif
"#
.parse()
.unwrap();
let mut cond = makefile.conditionals().next().unwrap();
cond.unwrap().unwrap();
let code = makefile.to_string();
let expected = "VAR = debug\nrule:\n\tcommand\n";
assert_eq!(code, expected);
assert_eq!(makefile.conditionals().count(), 0);
assert_eq!(makefile.variable_definitions().count(), 1);
assert_eq!(makefile.rules().count(), 1);
}
#[test]
fn test_conditional_unwrap_with_else_fails() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
else
VAR = release
endif
"#
.parse()
.unwrap();
let mut cond = makefile.conditionals().next().unwrap();
let result = cond.unwrap();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot unwrap conditional with else clause"));
}
#[test]
fn test_conditional_unwrap_nested() {
let makefile: Makefile = r#"ifdef OUTER
VAR = outer
ifdef INNER
VAR2 = inner
endif
endif
"#
.parse()
.unwrap();
let mut outer_cond = makefile.conditionals().next().unwrap();
outer_cond.unwrap().unwrap();
let code = makefile.to_string();
let expected = "VAR = outer\nifdef INNER\nVAR2 = inner\nendif\n";
assert_eq!(code, expected);
}
#[test]
fn test_conditional_unwrap_empty() {
let makefile: Makefile = r#"ifdef DEBUG
endif
"#
.parse()
.unwrap();
let mut cond = makefile.conditionals().next().unwrap();
cond.unwrap().unwrap();
let code = makefile.to_string();
assert_eq!(code, "");
}
#[test]
fn test_rule_parent() {
let makefile: Makefile = r#"all:
echo "test"
"#
.parse()
.unwrap();
let rule = makefile.rules().next().unwrap();
let parent = rule.parent();
assert!(parent.is_none());
}
#[test]
fn test_item_parent_in_conditional() {
let makefile: Makefile = r#"ifdef DEBUG
VAR = debug
rule:
command
endif
"#
.parse()
.unwrap();
let cond = makefile.conditionals().next().unwrap();
let items: Vec<_> = cond.if_items().collect();
assert_eq!(items.len(), 2);
if let MakefileItem::Variable(var) = &items[0] {
let parent = var.parent();
assert!(parent.is_some());
if let Some(MakefileItem::Conditional(_)) = parent {
} else {
panic!("Expected variable parent to be a Conditional");
}
} else {
panic!("Expected first item to be a Variable");
}
if let MakefileItem::Rule(rule) = &items[1] {
let parent = rule.parent();
assert!(parent.is_some());
if let Some(MakefileItem::Conditional(_)) = parent {
} else {
panic!("Expected rule parent to be a Conditional");
}
} else {
panic!("Expected second item to be a Rule");
}
}
#[test]
fn test_nested_conditional_parent() {
let makefile: Makefile = r#"ifdef OUTER
VAR = outer
ifdef INNER
VAR2 = inner
endif
endif
"#
.parse()
.unwrap();
let outer_cond = makefile.conditionals().next().unwrap();
let items: Vec<_> = outer_cond.if_items().collect();
let inner_cond = items
.iter()
.find_map(|item| {
if let MakefileItem::Conditional(c) = item {
Some(c)
} else {
None
}
})
.unwrap();
let parent = inner_cond.parent();
assert!(parent.is_some());
if let Some(MakefileItem::Conditional(_)) = parent {
} else {
panic!("Expected inner conditional's parent to be a Conditional");
}
}
#[test]
fn test_line_col() {
let text = r#"# Comment at line 0
VAR1 = value1
VAR2 = value2
rule1: dep1 dep2
command1
command2
rule2:
command3
ifdef DEBUG
CFLAGS = -g
endif
"#;
let makefile: Makefile = text.parse().unwrap();
let vars: Vec<_> = makefile.variable_definitions().collect();
assert_eq!(vars.len(), 3);
assert_eq!(vars[0].line(), 1);
assert_eq!(vars[0].column(), 0);
assert_eq!(vars[0].line_col(), (1, 0));
assert_eq!(vars[1].line(), 2);
assert_eq!(vars[1].column(), 0);
assert_eq!(vars[2].line(), 12);
assert_eq!(vars[2].column(), 0);
let rules: Vec<_> = makefile.rules().collect();
assert_eq!(rules.len(), 2);
assert_eq!(rules[0].line(), 4);
assert_eq!(rules[0].column(), 0);
assert_eq!(rules[0].line_col(), (4, 0));
assert_eq!(rules[1].line(), 8);
assert_eq!(rules[1].column(), 0);
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(conditionals.len(), 1);
assert_eq!(conditionals[0].line(), 11);
assert_eq!(conditionals[0].column(), 0);
assert_eq!(conditionals[0].line_col(), (11, 0));
}
#[test]
fn test_line_col_multiline() {
let text = "SOURCES = \\\n\tfile1.c \\\n\tfile2.c\n\ntarget: $(SOURCES)\n\tgcc -o target $(SOURCES)\n";
let makefile: Makefile = text.parse().unwrap();
let vars: Vec<_> = makefile.variable_definitions().collect();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].line(), 0);
assert_eq!(vars[0].column(), 0);
let rules: Vec<_> = makefile.rules().collect();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].line(), 4);
assert_eq!(rules[0].column(), 0);
}
#[test]
fn test_line_col_includes() {
let text = "VAR = value\n\ninclude config.mk\n-include optional.mk\n";
let makefile: Makefile = text.parse().unwrap();
let vars: Vec<_> = makefile.variable_definitions().collect();
assert_eq!(vars[0].line(), 0);
let includes: Vec<_> = makefile.includes().collect();
assert_eq!(includes.len(), 2);
assert_eq!(includes[0].line(), 2);
assert_eq!(includes[0].column(), 0);
assert_eq!(includes[1].line(), 3);
assert_eq!(includes[1].column(), 0);
}
#[test]
fn test_conditional_in_rule_vs_toplevel() {
let text1 = r#"rule:
command
ifeq (,$(X))
test
endif
"#;
let makefile: Makefile = text1.parse().unwrap();
let rules: Vec<_> = makefile.rules().collect();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(rules.len(), 1);
assert_eq!(
conditionals.len(),
0,
"Conditional should be part of rule, not top-level"
);
let text2 = r#"rule:
command
ifeq (,$(X))
test
endif
"#;
let makefile: Makefile = text2.parse().unwrap();
let rules: Vec<_> = makefile.rules().collect();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(rules.len(), 1);
assert_eq!(
conditionals.len(),
1,
"Conditional after blank line should be top-level"
);
assert_eq!(conditionals[0].line(), 3);
}
#[test]
fn test_nested_conditionals_line_tracking() {
let text = r#"ifdef OUTER
VAR1 = value1
ifdef INNER
VAR2 = value2
endif
VAR3 = value3
endif
"#;
let makefile: Makefile = text.parse().unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(
conditionals.len(),
1,
"Only outer conditional should be top-level"
);
assert_eq!(conditionals[0].line(), 0);
assert_eq!(conditionals[0].column(), 0);
}
#[test]
fn test_conditional_else_line_tracking() {
let text = r#"VAR1 = before
ifdef DEBUG
DEBUG_FLAGS = -g
else
DEBUG_FLAGS = -O2
endif
VAR2 = after
"#;
let makefile: Makefile = text.parse().unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(conditionals.len(), 1);
assert_eq!(conditionals[0].line(), 2);
assert_eq!(conditionals[0].column(), 0);
}
#[test]
fn test_broken_conditional_endif_without_if() {
let text = "VAR = value\nendif\n";
let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
let vars: Vec<_> = makefile.variable_definitions().collect();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].line(), 0);
}
#[test]
fn test_broken_conditional_else_without_if() {
let text = "VAR = value\nelse\nVAR2 = other\n";
let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
let vars: Vec<_> = makefile.variable_definitions().collect();
assert!(!vars.is_empty(), "Should parse at least the first variable");
assert_eq!(vars[0].line(), 0);
}
#[test]
fn test_broken_conditional_missing_endif() {
let text = r#"ifdef DEBUG
DEBUG_FLAGS = -g
VAR = value
"#;
let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
assert!(makefile.code().contains("ifdef DEBUG"));
}
#[test]
fn test_multiple_conditionals_line_tracking() {
let text = r#"ifdef A
VAR_A = a
endif
ifdef B
VAR_B = b
endif
ifdef C
VAR_C = c
endif
"#;
let makefile: Makefile = text.parse().unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(conditionals.len(), 3);
assert_eq!(conditionals[0].line(), 0);
assert_eq!(conditionals[1].line(), 4);
assert_eq!(conditionals[2].line(), 8);
}
#[test]
fn test_conditional_with_multiple_else_ifeq() {
let text = r#"ifeq ($(OS),Windows)
EXT = .exe
else ifeq ($(OS),Linux)
EXT = .bin
else
EXT = .out
endif
"#;
let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(conditionals.len(), 1);
assert_eq!(conditionals[0].line(), 0);
assert_eq!(conditionals[0].column(), 0);
}
#[test]
fn test_conditional_types_line_tracking() {
let text = r#"ifdef VAR1
A = 1
endif
ifndef VAR2
B = 2
endif
ifeq ($(X),y)
C = 3
endif
ifneq ($(Y),n)
D = 4
endif
"#;
let makefile: Makefile = text.parse().unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(conditionals.len(), 4);
assert_eq!(conditionals[0].line(), 0); assert_eq!(
conditionals[0].conditional_type(),
Some("ifdef".to_string())
);
assert_eq!(conditionals[1].line(), 4); assert_eq!(
conditionals[1].conditional_type(),
Some("ifndef".to_string())
);
assert_eq!(conditionals[2].line(), 8); assert_eq!(conditionals[2].conditional_type(), Some("ifeq".to_string()));
assert_eq!(conditionals[3].line(), 12); assert_eq!(
conditionals[3].conditional_type(),
Some("ifneq".to_string())
);
}
#[test]
fn test_conditional_in_rule_with_recipes() {
let text = r#"test:
echo "start"
ifdef VERBOSE
echo "verbose mode"
endif
echo "end"
"#;
let makefile: Makefile = text.parse().unwrap();
let rules: Vec<_> = makefile.rules().collect();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].line(), 0);
assert_eq!(conditionals.len(), 0);
}
#[test]
fn test_broken_conditional_double_else() {
let text = r#"ifdef DEBUG
A = 1
else
B = 2
else
C = 3
endif
"#;
let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
assert!(makefile.code().contains("ifdef DEBUG"));
}
#[test]
fn test_broken_conditional_mismatched_nesting() {
let text = r#"ifdef A
VAR = value
endif
endif
"#;
let makefile = Makefile::read_relaxed(&mut text.as_bytes()).unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert!(
!conditionals.is_empty(),
"Should parse at least the first conditional"
);
}
#[test]
fn test_conditional_with_comment_line_tracking() {
let text = r#"# This is a comment
ifdef DEBUG
# Another comment
CFLAGS = -g
endif
# Final comment
"#;
let makefile: Makefile = text.parse().unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(conditionals.len(), 1);
assert_eq!(conditionals[0].line(), 1);
assert_eq!(conditionals[0].column(), 0);
}
#[test]
fn test_conditional_after_variable_with_blank_lines() {
let text = r#"VAR1 = value1
ifdef DEBUG
VAR2 = value2
endif
"#;
let makefile: Makefile = text.parse().unwrap();
let vars: Vec<_> = makefile.variable_definitions().collect();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(vars.len(), 2);
assert_eq!(vars[0].line(), 0); assert_eq!(vars[1].line(), 4);
assert_eq!(conditionals.len(), 1);
assert_eq!(conditionals[0].line(), 3);
}
#[test]
fn test_empty_conditional_line_tracking() {
let text = r#"ifdef DEBUG
endif
ifndef RELEASE
endif
"#;
let makefile: Makefile = text.parse().unwrap();
let conditionals: Vec<_> = makefile.conditionals().collect();
assert_eq!(conditionals.len(), 2);
assert_eq!(conditionals[0].line(), 0);
assert_eq!(conditionals[1].line(), 3);
}
#[test]
fn test_recipe_line_tracking() {
let text = r#"build:
echo "Building..."
gcc -o app main.c
echo "Done"
test:
./run-tests
"#;
let makefile: Makefile = text.parse().unwrap();
let rule1 = makefile.rules().next().expect("Should have first rule");
let recipes: Vec<_> = rule1.recipe_nodes().collect();
assert_eq!(recipes.len(), 3);
assert_eq!(recipes[0].text(), "echo \"Building...\"");
assert_eq!(recipes[0].line(), 1);
assert_eq!(recipes[0].column(), 0);
assert_eq!(recipes[1].text(), "gcc -o app main.c");
assert_eq!(recipes[1].line(), 2);
assert_eq!(recipes[1].column(), 0);
assert_eq!(recipes[2].text(), "echo \"Done\"");
assert_eq!(recipes[2].line(), 3);
assert_eq!(recipes[2].column(), 0);
let rule2 = makefile.rules().nth(1).expect("Should have second rule");
let recipes2: Vec<_> = rule2.recipe_nodes().collect();
assert_eq!(recipes2.len(), 1);
assert_eq!(recipes2[0].text(), "./run-tests");
assert_eq!(recipes2[0].line(), 6);
assert_eq!(recipes2[0].column(), 0);
}
#[test]
fn test_recipe_with_variables_line_tracking() {
let text = r#"install:
mkdir -p $(DESTDIR)
cp $(BINARY) $(DESTDIR)/
"#;
let makefile: Makefile = text.parse().unwrap();
let rule = makefile.rules().next().expect("Should have rule");
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 2);
assert_eq!(recipes[0].line(), 1);
assert_eq!(recipes[1].line(), 2);
}
#[test]
fn test_recipe_text_no_leading_tab() {
let text = "test:\n\techo hello\n\t\techo nested\n\t echo with spaces\n";
let makefile: Makefile = text.parse().unwrap();
let rule = makefile.rules().next().expect("Should have rule");
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 3);
eprintln!("Recipe 0 syntax tree:\n{:#?}", recipes[0].syntax());
assert_eq!(recipes[0].text(), "echo hello");
eprintln!("Recipe 1 syntax tree:\n{:#?}", recipes[1].syntax());
assert_eq!(recipes[1].text(), "\techo nested");
eprintln!("Recipe 2 syntax tree:\n{:#?}", recipes[2].syntax());
assert_eq!(recipes[2].text(), " echo with spaces");
}
#[test]
fn test_recipe_parent() {
let makefile: Makefile = "all: dep\n\techo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let recipe = rule.recipe_nodes().next().unwrap();
let parent = recipe.parent().expect("Recipe should have parent");
assert_eq!(parent.targets().collect::<Vec<_>>(), vec!["all"]);
assert_eq!(parent.prerequisites().collect::<Vec<_>>(), vec!["dep"]);
}
#[test]
fn test_recipe_is_silent_various_prefixes() {
let makefile: Makefile = r#"test:
@echo silent
-echo ignore
+echo always
@-echo silent_ignore
-@echo ignore_silent
+@echo always_silent
echo normal
"#
.parse()
.unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 7);
assert!(recipes[0].is_silent(), "@echo should be silent");
assert!(!recipes[1].is_silent(), "-echo should not be silent");
assert!(!recipes[2].is_silent(), "+echo should not be silent");
assert!(recipes[3].is_silent(), "@-echo should be silent");
assert!(recipes[4].is_silent(), "-@echo should be silent");
assert!(recipes[5].is_silent(), "+@echo should be silent");
assert!(!recipes[6].is_silent(), "echo should not be silent");
}
#[test]
fn test_recipe_is_ignore_errors_various_prefixes() {
let makefile: Makefile = r#"test:
@echo silent
-echo ignore
+echo always
@-echo silent_ignore
-@echo ignore_silent
+-echo always_ignore
echo normal
"#
.parse()
.unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 7);
assert!(
!recipes[0].is_ignore_errors(),
"@echo should not ignore errors"
);
assert!(recipes[1].is_ignore_errors(), "-echo should ignore errors");
assert!(
!recipes[2].is_ignore_errors(),
"+echo should not ignore errors"
);
assert!(recipes[3].is_ignore_errors(), "@-echo should ignore errors");
assert!(recipes[4].is_ignore_errors(), "-@echo should ignore errors");
assert!(recipes[5].is_ignore_errors(), "+-echo should ignore errors");
assert!(
!recipes[6].is_ignore_errors(),
"echo should not ignore errors"
);
}
#[test]
fn test_recipe_set_prefix_add() {
let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let mut recipe = rule.recipe_nodes().next().unwrap();
recipe.set_prefix("@");
assert_eq!(recipe.text(), "@echo hello");
assert!(recipe.is_silent());
}
#[test]
fn test_recipe_set_prefix_change() {
let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let mut recipe = rule.recipe_nodes().next().unwrap();
recipe.set_prefix("-");
assert_eq!(recipe.text(), "-echo hello");
assert!(!recipe.is_silent());
assert!(recipe.is_ignore_errors());
}
#[test]
fn test_recipe_set_prefix_remove() {
let makefile: Makefile = "all:\n\t@-echo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let mut recipe = rule.recipe_nodes().next().unwrap();
recipe.set_prefix("");
assert_eq!(recipe.text(), "echo hello");
assert!(!recipe.is_silent());
assert!(!recipe.is_ignore_errors());
}
#[test]
fn test_recipe_set_prefix_combinations() {
let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let mut recipe = rule.recipe_nodes().next().unwrap();
recipe.set_prefix("@-");
assert_eq!(recipe.text(), "@-echo hello");
assert!(recipe.is_silent());
assert!(recipe.is_ignore_errors());
recipe.set_prefix("-@");
assert_eq!(recipe.text(), "-@echo hello");
assert!(recipe.is_silent());
assert!(recipe.is_ignore_errors());
}
#[test]
fn test_recipe_replace_text_basic() {
let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let mut recipe = rule.recipe_nodes().next().unwrap();
recipe.replace_text("echo world");
assert_eq!(recipe.text(), "echo world");
let rule = makefile.rules().next().unwrap();
assert_eq!(rule.recipes().collect::<Vec<_>>(), vec!["echo world"]);
}
#[test]
fn test_recipe_replace_text_with_prefix() {
let makefile: Makefile = "all:\n\t@echo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let mut recipe = rule.recipe_nodes().next().unwrap();
recipe.replace_text("@echo goodbye");
assert_eq!(recipe.text(), "@echo goodbye");
assert!(recipe.is_silent());
}
#[test]
fn test_recipe_insert_before_single() {
let makefile: Makefile = "all:\n\techo world\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let recipe = rule.recipe_nodes().next().unwrap();
recipe.insert_before("echo hello");
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(recipes, vec!["echo hello", "echo world"]);
}
#[test]
fn test_recipe_insert_before_multiple() {
let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
.parse()
.unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
recipes[1].insert_before("echo middle");
let rule = makefile.rules().next().unwrap();
let new_recipes: Vec<_> = rule.recipes().collect();
assert_eq!(
new_recipes,
vec!["echo one", "echo middle", "echo two", "echo three"]
);
}
#[test]
fn test_recipe_insert_before_first() {
let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
recipes[0].insert_before("echo zero");
let rule = makefile.rules().next().unwrap();
let new_recipes: Vec<_> = rule.recipes().collect();
assert_eq!(new_recipes, vec!["echo zero", "echo one", "echo two"]);
}
#[test]
fn test_recipe_insert_after_single() {
let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let recipe = rule.recipe_nodes().next().unwrap();
recipe.insert_after("echo world");
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(recipes, vec!["echo hello", "echo world"]);
}
#[test]
fn test_recipe_insert_after_multiple() {
let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
.parse()
.unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
recipes[1].insert_after("echo middle");
let rule = makefile.rules().next().unwrap();
let new_recipes: Vec<_> = rule.recipes().collect();
assert_eq!(
new_recipes,
vec!["echo one", "echo two", "echo middle", "echo three"]
);
}
#[test]
fn test_recipe_insert_after_last() {
let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
recipes[1].insert_after("echo three");
let rule = makefile.rules().next().unwrap();
let new_recipes: Vec<_> = rule.recipes().collect();
assert_eq!(new_recipes, vec!["echo one", "echo two", "echo three"]);
}
#[test]
fn test_recipe_remove_single() {
let makefile: Makefile = "all:\n\techo hello\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let recipe = rule.recipe_nodes().next().unwrap();
recipe.remove();
let rule = makefile.rules().next().unwrap();
assert_eq!(rule.recipes().count(), 0);
}
#[test]
fn test_recipe_remove_first() {
let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
.parse()
.unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
recipes[0].remove();
let rule = makefile.rules().next().unwrap();
let new_recipes: Vec<_> = rule.recipes().collect();
assert_eq!(new_recipes, vec!["echo two", "echo three"]);
}
#[test]
fn test_recipe_remove_middle() {
let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
.parse()
.unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
recipes[1].remove();
let rule = makefile.rules().next().unwrap();
let new_recipes: Vec<_> = rule.recipes().collect();
assert_eq!(new_recipes, vec!["echo one", "echo three"]);
}
#[test]
fn test_recipe_remove_last() {
let makefile: Makefile = "all:\n\techo one\n\techo two\n\techo three\n"
.parse()
.unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
recipes[2].remove();
let rule = makefile.rules().next().unwrap();
let new_recipes: Vec<_> = rule.recipes().collect();
assert_eq!(new_recipes, vec!["echo one", "echo two"]);
}
#[test]
fn test_recipe_multiple_operations() {
let makefile: Makefile = "all:\n\techo one\n\techo two\n".parse().unwrap();
let rule = makefile.rules().next().unwrap();
let mut recipe = rule.recipe_nodes().next().unwrap();
recipe.replace_text("echo modified");
assert_eq!(recipe.text(), "echo modified");
recipe.set_prefix("@");
assert_eq!(recipe.text(), "@echo modified");
recipe.insert_after("echo three");
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipes().collect();
assert_eq!(recipes, vec!["@echo modified", "echo three", "echo two"]);
}
#[test]
fn test_from_str_relaxed_valid() {
let input = "all: foo\n\tfoo bar\n";
let (makefile, errors) = Makefile::from_str_relaxed(input);
assert!(errors.is_empty());
assert_eq!(makefile.rules().count(), 1);
assert_eq!(makefile.to_string(), input);
}
#[test]
fn test_from_str_relaxed_with_errors() {
let input = "rule target\n\tcommand\n";
let (makefile, errors) = Makefile::from_str_relaxed(input);
assert!(!errors.is_empty());
assert_eq!(makefile.to_string(), input);
}
#[test]
fn test_positioned_errors_have_valid_ranges() {
let input = "rule target\n\tcommand\n";
let parsed = Makefile::parse(input);
assert!(!parsed.ok());
let positioned = parsed.positioned_errors();
assert!(!positioned.is_empty());
for err in positioned {
let start: u32 = err.range.start().into();
let end: u32 = err.range.end().into();
assert!(start <= end);
assert!((end as usize) <= input.len());
}
}
#[test]
fn test_positioned_errors_point_to_error_location() {
let input = "rule target\n\tcommand\n";
let parsed = Makefile::parse(input);
assert!(!parsed.ok());
let positioned = parsed.positioned_errors();
assert!(!positioned.is_empty());
let err = &positioned[0];
let start: usize = err.range.start().into();
let end: usize = err.range.end().into();
let error_text = &input[start..end];
assert!(!error_text.is_empty());
let tree = parsed.tree();
assert_eq!(tree.to_string(), input);
}
#[test]
fn test_tree_with_errors_preserves_text() {
let input = "rule target\n\tcommand\nVAR = value\n";
let parsed = Makefile::parse(input);
assert!(!parsed.ok());
let tree = parsed.tree();
assert_eq!(tree.to_string(), input);
assert_eq!(tree.variable_definitions().count(), 1);
}
}
#[cfg(test)]
mod test_continuation {
use super::*;
#[test]
fn test_recipe_continuation_lines() {
let makefile_content = r#"override_dh_autoreconf:
set -x; [ -f binoculars-ng/src/Hkl/H5.hs.orig ] || \
dpkg --compare-versions '$(HDF5_VERSION)' '<<' 1.12.0 || \
sed -i.orig 's/H5L_info_t/H5L_info1_t/g;s/h5l_iterate/h5l_iterate1/g' binoculars-ng/src/Hkl/H5.hs
dh_autoreconf
"#;
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 2);
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";
assert_eq!(recipes[0].text(), expected_first);
assert_eq!(recipes[1].text(), "dh_autoreconf");
}
#[test]
fn test_simple_continuation() {
let makefile_content = "test:\n\techo hello && \\\n\t echo world\n";
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 1);
assert_eq!(recipes[0].text(), "echo hello && \\\n echo world");
}
#[test]
fn test_multiple_continuations() {
let makefile_content = "test:\n\techo line1 && \\\n\t echo line2 && \\\n\t echo line3 && \\\n\t echo line4\n";
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 1);
assert_eq!(
recipes[0].text(),
"echo line1 && \\\n echo line2 && \\\n echo line3 && \\\n echo line4"
);
}
#[test]
fn test_continuation_round_trip() {
let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let output = makefile.to_string();
assert_eq!(output, makefile_content);
}
#[test]
fn test_continuation_with_silent_prefix() {
let makefile_content = "test:\n\t@echo hello && \\\n\t echo world\n";
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 1);
assert_eq!(recipes[0].text(), "@echo hello && \\\n echo world");
assert!(recipes[0].is_silent());
}
#[test]
fn test_mixed_continued_and_non_continued() {
let makefile_content = r#"test:
echo first
echo second && \
echo third
echo fourth
"#;
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 3);
assert_eq!(recipes[0].text(), "echo first");
assert_eq!(recipes[1].text(), "echo second && \\\n echo third");
assert_eq!(recipes[2].text(), "echo fourth");
}
#[test]
fn test_continuation_replace_command() {
let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let mut rule = makefile.rules().next().unwrap();
rule.replace_command(0, "echo replaced");
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 2);
assert_eq!(recipes[0].text(), "echo replaced");
assert_eq!(recipes[1].text(), "echo done");
}
#[test]
fn test_continuation_count() {
let makefile_content = "test:\n\techo hello && \\\n\t echo world\n\techo done\n";
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let rule = makefile.rules().next().unwrap();
assert_eq!(rule.recipe_count(), 2);
assert_eq!(rule.recipe_nodes().count(), 2);
let recipes_list: Vec<_> = rule.recipes().collect();
assert_eq!(
recipes_list,
vec!["echo hello && \\\n echo world", "echo done"]
);
}
#[test]
fn test_backslash_in_middle_of_line() {
let makefile_content = "test:\n\techo hello\\nworld\n\techo done\n";
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 2);
assert_eq!(recipes[0].text(), "echo hello\\nworld");
assert_eq!(recipes[1].text(), "echo done");
}
#[test]
fn test_shell_for_loop_with_continuation() {
let makefile_content = r#"override_dh_installman:
for i in foo bar; do \
pod2man --section=1 $$i ; \
done
"#;
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let rule = makefile.rules().next().unwrap();
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 1);
let recipe_text = recipes[0].text();
let expected_recipe = "for i in foo bar; do \\\n\tpod2man --section=1 $$i ; \\\ndone";
assert_eq!(recipe_text, expected_recipe);
let output = makefile.to_string();
assert_eq!(output, makefile_content);
}
#[test]
fn test_shell_for_loop_remove_command() {
let makefile_content = r#"override_dh_installman:
for i in foo bar; do \
pod2man --section=1 $$i ; \
done
echo "Done with man pages"
"#;
let makefile = Makefile::read_relaxed(makefile_content.as_bytes()).unwrap();
let mut rule = makefile.rules().next().unwrap();
assert_eq!(rule.recipe_count(), 2);
rule.remove_command(1);
let recipes: Vec<_> = rule.recipe_nodes().collect();
assert_eq!(recipes.len(), 1);
let output = makefile.to_string();
let expected_output = r#"override_dh_installman:
for i in foo bar; do \
pod2man --section=1 $$i ; \
done
"#;
assert_eq!(output, expected_output);
}
#[test]
fn test_variable_reference_paren() {
let makefile: Makefile = "CFLAGS = $(BASE_FLAGS) -Wall\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
assert_eq!(refs[0].to_string(), "$(BASE_FLAGS)");
}
#[test]
fn test_variable_reference_brace() {
let makefile: Makefile = "CFLAGS = ${BASE_FLAGS} -Wall\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].name(), Some("BASE_FLAGS".to_string()));
assert_eq!(refs[0].to_string(), "${BASE_FLAGS}");
}
#[test]
fn test_variable_reference_in_prerequisites() {
let makefile: Makefile = "all: $(TARGETS)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
assert!(names.contains(&"TARGETS".to_string()));
}
#[test]
fn test_variable_reference_multiple() {
let makefile: Makefile =
"CFLAGS = $(BASE_FLAGS) -Wall\nLDFLAGS = $(BASE_LDFLAGS) -lm\nall: $(TARGETS)\n"
.parse()
.unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
assert!(names.contains(&"BASE_FLAGS".to_string()));
assert!(names.contains(&"BASE_LDFLAGS".to_string()));
assert!(names.contains(&"TARGETS".to_string()));
}
#[test]
fn test_variable_reference_nested() {
let makefile: Makefile = "FOO = $($(INNER))\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
assert!(names.contains(&"INNER".to_string()));
}
#[test]
fn test_variable_reference_line_col() {
let makefile: Makefile = "A = 1\nB = $(FOO)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].name(), Some("FOO".to_string()));
assert_eq!(refs[0].line(), 1);
assert_eq!(refs[0].column(), 4);
assert_eq!(refs[0].line_col(), (1, 4));
}
#[test]
fn test_variable_reference_no_refs() {
let makefile: Makefile = "A = hello\nall:\n\techo done\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 0);
}
#[test]
fn test_variable_reference_mixed_styles() {
let makefile: Makefile = "A = $(FOO) ${BAR}\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
let names: Vec<_> = refs.iter().filter_map(|r| r.name()).collect();
assert_eq!(names.len(), 2);
assert!(names.contains(&"FOO".to_string()));
assert!(names.contains(&"BAR".to_string()));
}
#[test]
fn test_brace_variable_in_prerequisites() {
let makefile: Makefile = "all: ${OBJS}\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].name(), Some("OBJS".to_string()));
}
#[test]
fn test_parse_brace_variable_roundtrip() {
let input = "CFLAGS = ${BASE_FLAGS} -Wall\n";
let makefile: Makefile = input.parse().unwrap();
assert_eq!(makefile.to_string(), input);
}
#[test]
fn test_parse_nested_variable_in_value_roundtrip() {
let input = "FOO = $(BAR) baz $(QUUX)\n";
let makefile: Makefile = input.parse().unwrap();
assert_eq!(makefile.to_string(), input);
}
#[test]
fn test_is_function_call() {
let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 1);
assert!(refs[0].is_function_call());
}
#[test]
fn test_is_function_call_simple_variable() {
let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 1);
assert!(!refs[0].is_function_call());
}
#[test]
fn test_is_function_call_with_commas() {
let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 1);
assert!(refs[0].is_function_call());
}
#[test]
fn test_is_function_call_braces() {
let makefile: Makefile = "FILES = ${wildcard *.c}\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs.len(), 1);
assert!(refs[0].is_function_call());
}
#[test]
fn test_argument_count_simple_variable() {
let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs[0].argument_count(), 0);
}
#[test]
fn test_argument_count_one_arg() {
let makefile: Makefile = "FILES = $(wildcard *.c)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs[0].argument_count(), 1);
}
#[test]
fn test_argument_count_three_args() {
let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs[0].argument_count(), 3);
}
#[test]
fn test_argument_index_at_offset_subst() {
let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs[0].argument_index_at_offset(12), Some(0));
assert_eq!(refs[0].argument_index_at_offset(14), Some(1));
assert_eq!(refs[0].argument_index_at_offset(16), Some(2));
}
#[test]
fn test_argument_index_at_offset_outside() {
let makefile: Makefile = "X = $(subst a,b,text)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs[0].argument_index_at_offset(0), None);
assert_eq!(refs[0].argument_index_at_offset(22), None);
}
#[test]
fn test_argument_index_at_offset_simple_variable() {
let makefile: Makefile = "CFLAGS = $(CC)\n".parse().unwrap();
let refs: Vec<_> = makefile.variable_references().collect();
assert_eq!(refs[0].argument_index_at_offset(11), None);
}
#[test]
fn test_lex_braces() {
use crate::lex::lex;
let tokens = lex("${FOO}");
let kinds: Vec<_> = tokens.iter().map(|(k, _)| *k).collect();
assert!(kinds.contains(&DOLLAR));
assert!(kinds.contains(&LBRACE));
assert!(kinds.contains(&RBRACE));
}
}