rumk 0.0.1

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

pub struct TabInRecipe;

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

    fn name(&self) -> &'static str {
        "Recipe must use tab indentation"
    }

    fn description(&self) -> &'static str {
        "Makefile recipes (commands) must be indented with a tab character, not spaces. \
         This is a requirement of the Make syntax."
    }

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

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

        for rule in &makefile.rules {
            for recipe in &rule.recipes {
                if !recipe.indentation.starts_with('\t') {
                    let fix = Fix::new("Replace spaces with tab").add_edit(Edit::new(
                        recipe.line,
                        1,
                        recipe.line,
                        recipe.indentation.len() + 1,
                        "\t".to_string(),
                    ));

                    diagnostics.push(
                        Diagnostic::new(
                            self.id(),
                            Severity::Error,
                            "Recipe must be indented with tab, not spaces",
                            recipe.line,
                            recipe.column,
                        )
                        .with_fix(fix),
                    );
                }
            }
        }

        diagnostics
    }
}

pub struct InvalidVariableSyntax;

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

    fn name(&self) -> &'static str {
        "Invalid variable syntax"
    }

    fn description(&self) -> &'static str {
        "Variable names should follow Make conventions and not contain invalid characters."
    }

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

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

        for variable in makefile.variables.values() {
            if !is_valid_variable_name(&variable.name) {
                diagnostics.push(Diagnostic::new(
                    self.id(),
                    Severity::Error,
                    format!("Invalid variable name: '{}'", variable.name),
                    variable.line,
                    variable.column,
                ));
            }
        }

        diagnostics
    }
}

fn is_valid_variable_name(name: &str) -> bool {
    if name.is_empty() {
        return false;
    }

    name.chars()
        .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
}