code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use std::collections::BTreeMap;
use std::sync::LazyLock;

use regex::Regex;

use crate::cli::PreviewOptions;
use crate::config::Config;
use crate::findings::{Finding, Severity, read_baseline};

pub struct ControlledFindings {
    pub active: Vec<Finding>,
    pub actionable: Vec<Finding>,
    pub actionable_count: usize,
    pub advisory_summary: BTreeMap<String, usize>,
}

pub fn apply(
    findings: Vec<Finding>,
    options: &PreviewOptions,
    config: &Config,
    sources: &BTreeMap<String, String>,
    taxonomy_group: fn(&str) -> &'static str,
) -> Result<ControlledFindings, String> {
    let baseline = read_baseline(options.baseline.as_deref())?;
    let has_baseline = !baseline.is_empty();
    let mut suppressions = SuppressionIndex::new(sources);
    let mut active = Vec::new();
    let mut actionable = Vec::new();
    let mut actionable_count = 0;
    let mut advisory_summary = BTreeMap::new();
    for mut finding in findings {
        if !options.only.is_empty() && finding.code != options.only {
            continue;
        }
        if config.disabled_rules.contains(&finding.code)
            || (has_baseline
                && (baseline.contains(&finding.key()) || baseline.contains(&finding.fingerprint())))
            || suppressions.suppresses(&finding)
        {
            continue;
        }
        if let Some(severity) = config
            .severity_overrides
            .get(&finding.code)
            .and_then(|value| Severity::parse(value))
        {
            finding.severity = severity;
        }
        let group = taxonomy_group(&finding.code);
        match mode_for_code(&finding.code, config, group) {
            "off" => {}
            "count" => {
                *advisory_summary.entry(group.to_owned()).or_insert(0) += 1;
                active.push(finding.enrich());
            }
            _ => {
                let finding = finding.enrich();
                actionable_count += 1;
                actionable.push(finding.clone());
                active.push(finding);
            }
        }
    }
    Ok(ControlledFindings {
        active,
        actionable,
        actionable_count,
        advisory_summary,
    })
}

pub fn detailed(
    controlled: ControlledFindings,
    options: &PreviewOptions,
    taxonomy_group: fn(&str) -> &'static str,
) -> (Vec<Finding>, usize, BTreeMap<String, usize>) {
    let findings = if options.finding_group.is_empty() {
        controlled.active
    } else {
        controlled
            .active
            .into_iter()
            .filter(|finding| taxonomy_group(&finding.code) == options.finding_group)
            .collect()
    };
    (
        findings,
        controlled.actionable_count,
        controlled.advisory_summary,
    )
}

struct SuppressionIndex<'source> {
    sources: &'source BTreeMap<String, String>,
    directives: BTreeMap<&'source str, Vec<SuppressionDirective>>,
}

impl<'source> SuppressionIndex<'source> {
    fn new(sources: &'source BTreeMap<String, String>) -> Self {
        Self {
            sources,
            directives: BTreeMap::new(),
        }
    }

    fn suppresses(&mut self, finding: &Finding) -> bool {
        let Some((path, source)) = self.sources.get_key_value(&finding.path) else {
            return false;
        };
        let directives = self
            .directives
            .entry(path)
            .or_insert_with(|| suppression_directives(source));
        directives_suppress(directives, finding)
    }
}

struct SuppressionDirective {
    line: usize,
    file: bool,
    target: String,
}

fn suppression_directives(source: &str) -> Vec<SuppressionDirective> {
    if !source.contains("code-buster-ignore") {
        return Vec::new();
    }
    source
        .split('\n')
        .enumerate()
        .filter_map(|(index, line)| {
            let directive = comment_text(line)?;
            if let Some(target) = directive.strip_prefix("code-buster-ignore-file") {
                return Some(SuppressionDirective {
                    line: index + 1,
                    file: true,
                    target: target.to_owned(),
                });
            }
            directive
                .strip_prefix("code-buster-ignore")
                .map(|target| SuppressionDirective {
                    line: index + 1,
                    file: false,
                    target: target.to_owned(),
                })
        })
        .collect()
}

fn directives_suppress(directives: &[SuppressionDirective], finding: &Finding) -> bool {
    directives.iter().any(|directive| {
        (directive.file || directive.line.abs_diff(finding.line) <= 1)
            && targets_rule(&directive.target, &finding.code)
    })
}

fn targets_rule(rest: &str, code: &str) -> bool {
    let trimmed = rest.trim();
    trimmed.is_empty()
        || trimmed.starts_with(':')
        || trimmed.split_whitespace().any(|token| {
            token
                .chars()
                .filter(|character| !matches!(character, ':' | ','))
                .eq(code.chars())
        })
}

fn comment_text(line: &str) -> Option<String> {
    let masked = STRING_LITERAL.replace_all(line, " ");
    let (index, marker) = ["//", "/*", "#", "--"]
        .into_iter()
        .filter_map(|marker| masked.find(marker).map(|index| (index, marker)))
        .min_by_key(|(index, _)| *index)?;
    Some(
        masked[index + marker.len()..]
            .trim_start_matches(|character: char| {
                character.is_whitespace() || matches!(character, '/' | '*' | '#' | '-')
            })
            .to_owned(),
    )
}

static STRING_LITERAL: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#""(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'"#)
        .expect("finding-control string literal regex must compile")
});

pub(crate) fn mode_for_code<'a>(code: &str, config: &'a Config, group: &str) -> &'a str {
    if let Some(mode) = config.rule_modes.get(code) {
        return mode;
    }
    if config.severity_overrides.contains_key(code) {
        return "report";
    }
    if let Some(mode) = config.group_modes.get(group) {
        return mode;
    }
    if matches!(
        code,
        "flutter-gesture-semantic-gap" | "flutter-listener-without-remove"
    ) {
        return "count";
    }
    if code.starts_with("dart-")
        && matches!(group, "correctness" | "reliability")
        && code != "dart-unreachable-statement"
    {
        return "count";
    }
    default_mode(group)
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn applies_adjacent_targeted_and_file_suppressions() {
        let source = "// code-buster-ignore complex-function\nvoid large() {}\n\
                      // code-buster-ignore-file dart-print\nvoid other() {}\n";

        assert!(suppressed(source, &finding("complex-function", 2)));
        assert!(suppressed(source, &finding("dart-print", 40)));
        assert!(!suppressed(source, &finding("duplicate-block", 2)));
    }

    #[test]
    fn ignores_directive_text_inside_strings() {
        let source = "const value = '// code-buster-ignore';\nrun();\n";

        assert!(!suppressed(source, &finding("complex-function", 2)));
    }

    fn suppressed(source: &str, finding: &Finding) -> bool {
        directives_suppress(&suppression_directives(source), finding)
    }

    fn finding(code: &str, line: usize) -> Finding {
        Finding {
            code: code.to_owned(),
            severity: Severity::Warn,
            path: "lib/main.dart".to_owned(),
            line,
            end_line: line,
            message: String::new(),
            confidence: String::new(),
            why: String::new(),
            suggestion: String::new(),
            related_files: Vec::new(),
            snippet: String::new(),
            code_flow: Vec::new(),
        }
    }
}