deslop 0.2.0

A static analyzer that spots low-context and AI-assisted code patterns across naming, concurrency, security, performance, and test quality.
Documentation
use std::collections::BTreeMap;
use std::fmt::Write as _;

use anyhow::Result;

use deslop::{RuleConfigurability, RuleLanguage, RuleMetadata, RuleStatus, rule_registry};

pub(crate) fn filtered_rules(
    language: Option<RuleLanguage>,
    status: Option<RuleStatus>,
) -> Vec<&'static RuleMetadata> {
    rule_registry()
        .iter()
        .filter(|metadata| {
            language
                .as_ref()
                .is_none_or(|value| metadata.language == *value)
        })
        .filter(|metadata| {
            status
                .as_ref()
                .is_none_or(|value| metadata.status == *value)
        })
        .collect()
}

pub(crate) fn format_rules_report(
    rules: &[&RuleMetadata],
    language: Option<RuleLanguage>,
    status: Option<RuleStatus>,
) -> String {
    let mut output = String::new();

    let _ = writeln!(
        &mut output,
        "deslop rules: {} matching rule entries",
        rules.len()
    );
    if language.is_some() || status.is_some() {
        let _ = writeln!(
            &mut output,
            "filters: language={} status={}",
            language
                .as_ref()
                .map(language_label)
                .unwrap_or("<all-languages>"),
            status
                .as_ref()
                .map(status_label)
                .unwrap_or("<all-statuses>")
        );
    }

    let mut by_language = BTreeMap::new();
    let mut by_status = BTreeMap::new();
    for metadata in rules {
        *by_language
            .entry(language_label(&metadata.language))
            .or_insert(0usize) += 1;
        *by_status
            .entry(status_label(&metadata.status))
            .or_insert(0usize) += 1;
    }

    if !by_language.is_empty() {
        let _ = writeln!(
            &mut output,
            "by language: {}",
            by_language
                .iter()
                .map(|(language, count)| format!("{language}={count}"))
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    if !by_status.is_empty() {
        let _ = writeln!(
            &mut output,
            "by status: {}",
            by_status
                .iter()
                .map(|(status, count)| format!("{status}={count}"))
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    if rules.is_empty() {
        return output;
    }

    output.push('\n');

    for metadata in rules {
        let _ = writeln!(
            &mut output,
            "- {} [{} / {} / {} / {}]",
            metadata.id,
            language_label(&metadata.language),
            metadata.family,
            severity_label(&metadata.default_severity),
            status_label(&metadata.status),
        );
        let _ = writeln!(&mut output, "  {}", metadata.description);
        let _ = writeln!(
            &mut output,
            "  configurable via: {}",
            metadata
                .configurability
                .iter()
                .map(config_label)
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    output
}

pub(crate) fn format_rules_report_json(rules: &[&RuleMetadata]) -> Result<String> {
    Ok(serde_json::to_string_pretty(rules)?)
}

fn language_label(language: &RuleLanguage) -> &'static str {
    match language {
        RuleLanguage::Common => "common",
        RuleLanguage::Go => "go",
        RuleLanguage::Python => "python",
        RuleLanguage::Rust => "rust",
    }
}

fn status_label(status: &RuleStatus) -> &'static str {
    match status {
        RuleStatus::Stable => "stable",
        RuleStatus::Experimental => "experimental",
        RuleStatus::Research => "research",
    }
}

fn severity_label(severity: &deslop::RuleDefaultSeverity) -> &'static str {
    match severity {
        deslop::RuleDefaultSeverity::Info => "info",
        deslop::RuleDefaultSeverity::Warning => "warning",
        deslop::RuleDefaultSeverity::Error => "error",
        deslop::RuleDefaultSeverity::Contextual => "contextual",
    }
}

fn config_label(config: &RuleConfigurability) -> &'static str {
    match config {
        RuleConfigurability::Disable => "disabled_rules",
        RuleConfigurability::Ignore => "scan --ignore",
        RuleConfigurability::SeverityOverride => "severity_overrides",
        RuleConfigurability::DetailsOnly => "--details",
        RuleConfigurability::GoSemanticExperimental => {
            "go_semantic_experimental / --enable-semantic"
        }
        RuleConfigurability::RustAsyncExperimental => "rust_async_experimental",
    }
}

#[cfg(test)]
mod tests {
    use deslop::{RuleLanguage, RuleStatus};

    use super::{filtered_rules, format_rules_report, format_rules_report_json};

    #[test]
    fn filters_rules_by_language_and_status() {
        let go_rules = filtered_rules(Some(RuleLanguage::Go), None);
        assert!(
            go_rules
                .iter()
                .all(|metadata| metadata.language == RuleLanguage::Go)
        );

        let experimental = filtered_rules(None, Some(RuleStatus::Experimental));
        assert!(
            experimental
                .iter()
                .all(|metadata| metadata.status == RuleStatus::Experimental)
        );
    }

    #[test]
    fn text_rules_report_includes_summary_and_config() {
        let rules = filtered_rules(Some(RuleLanguage::Common), None);
        let output = format_rules_report(&rules, Some(RuleLanguage::Common), None);

        assert!(output.contains("deslop rules:"));
        assert!(output.contains("filters: language=common status=<all-statuses>"));
        assert!(output.contains("hallucinated_import_call"));
        assert!(
            output.contains("configurable via: disabled_rules, scan --ignore, severity_overrides")
        );
    }

    #[test]
    fn json_rules_report_renders_metadata() {
        let rules = filtered_rules(None, Some(RuleStatus::Experimental));
        let output = format_rules_report_json(&rules).expect("json should render");

        assert!(output.contains("\"status\": \"experimental\""));
        assert!(output.contains("\"configurability\""));
    }
}