cargo-mend 0.16.1

Opinionated visibility auditing for Rust crates and workspaces
use std::collections::BTreeMap;
use std::collections::BTreeSet;

use super::StoredReport;

pub(super) fn apply_caller_aware_suppression(reports: &mut [StoredReport]) {
    let mut callers: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
    for report in reports.iter() {
        for site in &report.use_sites {
            callers
                .entry(site.target_def_path.clone())
                .or_default()
                .insert(site.caller_module_def_path.clone());
        }
    }

    for report in reports.iter_mut() {
        report.findings.retain(|finding| {
            let Some(item_path) = finding.item_def_path.as_deref() else {
                return true;
            };
            let Some(narrower_scope) = finding.narrower_scope_def_path.as_deref() else {
                return true;
            };
            let Some(caller_set) = callers.get(item_path) else {
                return true;
            };
            caller_set
                .iter()
                .all(|caller| def_path_is_descendant(caller, narrower_scope))
        });
    }
}

fn def_path_is_descendant(caller_path: &str, narrower_scope: &str) -> bool {
    if caller_path == narrower_scope {
        return true;
    }
    if let Some(rest) = caller_path.strip_prefix(narrower_scope)
        && rest.starts_with("::")
    {
        return true;
    }
    false
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::apply_caller_aware_suppression;
    use crate::compiler::constants::FINDINGS_SCHEMA_VERSION;
    use crate::compiler::persistence::StoredFinding;
    use crate::compiler::persistence::StoredReport;
    use crate::compiler::persistence::UseSite;
    use crate::compiler::settings;
    use crate::config::DiagnosticCode;
    use crate::reporting::CompilerWarningFacts;
    use crate::reporting::FixSupport;
    use crate::reporting::Severity;

    const CONFIG_FINGERPRINT: &str = "config-fingerprint";

    #[test]
    fn caller_aware_suppression_drops_narrowing_outside_scope() {
        let item_path = "crate::module::item";
        let narrower_scope = "crate::module";
        let mut reports = vec![StoredReport {
            findings: vec![narrowing_finding(
                DiagnosticCode::SuspiciousPub,
                item_path,
                narrower_scope,
            )],
            use_sites: vec![UseSite {
                target_def_path:        item_path.to_string(),
                caller_module_def_path: "crate::other".to_string(),
            }],
            ..report_for_test()
        }];

        apply_caller_aware_suppression(&mut reports);

        assert!(reports[0].findings.is_empty());
    }

    fn narrowing_finding(
        diagnostic_code: DiagnosticCode,
        item_path: &str,
        narrower_scope: &str,
    ) -> StoredFinding {
        StoredFinding {
            item_def_path: Some(item_path.to_string()),
            narrower_scope_def_path: Some(narrower_scope.to_string()),
            ..stored_finding(diagnostic_code, Path::new("/package/src/lib.rs"), "item", 1)
        }
    }

    fn stored_finding(
        diagnostic_code: DiagnosticCode,
        path: &Path,
        item: &str,
        line: usize,
    ) -> StoredFinding {
        StoredFinding {
            severity: Severity::Warning,
            diagnostic_code,
            path: path.to_string_lossy().into_owned(),
            line,
            column: 1,
            highlight_len: 3,
            source_line: "pub fn item() {}".to_string(),
            item: Some(item.to_string()),
            message: format!("{item} should change visibility"),
            suggestion: Some("use narrower visibility".to_string()),
            fix_support: FixSupport::None,
            related: None,
            item_def_path: None,
            narrower_scope_def_path: None,
        }
    }

    fn report_for_test() -> StoredReport {
        StoredReport {
            version:                FINDINGS_SCHEMA_VERSION,
            analysis_fingerprint:   settings::current_analysis_fingerprint(),
            scope_fingerprint:      "scope".to_string(),
            package_root:           "/package".to_string(),
            crate_root_file:        "/package/src/lib.rs".to_string(),
            config_fingerprint:     CONFIG_FINGERPRINT.to_string(),
            findings:               Vec::new(),
            pub_use_fix_facts:      Vec::new(),
            compiler_warning_facts: CompilerWarningFacts::None,
            use_sites:              Vec::new(),
        }
    }
}