use allow_core::{AllowConfig, CargoAllowError, CargoAllowResult};
use allow_inventory::{InventoryOptions, inventory, resolve_source_tree_root};
use allow_policy::load_policy;
use std::collections::BTreeSet;
use std::env;
use std::path::Path;
#[path = "doctor_args.rs"]
mod doctor_args;
pub(crate) use doctor_args::DoctorArgs;
use doctor_args::DoctorFormat;
use crate::{
InventoryFacts, SourceTreeReportContext, config_path, emit_text,
evidence_inventory::{
PolicyReferenceDiagnostic, current_evidence_source_tree_files,
policy_reference_diagnostics_for_source_tree,
},
};
pub(crate) fn cmd_doctor(args: &DoctorArgs) -> CargoAllowResult<()> {
let cwd =
env::current_dir().map_err(|e| CargoAllowError::new(format!("failed to read cwd: {e}")))?;
let root = resolve_source_tree_root(args.root.root.as_deref(), &cwd)?;
let root_discovery = root_discovery_kind(args.root.root.as_deref(), &root);
let config = config_path(&root, args.config.as_deref());
let policy = load_doctor_policy(config.as_deref());
let opts = doctor_inventory_options(policy.as_ref());
let inventory = inventory(&root, &opts)?;
let files_scanned = inventory.files.len();
let evidence_source_tree_files = current_evidence_source_tree_files(&root, false);
let source_context = SourceTreeReportContext::new(
&root,
InventoryFacts::scanned(inventory.source, files_scanned),
);
let config_text = config
.as_ref()
.map(|path| allow_report::source_tree_path_text(path));
let (config_valid, config_diagnostic) =
config_status(&root, policy.as_ref(), evidence_source_tree_files.as_ref());
let (broken_evidence_links, weak_evidence_references) =
doctor_evidence_health(&root, policy.as_ref(), evidence_source_tree_files.as_ref());
let config_schema_version = policy
.as_ref()
.and_then(|result| result.as_ref().ok())
.map(|cfg| cfg.schema_version.as_str());
let config_policy = policy
.as_ref()
.and_then(|result| result.as_ref().ok())
.map(|cfg| cfg.policy.as_str());
let config_owner = policy
.as_ref()
.and_then(|result| result.as_ref().ok())
.and_then(|cfg| cfg.owner.as_deref());
let config_status = policy
.as_ref()
.and_then(|result| result.as_ref().ok())
.and_then(|cfg| cfg.status.as_deref());
let report = allow_report::DoctorReport {
source_tree_root: source_context.source_tree_root(),
root_discovery,
config_path: config_text.as_deref(),
config_schema_version,
config_policy,
config_owner,
config_status,
config_valid,
config_diagnostic: config_diagnostic.as_deref(),
broken_evidence_links,
weak_evidence_references,
inventory_source: source_context.inventory_source(),
files_scanned,
};
let text = match args.format {
DoctorFormat::Human => allow_report::render_doctor_human(report),
DoctorFormat::Json => allow_report::render_doctor_json(report),
};
emit_text(args.output.as_deref(), &text)?;
Ok(())
}
fn load_doctor_policy(config: Option<&Path>) -> Option<CargoAllowResult<AllowConfig>> {
config.map(load_policy)
}
fn config_status(
root: &Path,
policy: Option<&CargoAllowResult<AllowConfig>>,
source_tree_files: Option<&BTreeSet<String>>,
) -> (Option<bool>, Option<String>) {
match policy {
None => (None, None),
Some(Ok(cfg)) => match first_broken_evidence_diagnostic(root, cfg, source_tree_files) {
Some((entry_id, reference)) => (
Some(false),
Some(format!(
"{} {} `{}`: {}",
entry_id,
reference.source.label(),
reference.diagnostic.raw,
reference.source.message(&reference.diagnostic.message)
)),
),
None => (Some(true), None),
},
Some(Err(err)) => (Some(false), Some(err.to_string())),
}
}
fn doctor_evidence_health(
root: &Path,
policy: Option<&CargoAllowResult<AllowConfig>>,
source_tree_files: Option<&BTreeSet<String>>,
) -> (Option<usize>, Option<usize>) {
match policy {
Some(Ok(cfg)) => {
let diagnostics = evidence_diagnostics(root, cfg, source_tree_files);
let broken = diagnostics
.iter()
.filter(|reference| reference.diagnostic.status.is_broken_local_link())
.count();
let weak = diagnostics
.iter()
.filter(|reference| reference.diagnostic.status.is_weak_reference())
.count();
(Some(broken), Some(weak))
}
_ => (None, None),
}
}
fn first_broken_evidence_diagnostic(
root: &Path,
cfg: &AllowConfig,
source_tree_files: Option<&BTreeSet<String>>,
) -> Option<(String, PolicyReferenceDiagnostic)> {
cfg.allow.iter().find_map(|entry| {
policy_reference_diagnostics_for_source_tree(root, entry, source_tree_files)
.into_iter()
.find(|reference| reference.diagnostic.status.is_broken_local_link())
.map(|reference| (entry.id.clone(), reference))
})
}
fn evidence_diagnostics(
root: &Path,
cfg: &AllowConfig,
source_tree_files: Option<&BTreeSet<String>>,
) -> Vec<PolicyReferenceDiagnostic> {
cfg.allow
.iter()
.flat_map(|entry| {
policy_reference_diagnostics_for_source_tree(root, entry, source_tree_files)
})
.collect()
}
fn doctor_inventory_options(policy: Option<&CargoAllowResult<AllowConfig>>) -> InventoryOptions {
match policy {
Some(Ok(cfg)) => InventoryOptions {
ignored: cfg.workspace.ignored.clone(),
generated: cfg.workspace.generated.clone(),
include_untracked: false,
},
_ => InventoryOptions::default(),
}
}
fn root_discovery_kind(explicit_root: Option<&Path>, root: &Path) -> &'static str {
if explicit_root.is_some() {
"explicit_root"
} else if root.join(".git").exists() {
"nearest_git_root"
} else {
"current_directory_fallback"
}
}
#[cfg(test)]
pub(crate) fn sample_doctor_json_for_contract_test() -> String {
allow_report::render_doctor_json(allow_report::DoctorReport {
source_tree_root: "H:/Code/Rust/cargo-allow",
root_discovery: "nearest_git_root",
config_path: Some("H:/Code/Rust/cargo-allow/policy/allow.toml"),
config_schema_version: Some("0.1"),
config_policy: Some("cargo-allow"),
config_owner: Some("core/policy"),
config_status: Some("active"),
config_valid: Some(true),
config_diagnostic: None,
broken_evidence_links: Some(0),
weak_evidence_references: Some(0),
inventory_source: "git_tracked",
files_scanned: 50,
})
}
#[cfg(test)]
#[path = "doctor_tests.rs"]
mod tests;