rumk 0.0.1

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

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NamingStyle {
    Upper,
    Lower,
}

pub struct LineLength {
    max_length: usize,
}

impl LineLength {
    pub fn new(max_length: usize) -> Self {
        Self { max_length }
    }
}

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

    fn name(&self) -> &'static str {
        "Line exceeds maximum length"
    }

    fn description(&self) -> &'static str {
        "Lines should not exceed the configured maximum length for better readability."
    }

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

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

        for (line_num, line) in content.lines().enumerate() {
            if line.len() > self.max_length {
                diagnostics.push(Diagnostic::new(
                    self.id(),
                    Severity::Warning,
                    format!(
                        "Line length {} exceeds maximum of {}",
                        line.len(),
                        self.max_length
                    ),
                    line_num + 1,
                    self.max_length + 1,
                ));
            }
        }

        diagnostics
    }
}

pub struct VariableNaming {
    style: NamingStyle,
}

impl VariableNaming {
    pub fn new(style: NamingStyle) -> Self {
        Self { style }
    }
}

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

    fn name(&self) -> &'static str {
        "Variable naming convention"
    }

    fn description(&self) -> &'static str {
        "Variables should follow the configured naming convention for consistency."
    }

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

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

        for variable in makefile.variables.values() {
            if !matches_naming_style(&variable.name, self.style) {
                let expected = naming_style_description(self.style);
                diagnostics.push(Diagnostic::new(
                    self.id(),
                    Severity::Warning,
                    format!(
                        "Variable '{}' does not follow {} convention",
                        variable.name, expected
                    ),
                    variable.line,
                    variable.column,
                ));
            }
        }

        diagnostics
    }
}

pub struct TargetNaming {
    style: NamingStyle,
}

impl TargetNaming {
    pub fn new(style: NamingStyle) -> Self {
        Self { style }
    }
}

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

    fn name(&self) -> &'static str {
        "Target naming convention"
    }

    fn description(&self) -> &'static str {
        "Targets should follow the configured naming convention for consistency."
    }

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

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

        for rule in &makefile.rules {
            for target in &rule.targets {
                if !target.starts_with('.') && !matches_naming_style(target, self.style) {
                    let expected = naming_style_description(self.style);
                    diagnostics.push(Diagnostic::new(
                        self.id(),
                        Severity::Warning,
                        format!("Target '{target}' does not follow {expected} convention"),
                        rule.line,
                        rule.column,
                    ));
                }
            }
        }

        diagnostics
    }
}

fn matches_naming_style(name: &str, style: NamingStyle) -> bool {
    match style {
        NamingStyle::Upper => name.chars().all(|c| !c.is_alphabetic() || c.is_uppercase()),
        NamingStyle::Lower => name.chars().all(|c| !c.is_alphabetic() || c.is_lowercase()),
    }
}

fn naming_style_description(style: NamingStyle) -> &'static str {
    match style {
        NamingStyle::Upper => "UPPER_CASE",
        NamingStyle::Lower => "lower_case",
    }
}