code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use crate::config::Config;
use crate::discovery::SourceFile;
use crate::findings::Finding;

pub fn analyze(files: &[SourceFile], config: &Config) -> Vec<Finding> {
    if !structure_analysis_enabled(config) {
        return Vec::new();
    }

    let default_roots = ["src".to_owned()];
    let roots = if config.structure_source_roots.is_empty() {
        default_roots.as_slice()
    } else {
        &config.structure_source_roots
    };
    let mut findings = Vec::new();
    for root in roots {
        analyze_source_root(files, config, root, &mut findings);
    }
    findings
}

fn structure_analysis_enabled(config: &Config) -> bool {
    config.structure_max_top_level_files >= 0
        || !config.structure_allowed_top_level.is_empty()
        || !config.structure_required_directories.is_empty()
}

fn analyze_source_root(
    files: &[SourceFile],
    config: &Config,
    root: &str,
    findings: &mut Vec<Finding>,
) {
    let normalized_root = root.replace('\\', "/").trim_matches('/').to_owned();
    if !config.root.join(&normalized_root).is_dir() {
        emit_missing_source_root(config, &normalized_root, findings);
        return;
    }

    emit_missing_required_directories(config, &normalized_root, findings);
    let candidates = aggregate_top_level_candidates(files, &normalized_root);
    let qualified = qualify_top_level_candidates(candidates, config, &normalized_root);
    emit_top_level_finding(config, &normalized_root, qualified, findings);
}

fn emit_missing_source_root(config: &Config, normalized_root: &str, findings: &mut Vec<Finding>) {
    if config.structure_required_directories.is_empty() {
        return;
    }
    findings.push(Finding::structure(
        "structure-missing-source-root",
        normalized_root.to_owned(),
        format!("configured source root is missing: {normalized_root}"),
        "Create the source root or remove it from [structure].source_roots.",
        Vec::new(),
    ));
}

fn emit_missing_required_directories(
    config: &Config,
    normalized_root: &str,
    findings: &mut Vec<Finding>,
) {
    for required in &config.structure_required_directories {
        let required_path = format!("{normalized_root}/{required}").replace("//", "/");
        if !config.root.join(&required_path).is_dir() {
            findings.push(Finding::structure(
                "structure-missing-required-dir",
                required_path.clone(),
                format!("required source subdirectory is missing: {required_path}"),
                "Create the subsystem folder or update [structure].required_dirs.",
                Vec::new(),
            ));
        }
    }
}

fn aggregate_top_level_candidates<'a>(
    files: &'a [SourceFile],
    normalized_root: &str,
) -> Vec<&'a str> {
    let prefix = format!("{normalized_root}/");
    files
        .iter()
        .map(|file| file.path.as_str())
        .filter(|relative| relative.starts_with(&prefix))
        .filter(|relative| !relative[prefix.len()..].contains('/'))
        .collect()
}

fn qualify_top_level_candidates<'a>(
    mut candidates: Vec<&'a str>,
    config: &Config,
    normalized_root: &str,
) -> Vec<&'a str> {
    let prefix_len = normalized_root.len() + 1;
    candidates.retain(|relative| {
        let name = &relative[prefix_len..];
        !config
            .structure_allowed_top_level
            .iter()
            .any(|allowed| allowed == name || allowed == *relative)
    });
    candidates.sort();
    candidates
}

fn emit_top_level_finding(
    config: &Config,
    normalized_root: &str,
    qualified: Vec<&str>,
    findings: &mut Vec<Finding>,
) {
    if config.structure_max_top_level_files < 0
        || qualified.len() as i64 <= config.structure_max_top_level_files
    {
        return;
    }

    findings.push(Finding::structure(
        "structure-top-level-file",
        normalized_root.to_owned(),
        format!(
            "source root has {} top-level file(s), allowed {}",
            qualified.len(),
            config.structure_max_top_level_files
        ),
        "Move implementation files into subsystem folders or add intentional entry/facade files to [structure].allowed_top_level.",
        qualified.into_iter().map(str::to_owned).collect(),
    ));
}

#[cfg(test)]
mod tests {
    use super::analyze;
    use crate::config::Config;
    use crate::discovery::SourceFile;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn reports_missing_directories_and_excess_top_level_files() {
        let root = tempdir().expect("temporary repository must be created");
        fs::create_dir_all(root.path().join("lib")).unwrap();
        let mut config = Config::defaults(root.path().to_path_buf());
        config.structure_source_roots = vec!["lib".to_owned()];
        config.structure_required_directories = vec!["features".to_owned()];
        config.structure_max_top_level_files = 1;
        let files = vec![source("lib/a.dart"), source("lib/b.dart")];

        let findings = analyze(&files, &config);
        assert_eq!(
            findings
                .iter()
                .map(|finding| finding.code.as_str())
                .collect::<Vec<_>>(),
            ["structure-missing-required-dir", "structure-top-level-file"]
        );
    }

    fn source(path: &str) -> SourceFile {
        SourceFile {
            path: path.to_owned(),
            language: "dart".to_owned(),
        }
    }
}