cargo-mend 0.14.0

Opinionated visibility auditing for Rust crates and workspaces
use std::fs;
use std::path::Path;

use anyhow::Context;
use anyhow::Result;
use rustc_middle::middle::privacy::EffectiveVisibilities;
use rustc_middle::ty::TyCtxt;
use rustc_span::Span;
use rustc_span::def_id::CRATE_DEF_ID;
use rustc_span::def_id::LocalDefId;

mod classify;
mod record;
mod visit;

pub(super) use classify::CrateKind;
pub(super) use classify::ModuleLocation;
pub(super) use classify::ParentVisibility;

use super::field_visibility;
use super::source;
use super::use_sites;
use crate::compiler::facade::ParentFacadeExportStatus;
use crate::compiler::persistence;
use crate::compiler::persistence::CacheBuildKind;
use crate::compiler::persistence::FINDINGS_SCHEMA_VERSION;
use crate::compiler::persistence::FindingsSink;
use crate::compiler::persistence::StoredReport;
use crate::compiler::settings::DriverSettings;
use crate::compiler::source_cache;
use crate::compiler::source_cache::SourceCache;
use crate::config::DiagnosticCode;
use crate::reporting::CompilerWarningFacts;
use crate::reporting::FixSupport;
use crate::reporting::Severity;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ItemCategory {
    Module,
    NonModule,
}

pub(super) struct VisibilityContext<'a, 'tcx> {
    pub(super) tcx:                    TyCtxt<'tcx>,
    pub(super) settings:               &'a DriverSettings,
    pub(super) source_root:            &'a Path,
    pub(super) root_module:            &'a Path,
    pub(super) effective_visibilities: &'a EffectiveVisibilities,
    pub(super) source_cache:           &'a SourceCache,
}

struct ItemInfo<'a> {
    def_id:         LocalDefId,
    file_path:      &'a Path,
    vis_text:       &'a str,
    kind_label:     Option<&'static str>,
    name:           Option<&'a str>,
    highlight_span: Span,
    category:       ItemCategory,
    impl_self_name: Option<String>,
}

pub(super) struct SuspiciousPubInput<'a> {
    pub(super) def_id:            LocalDefId,
    pub(super) file_path:         &'a Path,
    pub(super) config_rel_path:   Option<&'a str>,
    pub(super) parent_visibility: ParentVisibility,
    pub(super) module_location:   ModuleLocation,
    pub(super) crate_kind:        CrateKind,
    pub(super) kind_label:        Option<&'static str>,
    pub(super) name:              Option<&'a str>,
    pub(super) highlight_span:    Span,
}

pub(super) struct FindingParams {
    pub(super) severity:                Severity,
    pub(super) code:                    DiagnosticCode,
    pub(super) item:                    Option<String>,
    pub(super) message:                 String,
    pub(super) suggestion:              Option<String>,
    pub(super) fixability:              FixSupport,
    pub(super) related:                 Option<String>,
    pub(super) item_def_path:           Option<String>,
    pub(super) narrower_scope_def_path: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum AllowanceReason {
    Allowlist,
    ParentIsPublic,
    ShallowPrivatePolicy,
    ReachablePublicApi,
    ParentFacadeUsedOutsideParent,
    InternalParentFacadeBoundary,
    ExposedByOtherCrateVisibleSignature,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum SuspiciousPubAssessment {
    Allowed(AllowanceReason),
    ReviewInternalParentFacade {
        related: Option<String>,
    },
    Warn {
        fixability:           FixSupport,
        related:              Option<String>,
        stale_parent_pub_use: Option<ParentFacadeExportStatus>,
    },
}

pub(super) fn collect_and_store_findings(
    tcx: TyCtxt<'_>,
    settings: &DriverSettings,
) -> Result<bool> {
    let crate_root_file = source::real_file_path(tcx, tcx.def_span(CRATE_DEF_ID))
        .context("failed to determine local crate root file")?;
    let Some(source_root) =
        source_cache::analysis_source_root_for(&crate_root_file, &settings.package_root)
    else {
        return Ok(false);
    };

    let mut sink = FindingsSink::default();
    let crate_items = tcx.hir_crate_items(());
    let cache_roots: Vec<&Path> = if settings.config_root == settings.package_root {
        vec![&source_root]
    } else {
        vec![&source_root, &settings.config_root]
    };
    let source_cache = SourceCache::build(&cache_roots)?;
    let ctx = VisibilityContext {
        tcx,
        settings,
        source_root: &source_root,
        root_module: &crate_root_file,
        effective_visibilities: tcx.effective_visibilities(()),
        source_cache: &source_cache,
    };

    for item_id in crate_items.free_items() {
        let item = tcx.hir_item(item_id);
        visit::visit_item(&ctx, item, &mut sink)?;
        field_visibility::check_item(&ctx, item, &mut sink)?;
    }

    for item_id in crate_items.impl_items() {
        visit::visit_impl_item(&ctx, tcx.hir_impl_item(item_id), &mut sink)?;
    }

    for item_id in crate_items.foreign_items() {
        visit::visit_foreign_item(&ctx, tcx.hir_foreign_item(item_id), &mut sink)?;
    }

    use_sites::collect_use_sites(tcx, &mut sink.use_sites);

    let build_kind = if tcx.sess.opts.test {
        CacheBuildKind::Test
    } else {
        CacheBuildKind::Library
    };
    let output_path = settings.findings_dir.join(persistence::cache_filename_for(
        &settings.package_root,
        &crate_root_file,
        build_kind,
    ));
    let stored_crate_root = if crate_root_file.is_absolute() {
        crate_root_file.clone()
    } else {
        settings.config_root.join(&crate_root_file)
    };
    if !sink.findings.is_empty() {
        sink.findings.sort_by(|a, b| {
            (&a.path, a.line, a.column, &a.code, &a.item, &a.message)
                .cmp(&(&b.path, b.line, b.column, &b.code, &b.item, &b.message))
        });
        sink.findings.dedup_by(|a, b| {
            a.code == b.code
                && a.path == b.path
                && a.line == b.line
                && a.column == b.column
                && a.message == b.message
                && a.item == b.item
        });
    }

    let report = StoredReport {
        version:              FINDINGS_SCHEMA_VERSION,
        analysis_fingerprint: settings.analysis_fingerprint.clone(),
        scope_fingerprint:    settings.scope_fingerprint.clone(),
        package_root:         settings.package_root.to_string_lossy().into_owned(),
        crate_root_file:      stored_crate_root.to_string_lossy().into_owned(),
        config_fingerprint:   settings.config_fingerprint.clone(),
        findings:             sink.findings,
        pub_use_fix_facts:    sink.pub_use_fix_facts,
        compiler_warnings:    CompilerWarningFacts::None,
        use_sites:            sink.use_sites,
    };
    fs::write(&output_path, serde_json::to_vec_pretty(&report)?)
        .with_context(|| format!("failed to write findings file {}", output_path.display()))?;
    Ok(true)
}