repopilot 0.20.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
use crate::analysis::SyntaxSummary;
use crate::analysis::exports::extract_exports;
use crate::analysis::parse::ParsedFile;
use crate::audits::pipeline::FileAuditRegistration;
use crate::findings::types::Finding;
use crate::graph::imports::{extract_deferred_imports_from, extract_imports_from};
use crate::scan::config::ScanConfig;
use crate::scan::facts::FileFacts;
use rayon::prelude::*;

pub(super) struct FileAnalysisResult {
    pub(super) findings: Vec<Finding>,
    pub(super) imports: Vec<String>,
    pub(super) deferred_imports: Vec<String>,
    pub(super) exports: Vec<String>,
    pub(super) syntax: SyntaxSummary,
}

pub(super) fn analyze_file(
    file: &FileFacts,
    file_audits: &[FileAuditRegistration],
    config: &ScanConfig,
) -> FileAnalysisResult {
    let parsed = ParsedFile::for_facts(file);
    let (parsed_required, text_only): (Vec<_>, Vec<_>) = file_audits
        .iter()
        .partition(|registration| registration.requires_parsed_syntax());

    let mut findings = run_file_audits(file, config, &text_only);
    let (parsed_findings, (imports, deferred_imports, exports, syntax)) =
        if parsed_required.is_empty() {
            (
                Vec::new(),
                extract_parsed_artifacts(&parsed, file.language.as_deref()),
            )
        } else {
            rayon::join(
                || run_parsed_file_audits(file, &parsed, config, &parsed_required),
                || extract_parsed_artifacts(&parsed, file.language.as_deref()),
            )
        };

    findings.extend(parsed_findings);

    FileAnalysisResult {
        findings,
        imports,
        deferred_imports,
        exports,
        syntax,
    }
}

fn run_file_audits(
    file: &FileFacts,
    config: &ScanConfig,
    registrations: &[&FileAuditRegistration],
) -> Vec<Finding> {
    registrations
        .par_iter()
        .map(|registration| registration.run(file, config))
        .collect::<Vec<_>>()
        .into_iter()
        .flatten()
        .collect()
}

fn run_parsed_file_audits(
    file: &FileFacts,
    parsed: &ParsedFile,
    config: &ScanConfig,
    registrations: &[&FileAuditRegistration],
) -> Vec<Finding> {
    registrations
        .par_iter()
        .map(|registration| registration.run_parsed(file, parsed, config))
        .collect::<Vec<_>>()
        .into_iter()
        .flatten()
        .collect()
}

fn extract_parsed_artifacts(
    parsed: &ParsedFile,
    language: Option<&str>,
) -> (Vec<String>, Vec<String>, Vec<String>, SyntaxSummary) {
    let imports = extract_imports_from(parsed, language);
    let deferred_imports = extract_deferred_imports_from(parsed, language);
    let exports = extract_exports(parsed.content(), language);
    let syntax = parsed.syntax_summary();
    (imports, deferred_imports, exports, syntax)
}