code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use std::collections::BTreeSet;

use serde::Serialize;

use crate::cli::{PreviewOptions, ReportFormat};
use crate::config::{Config, absolute_root};
use crate::rule_catalog::{RULE_DESCRIPTORS, RuleDescriptor};

#[derive(Serialize)]
struct RulesReport<'a> {
    #[serde(rename = "schemaVersion")]
    schema_version: u8,
    command: &'static str,
    rules: &'a [RenderedRule],
}

#[derive(Serialize)]
struct RenderedRule {
    id: &'static str,
    language: &'static str,
    group: &'static str,
    mode: &'static str,
    title: &'static str,
    #[serde(rename = "inactiveReason", skip_serializing_if = "Option::is_none")]
    inactive_reason: Option<&'static str>,
}

pub fn execute(options: &PreviewOptions) -> Result<(), String> {
    let root = absolute_root(&options.root)?;
    let config = Config::load_from_root(root)?;
    let rules = select_catalog_rules(&config, options.inactive_rules);

    match &options.format {
        ReportFormat::Json => print_json(&rules),
        _ => {
            print_text(&rules);
            Ok(())
        }
    }
}

fn select_catalog_rules(config: &Config, inactive_only: bool) -> Vec<RenderedRule> {
    let languages = configured_languages(config);
    RULE_DESCRIPTORS
        .iter()
        .filter(|descriptor| catalog_includes(descriptor, config, &languages))
        .filter_map(|descriptor| {
            let mode = mode_for(descriptor, config);
            if inactive_only && mode != "off" {
                return None;
            }
            Some(render_rule(descriptor, config, mode))
        })
        .collect()
}

fn configured_languages(config: &Config) -> BTreeSet<&str> {
    if config.languages.is_empty() {
        BTreeSet::from([config.language.as_str()])
    } else {
        config.languages.iter().map(String::as_str).collect()
    }
}

fn catalog_includes(
    descriptor: &RuleDescriptor,
    config: &Config,
    languages: &BTreeSet<&str>,
) -> bool {
    if descriptor.mvvm && !config.dart_mvvm_enabled() {
        return false;
    }
    languages.contains("auto")
        || descriptor.language == "all"
        || languages.contains(descriptor.language)
}

fn render_rule(descriptor: &RuleDescriptor, config: &Config, mode: &'static str) -> RenderedRule {
    RenderedRule {
        id: descriptor.id,
        language: descriptor.language,
        group: descriptor.group,
        mode,
        title: descriptor.title,
        inactive_reason: inactive_reason(descriptor, config, mode),
    }
}

fn inactive_reason(
    descriptor: &RuleDescriptor,
    config: &Config,
    mode: &'static str,
) -> Option<&'static str> {
    if mode != "off" {
        return None;
    }
    Some(if config.disabled_rules.contains(descriptor.id) {
        "explicitly disabled"
    } else if descriptor.group == "architecture" {
        "requires architecture configuration"
    } else if descriptor.group == "domain" {
        "requires a detected or configured domain profile"
    } else {
        "disabled by group or rule mode"
    })
}

fn print_json(rules: &[RenderedRule]) -> Result<(), String> {
    let output = serde_json::to_string(&RulesReport {
        schema_version: 3,
        command: "rules",
        rules,
    })
    .map_err(|error| format!("failed to encode rules report: {error}"))?;
    println!("{output}");
    Ok(())
}

fn print_text(rules: &[RenderedRule]) {
    println!("Code Buster rules");
    for rule in rules {
        let reason = rule
            .inactive_reason
            .map_or_else(String::new, |reason| format!(" ({reason})"));
        println!(
            "{:<7} {} [{}/{}] — {}{}",
            rule.mode, rule.id, rule.language, rule.group, rule.title, reason
        );
    }
}

fn mode_for(descriptor: &RuleDescriptor, config: &Config) -> &'static str {
    if config.disabled_rules.contains(descriptor.id) {
        return "off";
    }
    if let Some(mode) = config.rule_modes.get(descriptor.id) {
        return stable_mode(mode);
    }
    if config.severity_overrides.contains_key(descriptor.id) {
        return "report";
    }
    if let Some(mode) = config.group_modes.get(descriptor.group) {
        return stable_mode(mode);
    }
    if matches!(
        descriptor.id,
        "flutter-gesture-semantic-gap" | "flutter-listener-without-remove"
    ) {
        return "count";
    }
    if descriptor.id.starts_with("dart-")
        && matches!(descriptor.group, "correctness" | "reliability")
        && descriptor.id != "dart-unreachable-statement"
    {
        return "count";
    }
    default_mode(descriptor.group)
}

fn stable_mode(mode: &str) -> &'static str {
    match mode {
        "off" => "off",
        "report" => "report",
        _ => "count",
    }
}

fn default_mode(group: &str) -> &'static str {
    match group {
        "core" | "correctness" | "security" | "reliability" | "accessibility" => "report",
        "architecture" | "game-engine" | "domain" | "experimental" => "off",
        _ => "count",
    }
}