rumk 0.0.1

A fast linter for Makefiles
Documentation
use crate::diagnostic::{Diagnostic, Severity};
use crate::parser::Makefile;
use crate::rules::{Rule, RuleCategory};

pub struct MissingPhony;

impl Rule for MissingPhony {
    fn id(&self) -> &'static str {
        "MK201"
    }

    fn name(&self) -> &'static str {
        "Non-file targets should be .PHONY"
    }

    fn description(&self) -> &'static str {
        "Targets that don't represent actual files should be declared as .PHONY to ensure \
         they always run and to improve performance."
    }

    fn category(&self) -> RuleCategory {
        RuleCategory::BestPractices
    }

    fn check(&self, makefile: &Makefile, _content: &str) -> Vec<Diagnostic> {
        let mut diagnostics = Vec::new();
        let common_phony_targets = ["all", "clean", "test", "check", "install", "build", "help"];

        for rule in &makefile.rules {
            for target in &rule.targets {
                if common_phony_targets.contains(&target.as_str())
                    && !makefile.phonies.contains(target)
                {
                    diagnostics.push(Diagnostic::new(
                        self.id(),
                        Severity::Warning,
                        format!("Target '{target}' should be declared .PHONY"),
                        rule.line,
                        rule.column,
                    ));
                }
            }
        }

        diagnostics
    }
}

pub struct HardcodedPath;

impl Rule for HardcodedPath {
    fn id(&self) -> &'static str {
        "MK202"
    }

    fn name(&self) -> &'static str {
        "Avoid hardcoded absolute paths"
    }

    fn description(&self) -> &'static str {
        "Hardcoded absolute paths reduce portability and make the Makefile less flexible. \
         Use variables or relative paths instead."
    }

    fn category(&self) -> RuleCategory {
        RuleCategory::BestPractices
    }

    fn check(&self, makefile: &Makefile, _content: &str) -> Vec<Diagnostic> {
        let mut diagnostics = Vec::new();

        for variable in makefile.variables.values() {
            if contains_absolute_path(&variable.value) {
                diagnostics.push(Diagnostic::new(
                    self.id(),
                    Severity::Warning,
                    format!(
                        "Variable '{}' contains hardcoded absolute path",
                        variable.name
                    ),
                    variable.line,
                    variable.column,
                ));
            }
        }

        for rule in &makefile.rules {
            for recipe in &rule.recipes {
                if contains_absolute_path(&recipe.command) {
                    diagnostics.push(Diagnostic::new(
                        self.id(),
                        Severity::Warning,
                        "Recipe contains hardcoded absolute path",
                        recipe.line,
                        recipe.column,
                    ));
                }
            }
        }

        diagnostics
    }
}

fn contains_absolute_path(text: &str) -> bool {
    text.split_whitespace().any(|word| {
        (word.starts_with('/') && word.len() > 1 && !word.starts_with("//"))
            || (word.len() > 2
                && word.chars().nth(1) == Some(':')
                && word.chars().nth(2) == Some('\\'))
    })
}