code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use std::collections::{BTreeMap, BTreeSet};
#[cfg(test)]
use std::sync::LazyLock;

use crate::generic_rules::{mask_generic_rule_strings, mask_rust_test_regions};

pub struct SourceAnalysisContext<'a> {
    sources: BTreeMap<String, String>,
    masked_source_lines: BTreeMap<String, Vec<String>>,
    base_sources: BTreeMap<String, String>,
    changed_paths: &'a BTreeSet<String>,
    auxiliary_files: &'a BTreeMap<String, String>,
    changed_base: bool,
}

impl<'a> SourceAnalysisContext<'a> {
    #[cfg(test)]
    pub fn new(
        sources: &'a BTreeMap<String, String>,
        base_sources: &'a BTreeMap<String, String>,
        changed_paths: &'a BTreeSet<String>,
        changed_base: bool,
    ) -> Self {
        static EMPTY_AUXILIARY: LazyLock<BTreeMap<String, String>> = LazyLock::new(BTreeMap::new);
        Self::with_auxiliary(
            sources,
            base_sources,
            changed_paths,
            changed_base,
            &EMPTY_AUXILIARY,
        )
    }

    #[cfg(test)]
    pub fn unchanged(sources: &'a BTreeMap<String, String>) -> Self {
        static EMPTY_SOURCES: LazyLock<BTreeMap<String, String>> = LazyLock::new(BTreeMap::new);
        static EMPTY_PATHS: LazyLock<BTreeSet<String>> = LazyLock::new(BTreeSet::new);
        Self::new(sources, &EMPTY_SOURCES, &EMPTY_PATHS, false)
    }

    pub fn with_auxiliary(
        sources: &'a BTreeMap<String, String>,
        base_sources: &'a BTreeMap<String, String>,
        changed_paths: &'a BTreeSet<String>,
        changed_base: bool,
        auxiliary_files: &'a BTreeMap<String, String>,
    ) -> Self {
        let sources = sources
            .iter()
            .map(|(path, source)| (path.clone(), mask_rust_test_regions(path, source)))
            .collect::<BTreeMap<_, _>>();
        let base_sources = base_sources
            .iter()
            .map(|(path, source)| (path.clone(), mask_rust_test_regions(path, source)))
            .collect::<BTreeMap<_, _>>();
        let masked_source_lines = sources
            .iter()
            .map(|(path, source)| {
                let lines = source.split('\n').map(str::to_owned).collect::<Vec<_>>();
                (path.clone(), mask_generic_rule_strings(&lines, path))
            })
            .collect();
        Self {
            sources,
            masked_source_lines,
            base_sources,
            changed_paths,
            auxiliary_files,
            changed_base,
        }
    }

    pub fn sources(&self) -> &BTreeMap<String, String> {
        &self.sources
    }

    pub fn masked_lines(&self, path: &str) -> &[String] {
        self.masked_source_lines
            .get(path)
            .map(Vec::as_slice)
            .unwrap_or(&[])
    }

    pub fn base_sources(&self) -> &BTreeMap<String, String> {
        &self.base_sources
    }

    pub fn changed_paths(&self) -> &BTreeSet<String> {
        self.changed_paths
    }

    pub fn auxiliary_files(&self) -> &BTreeMap<String, String> {
        self.auxiliary_files
    }

    pub fn has_changed_base(&self) -> bool {
        self.changed_base
    }
}

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

    #[test]
    fn removes_rust_test_regions_from_rule_sources() {
        let source = "fn production() {}\n\
#[cfg(test)]\n\
mod tests {\n\
    #[test]\n\
    fn fixture() { let sample = \"TODO: fixture\"; }\n\
}\n\
fn after() {}\n";
        let sources = BTreeMap::from([("src/lib.rs".to_owned(), source.to_owned())]);
        let empty_sources = BTreeMap::new();
        let empty_paths = BTreeSet::new();
        let context = SourceAnalysisContext::with_auxiliary(
            &sources,
            &empty_sources,
            &empty_paths,
            false,
            &empty_sources,
        );
        let analyzed = &context.sources()["src/lib.rs"];

        assert!(analyzed.contains("fn production() {}"));
        assert!(analyzed.contains("fn after() {}"));
        assert!(!analyzed.contains("TODO: fixture"));
        assert_eq!(analyzed.lines().count(), source.lines().count());
    }
}