unmake 0.0.27

a makefile linter
Documentation
//! CLI unmake tool

extern crate die;
extern crate getopts;
extern crate unmake;
extern crate walkdir;

use self::unmake::{inspect, warnings};

use die::{Die, die};

use std::env;
use std::fs;
use std::io;
use std::path;
use std::process;
use std::sync;

/// DIRECTORY_EXCLUSIONS collects common junk directories.
pub static DIRECTORY_EXCLUSIONS: sync::LazyLock<Vec<&str>> =
    sync::LazyLock::new(|| vec![".git", "node_modules", "vendor"]);

/// CLI entrypoint
fn main() {
    let brief: String = format!(
        "Usage: {} <OPTIONS> <path> [<path> ...]",
        env!("CARGO_PKG_NAME")
    );

    let mut opts: getopts::Options = getopts::Options::new();
    opts.optopt("i", "inspect", "summarize file details", "<makefile>");
    opts.optflag("d", "debug", "emit additional logs");
    opts.optflag("h", "help", "print usage info");
    opts.optflag("l", "list", "list makefile paths");
    opts.optflag("", "print0", "null delimit paths");
    opts.optflag(
        "n",
        "dry-run",
        "process makefiles through external build tools",
    );
    opts.optflag("v", "version", "print version info");

    let usage: String = opts.usage(&brief);
    let arguments: Vec<String> = env::args().collect();
    let optmatches: getopts::Matches = opts.parse(&arguments[1..]).die(&usage);

    if optmatches.opt_present("h") {
        die!(0; usage);
    }

    if optmatches.opt_present("v") {
        die!(0; format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")));
    }

    let debug: bool = optmatches.opt_present("d");
    let list_makefile_paths: bool = optmatches.opt_present("l");
    let null_delimit_paths: bool = optmatches.opt_present("print0");
    let process_dry_run: bool = optmatches.opt_present("n");

    if optmatches.opt_present("i") {
        let pth_string = optmatches.opt_str("i").die(&usage);
        let pth: &path::Path = path::Path::new(&pth_string);
        let metadata: inspect::Metadata = inspect::analyze(pth).map_err(|err| die!(err)).unwrap();
        println!("{}", metadata);
        die!(0);
    }

    let pth_strings: Vec<String> = optmatches.free;

    if pth_strings.is_empty() {
        die!(1; usage);
    }

    let mut found_quirk = false;
    let mut ws: Vec<warnings::Warning> = Vec::new();

    let cwd: std::path::PathBuf =
        env::current_dir().die("error: unable to query current working directory");

    let mut action = |p: &path::Path| {
        let pth_string: String = p.display().to_string();
        let metadata_result: Result<unmake::inspect::Metadata, String> =
            unmake::inspect::analyze(p);

        if let Err(err) = &metadata_result {
            found_quirk = true;
            println!("{}", err);
            return;
        }

        let metadata: inspect::Metadata = metadata_result.unwrap();

        if !metadata.is_makefile {
            return;
        }

        if metadata.is_machine_generated {
            if debug {
                eprintln!(
                    "debug: skipping {}: likely machine-generated by {}",
                    pth_string, metadata.build_system
                );
            }

            return;
        }

        if list_makefile_paths {
            if null_delimit_paths {
                print!("{}\0", pth_string);
            } else {
                println!("{}", pth_string);
            }

            return;
        }

        if process_dry_run {
            if metadata.is_include_file {
                if debug {
                    eprintln!(
                        "debug: skipping include makefile for dry run analysis: {}",
                        pth_string
                    );
                }

                return;
            }

            let dir: &std::path::Path = p
                .parent()
                .map(|e| match e.display().to_string().as_str() {
                    "" => cwd.as_path(),
                    _ => e,
                })
                .unwrap_or(cwd.as_path());

            let dry_run_output: process::Output = process::Command::new(&metadata.build_system)
                .args(["-nf", &metadata.filename])
                .current_dir(dir)
                .output()
                .die(
                    &format!(
                        "error: unable to run build tool: {}",
                        &metadata.build_system
                    )
                    .to_string(),
                );

            if !dry_run_output.status.success() {
                found_quirk = true;
                println!("{}", pth_string);
                print!(
                    "{}",
                    String::from_utf8(dry_run_output.stdout).unwrap_or(
                        format!(
                            "error: unable to decode {} stdout stream",
                            &metadata.build_system
                        )
                        .to_string()
                    )
                );
                eprint!(
                    "{}",
                    String::from_utf8(dry_run_output.stderr).unwrap_or(
                        format!(
                            "error: unable to decode {} stderr stream",
                            &metadata.build_system
                        )
                        .to_string()
                    )
                );
            }

            return;
        }

        if metadata.build_system != "make" {
            if debug {
                eprintln!(
                    "debug: skipping {}: non-strict implementation {}",
                    pth_string, metadata.build_system
                );
            }

            return;
        }

        let makefile_str_result: Result<String, io::Error> = fs::read_to_string(p);

        if let Err(err) = &makefile_str_result {
            found_quirk = true;
            println!("error: {}: {}", p.display(), err);
            return;
        }

        let makefile_str: &str = &makefile_str_result.unwrap();

        let ws2_result: Result<Vec<warnings::Warning>, String> =
            warnings::lint(&metadata, makefile_str);

        if let Err(err) = ws2_result {
            found_quirk = true;
            println!("{}", err);
            return;
        }

        let ws2: Vec<warnings::Warning> = ws2_result.unwrap();

        if !ws2.is_empty() {
            found_quirk = true;
        }

        ws.extend(ws2);
    };

    for pth_string in pth_strings {
        let pth: &path::Path = path::Path::new(&pth_string);

        if pth.is_dir() {
            let walker = walkdir::WalkDir::new(pth)
                .sort_by_file_name()
                .into_iter()
                .filter_entry(|e| {
                    !DIRECTORY_EXCLUSIONS.contains(&e.file_name().to_str().unwrap_or(""))
                });

            for entry_result in walker {
                let entry: walkdir::DirEntry = entry_result.unwrap();
                let child_pth: &path::Path = entry.path();

                if child_pth.is_dir() || child_pth.is_symlink() {
                    continue;
                }

                action(child_pth);
            }
        } else {
            action(pth);
        }
    }

    ws.sort_by(|a, b| a.line.cmp(&b.line));

    for w in ws {
        println!("{}", w);
    }

    if found_quirk {
        die!(1);
    }
}