rta-core 0.3.0

Core analyzers and report model for the Rust Technical Audit Toolkit
Documentation
use std::collections::BTreeMap;

use crate::{
    analyzers::Analyzer,
    collector::RepositorySnapshot,
    model::{LanguageStat, Overview},
};

pub struct OverviewAnalyzer;

impl Analyzer<Overview> for OverviewAnalyzer {
    fn analyze(&self, snapshot: &RepositorySnapshot) -> Overview {
        let mut language_map: BTreeMap<String, (usize, usize)> = BTreeMap::new();
        for file in &snapshot.files {
            let language = language_for(file.extension.as_deref());
            let entry = language_map.entry(language.to_string()).or_default();
            entry.0 += 1;
            entry.1 += file.lines;
        }

        let languages = language_map
            .into_iter()
            .map(|(language, (files, lines))| LanguageStat {
                language,
                files,
                lines,
            })
            .collect::<Vec<_>>();
        let workspace_members = snapshot
            .manifests
            .iter()
            .flat_map(|manifest| manifest.workspace_members.clone())
            .collect::<Vec<_>>();
        let package_count = snapshot
            .manifests
            .iter()
            .filter(|manifest| manifest.package_name.is_some())
            .count();
        let total_bytes = snapshot.files.iter().map(|file| file.bytes).sum();
        let crate_count = package_count.max(workspace_members.len());
        let cargo_configs = snapshot
            .manifests
            .iter()
            .map(|manifest| manifest.relative_path.clone())
            .collect::<Vec<_>>();
        let summary = if workspace_members.is_empty() {
            format!("Single-package Rust repository with {package_count} Cargo manifest(s).")
        } else {
            format!(
                "Cargo workspace with {} declared member(s) and {package_count} package manifest(s).",
                workspace_members.len()
            )
        };

        Overview {
            crate_count,
            package_count,
            workspace_members,
            total_files: snapshot.files.len(),
            total_bytes,
            languages,
            cargo_configs,
            summary,
        }
    }
}

fn language_for(extension: Option<&str>) -> &'static str {
    match extension {
        Some("rs") => "Rust",
        Some("toml") => "TOML",
        Some("md") => "Markdown",
        Some("yml") | Some("yaml") => "YAML",
        Some("json") => "JSON",
        Some("sh") => "Shell",
        Some("js") | Some("ts") => "JavaScript/TypeScript",
        _ => "Other",
    }
}

#[cfg(test)]
mod tests {
    use std::{collections::BTreeMap, path::PathBuf};

    use crate::{
        analyzers::Analyzer,
        collector::{CargoManifest, FileSnapshot, RepositorySnapshot},
    };

    use super::OverviewAnalyzer;

    #[test]
    fn empty_repository_reports_empty_overview() {
        let overview = OverviewAnalyzer.analyze(&snapshot(Vec::new(), Vec::new()));

        assert_eq!(overview.package_count, 0);
        assert_eq!(overview.total_files, 0);
        assert!(overview.summary.contains("Single-package"));
    }

    #[test]
    fn typical_workspace_reports_members_and_languages() {
        let overview = OverviewAnalyzer.analyze(&snapshot(
            vec![
                file("src/lib.rs", "rs", "pub fn main() {}\n"),
                file("README.md", "md", "# App\n"),
            ],
            vec![manifest(
                "Cargo.toml",
                None,
                vec!["crates/core", "crates/cli"],
            )],
        ));

        assert_eq!(overview.crate_count, 2);
        assert_eq!(overview.workspace_members.len(), 2);
        assert!(overview
            .languages
            .iter()
            .any(|language| language.language == "Rust"));
    }

    #[test]
    fn extreme_file_sizes_are_summed_without_scoring_side_effects() {
        let overview = OverviewAnalyzer.analyze(&snapshot(
            vec![FileSnapshot {
                path: PathBuf::from("/tmp/repo/big.bin"),
                relative_path: "big.bin".into(),
                extension: None,
                bytes: 10_000_000,
                lines: 0,
                content: None,
            }],
            Vec::new(),
        ));

        assert_eq!(overview.total_bytes, 10_000_000);
        assert_eq!(overview.total_files, 1);
    }

    #[test]
    fn adversarial_unknown_extension_is_bucketed_as_other() {
        let overview = OverviewAnalyzer.analyze(&snapshot(
            vec![file("src/fake.rs.txt", "txt", "fn fake() {}\n")],
            Vec::new(),
        ));

        assert!(overview
            .languages
            .iter()
            .any(|language| language.language == "Other" && language.files == 1));
    }

    fn snapshot(files: Vec<FileSnapshot>, manifests: Vec<CargoManifest>) -> RepositorySnapshot {
        RepositorySnapshot {
            root: PathBuf::from("/tmp/repo"),
            files,
            manifests,
        }
    }

    fn file(relative_path: &str, extension: &str, content: &str) -> FileSnapshot {
        FileSnapshot {
            path: PathBuf::from("/tmp/repo").join(relative_path),
            relative_path: relative_path.into(),
            extension: Some(extension.into()),
            bytes: content.len() as u64,
            lines: content.lines().count(),
            content: Some(content.into()),
        }
    }

    fn manifest(
        relative_path: &str,
        package_name: Option<&str>,
        workspace_members: Vec<&str>,
    ) -> CargoManifest {
        CargoManifest {
            relative_path: relative_path.into(),
            package_name: package_name.map(str::to_string),
            workspace_members: workspace_members.into_iter().map(str::to_string).collect(),
            dependencies: BTreeMap::new(),
            dev_dependencies: BTreeMap::new(),
            build_dependencies: BTreeMap::new(),
        }
    }
}