#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use dev_report::{CheckResult, Report, Severity};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DepScope {
Unused,
Outdated,
All,
}
#[derive(Debug, Clone)]
pub struct DepCheck {
name: String,
version: String,
scope: DepScope,
}
impl DepCheck {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
scope: DepScope::All,
}
}
pub fn scope(mut self, scope: DepScope) -> Self {
self.scope = scope;
self
}
pub fn dep_scope(&self) -> DepScope {
self.scope
}
pub fn execute(&self) -> Result<DepResult, DepError> {
Ok(DepResult {
name: self.name.clone(),
version: self.version.clone(),
scope: self.scope,
unused: Vec::new(),
outdated: Vec::new(),
})
}
}
#[derive(Debug, Clone)]
pub struct UnusedDep {
pub crate_name: String,
pub kind: String,
}
#[derive(Debug, Clone)]
pub struct OutdatedDep {
pub crate_name: String,
pub current: String,
pub latest: String,
pub major_behind: u32,
}
#[derive(Debug, Clone)]
pub struct DepResult {
pub name: String,
pub version: String,
pub scope: DepScope,
pub unused: Vec<UnusedDep>,
pub outdated: Vec<OutdatedDep>,
}
impl DepResult {
pub fn total_findings(&self) -> usize {
self.unused.len() + self.outdated.len()
}
pub fn into_report(self) -> Report {
let mut report = Report::new(&self.name, &self.version).with_producer("dev-deps");
if self.total_findings() == 0 {
report.push(CheckResult::pass("deps::health"));
} else {
for u in &self.unused {
report.push(
CheckResult::warn(format!("deps::unused::{}", u.crate_name), Severity::Warning)
.with_detail(format!("unused in {}", u.kind)),
);
}
for o in &self.outdated {
let sev = if o.major_behind >= 2 {
Severity::Warning
} else {
Severity::Info
};
report.push(
CheckResult::warn(format!("deps::outdated::{}", o.crate_name), sev)
.with_detail(format!(
"{} -> {} ({} major behind)",
o.current, o.latest, o.major_behind
)),
);
}
}
report.finish();
report
}
}
#[derive(Debug)]
pub enum DepError {
UdepsToolNotInstalled,
OutdatedToolNotInstalled,
SubprocessFailed(String),
ParseError(String),
}
impl std::fmt::Display for DepError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UdepsToolNotInstalled => write!(f, "cargo-udeps is not installed"),
Self::OutdatedToolNotInstalled => write!(f, "cargo-outdated is not installed"),
Self::SubprocessFailed(s) => write!(f, "subprocess failed: {s}"),
Self::ParseError(s) => write!(f, "parse error: {s}"),
}
}
}
impl std::error::Error for DepError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_builds() {
let c = DepCheck::new("x", "0.1.0").scope(DepScope::Unused);
assert_eq!(c.dep_scope(), DepScope::Unused);
}
#[test]
fn empty_result_passes() {
let r = DepResult {
name: "x".into(),
version: "0.1.0".into(),
scope: DepScope::All,
unused: Vec::new(),
outdated: Vec::new(),
};
assert_eq!(r.total_findings(), 0);
let report = r.into_report();
assert!(report.passed());
}
#[test]
fn findings_produce_warnings() {
let r = DepResult {
name: "x".into(),
version: "0.1.0".into(),
scope: DepScope::All,
unused: vec![UnusedDep {
crate_name: "foo".into(),
kind: "dependencies".into(),
}],
outdated: vec![OutdatedDep {
crate_name: "bar".into(),
current: "1.0.0".into(),
latest: "3.0.0".into(),
major_behind: 2,
}],
};
let report = r.into_report();
assert!(report.warned());
}
}