garbage_code_hunter/deps_shamer/
rules.rs1use super::types::{DepFile, DepIssue, DepSource, Ecosystem, Severity};
6
7pub trait DepRule: Send + Sync {
9 fn id(&self) -> &str;
10 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue>;
11}
12
13pub struct TooManyDepsRule {
15 pub threshold: usize,
16}
17
18impl DepRule for TooManyDepsRule {
19 fn id(&self) -> &str {
20 "too-many-deps"
21 }
22
23 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue> {
24 let total = dep_file.dependencies.len();
25 if total > self.threshold {
26 vec![DepIssue {
27 rule_id: self.id().to_string(),
28 severity: Severity::Medium,
29 message: format!(
30 "{} dependencies? Are you building a supermarket or a software project?",
31 total
32 ),
33 dep_name: None,
34 }]
35 } else {
36 vec![]
37 }
38 }
39}
40
41pub struct GitDepsRule;
43
44impl DepRule for GitDepsRule {
45 fn id(&self) -> &str {
46 "git-deps"
47 }
48
49 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue> {
50 dep_file
51 .dependencies
52 .iter()
53 .filter_map(|dep| {
54 if let DepSource::Git { url } = &dep.source {
55 Some(DepIssue {
56 rule_id: self.id().to_string(),
57 severity: Severity::Medium,
58 message: format!(
59 "Directly referencing git repo '{}' — don't trust package managers?",
60 url
61 ),
62 dep_name: Some(dep.name.clone()),
63 })
64 } else {
65 None
66 }
67 })
68 .collect()
69 }
70}
71
72pub struct WildcardVersionRule;
74
75impl DepRule for WildcardVersionRule {
76 fn id(&self) -> &str {
77 "wildcard-version"
78 }
79
80 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue> {
81 dep_file
82 .dependencies
83 .iter()
84 .filter_map(|dep| {
85 let v = dep.version.trim();
86 if v == "*" || v == ">=0" || v.is_empty() {
87 Some(DepIssue {
88 rule_id: self.id().to_string(),
89 severity: Severity::High,
90 message: format!(
91 "Version '{}' for '{}' — enjoy your daily breaking changes?",
92 v, dep.name
93 ),
94 dep_name: Some(dep.name.clone()),
95 })
96 } else {
97 None
98 }
99 })
100 .collect()
101 }
102}
103
104pub struct PreReleaseRule;
106
107impl DepRule for PreReleaseRule {
108 fn id(&self) -> &str {
109 "pre-release"
110 }
111
112 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue> {
113 dep_file
114 .dependencies
115 .iter()
116 .filter(|dep| !dep.is_dev)
117 .filter_map(|dep| {
118 let v = dep.version.to_lowercase();
119 if v.contains("alpha")
120 || v.contains("beta")
121 || v.contains("rc")
122 || v.contains("pre")
123 || v.contains("snapshot")
124 {
125 Some(DepIssue {
126 rule_id: self.id().to_string(),
127 severity: Severity::Medium,
128 message: format!(
129 "Production dependency '{}' uses pre-release version '{}' — brave!",
130 dep.name, dep.version
131 ),
132 dep_name: Some(dep.name.clone()),
133 })
134 } else {
135 None
136 }
137 })
138 .collect()
139 }
140}
141
142pub struct DeprecatedDepRule {
144 pub ecosystem: Ecosystem,
145}
146
147impl DepRule for DeprecatedDepRule {
148 fn id(&self) -> &str {
149 "deprecated-dep"
150 }
151
152 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue> {
153 let deprecated = match &self.ecosystem {
154 Ecosystem::Rust => vec![
155 "failure",
156 "iron",
157 "nickel",
158 "rustc-serialize",
159 "quickcheck",
160 "tempdir",
161 "toml_query",
162 ],
163 Ecosystem::Node => vec![
164 "request",
165 "bower",
166 "node-uuid",
167 "nomnom",
168 "optimist",
169 "colors",
170 "left-pad",
171 ],
172 _ => vec![],
173 };
174
175 dep_file
176 .dependencies
177 .iter()
178 .filter_map(|dep| {
179 if deprecated.contains(&dep.name.as_str()) {
180 Some(DepIssue {
181 rule_id: self.id().to_string(),
182 severity: Severity::High,
183 message: format!(
184 "'{}' is deprecated — are you an archaeologist?",
185 dep.name
186 ),
187 dep_name: Some(dep.name.clone()),
188 })
189 } else {
190 None
191 }
192 })
193 .collect()
194 }
195}
196
197pub struct DuplicatedDepRule;
199
200impl DepRule for DuplicatedDepRule {
201 fn id(&self) -> &str {
202 "duplicated-dep"
203 }
204
205 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue> {
206 let mut seen = std::collections::HashSet::new();
207 let mut duplicates = Vec::new();
208
209 for dep in &dep_file.dependencies {
210 if !seen.insert(&dep.name) {
211 duplicates.push(DepIssue {
212 rule_id: self.id().to_string(),
213 severity: Severity::Medium,
214 message: format!(
215 "'{}' appears more than once — Ctrl+C and Ctrl+V are working overtime?",
216 dep.name
217 ),
218 dep_name: Some(dep.name.clone()),
219 });
220 }
221 }
222
223 duplicates
224 }
225}
226
227pub struct TooManyDevDepsRule {
229 pub threshold: usize,
230}
231
232impl DepRule for TooManyDevDepsRule {
233 fn id(&self) -> &str {
234 "too-many-dev-deps"
235 }
236
237 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue> {
238 let dev_count = dep_file.dependencies.iter().filter(|d| d.is_dev).count();
239 if dev_count > self.threshold {
240 vec![DepIssue {
241 rule_id: self.id().to_string(),
242 severity: Severity::Low,
243 message: format!(
244 "{} dev dependencies — your test setup is heavier than the app itself?",
245 dev_count
246 ),
247 dep_name: None,
248 }]
249 } else {
250 vec![]
251 }
252 }
253}
254
255pub struct TooManyOptionalRule {
257 pub ratio_threshold: f64,
258}
259
260impl DepRule for TooManyOptionalRule {
261 fn id(&self) -> &str {
262 "too-many-optional"
263 }
264
265 fn check(&self, dep_file: &DepFile) -> Vec<DepIssue> {
266 let total = dep_file.dependencies.len();
267 if total == 0 {
268 return vec![];
269 }
270 let optional_count = dep_file
271 .dependencies
272 .iter()
273 .filter(|d| d.is_optional)
274 .count();
275 let ratio = optional_count as f64 / total as f64;
276 if ratio > self.ratio_threshold {
277 vec![DepIssue {
278 rule_id: self.id().to_string(),
279 severity: Severity::Low,
280 message: format!(
281 "{}% of dependencies are optional ({}/{}) — are you sure what you actually need?",
282 (ratio * 100.0) as usize,
283 optional_count,
284 total
285 ),
286 dep_name: None,
287 }]
288 } else {
289 vec![]
290 }
291 }
292}
293
294pub fn default_rules(ecosystem: &Ecosystem) -> Vec<Box<dyn DepRule>> {
296 vec![
297 Box::new(TooManyDepsRule { threshold: 50 }),
298 Box::new(GitDepsRule),
299 Box::new(WildcardVersionRule),
300 Box::new(PreReleaseRule),
301 Box::new(DeprecatedDepRule {
302 ecosystem: ecosystem.clone(),
303 }),
304 Box::new(DuplicatedDepRule),
305 Box::new(TooManyDevDepsRule { threshold: 20 }),
306 Box::new(TooManyOptionalRule {
307 ratio_threshold: 0.5,
308 }),
309 ]
310}
311
312pub fn check_dep_file(dep_file: &DepFile) -> Vec<DepIssue> {
314 let rules = default_rules(&dep_file.ecosystem);
315 rules.iter().flat_map(|rule| rule.check(dep_file)).collect()
316}
317
318#[cfg(test)]
319mod tests {
320 use super::super::types::Dependency;
321 use super::*;
322
323 fn make_dep(name: &str, version: &str) -> Dependency {
324 Dependency {
325 name: name.to_string(),
326 version: version.to_string(),
327 source: DepSource::Registry,
328 is_dev: false,
329 is_optional: false,
330 }
331 }
332
333 fn make_dep_file(deps: Vec<Dependency>) -> DepFile {
334 DepFile {
335 path: "test.toml".to_string(),
336 ecosystem: Ecosystem::Rust,
337 dependencies: deps,
338 }
339 }
340
341 #[test]
342 fn test_too_many_deps_triggers() {
343 let rule = TooManyDepsRule { threshold: 5 };
344 let deps: Vec<Dependency> = (0..10)
345 .map(|i| make_dep(&format!("dep{}", i), "1.0"))
346 .collect();
347 let dep_file = make_dep_file(deps);
348 let issues = rule.check(&dep_file);
349 assert_eq!(issues.len(), 1);
350 assert!(issues[0].message.contains("10"));
351 }
352
353 #[test]
354 fn test_too_many_deps_no_trigger() {
355 let rule = TooManyDepsRule { threshold: 50 };
356 let deps = vec![make_dep("serde", "1.0")];
357 let dep_file = make_dep_file(deps);
358 let issues = rule.check(&dep_file);
359 assert!(issues.is_empty());
360 }
361
362 #[test]
363 fn test_git_deps_detected() {
364 let rule = GitDepsRule;
365 let dep_file = make_dep_file(vec![
366 make_dep("serde", "1.0"),
367 Dependency {
368 name: "my-lib".to_string(),
369 version: "main".to_string(),
370 source: DepSource::Git {
371 url: "https://github.com/foo/bar".to_string(),
372 },
373 is_dev: false,
374 is_optional: false,
375 },
376 ]);
377 let issues = rule.check(&dep_file);
378 assert_eq!(issues.len(), 1);
379 assert!(issues[0].dep_name.as_deref() == Some("my-lib"));
380 }
381
382 #[test]
383 fn test_wildcard_version_detected() {
384 let rule = WildcardVersionRule;
385 let dep_file = make_dep_file(vec![
386 make_dep("ok-dep", "1.0"),
387 make_dep("bad-dep", "*"),
388 make_dep("also-bad", ">=0"),
389 ]);
390 let issues = rule.check(&dep_file);
391 assert_eq!(issues.len(), 2);
392 }
393
394 #[test]
395 fn test_pre_release_detected() {
396 let rule = PreReleaseRule;
397 let dep_file = make_dep_file(vec![
398 make_dep("stable", "1.0"),
399 make_dep("beta-pkg", "2.0.0-beta.1"),
400 make_dep("alpha-pkg", "1.0.0-alpha"),
401 ]);
402 let issues = rule.check(&dep_file);
403 assert_eq!(issues.len(), 2);
404 }
405
406 #[test]
407 fn test_pre_release_ignores_dev_deps() {
408 let rule = PreReleaseRule;
409 let mut dep = make_dep("dev-beta", "1.0.0-beta");
410 dep.is_dev = true;
411 let dep_file = make_dep_file(vec![dep]);
412 let issues = rule.check(&dep_file);
413 assert!(issues.is_empty());
414 }
415
416 #[test]
417 fn test_deprecated_dep_rust() {
418 let rule = DeprecatedDepRule {
419 ecosystem: Ecosystem::Rust,
420 };
421 let dep_file = make_dep_file(vec![make_dep("serde", "1.0"), make_dep("failure", "0.1")]);
422 let issues = rule.check(&dep_file);
423 assert_eq!(issues.len(), 1);
424 assert!(issues[0].dep_name.as_deref() == Some("failure"));
425 }
426
427 #[test]
428 fn test_duplicated_dep_detected() {
429 let rule = DuplicatedDepRule;
430 let dep_file = make_dep_file(vec![make_dep("serde", "1.0"), make_dep("serde", "1.1")]);
431 let issues = rule.check(&dep_file);
432 assert_eq!(issues.len(), 1);
433 }
434
435 #[test]
436 fn test_check_dep_file_integration() {
437 let deps = vec![
438 make_dep("serde", "1.0"),
439 make_dep("tokio", "*"),
440 make_dep("failure", "0.1"),
441 ];
442 let dep_file = make_dep_file(deps);
443 let issues = check_dep_file(&dep_file);
444 assert!(issues.len() >= 2);
446 assert!(issues.iter().any(|i| i.rule_id == "wildcard-version"));
447 assert!(issues.iter().any(|i| i.rule_id == "deprecated-dep"));
448 }
449}