makectl 0.2.0

Generate and manage targets in your Makefiles
use std::fs;
use std::path::Path;
use std::process;

use anyhow::{bail, Result};
use colored::Colorize;

use crate::parser::Makefile;

pub fn execute(file: &str, check: bool) -> Result<()> {
    if !Path::new(file).exists() {
        bail!("Makefile not found at {}", file);
    }

    let content = fs::read_to_string(file)?;
    let makefile = Makefile::parse(&content)?;
    let formatted = format_makefile(&makefile);

    if content == formatted {
        println!("{} {} is already formatted", "OK".green().bold(), file);
        return Ok(());
    }

    if check {
        println!("{} {} needs formatting", "FAIL".red().bold(), file);
        process::exit(1);
    }

    fs::write(file, &formatted)?;
    println!("{} Formatted {}", "OK".green().bold(), file);

    Ok(())
}

fn format_makefile(makefile: &Makefile) -> String {
    let raw = makefile.to_string();
    let mut output = String::new();
    let mut prev_blank = false;
    let mut first = true;

    for line in raw.lines() {
        let trimmed_right = line.trim_end();

        if trimmed_right.is_empty() {
            if prev_blank || first {
                continue;
            }
            output.push('\n');
            prev_blank = true;
            first = false;
            continue;
        }

        prev_blank = false;
        first = false;
        output.push_str(trimmed_right);
        output.push('\n');
    }

    if !output.is_empty() && !output.ends_with('\n') {
        output.push('\n');
    }

    output
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn removes_trailing_whitespace() {
        let input = "build:   \n\tcargo build  \n";
        let mf = Makefile::parse(input).unwrap();
        let formatted = format_makefile(&mf);
        assert!(!formatted.contains("   \n"));
        assert!(formatted.contains("build:\n"));
    }

    #[test]
    fn collapses_multiple_blank_lines() {
        let input = "build:\n\tcargo build\n\n\n\ntest:\n\tcargo test\n";
        let mf = Makefile::parse(input).unwrap();
        let formatted = format_makefile(&mf);
        assert!(!formatted.contains("\n\n\n"));
    }

    #[test]
    fn ends_with_newline() {
        let input = "build:\n\tcargo build";
        let mf = Makefile::parse(input).unwrap();
        let formatted = format_makefile(&mf);
        assert!(formatted.ends_with('\n'));
    }

    #[test]
    fn idempotent() {
        let input = ".PHONY: test\n\ntest:\n\tpytest\n";
        let mf = Makefile::parse(input).unwrap();
        let first = format_makefile(&mf);
        let mf2 = Makefile::parse(&first).unwrap();
        let second = format_makefile(&mf2);
        assert_eq!(first, second);
    }
}