1#![cfg_attr(docsrs, feature(doc_cfg))]
30#![warn(missing_docs)]
31#![warn(rust_2018_idioms)]
32
33use dev_report::{CheckResult, Report, Severity};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DepScope {
38 Unused,
40 Outdated,
42 All,
44}
45
46#[derive(Debug, Clone)]
48pub struct DepCheck {
49 name: String,
50 version: String,
51 scope: DepScope,
52}
53
54impl DepCheck {
55 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
57 Self {
58 name: name.into(),
59 version: version.into(),
60 scope: DepScope::All,
61 }
62 }
63
64 pub fn scope(mut self, scope: DepScope) -> Self {
66 self.scope = scope;
67 self
68 }
69
70 pub fn dep_scope(&self) -> DepScope {
72 self.scope
73 }
74
75 pub fn execute(&self) -> Result<DepResult, DepError> {
79 Ok(DepResult {
80 name: self.name.clone(),
81 version: self.version.clone(),
82 scope: self.scope,
83 unused: Vec::new(),
84 outdated: Vec::new(),
85 })
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct UnusedDep {
92 pub crate_name: String,
94 pub kind: String,
96}
97
98#[derive(Debug, Clone)]
100pub struct OutdatedDep {
101 pub crate_name: String,
103 pub current: String,
105 pub latest: String,
107 pub major_behind: u32,
109}
110
111#[derive(Debug, Clone)]
113pub struct DepResult {
114 pub name: String,
116 pub version: String,
118 pub scope: DepScope,
120 pub unused: Vec<UnusedDep>,
122 pub outdated: Vec<OutdatedDep>,
124}
125
126impl DepResult {
127 pub fn total_findings(&self) -> usize {
129 self.unused.len() + self.outdated.len()
130 }
131
132 pub fn into_report(self) -> Report {
134 let mut report = Report::new(&self.name, &self.version).with_producer("dev-deps");
135 if self.total_findings() == 0 {
136 report.push(CheckResult::pass("deps::health"));
137 } else {
138 for u in &self.unused {
139 report.push(
140 CheckResult::warn(format!("deps::unused::{}", u.crate_name), Severity::Warning)
141 .with_detail(format!("unused in {}", u.kind)),
142 );
143 }
144 for o in &self.outdated {
145 let sev = if o.major_behind >= 2 {
146 Severity::Warning
147 } else {
148 Severity::Info
149 };
150 report.push(
151 CheckResult::warn(format!("deps::outdated::{}", o.crate_name), sev)
152 .with_detail(format!(
153 "{} -> {} ({} major behind)",
154 o.current, o.latest, o.major_behind
155 )),
156 );
157 }
158 }
159 report.finish();
160 report
161 }
162}
163
164#[derive(Debug)]
166pub enum DepError {
167 UdepsToolNotInstalled,
169 OutdatedToolNotInstalled,
171 SubprocessFailed(String),
173 ParseError(String),
175}
176
177impl std::fmt::Display for DepError {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 match self {
180 Self::UdepsToolNotInstalled => write!(f, "cargo-udeps is not installed"),
181 Self::OutdatedToolNotInstalled => write!(f, "cargo-outdated is not installed"),
182 Self::SubprocessFailed(s) => write!(f, "subprocess failed: {s}"),
183 Self::ParseError(s) => write!(f, "parse error: {s}"),
184 }
185 }
186}
187
188impl std::error::Error for DepError {}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn check_builds() {
196 let c = DepCheck::new("x", "0.1.0").scope(DepScope::Unused);
197 assert_eq!(c.dep_scope(), DepScope::Unused);
198 }
199
200 #[test]
201 fn empty_result_passes() {
202 let r = DepResult {
203 name: "x".into(),
204 version: "0.1.0".into(),
205 scope: DepScope::All,
206 unused: Vec::new(),
207 outdated: Vec::new(),
208 };
209 assert_eq!(r.total_findings(), 0);
210 let report = r.into_report();
211 assert!(report.passed());
212 }
213
214 #[test]
215 fn findings_produce_warnings() {
216 let r = DepResult {
217 name: "x".into(),
218 version: "0.1.0".into(),
219 scope: DepScope::All,
220 unused: vec![UnusedDep {
221 crate_name: "foo".into(),
222 kind: "dependencies".into(),
223 }],
224 outdated: vec![OutdatedDep {
225 crate_name: "bar".into(),
226 current: "1.0.0".into(),
227 latest: "3.0.0".into(),
228 major_behind: 2,
229 }],
230 };
231 let report = r.into_report();
232 assert!(report.warned());
234 }
235}