1use log::{debug, info};
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12use std::fs;
13use std::io;
14use std::panic::Location;
15use syn::{visit::Visit, File};
16use walkdir::WalkDir;
17
18#[track_caller]
33pub fn error_with_location<E>(err: E) -> Box<dyn std::error::Error>
34where
35 E: std::fmt::Display,
36{
37 let loc = Location::caller();
38 Box::new(io::Error::other(format!(
39 "{} at {}:{}",
40 err,
41 loc.file(),
42 loc.line()
43 )))
44}
45
46#[macro_export]
52macro_rules! loc_try {
53 ($expr:expr) => {
54 match $expr {
55 Ok(val) => val,
56 Err(err) => {
57 return Err($crate::error_with_location(err));
58 }
59 }
60 };
61}
62
63fn has_test_attr(attrs: &[syn::Attribute]) -> bool {
64 attrs.iter().any(|a| {
65 if a.path().is_ident("test") {
66 true
67 } else if a.path().is_ident("cfg") {
68 match &a.meta {
69 syn::Meta::List(l) => l.tokens.to_string().contains("test"),
70 _ => false,
71 }
72 } else {
73 false
74 }
75 })
76}
77
78#[derive(Debug, Serialize, Clone)]
83pub enum ClassKind {
84 Struct,
86 Enum,
88 Trait,
90 TypeAlias,
92 Macro,
94}
95
96#[derive(Debug, Serialize, Clone)]
98pub struct ClassInfo {
99 pub name: String,
101 pub kind: ClassKind,
103}
104
105#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
108pub enum CrateKind {
109 Workspace,
111 External,
113}
114
115#[derive(Debug, Serialize, Clone)]
121pub struct Metrics {
122 pub r: usize,
124 pub n: usize,
126 pub h: f64,
128 pub ca: usize,
130 pub ce: usize,
132 pub a: f64,
134 pub i: f64,
136 pub d: f64,
138 pub d_prime: f64,
140}
141
142#[derive(Debug, Serialize, Clone)]
144#[serde(rename_all = "lowercase")]
145pub enum AbstractionEval {
146 Abstract,
148 Mixed,
150 Concrete,
152}
153
154#[derive(Debug, Serialize, Clone)]
156#[serde(rename_all = "lowercase")]
157pub enum CohesionEval {
158 High,
160 Low,
162}
163
164#[derive(Debug, Serialize, Clone)]
166#[serde(rename_all = "lowercase")]
167pub enum StabilityEval {
168 Stable,
170 Moderate,
172 Unstable,
174}
175
176#[derive(Debug, Serialize, Clone)]
178#[serde(rename_all = "lowercase")]
179pub enum DistanceEval {
180 Good,
182 Balanced,
184 Painful,
186 Useless,
188}
189
190#[derive(Debug, Serialize, Clone)]
192pub struct Evaluation {
193 pub a: AbstractionEval,
195 pub h: CohesionEval,
197 pub i: StabilityEval,
199 pub d_prime: DistanceEval,
201}
202
203#[derive(Debug, Serialize, Clone)]
205pub struct MetricsResult {
206 pub metrics: Metrics,
208 pub evaluation: Evaluation,
210}
211
212#[derive(Debug, Clone, Deserialize, Serialize, Default)]
214pub struct EvaluationThresholds {
215 #[serde(default)]
217 pub abstraction: AbstractionThresholds,
218 #[serde(default)]
220 pub cohesion: CohesionThresholds,
221 #[serde(default)]
223 pub instability: InstabilityThresholds,
224 #[serde(default)]
226 pub distance: DistanceThresholds,
227}
228
229#[derive(Debug, Clone, Deserialize, Serialize, Default)]
231pub struct Config {
232 #[serde(default)]
234 pub evaluation: EvaluationThresholds,
235}
236
237#[derive(Debug, Clone, Deserialize, Serialize)]
239pub struct AbstractionThresholds {
240 #[serde(default = "default_abstract_min")]
242 pub abstract_min: f64,
243 #[serde(default = "default_concrete_max")]
245 pub concrete_max: f64,
246}
247
248fn default_abstract_min() -> f64 {
249 0.7
250}
251
252fn default_concrete_max() -> f64 {
253 0.3
254}
255
256impl Default for AbstractionThresholds {
257 fn default() -> Self {
258 Self {
259 abstract_min: default_abstract_min(),
260 concrete_max: default_concrete_max(),
261 }
262 }
263}
264
265#[derive(Debug, Clone, Deserialize, Serialize)]
267pub struct CohesionThresholds {
268 #[serde(default = "default_high_gt")]
270 pub high_gt: f64,
271}
272
273fn default_high_gt() -> f64 {
274 1.0
275}
276
277impl Default for CohesionThresholds {
278 fn default() -> Self {
279 Self {
280 high_gt: default_high_gt(),
281 }
282 }
283}
284
285#[derive(Debug, Clone, Deserialize, Serialize)]
287pub struct InstabilityThresholds {
288 #[serde(default = "default_unstable_min")]
290 pub unstable_min: f64,
291 #[serde(default = "default_stable_max")]
293 pub stable_max: f64,
294}
295
296fn default_unstable_min() -> f64 {
297 0.7
298}
299
300fn default_stable_max() -> f64 {
301 0.3
302}
303
304impl Default for InstabilityThresholds {
305 fn default() -> Self {
306 Self {
307 unstable_min: default_unstable_min(),
308 stable_max: default_stable_max(),
309 }
310 }
311}
312
313#[derive(Debug, Clone, Deserialize, Serialize)]
315pub struct DistanceThresholds {
316 #[serde(default = "default_good_max")]
318 pub good_max: f64,
319 #[serde(default = "default_bad_min")]
321 pub bad_min: f64,
322}
323
324fn default_good_max() -> f64 {
325 0.4
326}
327
328fn default_bad_min() -> f64 {
329 0.6
330}
331
332impl Default for DistanceThresholds {
333 fn default() -> Self {
334 Self {
335 good_max: default_good_max(),
336 bad_min: default_bad_min(),
337 }
338 }
339}
340
341pub fn evaluate_metrics(m: &Metrics) -> Evaluation {
347 evaluate_metrics_with(m, &EvaluationThresholds::default())
348}
349
350pub fn evaluate_metrics_with(m: &Metrics, t: &EvaluationThresholds) -> Evaluation {
352 let a_label = if m.a >= t.abstraction.abstract_min {
353 AbstractionEval::Abstract
354 } else if m.a <= t.abstraction.concrete_max {
355 AbstractionEval::Concrete
356 } else {
357 AbstractionEval::Mixed
358 };
359
360 let h_label = if m.h > t.cohesion.high_gt {
361 CohesionEval::High
362 } else {
363 CohesionEval::Low
364 };
365
366 let i_label = if m.i >= t.instability.unstable_min {
367 StabilityEval::Unstable
368 } else if m.i <= t.instability.stable_max {
369 StabilityEval::Stable
370 } else {
371 StabilityEval::Moderate
372 };
373
374 let d_label = if m.d_prime <= t.distance.good_max {
375 DistanceEval::Good
376 } else if m.d_prime >= t.distance.bad_min {
377 if m.a + m.i - 1.0 >= 0.0 {
378 DistanceEval::Useless
379 } else {
380 DistanceEval::Painful
381 }
382 } else {
383 DistanceEval::Balanced
384 };
385
386 Evaluation {
387 a: a_label,
388 h: h_label,
389 i: i_label,
390 d_prime: d_label,
391 }
392}
393
394#[derive(Debug, Serialize, Clone)]
396pub struct CrateDetail {
397 pub kind: CrateKind,
399 pub metrics: Metrics,
401 pub evaluation: Evaluation,
403 pub classes: Vec<ClassInfo>,
405 pub internal_depends_on: HashMap<String, Vec<String>>,
407 pub internal_depended_by: HashMap<String, Vec<String>>,
409 pub external_depends_on: HashMap<String, HashMap<String, Vec<String>>>,
411 pub external_depended_by: HashMap<String, HashMap<String, Vec<String>>>,
413}
414
415pub fn collect_defined(files: &[File]) -> (HashMap<String, ClassKind>, usize) {
420 fn visit_items(
421 items: &[syn::Item],
422 defined: &mut HashMap<String, ClassKind>,
423 abstract_count: &mut usize,
424 ) {
425 for item in items {
426 match item {
427 syn::Item::Struct(item) if !has_test_attr(&item.attrs) => {
428 defined.insert(item.ident.to_string(), ClassKind::Struct);
429 }
430 syn::Item::Enum(item) if !has_test_attr(&item.attrs) => {
431 defined.insert(item.ident.to_string(), ClassKind::Enum);
432 }
433 syn::Item::Trait(item) if !has_test_attr(&item.attrs) => {
434 defined.insert(item.ident.to_string(), ClassKind::Trait);
435 *abstract_count += 1;
436 }
437 syn::Item::Type(item) if !has_test_attr(&item.attrs) => {
438 defined.insert(item.ident.to_string(), ClassKind::TypeAlias);
439 }
440 syn::Item::Macro(item) if !has_test_attr(&item.attrs) => {
441 if let Some(id) = &item.ident {
442 defined.insert(id.to_string(), ClassKind::Macro);
443 }
444 }
445 syn::Item::Mod(m) if !has_test_attr(&m.attrs) => {
446 if let Some((_, items)) = &m.content {
447 visit_items(items, defined, abstract_count);
448 }
449 }
450 _ => {}
451 }
452 }
453 }
454
455 let mut defined = HashMap::new();
456 let mut abstract_count = 0usize;
457
458 for file in files {
459 if has_test_attr(&file.attrs) {
460 continue;
461 }
462 visit_items(&file.items, &mut defined, &mut abstract_count);
463 }
464 (defined, abstract_count)
465}
466pub fn collect_methods(files: &[File]) -> HashMap<(String, String), String> {
472 let mut map = HashMap::new();
473 fn ret_ty(output: &syn::ReturnType, self_ty: &str) -> Option<String> {
474 fn from_impl_trait(it: &syn::TypeImplTrait) -> Option<String> {
475 for b in &it.bounds {
476 if let syn::TypeParamBound::Trait(t) = b {
477 if let Some(seg) = t.path.segments.last() {
478 return Some(seg.ident.to_string());
479 }
480 }
481 }
482 None
483 }
484
485 fn from_trait_object(obj: &syn::TypeTraitObject) -> Option<String> {
486 for b in &obj.bounds {
487 if let syn::TypeParamBound::Trait(t) = b {
488 if let Some(seg) = t.path.segments.last() {
489 return Some(seg.ident.to_string());
490 }
491 }
492 }
493 None
494 }
495
496 fn from_path(p: &syn::Path, self_ty: &str) -> Option<String> {
497 if let Some(last) = p.segments.last() {
498 if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
499 for arg in &args.args {
500 if let syn::GenericArgument::Type(t) = arg {
501 if let Some(name) = from_type(t, self_ty) {
502 return Some(name);
503 }
504 }
505 }
506 }
507 }
508 p.segments.last().map(|s| {
509 if s.ident == "Self" {
510 self_ty.to_string()
511 } else {
512 s.ident.to_string()
513 }
514 })
515 }
516
517 fn from_type(ty: &syn::Type, self_ty: &str) -> Option<String> {
518 match ty {
519 syn::Type::Path(p) => from_path(&p.path, self_ty),
520 syn::Type::Reference(r) => from_type(&r.elem, self_ty),
521 syn::Type::ImplTrait(it) => from_impl_trait(it),
522 syn::Type::TraitObject(obj) => from_trait_object(obj),
523 syn::Type::Paren(p) => from_type(&p.elem, self_ty),
524 syn::Type::Group(g) => from_type(&g.elem, self_ty),
525 syn::Type::Ptr(p) => from_type(&p.elem, self_ty),
526 _ => None,
527 }
528 }
529
530 match output {
531 syn::ReturnType::Type(_, ty) => from_type(ty, self_ty),
532 _ => None,
533 }
534 }
535
536 for file in files {
537 if has_test_attr(&file.attrs) {
538 continue;
539 }
540 for item in &file.items {
541 match item {
542 syn::Item::Impl(imp) if !has_test_attr(&imp.attrs) => {
543 if let syn::Type::Path(tp) = &*imp.self_ty {
544 if let Some(seg) = tp.path.segments.last() {
545 let self_ty = seg.ident.to_string();
546 for item in &imp.items {
547 if let syn::ImplItem::Fn(m) = item {
548 if has_test_attr(&m.attrs) {
549 continue;
550 }
551 if let Some(ret) = ret_ty(&m.sig.output, &self_ty) {
552 map.insert((self_ty.clone(), m.sig.ident.to_string()), ret);
553 }
554 }
555 }
556 }
557 }
558 }
559 syn::Item::Trait(t) if !has_test_attr(&t.attrs) => {
560 let trait_name = t.ident.to_string();
561 for item in &t.items {
562 if let syn::TraitItem::Fn(m) = item {
563 if has_test_attr(&m.attrs) {
564 continue;
565 }
566 if let Some(ret) = ret_ty(&m.sig.output, &trait_name) {
567 map.insert((trait_name.clone(), m.sig.ident.to_string()), ret);
568 }
569 }
570 }
571 }
572 _ => {}
573 }
574 }
575 }
576
577 map
578}
579pub fn collect_trait_bounds(files: &[File]) -> HashMap<String, Vec<String>> {
584 let mut map = HashMap::new();
585 for file in files {
586 if has_test_attr(&file.attrs) {
587 continue;
588 }
589 for item in &file.items {
590 if let syn::Item::Trait(t) = item {
591 if has_test_attr(&t.attrs) {
592 continue;
593 }
594 let name = t.ident.to_string();
595 let mut bounds = Vec::new();
596 for b in &t.supertraits {
597 if let syn::TypeParamBound::Trait(tb) = b {
598 if let Some(seg) = tb.path.segments.last() {
599 bounds.push(seg.ident.to_string());
600 }
601 }
602 }
603 map.insert(name, bounds);
604 }
605 }
606 }
607 map
608}
609
610pub fn collect_reexports(
615 files: &[File],
616 workspace: &HashSet<String>,
617) -> HashMap<String, (String, String)> {
618 struct ReexportVisitor<'a> {
619 workspace: &'a HashSet<String>,
620 map: HashMap<String, (String, String)>,
621 }
622
623 fn handle_use_tree(
624 tree: &syn::UseTree,
625 first: Option<String>,
626 workspace: &HashSet<String>,
627 map: &mut HashMap<String, (String, String)>,
628 ) {
629 match tree {
630 syn::UseTree::Path(p) => {
631 let root = first.clone().unwrap_or_else(|| p.ident.to_string());
632 handle_use_tree(&p.tree, Some(root), workspace, map);
633 }
634 syn::UseTree::Name(n) => {
635 if let Some(r) = &first {
636 if workspace.contains(r) {
637 map.insert(n.ident.to_string(), (r.clone(), n.ident.to_string()));
638 }
639 }
640 }
641 syn::UseTree::Rename(rn) => {
642 let root = first
643 .as_ref()
644 .cloned()
645 .unwrap_or_else(|| rn.ident.to_string());
646 if workspace.contains(&root) {
647 map.insert(rn.rename.to_string(), (root, rn.ident.to_string()));
648 }
649 }
650 syn::UseTree::Group(g) => {
651 for t in &g.items {
652 handle_use_tree(t, first.clone(), workspace, map);
653 }
654 }
655 syn::UseTree::Glob(_) => {}
656 }
657 }
658
659 impl<'ast> Visit<'ast> for ReexportVisitor<'_> {
660 fn visit_item_mod(&mut self, i: &'ast syn::ItemMod) {
661 if has_test_attr(&i.attrs) {
662 return;
663 }
664 syn::visit::visit_item_mod(self, i);
665 }
666
667 fn visit_item_use(&mut self, i: &'ast syn::ItemUse) {
668 if has_test_attr(&i.attrs) {
669 return;
670 }
671 if matches!(i.vis, syn::Visibility::Inherited) {
672 syn::visit::visit_item_use(self, i);
673 return;
674 }
675 handle_use_tree(&i.tree, None, self.workspace, &mut self.map);
676 syn::visit::visit_item_use(self, i);
677 }
678 }
679
680 let mut visitor = ReexportVisitor {
681 workspace,
682 map: HashMap::new(),
683 };
684
685 for file in files {
686 if has_test_attr(&file.attrs) {
687 continue;
688 }
689 visitor.visit_file(file);
690 }
691
692 visitor.map
693}
694fn package_source_dirs(package: &cargo_metadata::Package) -> HashSet<std::path::PathBuf> {
696 let manifest_dir = package.manifest_path.parent().unwrap();
697 let mut dirs = HashSet::new();
698 for target in &package.targets {
699 if target.kind.iter().any(|k| {
700 matches!(
701 k,
702 cargo_metadata::TargetKind::Lib | cargo_metadata::TargetKind::Bin
703 )
704 }) {
705 if let Some(parent) = std::path::Path::new(&target.src_path).parent() {
706 dirs.insert(parent.to_path_buf());
707 }
708 }
709 }
710 if dirs.is_empty() {
711 dirs.insert(manifest_dir.join("src").into());
712 }
713 dirs
714}
715
716fn parse_dir(dir: &std::path::Path) -> Result<Vec<File>, Box<dyn std::error::Error>> {
717 let mut files = Vec::new();
718 for entry in WalkDir::new(dir) {
719 let entry = crate::loc_try!(entry);
720 if entry.file_type().is_file()
721 && entry.path().extension().map(|s| s == "rs").unwrap_or(false)
722 {
723 if entry.path().components().any(|c| c.as_os_str() == "tests") {
724 continue;
725 }
726 debug!("parsing {}", entry.path().display());
727 let content = crate::loc_try!(fs::read_to_string(entry.path()));
728 let file = crate::loc_try!(syn::parse_file(&content));
729 files.push(file);
730 }
731 }
732 Ok(files)
733}
734
735pub fn parse_package(
743 package: &cargo_metadata::Package,
744) -> Result<Vec<File>, Box<dyn std::error::Error>> {
745 info!("reading crate {}", package.name);
746 let mut files = Vec::new();
747 for dir in package_source_dirs(package) {
748 files.extend(parse_dir(&dir)?);
749 }
750 Ok(files)
751}
752pub fn analyze_package(
758 package: &cargo_metadata::Package,
759 workspace_types: &HashSet<String>,
760) -> Result<Metrics, Box<dyn std::error::Error>> {
761 let files = crate::loc_try!(parse_package(package));
762 Ok(analyze_files(&files, workspace_types))
763}
764pub fn analyze_files(files: &[File], workspace_types: &HashSet<String>) -> Metrics {
769 debug!("collecting definitions from {} files", files.len());
770 let (defined, abstract_count) = collect_defined(files);
771
772 let class_count = defined.len();
773
774 let mut visitor = RefVisitor {
775 defined: &defined,
776 workspace: workspace_types,
777 internal: 0,
778 external: 0,
779 };
780 for file in files {
781 visitor.visit_file(file);
782 }
783
784 let n = class_count;
785 let r = visitor.internal;
786 let ca = 0usize; let ce = visitor.external;
788
789 let h = if n > 0 {
790 (r as f64 + 1.0) / n as f64
791 } else {
792 0.0
793 };
794 let a = if n > 0 {
795 abstract_count as f64 / n as f64
796 } else {
797 0.0
798 };
799 let i = if ca + ce > 0 {
800 ce as f64 / (ca + ce) as f64
801 } else {
802 0.0
803 };
804 let d_prime = (a + i - 1.0).abs();
805 let d = d_prime / 2f64.sqrt();
806
807 debug!(
808 "metrics: N={} R={} Ca={} Ce={} A={:.3} I={:.3} D={:.3} D'={:.3}",
809 n, r, ca, ce, a, i, d, d_prime
810 );
811
812 Metrics {
813 r,
814 n,
815 h,
816 ca,
817 ce,
818 a,
819 i,
820 d,
821 d_prime,
822 }
823}
824
825pub fn analyze_workspace(crates: &[(String, Vec<File>)]) -> HashMap<String, Metrics> {
830 analyze_workspace_with_thresholds(crates, &EvaluationThresholds::default())
831}
832
833pub fn analyze_workspace_with_thresholds(
837 crates: &[(String, Vec<File>)],
838 t: &EvaluationThresholds,
839) -> HashMap<String, Metrics> {
840 analyze_workspace_details_with_thresholds(crates, t)
841 .into_iter()
842 .map(|(k, v)| (k, v.metrics))
843 .collect()
844}
845
846pub fn analyze_workspace_details(crates: &[(String, Vec<File>)]) -> HashMap<String, CrateDetail> {
851 analyze_workspace_details_with_thresholds(crates, &EvaluationThresholds::default())
852}
853
854pub fn analyze_workspace_details_with_thresholds(
856 crates: &[(String, Vec<File>)],
857 t: &EvaluationThresholds,
858) -> HashMap<String, CrateDetail> {
859 debug!("analysing {} crates", crates.len());
860
861 let workspace_crates: HashSet<String> = crates.iter().map(|(name, _)| name.clone()).collect();
862
863 let mut crate_defined = HashMap::new();
864 let mut crate_abstract = HashMap::new();
865 let mut crate_reexports: HashMap<String, HashMap<String, (String, String)>> = HashMap::new();
866 let mut method_map: HashMap<(String, String), String> = HashMap::new();
867 let mut trait_bounds: HashMap<String, Vec<String>> = HashMap::new();
868
869 for (name, files) in crates {
870 let (defined, abstract_count) = collect_defined(files);
871 let methods = collect_methods(files);
872 let bounds = collect_trait_bounds(files);
873 let reexports = collect_reexports(files, &workspace_crates);
874 method_map.extend(methods);
875 for (k, v) in bounds {
876 trait_bounds.insert(k, v);
877 }
878 crate_defined.insert(name.clone(), defined);
879 crate_abstract.insert(name.clone(), abstract_count);
880 crate_reexports.insert(name.clone(), reexports);
881 }
882
883 let mut internal_refs: HashMap<String, HashMap<String, HashSet<String>>> = HashMap::new();
884 let mut external_refs: HashMap<String, HashMap<String, HashMap<String, HashSet<String>>>> =
885 HashMap::new();
886
887 for (name, files) in crates {
888 let defined = crate_defined.get(name).unwrap();
889 let mut visitor = DetailVisitor {
890 current: None,
891 defined,
892 crate_name: name,
893 workspace_crates: &workspace_crates,
894 all_defined: &crate_defined,
895 reexports: &crate_reexports,
896 imports: HashMap::new(),
897 internal: HashMap::new(),
898 external: HashMap::new(),
899 methods: &method_map,
900 trait_bounds: &trait_bounds,
901 };
902 for f in files {
903 visitor.visit_file(f);
904 }
905 internal_refs.insert(name.clone(), visitor.internal);
906 external_refs.insert(name.clone(), visitor.external);
907 }
908
909 let mut internal_rev: HashMap<String, HashMap<String, HashSet<String>>> = HashMap::new();
910 for (crate_name, map) in &internal_refs {
911 for (from, tos) in map {
912 for to in tos {
913 internal_rev
914 .entry(crate_name.clone())
915 .or_default()
916 .entry(to.clone())
917 .or_default()
918 .insert(from.clone());
919 }
920 }
921 }
922
923 let mut external_rev: HashMap<String, HashMap<String, HashMap<String, HashSet<String>>>> =
924 HashMap::new();
925 for (src_crate, map) in &external_refs {
926 for (from, crates_map) in map {
927 for (target_crate, types) in crates_map {
928 for ty in types {
929 external_rev
930 .entry(target_crate.clone())
931 .or_default()
932 .entry(ty.clone())
933 .or_default()
934 .entry(src_crate.clone())
935 .or_default()
936 .insert(from.clone());
937 }
938 }
939 }
940 }
941
942 let mut result = HashMap::new();
943 for (name, _) in crates {
944 let n = crate_defined.get(name).map(|s| s.len()).unwrap_or(0);
945 let r = internal_refs
946 .get(name)
947 .map(|m| m.values().map(|s| s.len()).sum())
948 .unwrap_or(0);
949
950 let mut ce_set = HashSet::new();
951 if let Some(map) = external_refs.get(name) {
952 for crate_map in map.values() {
953 for (c, types) in crate_map {
954 for ty in types {
955 ce_set.insert(format!("{}::{}", c, ty));
956 }
957 }
958 }
959 }
960 let ce = ce_set.len();
961
962 let mut ca_set = HashSet::new();
963 if let Some(map) = external_rev.get(name) {
964 for crate_map in map.values() {
965 for (c, from_set) in crate_map {
966 for src in from_set {
967 ca_set.insert(format!("{}::{}", c, src));
968 }
969 }
970 }
971 }
972 let ca = ca_set.len();
973
974 let a_count = *crate_abstract.get(name).unwrap_or(&0);
975 let h = if n > 0 {
976 (r as f64 + 1.0) / n as f64
977 } else {
978 0.0
979 };
980 let a = if n > 0 {
981 a_count as f64 / n as f64
982 } else {
983 0.0
984 };
985 let i = if ca + ce > 0 {
986 ce as f64 / (ca + ce) as f64
987 } else {
988 0.0
989 };
990 let d_prime = (a + i - 1.0).abs();
991 let d = d_prime / 2f64.sqrt();
992
993 let classes = crate_defined
994 .get(name)
995 .unwrap()
996 .iter()
997 .map(|(n, k)| ClassInfo {
998 name: n.clone(),
999 kind: k.clone(),
1000 })
1001 .collect();
1002
1003 let to_vec_map = |map: &HashMap<String, HashSet<String>>| {
1004 map.iter()
1005 .filter(|(k, _)| *k != DetailVisitor::ROOT_ITEM)
1006 .map(|(k, v)| {
1007 (
1008 k.clone(),
1009 v.iter()
1010 .filter(|t| *t != DetailVisitor::ROOT_ITEM)
1011 .cloned()
1012 .collect::<Vec<_>>(),
1013 )
1014 })
1015 .filter(|(_, v): &(_, Vec<String>)| !v.is_empty())
1016 .collect::<HashMap<_, _>>()
1017 };
1018
1019 let to_vec_nested = |map: &HashMap<String, HashMap<String, HashSet<String>>>| {
1020 map.iter()
1021 .filter(|(k, _)| *k != DetailVisitor::ROOT_ITEM)
1022 .map(|(k, v)| {
1023 (
1024 k.clone(),
1025 v.iter()
1026 .filter(|(k2, _)| *k2 != DetailVisitor::ROOT_ITEM)
1027 .map(|(k2, set)| {
1028 (
1029 k2.clone(),
1030 set.iter()
1031 .filter(|t| *t != DetailVisitor::ROOT_ITEM)
1032 .cloned()
1033 .collect::<Vec<_>>(),
1034 )
1035 })
1036 .filter(|(_, v)| !v.is_empty())
1037 .collect::<HashMap<_, _>>(),
1038 )
1039 })
1040 .filter(|(_, v)| !v.is_empty())
1041 .collect::<HashMap<_, _>>()
1042 };
1043
1044 result.insert(name.clone(), {
1045 let metrics = Metrics {
1046 r,
1047 n,
1048 h,
1049 ca,
1050 ce,
1051 a,
1052 i,
1053 d,
1054 d_prime,
1055 };
1056 CrateDetail {
1057 kind: CrateKind::Workspace,
1058 metrics: metrics.clone(),
1059 evaluation: evaluate_metrics_with(&metrics, t),
1060 classes,
1061 internal_depends_on: to_vec_map(internal_refs.get(name).unwrap_or(&HashMap::new())),
1062 internal_depended_by: to_vec_map(internal_rev.get(name).unwrap_or(&HashMap::new())),
1063 external_depends_on: to_vec_nested(
1064 external_refs.get(name).unwrap_or(&HashMap::new()),
1065 ),
1066 external_depended_by: to_vec_nested(
1067 external_rev.get(name).unwrap_or(&HashMap::new()),
1068 ),
1069 }
1070 });
1071 }
1072
1073 result
1074}
1075
1076pub fn dependency_cycles(details: &HashMap<String, CrateDetail>) -> Vec<Vec<String>> {
1082 let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
1084 for (name, detail) in details {
1085 let entry = graph.entry(name.clone()).or_default();
1086 for map in detail.external_depends_on.values() {
1087 for krate in map.keys() {
1088 entry.insert(krate.clone());
1089 }
1090 }
1091 }
1092
1093 let graph: HashMap<String, Vec<String>> = graph
1094 .into_iter()
1095 .map(|(k, v)| (k, v.into_iter().collect()))
1096 .collect();
1097
1098 #[allow(clippy::too_many_arguments)]
1099 fn strongconnect(
1100 v: &String,
1101 index: &mut usize,
1102 stack: &mut Vec<String>,
1103 indices: &mut HashMap<String, usize>,
1104 lowlink: &mut HashMap<String, usize>,
1105 on_stack: &mut HashSet<String>,
1106 graph: &HashMap<String, Vec<String>>,
1107 result: &mut Vec<Vec<String>>,
1108 ) {
1109 indices.insert(v.clone(), *index);
1110 lowlink.insert(v.clone(), *index);
1111 *index += 1;
1112 stack.push(v.clone());
1113 on_stack.insert(v.clone());
1114
1115 if let Some(neigh) = graph.get(v) {
1116 for w in neigh {
1117 if !indices.contains_key(w) {
1118 strongconnect(w, index, stack, indices, lowlink, on_stack, graph, result);
1119 let lw = *lowlink.get(w).unwrap();
1120 let lv = *lowlink.get(v).unwrap();
1121 if lw < lv {
1122 lowlink.insert(v.clone(), lw);
1123 }
1124 } else if on_stack.contains(w) {
1125 let iw = *indices.get(w).unwrap();
1126 let lv = *lowlink.get(v).unwrap();
1127 if iw < lv {
1128 lowlink.insert(v.clone(), iw);
1129 }
1130 }
1131 }
1132 }
1133
1134 if indices.get(v) == lowlink.get(v) {
1135 let mut scc = Vec::new();
1136 while let Some(w) = stack.pop() {
1137 on_stack.remove(&w);
1138 scc.push(w.clone());
1139 if &w == v {
1140 break;
1141 }
1142 }
1143 if scc.len() > 1 {
1144 scc.reverse();
1145 result.push(scc);
1146 }
1147 }
1148 }
1149
1150 let mut index = 0usize;
1151 let mut stack = Vec::new();
1152 let mut indices: HashMap<String, usize> = HashMap::new();
1153 let mut lowlink: HashMap<String, usize> = HashMap::new();
1154 let mut on_stack: HashSet<String> = HashSet::new();
1155 let mut result_vec = Vec::new();
1156
1157 for v in graph.keys() {
1158 if !indices.contains_key(v) {
1159 strongconnect(
1160 v,
1161 &mut index,
1162 &mut stack,
1163 &mut indices,
1164 &mut lowlink,
1165 &mut on_stack,
1166 &graph,
1167 &mut result_vec,
1168 );
1169 }
1170 }
1171
1172 result_vec
1173}
1174
1175struct RefVisitor<'a> {
1176 defined: &'a HashMap<String, ClassKind>,
1177 workspace: &'a HashSet<String>,
1178 internal: usize,
1179 external: usize,
1180}
1181
1182impl<'ast> Visit<'ast> for RefVisitor<'_> {
1183 fn visit_path(&mut self, node: &'ast syn::Path) {
1184 if let Some(seg) = node.segments.last() {
1185 let name = seg.ident.to_string();
1186 if self.defined.contains_key(&name) {
1187 self.internal += 1;
1188 } else if self.workspace.contains(&name) {
1189 self.external += 1;
1190 }
1191 }
1192 syn::visit::visit_path(self, node);
1193 }
1194}
1195
1196struct DetailVisitor<'a> {
1197 current: Option<String>,
1198 defined: &'a HashMap<String, ClassKind>,
1199 crate_name: &'a str,
1200 workspace_crates: &'a HashSet<String>,
1201 all_defined: &'a HashMap<String, HashMap<String, ClassKind>>,
1202 reexports: &'a HashMap<String, HashMap<String, (String, String)>>,
1203 imports: HashMap<String, (Option<String>, Option<String>)>,
1209 internal: HashMap<String, HashSet<String>>, external: HashMap<String, HashMap<String, HashSet<String>>>, methods: &'a HashMap<(String, String), String>,
1212 trait_bounds: &'a HashMap<String, Vec<String>>,
1213}
1214
1215impl<'ast> Visit<'ast> for DetailVisitor<'_> {
1216 fn visit_file(&mut self, i: &'ast syn::File) {
1217 if has_test_attr(&i.attrs) {
1218 return;
1219 }
1220 syn::visit::visit_file(self, i);
1221 }
1222 fn visit_item_struct(&mut self, i: &'ast syn::ItemStruct) {
1223 if has_test_attr(&i.attrs) {
1224 return;
1225 }
1226 let name = i.ident.to_string();
1227 self.current = Some(name);
1228 syn::visit::visit_item_struct(self, i);
1229 self.current = None;
1230 }
1231 fn visit_item_enum(&mut self, i: &'ast syn::ItemEnum) {
1232 if has_test_attr(&i.attrs) {
1233 return;
1234 }
1235 let name = i.ident.to_string();
1236 self.current = Some(name);
1237 syn::visit::visit_item_enum(self, i);
1238 self.current = None;
1239 }
1240 fn visit_item_trait(&mut self, i: &'ast syn::ItemTrait) {
1241 if has_test_attr(&i.attrs) {
1242 return;
1243 }
1244 let name = i.ident.to_string();
1245 self.current = Some(name);
1246 syn::visit::visit_item_trait(self, i);
1247 self.current = None;
1248 }
1249 fn visit_item_type(&mut self, i: &'ast syn::ItemType) {
1250 if has_test_attr(&i.attrs) {
1251 return;
1252 }
1253 let name = i.ident.to_string();
1254 self.current = Some(name);
1255 syn::visit::visit_item_type(self, i);
1256 self.current = None;
1257 }
1258 fn visit_item_const(&mut self, i: &'ast syn::ItemConst) {
1259 if has_test_attr(&i.attrs) {
1260 return;
1261 }
1262 let name = i.ident.to_string();
1263 self.current = Some(name);
1264 syn::visit::visit_item_const(self, i);
1265 self.current = None;
1266 }
1267 fn visit_item_static(&mut self, i: &'ast syn::ItemStatic) {
1268 if has_test_attr(&i.attrs) {
1269 return;
1270 }
1271 let name = i.ident.to_string();
1272 self.current = Some(name);
1273 syn::visit::visit_item_static(self, i);
1274 self.current = None;
1275 }
1276 fn visit_item_impl(&mut self, i: &'ast syn::ItemImpl) {
1277 if has_test_attr(&i.attrs) {
1278 return;
1279 }
1280 if let syn::Type::Path(tp) = &*i.self_ty {
1281 if let Some(seg) = tp.path.segments.last() {
1282 let name = seg.ident.to_string();
1283 if self.defined.contains_key(&name) {
1284 self.current = Some(name);
1285 syn::visit::visit_item_impl(self, i);
1286 self.current = None;
1287 return;
1288 }
1289 }
1290 }
1291 syn::visit::visit_item_impl(self, i);
1292 }
1293 fn visit_item_fn(&mut self, i: &'ast syn::ItemFn) {
1294 if has_test_attr(&i.attrs) {
1295 return;
1296 }
1297 let name = i.sig.ident.to_string();
1298 self.current = Some(name);
1299 syn::visit::visit_item_fn(self, i);
1300 self.current = None;
1301 }
1302 fn visit_item_mod(&mut self, i: &'ast syn::ItemMod) {
1303 if has_test_attr(&i.attrs) {
1304 return;
1305 }
1306 syn::visit::visit_item_mod(self, i);
1307 }
1308 fn visit_item_use(&mut self, i: &'ast syn::ItemUse) {
1309 fn handle(
1310 tree: &syn::UseTree,
1311 first: Option<String>,
1312 ws: &HashSet<String>,
1313 all_def: &HashMap<String, HashMap<String, ClassKind>>,
1314 map: &mut HashMap<String, (Option<String>, Option<String>)>,
1315 ) {
1316 match tree {
1317 syn::UseTree::Path(p) => {
1318 let root = first.clone().unwrap_or_else(|| p.ident.to_string());
1319 handle(&p.tree, Some(root), ws, all_def, map);
1320 }
1321 syn::UseTree::Name(n) => {
1322 if let Some(r) = &first {
1328 if ws.contains(r) {
1329 map.insert(
1330 n.ident.to_string(),
1331 (Some(r.clone()), Some(n.ident.to_string())),
1332 );
1333 } else {
1334 map.insert(n.ident.to_string(), (None, None));
1335 }
1336 }
1337 }
1338 syn::UseTree::Rename(rn) => {
1339 let root = first
1340 .as_ref()
1341 .cloned()
1342 .unwrap_or_else(|| rn.ident.to_string());
1343 if ws.contains(&root) {
1344 map.insert(
1345 rn.rename.to_string(),
1346 (Some(root), Some(rn.ident.to_string())),
1347 );
1348 } else {
1349 map.insert(rn.rename.to_string(), (None, None));
1350 }
1351 }
1352 syn::UseTree::Group(g) => {
1353 for t in &g.items {
1354 handle(t, first.clone(), ws, all_def, map);
1355 }
1356 }
1357 syn::UseTree::Glob(_) => {
1358 if let Some(r) = &first {
1359 if ws.contains(r) {
1360 if let Some(defs) = all_def.get(r) {
1361 for name in defs.keys() {
1362 map.insert(name.clone(), (Some(r.clone()), Some(name.clone())));
1363 }
1364 }
1365 }
1366 }
1367 }
1368 }
1369 }
1370
1371 handle(
1372 &i.tree,
1373 None,
1374 self.workspace_crates,
1375 self.all_defined,
1376 &mut self.imports,
1377 );
1378 syn::visit::visit_item_use(self, i);
1379 }
1380 fn visit_path(&mut self, node: &'ast syn::Path) {
1381 if let Some(seg) = node.segments.last() {
1382 let name = seg.ident.to_string();
1383 let root = self.path_root(node);
1384 self.record_use(name, root);
1385 }
1386 syn::visit::visit_path(self, node);
1387 }
1388
1389 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
1390 if let syn::Expr::Path(p) = &*node.func {
1391 if p.path.segments.len() >= 2 {
1392 let func = p.path.segments.last().unwrap().ident.to_string();
1393 let ty = p.path.segments[p.path.segments.len() - 2].ident.to_string();
1394 let root = self.path_root(&p.path);
1395 self.record_use(ty.clone(), root.clone());
1396 let _ = self.methods.get(&(ty, func));
1397 } else if let Some(seg) = p.path.segments.last() {
1398 let name = seg.ident.to_string();
1399 let root = self.path_root(&p.path);
1400 self.record_use(name, root);
1401 }
1402 }
1403 syn::visit::visit_expr_call(self, node);
1404 }
1405
1406 fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
1407 if let Some(seg) = node.mac.path.segments.last() {
1408 let name = seg.ident.to_string();
1409 let root = self.path_root(&node.mac.path);
1410 self.record_use(name, root);
1411 }
1412 syn::visit::visit_expr_macro(self, node);
1413 }
1414
1415 fn visit_item_macro(&mut self, i: &'ast syn::ItemMacro) {
1416 if i.ident.is_none() {
1417 if let Some(seg) = i.mac.path.segments.last() {
1418 let name = seg.ident.to_string();
1419 let root = self.path_root(&i.mac.path);
1420 self.record_use(name, root);
1421 }
1422 }
1423 syn::visit::visit_item_macro(self, i);
1424 }
1425
1426 fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
1427 if let Some((receiver_ty, root)) = self.infer_expr_type(&node.receiver) {
1428 self.record_use(receiver_ty, root);
1429 }
1430 syn::visit::visit_expr_method_call(self, node);
1431 }
1432}
1433
1434impl<'a> DetailVisitor<'a> {
1435 fn path_root(&self, path: &syn::Path) -> Option<String> {
1436 if let Some(first) = path.segments.first() {
1437 let ident = first.ident.to_string();
1438 match ident.as_str() {
1439 "crate" | "self" | "super" => Some(self.crate_name.to_string()),
1440 _ => {
1441 if self.workspace_crates.contains(&ident) {
1442 Some(ident)
1443 } else if let Some((Some(root), _)) = self.imports.get(&ident) {
1444 Some(root.clone())
1445 } else {
1446 None
1447 }
1448 }
1449 }
1450 } else {
1451 None
1452 }
1453 }
1454
1455 const ROOT_ITEM: &'static str = "__crate_root";
1456
1457 fn record_use(&mut self, name: String, root: Option<String>) {
1458 let current = self
1459 .current
1460 .clone()
1461 .unwrap_or_else(|| Self::ROOT_ITEM.to_string());
1462
1463 if name == current {
1464 return;
1465 }
1466
1467 match root {
1468 Some(ref r) if r == self.crate_name => {
1469 if self.defined.contains_key(&name) {
1470 self.internal
1471 .entry(current.clone())
1472 .or_default()
1473 .insert(name);
1474 } else if let Some((target_crate, target_name)) = self
1475 .reexports
1476 .get(self.crate_name)
1477 .and_then(|m| m.get(&name))
1478 .cloned()
1479 {
1480 if self.workspace_crates.contains(&target_crate) {
1481 self.external
1482 .entry(current.clone())
1483 .or_default()
1484 .entry(target_crate)
1485 .or_default()
1486 .insert(target_name);
1487 }
1488 }
1489 }
1490 Some(ref r) => {
1491 if self.workspace_crates.contains(r) {
1492 let lookup = if let Some((_, Some(orig))) = self.imports.get(&name) {
1493 orig
1494 } else {
1495 &name
1496 };
1497 if self
1498 .all_defined
1499 .get(r)
1500 .is_some_and(|d| d.contains_key(lookup))
1501 {
1502 self.external
1503 .entry(current.clone())
1504 .or_default()
1505 .entry(r.clone())
1506 .or_default()
1507 .insert(lookup.to_string());
1508 } else if let Some((target_crate, target_name)) =
1509 self.reexports.get(r).and_then(|m| m.get(lookup)).cloned()
1510 {
1511 if self.workspace_crates.contains(&target_crate) {
1512 self.external
1513 .entry(current.clone())
1514 .or_default()
1515 .entry(target_crate)
1516 .or_default()
1517 .insert(target_name);
1518 }
1519 }
1520 }
1521 }
1522 None => {
1523 if self.defined.contains_key(&name) {
1524 self.internal
1525 .entry(current.clone())
1526 .or_default()
1527 .insert(name);
1528 } else if let Some((Some(import_root), orig)) = self.imports.get(&name).cloned() {
1529 let lookup = orig.unwrap_or(name.clone());
1530 if self
1531 .all_defined
1532 .get(&import_root)
1533 .is_some_and(|d| d.contains_key(&lookup))
1534 {
1535 self.external
1536 .entry(current.clone())
1537 .or_default()
1538 .entry(import_root.clone())
1539 .or_default()
1540 .insert(lookup);
1541 } else if let Some((target_crate, target_name)) = self
1542 .reexports
1543 .get(&import_root)
1544 .and_then(|m| m.get(&lookup))
1545 .cloned()
1546 {
1547 if self.workspace_crates.contains(&target_crate) {
1548 self.external
1549 .entry(current.clone())
1550 .or_default()
1551 .entry(target_crate)
1552 .or_default()
1553 .insert(target_name);
1554 }
1555 }
1556 }
1557 }
1558 }
1559 }
1560 fn infer_from_call(&self, call: &syn::ExprCall) -> Option<(String, Option<String>)> {
1561 if let syn::Expr::Path(p) = &*call.func {
1562 if p.path.segments.len() >= 2 {
1563 let func = p.path.segments.last().unwrap().ident.to_string();
1564 let ty = p.path.segments[p.path.segments.len() - 2].ident.to_string();
1565 if let Some(ret) = self.methods.get(&(ty.clone(), func.clone())) {
1566 let root = self.path_root(&p.path);
1567 return Some((ret.clone(), root));
1568 }
1569 }
1570 if let Some(seg) = p.path.segments.last() {
1571 let name = seg.ident.to_string();
1572 if self.defined.contains_key(&name)
1573 || self.all_defined.values().any(|d| d.contains_key(&name))
1574 {
1575 let root = self.path_root(&p.path);
1576 return Some((name, root));
1577 }
1578 }
1579 }
1580 None
1581 }
1582
1583 fn infer_from_method_call(&self, mc: &syn::ExprMethodCall) -> Option<(String, Option<String>)> {
1584 if let Some((receiver_ty, root)) = self.infer_expr_type(&mc.receiver) {
1585 if let Some(ret) = self
1586 .methods
1587 .get(&(receiver_ty.clone(), mc.method.to_string()))
1588 {
1589 return Some((ret.clone(), root));
1590 }
1591 if let Some(bounds) = self.trait_bounds.get(&receiver_ty) {
1592 let mut found = None;
1593 for b in bounds {
1594 if let Some(ret) = self.methods.get(&(b.clone(), mc.method.to_string())) {
1595 if found.is_some() {
1596 return None;
1597 }
1598 found = Some(ret.clone());
1599 }
1600 }
1601 if let Some(ret) = found {
1602 return Some((ret, None));
1603 }
1604 }
1605 return Some((receiver_ty, root));
1606 }
1607 let mut ret = None;
1608 for ((_, name), r) in self.methods.iter() {
1609 if name == &mc.method.to_string() {
1610 if ret.is_some() {
1611 return None;
1612 }
1613 ret = Some(r.clone());
1614 }
1615 }
1616 ret.map(|r| (r, None))
1617 }
1618
1619 fn infer_from_path(&self, p: &syn::ExprPath) -> Option<(String, Option<String>)> {
1620 if p.path.segments.len() == 1 && p.path.segments[0].ident == "self" {
1621 return self
1622 .current
1623 .as_ref()
1624 .map(|c| (c.clone(), Some(self.crate_name.to_string())));
1625 }
1626 if let Some(seg) = p.path.segments.last() {
1627 let name = seg.ident.to_string();
1628 if self.defined.contains_key(&name)
1629 || self.all_defined.values().any(|d| d.contains_key(&name))
1630 {
1631 let root = self.path_root(&p.path);
1632 return Some((name, root));
1633 }
1634 }
1635 None
1636 }
1637
1638 fn infer_expr_type(&self, expr: &syn::Expr) -> Option<(String, Option<String>)> {
1639 match expr {
1640 syn::Expr::Call(call) => self.infer_from_call(call),
1641 syn::Expr::MethodCall(mc) => self.infer_from_method_call(mc),
1642 syn::Expr::Path(p) => self.infer_from_path(p),
1643 _ => None,
1644 }
1645 }
1646}
1647
1648#[cfg(test)]
1649mod tests {
1650 use super::*;
1651
1652 #[test]
1653 fn simple_metrics() {
1654 let src = r#"
1655 pub struct A {
1656 b: B,
1657 map: std::collections::HashMap<String, String>,
1658 }
1659 pub struct B;
1660 pub trait MyTrait {
1661 fn do_it(&self, a: A);
1662 }
1663 "#;
1664 let file: syn::File = syn::parse_str(src).unwrap();
1665 let defs = collect_defined(&[file.clone()]);
1666 let workspace: HashSet<String> = defs.0.keys().cloned().collect();
1667 let metrics = analyze_files(&[file], &workspace);
1668 assert_eq!(metrics.n, 3);
1669 assert_eq!(metrics.ca, 0);
1670 assert_eq!(metrics.ce, 0); assert!((metrics.h - ((2.0 + 1.0) / 3.0)).abs() < 1e-6);
1672 assert!((metrics.a - (1.0 / 3.0)).abs() < 1e-6);
1673 assert!(metrics.i.abs() < 1e-6);
1675 }
1676
1677 #[test]
1678 fn cross_crate_metrics() {
1679 let src_a = "pub struct A;";
1680 let src_b = "pub struct B { a: crate_a::A }";
1681 let file_a: syn::File = syn::parse_str(src_a).unwrap();
1682 let file_b: syn::File = syn::parse_str(src_b).unwrap();
1683
1684 let crates = vec![
1685 ("crate_a".to_string(), vec![file_a.clone()]),
1686 ("crate_b".to_string(), vec![file_b.clone()]),
1687 ];
1688 let info = analyze_workspace_details(&crates);
1689 let a_info = info.get("crate_a").unwrap();
1690 let b_info = info.get("crate_b").unwrap();
1691
1692 assert_eq!(a_info.metrics.ca, 1);
1693 assert_eq!(a_info.metrics.ce, 0);
1694 assert_eq!(b_info.metrics.ce, 1);
1695
1696 assert_eq!(
1697 b_info
1698 .external_depends_on
1699 .get("B")
1700 .and_then(|m| m.get("crate_a"))
1701 .map(|v| v.contains(&"A".to_string()))
1702 .unwrap_or(false),
1703 true
1704 );
1705 assert_eq!(
1706 a_info
1707 .external_depended_by
1708 .get("A")
1709 .and_then(|m| m.get("crate_b"))
1710 .map(|v| v.contains(&"B".to_string()))
1711 .unwrap_or(false),
1712 true
1713 );
1714 }
1715
1716 #[test]
1717 fn detailed_info() {
1718 let src_a = "pub struct A;";
1719 let src_b = "pub struct B { a: crate_a::A }";
1720 let file_a: syn::File = syn::parse_str(src_a).unwrap();
1721 let file_b: syn::File = syn::parse_str(src_b).unwrap();
1722
1723 let crates = vec![
1724 ("crate_a".to_string(), vec![file_a.clone()]),
1725 ("crate_b".to_string(), vec![file_b.clone()]),
1726 ];
1727 let info = analyze_workspace_details(&crates);
1728 let a_info = info.get("crate_a").unwrap();
1729 assert_eq!(a_info.classes.len(), 1);
1730 assert_eq!(a_info.classes[0].name, "A");
1731 assert!(a_info
1732 .external_depended_by
1733 .get("A")
1734 .and_then(|m| m.get("crate_b"))
1735 .map(|v| v.contains(&"B".to_string()))
1736 .unwrap_or(false));
1737 }
1738
1739 #[test]
1740 fn trait_bound_dependency() {
1741 let src_a = "pub trait Foo {}";
1742 let src_b = "use crate_a::Foo; pub struct Bar<U: Foo>(U);";
1743
1744 let file_a: syn::File = syn::parse_str(src_a).unwrap();
1745 let file_b: syn::File = syn::parse_str(src_b).unwrap();
1746
1747 let crates = vec![
1748 ("crate_a".to_string(), vec![file_a.clone()]),
1749 ("crate_b".to_string(), vec![file_b.clone()]),
1750 ];
1751
1752 let info = analyze_workspace_details(&crates);
1753 let a_info = info.get("crate_a").unwrap();
1754 let b_info = info.get("crate_b").unwrap();
1755
1756 assert_eq!(a_info.metrics.ca, 1);
1757 assert_eq!(b_info.metrics.ce, 1);
1758
1759 assert!(b_info
1760 .external_depends_on
1761 .get("Bar")
1762 .and_then(|m| m.get("crate_a"))
1763 .map(|v| v.contains(&"Foo".to_string()))
1764 .unwrap_or(false));
1765 assert!(a_info
1766 .external_depended_by
1767 .get("Foo")
1768 .and_then(|m| m.get("crate_b"))
1769 .map(|v| v.contains(&"Bar".to_string()))
1770 .unwrap_or(false));
1771 }
1772
1773 #[test]
1774 fn reexported_dependency_counts_as_external() {
1775 let src_infra = r#"
1776 pub mod repo {
1777 pub struct HashDB;
1778 }
1779 "#;
1780 let src_sample = r#"
1781 pub mod repository {
1782 pub use infra::repo::HashDB;
1783 }
1784 "#;
1785 let src_app = r#"
1786 use sample::repository::HashDB;
1787
1788 pub struct App(HashDB);
1789 "#;
1790
1791 let file_infra: syn::File = syn::parse_str(src_infra).unwrap();
1792 let file_sample: syn::File = syn::parse_str(src_sample).unwrap();
1793 let file_app: syn::File = syn::parse_str(src_app).unwrap();
1794
1795 let crates = vec![
1796 ("infra".to_string(), vec![file_infra.clone()]),
1797 ("sample".to_string(), vec![file_sample.clone()]),
1798 ("app".to_string(), vec![file_app.clone()]),
1799 ];
1800
1801 let info = analyze_workspace_details(&crates);
1802 let app_info = info.get("app").unwrap();
1803
1804 assert_eq!(app_info.metrics.ce, 1);
1805 assert!(app_info
1806 .external_depends_on
1807 .get("App")
1808 .and_then(|m| m.get("infra"))
1809 .map(|v| v.contains(&"HashDB".to_string()))
1810 .unwrap_or(false));
1811 }
1812
1813 #[test]
1814 fn unique_counts() {
1815 let src_a = "pub struct A;";
1816 let src_b = "pub struct B { a1: crate_a::A, a2: crate_a::A }";
1817 let file_a: syn::File = syn::parse_str(src_a).unwrap();
1818 let file_b: syn::File = syn::parse_str(src_b).unwrap();
1819
1820 let crates = vec![
1821 ("crate_a".to_string(), vec![file_a.clone()]),
1822 ("crate_b".to_string(), vec![file_b.clone()]),
1823 ];
1824 let info = analyze_workspace_details(&crates);
1825 let a_info = info.get("crate_a").unwrap();
1826 let b_info = info.get("crate_b").unwrap();
1827
1828 assert_eq!(b_info.metrics.ce, 1);
1829 assert_eq!(a_info.metrics.ca, 1);
1830
1831 assert!(b_info
1832 .external_depends_on
1833 .get("B")
1834 .and_then(|m| m.get("crate_a"))
1835 .map(|v| v.len() == 1 && v.contains(&"A".to_string()))
1836 .unwrap_or(false));
1837 assert!(a_info
1838 .external_depended_by
1839 .get("A")
1840 .and_then(|m| m.get("crate_b"))
1841 .map(|v| v.len() == 1 && v.contains(&"B".to_string()))
1842 .unwrap_or(false));
1843 }
1844
1845 #[test]
1846 fn same_name_internal_external_dependency() {
1847 let src_a = "pub struct Foo;";
1848 let src_b = "pub struct Foo; pub struct Bar { ext: crate_a::Foo, int: Foo }";
1849
1850 let file_a: syn::File = syn::parse_str(src_a).unwrap();
1851 let file_b: syn::File = syn::parse_str(src_b).unwrap();
1852
1853 let crates = vec![
1854 ("crate_a".to_string(), vec![file_a.clone()]),
1855 ("crate_b".to_string(), vec![file_b.clone()]),
1856 ];
1857
1858 let info = analyze_workspace_details(&crates);
1859 let b_info = info.get("crate_b").unwrap();
1860
1861 assert!(b_info
1862 .internal_depends_on
1863 .get("Bar")
1864 .map(|v| v.contains(&"Foo".to_string()))
1865 .unwrap_or(false));
1866 assert!(b_info
1867 .external_depends_on
1868 .get("Bar")
1869 .and_then(|m| m.get("crate_a"))
1870 .map(|v| v.contains(&"Foo".to_string()))
1871 .unwrap_or(false));
1872 }
1873
1874 #[test]
1875 fn method_call_dependency() {
1876 let src_a = r#"
1877 pub struct Dao;
1878 impl Dao {
1879 pub fn new() -> Self { Dao }
1880 pub fn delete(&self) {}
1881 }
1882 "#;
1883 let src_b = r#"
1884 pub struct Use;
1885 impl Use {
1886 pub fn run() {
1887 crate_a::Dao::new().delete();
1888 }
1889 }
1890 "#;
1891
1892 let file_a: syn::File = syn::parse_str(src_a).unwrap();
1893 let file_b: syn::File = syn::parse_str(src_b).unwrap();
1894
1895 let crates = vec![
1896 ("crate_a".to_string(), vec![file_a.clone()]),
1897 ("crate_b".to_string(), vec![file_b.clone()]),
1898 ];
1899
1900 let info = analyze_workspace_details(&crates);
1901 let a_info = info.get("crate_a").unwrap();
1902 let b_info = info.get("crate_b").unwrap();
1903
1904 assert_eq!(b_info.metrics.ce, 1);
1905 assert_eq!(a_info.metrics.ca, 1);
1906
1907 assert!(b_info
1908 .external_depends_on
1909 .get("Use")
1910 .and_then(|m| m.get("crate_a"))
1911 .map(|v| v.contains(&"Dao".to_string()))
1912 .unwrap_or(false));
1913 assert!(a_info
1914 .external_depended_by
1915 .get("Dao")
1916 .and_then(|m| m.get("crate_b"))
1917 .map(|v| v.contains(&"Use".to_string()))
1918 .unwrap_or(false));
1919 }
1920
1921 #[test]
1922 fn chained_method_call_dependency() {
1923 let src_a = r#"
1924 pub struct Dao;
1925 pub trait HaveDao {
1926 fn dao(&self) -> Dao;
1927 }
1928 impl Dao {
1929 pub fn delete(&self) {}
1930 }
1931 "#;
1932 let src_b = r#"
1933 use crate_a::{Dao, HaveDao};
1934 pub struct Use<D: HaveDao> { inner: D }
1935 impl<D: HaveDao> Use<D> {
1936 pub fn run(&self) {
1937 self.inner.dao().delete();
1938 }
1939 }
1940 "#;
1941
1942 let file_a: syn::File = syn::parse_str(src_a).unwrap();
1943 let file_b: syn::File = syn::parse_str(src_b).unwrap();
1944
1945 let crates = vec![
1946 ("crate_a".to_string(), vec![file_a.clone()]),
1947 ("crate_b".to_string(), vec![file_b.clone()]),
1948 ];
1949
1950 let info = analyze_workspace_details(&crates);
1951 let a_info = info.get("crate_a").unwrap();
1952 let b_info = info.get("crate_b").unwrap();
1953
1954 assert_eq!(b_info.metrics.ce, 2);
1955 assert_eq!(b_info.metrics.ca, 0);
1956 assert_eq!(a_info.metrics.ca, 1);
1957
1958 let b_deps = b_info
1959 .external_depends_on
1960 .get("Use")
1961 .and_then(|m| m.get("crate_a"))
1962 .cloned()
1963 .unwrap_or_default();
1964 assert!(b_deps.contains(&"Dao".to_string()));
1965 assert!(b_deps.contains(&"HaveDao".to_string()));
1966
1967 assert!(a_info
1968 .external_depended_by
1969 .get("Dao")
1970 .and_then(|m| m.get("crate_b"))
1971 .map(|v| v.contains(&"Use".to_string()))
1972 .unwrap_or(false));
1973 assert!(a_info
1974 .external_depended_by
1975 .get("HaveDao")
1976 .and_then(|m| m.get("crate_b"))
1977 .map(|v| v.contains(&"Use".to_string()))
1978 .unwrap_or(false));
1979 }
1980
1981 #[test]
1982 fn dyn_trait_return() {
1983 let src_a = r#"
1984 pub trait Dao { fn delete(&self); }
1985 pub trait HaveDao { fn dao(&self) -> Box<dyn Dao>; }
1986 "#;
1987 let src_b = r#"
1988 use crate_a::{Dao, HaveDao};
1989 pub struct Use<D: HaveDao> { inner: D }
1990 impl<D: HaveDao> Use<D> {
1991 pub fn run(&self) {
1992 self.inner.dao().delete();
1993 }
1994 }
1995 "#;
1996
1997 let file_a: syn::File = syn::parse_str(src_a).unwrap();
1998 let file_b: syn::File = syn::parse_str(src_b).unwrap();
1999
2000 let crates = vec![
2001 ("crate_a".to_string(), vec![file_a.clone()]),
2002 ("crate_b".to_string(), vec![file_b.clone()]),
2003 ];
2004
2005 let info = analyze_workspace_details(&crates);
2006 let a_info = info.get("crate_a").unwrap();
2007 let b_info = info.get("crate_b").unwrap();
2008
2009 assert_eq!(b_info.metrics.ce, 2);
2010 assert_eq!(a_info.metrics.ca, 1);
2011
2012 let b_deps = b_info
2013 .external_depends_on
2014 .get("Use")
2015 .and_then(|m| m.get("crate_a"))
2016 .cloned()
2017 .unwrap_or_default();
2018 assert!(b_deps.contains(&"Dao".to_string()));
2019 assert!(b_deps.contains(&"HaveDao".to_string()));
2020
2021 assert!(a_info
2022 .external_depended_by
2023 .get("Dao")
2024 .and_then(|m| m.get("crate_b"))
2025 .map(|v| v.contains(&"Use".to_string()))
2026 .unwrap_or(false));
2027 }
2028
2029 #[test]
2030 fn ignore_non_workspace_crate() {
2031 let src_a = "pub struct Tx;";
2032 let src_b = "use tx_rs::Tx; pub struct Use { t: Tx }";
2033 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2034 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2035
2036 let crates = vec![
2037 ("crate_a".to_string(), vec![file_a.clone()]),
2038 ("crate_b".to_string(), vec![file_b.clone()]),
2039 ];
2040
2041 let info = analyze_workspace_details(&crates);
2042 let a_info = info.get("crate_a").unwrap();
2043 let b_info = info.get("crate_b").unwrap();
2044
2045 assert_eq!(b_info.metrics.ce, 0);
2046 assert_eq!(a_info.metrics.ca, 0);
2047 assert!(b_info.external_depends_on.is_empty());
2048 assert!(a_info.external_depended_by.is_empty());
2049 }
2050
2051 #[test]
2052 fn struct_usage_in_trait() {
2053 let src_a = r#"
2054 pub struct Paycheck;
2055 impl Paycheck { pub fn new() -> Self { Paycheck } }
2056 "#;
2057 let src_c = "pub struct Paycheck;";
2058 let src_b = r#"
2059 use crate_a::Paycheck;
2060 pub trait Payday {
2061 fn run(&self) {
2062 self.run_tx(|_| {
2063 let _ = Paycheck::new();
2064 });
2065 }
2066 fn run_tx<F>(&self, f: F) where F: FnOnce(i32) {}
2067 }
2068 "#;
2069
2070 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2071 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2072 let file_c: syn::File = syn::parse_str(src_c).unwrap();
2073
2074 let crates = vec![
2075 ("crate_a".to_string(), vec![file_a.clone()]),
2076 ("crate_b".to_string(), vec![file_b.clone()]),
2077 ("crate_c".to_string(), vec![file_c.clone()]),
2078 ];
2079
2080 let info = analyze_workspace_details(&crates);
2081 let a_info = info.get("crate_a").unwrap();
2082 let b_info = info.get("crate_b").unwrap();
2083
2084 assert!(b_info
2085 .external_depends_on
2086 .get("Payday")
2087 .and_then(|m| m.get("crate_a"))
2088 .map(|v| v.contains(&"Paycheck".to_string()))
2089 .unwrap_or(false));
2090 assert!(a_info
2091 .external_depended_by
2092 .get("Paycheck")
2093 .and_then(|m| m.get("crate_b"))
2094 .map(|v| v.contains(&"Payday".to_string()))
2095 .unwrap_or(false));
2096 }
2097 #[test]
2098 fn r_counts_unique_edges() {
2099 let src = "pub struct B; pub struct A { b1: B, b2: B }";
2100 let file: syn::File = syn::parse_str(src).unwrap();
2101 let crates = vec![("crate_a".to_string(), vec![file.clone()])];
2102 let info = analyze_workspace_details(&crates);
2103 let a = info.get("crate_a").unwrap();
2104 assert_eq!(a.metrics.r, 1);
2105 let deps = a.internal_depends_on.get("A").cloned().unwrap_or_default();
2106 assert_eq!(deps.len(), 1);
2107 assert!(deps.contains(&"B".to_string()));
2108 }
2109
2110 #[test]
2111 fn r_multiple_edges() {
2112 let src = "pub struct B; pub struct C { b1: B, b2: B } pub struct A { b: B, c: C }";
2113 let file: syn::File = syn::parse_str(src).unwrap();
2114 let crates = vec![("crate_a".to_string(), vec![file.clone()])];
2115 let info = analyze_workspace_details(&crates);
2116 let a = info.get("crate_a").unwrap();
2117 assert_eq!(a.metrics.r, 3);
2118 let a_deps = a.internal_depends_on.get("A").cloned().unwrap_or_default();
2119 let c_deps = a.internal_depends_on.get("C").cloned().unwrap_or_default();
2120 assert!(a_deps.contains(&"B".to_string()));
2121 assert!(c_deps.contains(&"B".to_string()));
2122 }
2123
2124 #[test]
2125 fn r_counts_method_body() {
2126 let src = "pub struct B; pub struct A; impl A { fn make() -> B { B } }";
2127 let file: syn::File = syn::parse_str(src).unwrap();
2128 let crates = vec![("crate_a".to_string(), vec![file.clone()])];
2129 let info = analyze_workspace_details(&crates);
2130 let a = info.get("crate_a").unwrap();
2131 assert_eq!(a.metrics.r, 1);
2132 let deps = a.internal_depends_on.get("A").cloned().unwrap_or_default();
2133 assert_eq!(deps.len(), 1);
2134 assert!(deps.contains(&"B".to_string()));
2135 }
2136
2137 #[test]
2138 fn free_function_dependency() {
2139 let src_a = "pub struct Helper;";
2140 let src_b = "use crate_a::Helper; fn main() { let _ = Helper; }";
2141 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2142 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2143
2144 let crates = vec![
2145 ("crate_a".to_string(), vec![file_a.clone()]),
2146 ("crate_b".to_string(), vec![file_b.clone()]),
2147 ];
2148
2149 let info = analyze_workspace_details(&crates);
2150 let a_info = info.get("crate_a").unwrap();
2151 let b_info = info.get("crate_b").unwrap();
2152
2153 assert_eq!(b_info.metrics.ce, 1);
2154 assert_eq!(a_info.metrics.ca, 1);
2155
2156 assert!(b_info
2157 .external_depends_on
2158 .get("main")
2159 .and_then(|m| m.get("crate_a"))
2160 .map(|v| v.contains(&"Helper".to_string()))
2161 .unwrap_or(false));
2162 assert!(a_info
2163 .external_depended_by
2164 .get("Helper")
2165 .and_then(|m| m.get("crate_b"))
2166 .map(|v| v.contains(&"main".to_string()))
2167 .unwrap_or(false));
2168 }
2169
2170 #[test]
2171 fn async_function_dependency() {
2172 let src_a = "pub struct Helper;";
2173 let src_b = "use crate_a::Helper; async fn run() { let _ = Helper; }";
2174 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2175 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2176
2177 let crates = vec![
2178 ("crate_a".to_string(), vec![file_a.clone()]),
2179 ("crate_b".to_string(), vec![file_b.clone()]),
2180 ];
2181
2182 let info = analyze_workspace_details(&crates);
2183 let a_info = info.get("crate_a").unwrap();
2184 let b_info = info.get("crate_b").unwrap();
2185
2186 assert_eq!(b_info.metrics.ce, 1);
2187 assert_eq!(a_info.metrics.ca, 1);
2188
2189 assert!(b_info
2190 .external_depends_on
2191 .get("run")
2192 .and_then(|m| m.get("crate_a"))
2193 .map(|v| v.contains(&"Helper".to_string()))
2194 .unwrap_or(false));
2195 assert!(a_info
2196 .external_depended_by
2197 .get("Helper")
2198 .and_then(|m| m.get("crate_b"))
2199 .map(|v| v.contains(&"run".to_string()))
2200 .unwrap_or(false));
2201 }
2202
2203 #[test]
2204 fn module_metrics() {
2205 let root = r#"
2206 mod foo;
2207 pub mod bar;
2208
2209 pub struct Root {
2210 f: foo::Foo,
2211 b: bar::Bar,
2212 }
2213 "#;
2214 let foo = "pub struct Foo;";
2215 let bar = "pub struct Bar;";
2216
2217 let file_root: syn::File = syn::parse_str(root).unwrap();
2218 let file_foo: syn::File = syn::parse_str(foo).unwrap();
2219 let file_bar: syn::File = syn::parse_str(bar).unwrap();
2220
2221 let defs = collect_defined(&[file_root.clone(), file_foo.clone(), file_bar.clone()]);
2222 let workspace: HashSet<String> = defs.0.keys().cloned().collect();
2223 let metrics = analyze_files(&[file_root, file_foo, file_bar], &workspace);
2224
2225 assert_eq!(metrics.n, 3);
2226 assert_eq!(metrics.r, 2);
2227 assert_eq!(metrics.ce, 0);
2228 assert_eq!(metrics.ca, 0);
2229 }
2230
2231 #[test]
2232 fn inline_module_metrics() {
2233 let src = r#"
2234 mod foo {
2235 pub struct Foo;
2236 }
2237 pub mod bar {
2238 pub struct Bar;
2239 }
2240
2241 pub struct Root {
2242 f: foo::Foo,
2243 b: bar::Bar,
2244 }
2245 "#;
2246
2247 let file: syn::File = syn::parse_str(src).unwrap();
2248 let defs = collect_defined(&[file.clone()]);
2249 let workspace: HashSet<String> = defs.0.keys().cloned().collect();
2250 let metrics = analyze_files(&[file], &workspace);
2251
2252 assert_eq!(metrics.n, 3);
2253 assert_eq!(metrics.r, 2);
2254 assert_eq!(metrics.ce, 0);
2255 assert_eq!(metrics.ca, 0);
2256 }
2257
2258 #[test]
2259 fn macro_dependencies() {
2260 let src_a = r#"
2261 #[macro_export]
2262 macro_rules! my_macro {
2263 () => {};
2264 }
2265 "#;
2266 let src_b = r#"
2267 pub struct Use;
2268 impl Use {
2269 pub fn run() {
2270 crate_a::my_macro!();
2271 }
2272 }
2273 "#;
2274
2275 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2276 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2277
2278 let crates = vec![
2279 ("crate_a".to_string(), vec![file_a.clone()]),
2280 ("crate_b".to_string(), vec![file_b.clone()]),
2281 ];
2282
2283 let info = analyze_workspace_details(&crates);
2284 let a_info = info.get("crate_a").unwrap();
2285 let b_info = info.get("crate_b").unwrap();
2286
2287 assert_eq!(b_info.metrics.ce, 1);
2288 assert_eq!(a_info.metrics.ca, 1);
2289 }
2290
2291 #[test]
2292 fn glob_import_dependency() {
2293 let src_a = r#"
2294 pub struct Foo;
2295 "#;
2296 let src_b = r#"
2297 use crate_a::*;
2298 pub struct Use(Foo);
2299 "#;
2300
2301 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2302 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2303
2304 let crates = vec![
2305 ("crate_a".to_string(), vec![file_a.clone()]),
2306 ("crate_b".to_string(), vec![file_b.clone()]),
2307 ];
2308
2309 let info = analyze_workspace_details(&crates);
2310 let a_info = info.get("crate_a").unwrap();
2311 let b_info = info.get("crate_b").unwrap();
2312
2313 assert_eq!(b_info.metrics.ce, 1);
2314 assert_eq!(a_info.metrics.ca, 1);
2315 }
2316
2317 #[test]
2318 fn top_level_macro_invocation() {
2319 let src_a = r#"
2320 #[macro_export]
2321 macro_rules! my_macro {
2322 () => {};
2323 }
2324 "#;
2325 let src_b = r#"
2326 use crate_a::*;
2327 my_macro!();
2328 fn main() {}
2329 "#;
2330
2331 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2332 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2333
2334 let crates = vec![
2335 ("crate_a".to_string(), vec![file_a.clone()]),
2336 ("crate_b".to_string(), vec![file_b.clone()]),
2337 ];
2338
2339 let info = analyze_workspace_details(&crates);
2340 let a_info = info.get("crate_a").unwrap();
2341 let b_info = info.get("crate_b").unwrap();
2342
2343 assert_eq!(b_info.metrics.ce, 1);
2344 assert_eq!(a_info.metrics.ca, 1);
2345 }
2346
2347 #[test]
2348 fn const_dependency() {
2349 let src_a = r#"
2350 pub enum Buz { X }
2351 "#;
2352 let src_b = r#"
2353 pub const FOO: &crate_a::Buz = &crate_a::Buz::X;
2354 "#;
2355
2356 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2357 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2358
2359 let crates = vec![
2360 ("crate_a".to_string(), vec![file_a.clone()]),
2361 ("crate_b".to_string(), vec![file_b.clone()]),
2362 ];
2363
2364 let info = analyze_workspace_details(&crates);
2365 let a_info = info.get("crate_a").unwrap();
2366 let b_info = info.get("crate_b").unwrap();
2367
2368 assert_eq!(b_info.metrics.ce, 1);
2369 assert_eq!(a_info.metrics.ca, 1);
2370 }
2371
2372 #[test]
2373 fn import_const_dependency() {
2374 let src_a = r#"
2375 pub mod foo {
2376 pub enum Foo { A }
2377 }
2378 "#;
2379 let src_c = r#"
2380 use crate_a::foo;
2381 pub const X: foo::Foo = foo::Foo::A;
2382 "#;
2383
2384 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2385 let file_c: syn::File = syn::parse_str(src_c).unwrap();
2386
2387 let crates = vec![
2388 ("crate_a".to_string(), vec![file_a.clone()]),
2389 ("crate_c".to_string(), vec![file_c.clone()]),
2390 ];
2391
2392 let info = analyze_workspace_details(&crates);
2393 let a_info = info.get("crate_a").unwrap();
2394 let c_info = info.get("crate_c").unwrap();
2395
2396 assert_eq!(c_info.metrics.ce, 1);
2397 assert_eq!(a_info.metrics.ca, 1);
2398 }
2399
2400 #[test]
2401 fn evaluate_metrics_thresholds() {
2402 let m = Metrics {
2403 r: 0,
2404 n: 0,
2405 h: 1.1,
2406 ca: 0,
2407 ce: 0,
2408 a: 0.8,
2409 i: 0.8,
2410 d: 0.0,
2411 d_prime: 0.7,
2412 };
2413 let eval = evaluate_metrics(&m);
2414 assert!(matches!(eval.a, AbstractionEval::Abstract));
2415 assert!(matches!(eval.h, CohesionEval::High));
2416 assert!(matches!(eval.i, StabilityEval::Unstable));
2417 assert!(matches!(eval.d_prime, DistanceEval::Useless));
2418
2419 let m = Metrics {
2420 r: 0,
2421 n: 0,
2422 h: 0.8,
2423 ca: 0,
2424 ce: 0,
2425 a: 0.2,
2426 i: 0.2,
2427 d: 0.0,
2428 d_prime: 0.7,
2429 };
2430 let eval = evaluate_metrics(&m);
2431 assert!(matches!(eval.a, AbstractionEval::Concrete));
2432 assert!(matches!(eval.h, CohesionEval::Low));
2433 assert!(matches!(eval.i, StabilityEval::Stable));
2434 assert!(matches!(eval.d_prime, DistanceEval::Painful));
2435
2436 let m = Metrics {
2437 r: 0,
2438 n: 0,
2439 h: 1.0,
2440 ca: 0,
2441 ce: 0,
2442 a: 0.5,
2443 i: 0.5,
2444 d: 0.0,
2445 d_prime: 0.5,
2446 };
2447 let eval = evaluate_metrics(&m);
2448 assert!(matches!(eval.a, AbstractionEval::Mixed));
2449 assert!(matches!(eval.h, CohesionEval::Low));
2450 assert!(matches!(eval.i, StabilityEval::Moderate));
2451 assert!(matches!(eval.d_prime, DistanceEval::Balanced));
2452
2453 let m = Metrics {
2454 r: 0,
2455 n: 0,
2456 h: 1.0,
2457 ca: 0,
2458 ce: 0,
2459 a: 0.5,
2460 i: 0.5,
2461 d: 0.0,
2462 d_prime: 0.3,
2463 };
2464 let eval = evaluate_metrics(&m);
2465 assert!(matches!(eval.d_prime, DistanceEval::Good));
2466 }
2467
2468 #[test]
2469 fn detects_two_crate_cycle() {
2470 let src_a = "use crate_b::B; pub struct A(B);";
2471 let src_b = "use crate_a::A; pub struct B(A);";
2472
2473 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2474 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2475
2476 let crates = vec![
2477 ("crate_a".to_string(), vec![file_a]),
2478 ("crate_b".to_string(), vec![file_b]),
2479 ];
2480
2481 let info = analyze_workspace_details(&crates);
2482 let cycles = dependency_cycles(&info);
2483 assert_eq!(cycles.len(), 1);
2484 let cyc = &cycles[0];
2485 assert!(cyc.contains(&"crate_a".to_string()));
2486 assert!(cyc.contains(&"crate_b".to_string()));
2487 }
2488
2489 #[test]
2490 fn detects_three_crate_cycle() {
2491 let src_a = "use crate_b::B; pub struct A(B);";
2492 let src_b = "use crate_c::C; pub struct B(C);";
2493 let src_c = "use crate_a::A; pub struct C(A);";
2494
2495 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2496 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2497 let file_c: syn::File = syn::parse_str(src_c).unwrap();
2498
2499 let crates = vec![
2500 ("crate_a".to_string(), vec![file_a]),
2501 ("crate_b".to_string(), vec![file_b]),
2502 ("crate_c".to_string(), vec![file_c]),
2503 ];
2504
2505 let info = analyze_workspace_details(&crates);
2506 let cycles = dependency_cycles(&info);
2507 assert_eq!(cycles.len(), 1);
2508 let cyc = &cycles[0];
2509 assert!(cyc.contains(&"crate_a".to_string()));
2510 assert!(cyc.contains(&"crate_b".to_string()));
2511 assert!(cyc.contains(&"crate_c".to_string()));
2512 }
2513
2514 #[test]
2515 fn unrelated_crate_not_included() {
2516 let src_a = "use crate_b::B; pub struct A(B);";
2517 let src_b = "use crate_a::A; pub struct B(A);";
2518 let src_c = "pub struct C;";
2519
2520 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2521 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2522 let file_c: syn::File = syn::parse_str(src_c).unwrap();
2523
2524 let crates = vec![
2525 ("crate_a".to_string(), vec![file_a]),
2526 ("crate_b".to_string(), vec![file_b]),
2527 ("crate_c".to_string(), vec![file_c]),
2528 ];
2529
2530 let info = analyze_workspace_details(&crates);
2531 let cycles = dependency_cycles(&info);
2532 assert_eq!(cycles.len(), 1);
2533 let cyc = &cycles[0];
2534 assert!(cyc.contains(&"crate_a".to_string()));
2535 assert!(cyc.contains(&"crate_b".to_string()));
2536 assert!(!cyc.contains(&"crate_c".to_string()));
2537 }
2538
2539 #[test]
2540 fn detects_cycle_via_method_calls() {
2541 let src_a = r#"
2542 pub struct A;
2543 impl A {
2544 pub fn call_b() {
2545 crate_b::B::bar();
2546 }
2547 pub fn bar() {}
2548 }
2549 "#;
2550 let src_b = r#"
2551 pub struct B;
2552 impl B {
2553 pub fn bar() {
2554 crate_a::A::call_b();
2555 }
2556 }
2557 "#;
2558
2559 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2560 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2561
2562 let crates = vec![
2563 ("crate_a".to_string(), vec![file_a]),
2564 ("crate_b".to_string(), vec![file_b]),
2565 ];
2566
2567 let info = analyze_workspace_details(&crates);
2568 let cycles = dependency_cycles(&info);
2569 assert_eq!(cycles.len(), 1);
2570 let cyc = &cycles[0];
2571 assert!(cyc.contains(&"crate_a".to_string()));
2572 assert!(cyc.contains(&"crate_b".to_string()));
2573 }
2574
2575 #[test]
2576 fn parse_package_ignores_tests_dir() {
2577 use cargo_metadata::MetadataCommand;
2578 let dir = tempfile::tempdir().unwrap();
2579 std::fs::create_dir_all(dir.path().join("pkg/src")).unwrap();
2580 std::fs::create_dir_all(dir.path().join("pkg/tests")).unwrap();
2581 std::fs::write(
2582 dir.path().join("pkg/Cargo.toml"),
2583 "[package]\nname = \"pkg\"\nversion = \"0.1.0\"\n",
2584 )
2585 .unwrap();
2586 std::fs::write(dir.path().join("pkg/src/lib.rs"), "pub struct Foo;\n").unwrap();
2587 std::fs::write(
2588 dir.path().join("pkg/tests/integration.rs"),
2589 "pub struct Bar;\n",
2590 )
2591 .unwrap();
2592 let metadata = MetadataCommand::new()
2593 .no_deps()
2594 .current_dir(dir.path().join("pkg"))
2595 .exec()
2596 .unwrap();
2597 let package = metadata.packages.first().unwrap();
2598 let files = parse_package(package).unwrap();
2599 assert_eq!(files.len(), 1);
2600 }
2601
2602 #[test]
2603 fn lib_and_main_dependency() {
2604 use cargo_metadata::MetadataCommand;
2605 let dir = tempfile::tempdir().unwrap();
2606 std::fs::create_dir_all(dir.path().join("dep/src")).unwrap();
2607 std::fs::write(
2608 dir.path().join("dep/Cargo.toml"),
2609 "[package]\nname = \"dep\"\nversion = \"0.1.0\"\n",
2610 )
2611 .unwrap();
2612 std::fs::write(dir.path().join("dep/src/lib.rs"), "pub struct Dep;\n").unwrap();
2613
2614 std::fs::create_dir_all(dir.path().join("app/src")).unwrap();
2615 std::fs::write(
2616 dir.path().join("app/Cargo.toml"),
2617 "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\ndep = { path = \"../dep\" }\n",
2618 )
2619 .unwrap();
2620 std::fs::write(dir.path().join("app/src/lib.rs"), "pub struct App;\n").unwrap();
2621 std::fs::write(
2622 dir.path().join("app/src/main.rs"),
2623 "use dep::Dep; fn main() { let _ = Dep; }\n",
2624 )
2625 .unwrap();
2626
2627 std::fs::write(
2628 dir.path().join("Cargo.toml"),
2629 "[workspace]\nmembers = [\"app\", \"dep\"]\n",
2630 )
2631 .unwrap();
2632
2633 let metadata = MetadataCommand::new()
2634 .no_deps()
2635 .current_dir(dir.path())
2636 .exec()
2637 .unwrap();
2638 let mut crates = Vec::new();
2639 for pkg in &metadata.packages {
2640 crates.push((pkg.name.as_str().to_string(), parse_package(pkg).unwrap()));
2641 }
2642
2643 let info = analyze_workspace_details(&crates);
2644 assert_eq!(info["app"].metrics.ce, 1);
2645 assert_eq!(info["dep"].metrics.ca, 1);
2646 }
2647
2648 #[test]
2649 fn path_dep_import_dependency() {
2650 use cargo_metadata::MetadataCommand;
2651 let dir = tempfile::tempdir().unwrap();
2652 std::fs::create_dir_all(dir.path().join("a/src")).unwrap();
2653 std::fs::create_dir_all(dir.path().join("b/c/src")).unwrap();
2654 std::fs::create_dir_all(dir.path().join("b/src")).unwrap();
2655
2656 std::fs::write(
2657 dir.path().join("a/Cargo.toml"),
2658 "[package]\nname = \"a\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
2659 )
2660 .unwrap();
2661 std::fs::write(dir.path().join("a/src/lib.rs"), "pub mod foo;\n").unwrap();
2662 std::fs::write(
2663 dir.path().join("a/src/foo.rs"),
2664 "#[derive(Debug)]\npub enum Foo { A }\n",
2665 )
2666 .unwrap();
2667
2668 std::fs::write(
2669 dir.path().join("b/Cargo.toml"),
2670 "[package]\nname = \"b\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\na = { path = \"../a\" }\nc = { path = \"./c\" }\n",
2671 )
2672 .unwrap();
2673 std::fs::write(dir.path().join("b/src/lib.rs"), "pub use c;\n").unwrap();
2674
2675 std::fs::write(
2676 dir.path().join("b/c/Cargo.toml"),
2677 "[package]\nname = \"c\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\na = { path = \"../../a\" }\n",
2678 )
2679 .unwrap();
2680 std::fs::write(
2681 dir.path().join("b/c/src/lib.rs"),
2682 "use a::foo;\npub const X: foo::Foo = foo::Foo::A;\n",
2683 )
2684 .unwrap();
2685
2686 std::fs::write(
2687 dir.path().join("Cargo.toml"),
2688 "[workspace]\nmembers = [\"a\", \"b\"]\nresolver = \"3\"\n",
2689 )
2690 .unwrap();
2691
2692 let metadata = MetadataCommand::new()
2693 .no_deps()
2694 .current_dir(dir.path())
2695 .exec()
2696 .unwrap();
2697 let mut crates = Vec::new();
2698 for pkg in &metadata.packages {
2699 crates.push((pkg.name.as_str().to_string(), parse_package(pkg).unwrap()));
2700 }
2701
2702 let info = analyze_workspace_details(&crates);
2703 assert_eq!(info["c"].metrics.ce, 1);
2704 assert_eq!(info["a"].metrics.ca, 1);
2705 }
2706
2707 #[test]
2708 fn external_crate_alias() {
2709 use cargo_metadata::MetadataCommand;
2710 let dir = tempfile::tempdir().unwrap();
2711 let external = tempfile::tempdir().unwrap();
2712 std::fs::create_dir_all(external.path().join("src")).unwrap();
2713 std::fs::write(
2714 external.path().join("Cargo.toml"),
2715 "[package]\nname = \"foo_bar\"\nversion = \"0.1.0\"\n",
2716 )
2717 .unwrap();
2718 std::fs::write(external.path().join("src/lib.rs"), "pub struct FooBar;\n").unwrap();
2719
2720 std::fs::create_dir(dir.path().join("app")).unwrap();
2721 std::fs::create_dir(dir.path().join("app/src")).unwrap();
2722 std::fs::write(
2723 dir.path().join("app/Cargo.toml"),
2724 format!(
2725 "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nfoo_bar = {{ path = \"{}\" }}\n",
2726 external.path().display()
2727 ),
2728 )
2729 .unwrap();
2730 std::fs::write(
2731 dir.path().join("app/src/lib.rs"),
2732 "use foo_bar as foo; pub struct App { d: foo::FooBar }\n",
2733 )
2734 .unwrap();
2735
2736 std::fs::write(
2737 dir.path().join("Cargo.toml"),
2738 "[workspace]\nmembers = [\"app\"]\n",
2739 )
2740 .unwrap();
2741
2742 let metadata_app = MetadataCommand::new()
2743 .no_deps()
2744 .current_dir(dir.path().join("app"))
2745 .exec()
2746 .unwrap();
2747 let app_pkg = metadata_app.packages.first().unwrap();
2748 let app_files = parse_package(app_pkg).unwrap();
2749
2750 let metadata_dep = MetadataCommand::new()
2751 .no_deps()
2752 .current_dir(external.path())
2753 .exec()
2754 .unwrap();
2755 let dep_pkg = metadata_dep.packages.first().unwrap();
2756 let dep_files = parse_package(dep_pkg).unwrap();
2757
2758 let crates = vec![
2759 ("app".to_string(), app_files),
2760 ("foo_bar".to_string(), dep_files),
2761 ];
2762
2763 let info = analyze_workspace_details(&crates);
2764 let app = info.get("app").unwrap();
2765 let dep = info.get("foo_bar").unwrap();
2766 assert_eq!(app.metrics.ce, 1);
2767 assert_eq!(dep.metrics.ca, 1);
2768 assert_eq!(dep.metrics.ce, 0);
2769 assert!(app
2770 .external_depends_on
2771 .get("App")
2772 .and_then(|m| m.get("foo_bar"))
2773 .map(|v| v.contains(&"FooBar".to_string()))
2774 .unwrap_or(false));
2775 }
2776
2777 #[test]
2778 fn type_alias_dependency() {
2779 use cargo_metadata::MetadataCommand;
2780 let dir = tempfile::tempdir().unwrap();
2781 std::fs::create_dir_all(dir.path().join("a/src")).unwrap();
2782 std::fs::write(
2783 dir.path().join("a/Cargo.toml"),
2784 "[package]\nname = \"a\"\nversion = \"0.1.0\"\n",
2785 )
2786 .unwrap();
2787 std::fs::write(dir.path().join("a/src/lib.rs"), "pub struct Cache;\n").unwrap();
2788
2789 std::fs::create_dir_all(dir.path().join("b/src")).unwrap();
2790 std::fs::write(
2791 dir.path().join("b/Cargo.toml"),
2792 "[package]\nname = \"b\"\nversion = \"0.1.0\"\n\n[[bin]]\nname = \"b\"\n",
2793 )
2794 .unwrap();
2795 std::fs::write(
2796 dir.path().join("b/src/main.rs"),
2797 "use a::Cache as CacheImpl; fn main() { let _ = CacheImpl; }\n",
2798 )
2799 .unwrap();
2800
2801 std::fs::write(
2802 dir.path().join("Cargo.toml"),
2803 "[workspace]\nmembers = [\"b\", \"a\"]\n",
2804 )
2805 .unwrap();
2806
2807 let metadata = MetadataCommand::new()
2808 .no_deps()
2809 .current_dir(dir.path())
2810 .exec()
2811 .unwrap();
2812 let mut crates = Vec::new();
2813 for pkg in &metadata.packages {
2814 crates.push((pkg.name.as_str().to_string(), parse_package(pkg).unwrap()));
2815 }
2816
2817 let info = analyze_workspace_details(&crates);
2818 assert_eq!(info["b"].metrics.ce, 1);
2819 assert_eq!(info["a"].metrics.ca, 1);
2820 }
2821
2822 #[test]
2823 fn bin_target_dependency() {
2824 use cargo_metadata::MetadataCommand;
2825 let dir = tempfile::tempdir().unwrap();
2826 std::fs::create_dir_all(dir.path().join("dep/src")).unwrap();
2827 std::fs::write(
2828 dir.path().join("dep/Cargo.toml"),
2829 "[package]\nname = \"dep\"\nversion = \"0.1.0\"\n",
2830 )
2831 .unwrap();
2832 std::fs::write(dir.path().join("dep/src/lib.rs"), "pub struct Dep;\n").unwrap();
2833
2834 std::fs::create_dir_all(dir.path().join("app/src/bin")).unwrap();
2835 std::fs::write(
2836 dir.path().join("app/Cargo.toml"),
2837 "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\ndep = { path = \"../dep\" }\n\n[[bin]]\nname = \"cli\"\npath = \"src/bin/cli.rs\"\n",
2838 )
2839 .unwrap();
2840 std::fs::write(dir.path().join("app/src/lib.rs"), "pub struct App;\n").unwrap();
2841 std::fs::write(
2842 dir.path().join("app/src/bin/cli.rs"),
2843 "use dep::Dep; fn main() { let _ = Dep; }\n",
2844 )
2845 .unwrap();
2846
2847 std::fs::write(
2848 dir.path().join("Cargo.toml"),
2849 "[workspace]\nmembers = [\"app\", \"dep\"]\n",
2850 )
2851 .unwrap();
2852
2853 let metadata = MetadataCommand::new()
2854 .no_deps()
2855 .current_dir(dir.path())
2856 .exec()
2857 .unwrap();
2858 let mut crates = Vec::new();
2859 for pkg in &metadata.packages {
2860 crates.push((pkg.name.as_str().to_string(), parse_package(pkg).unwrap()));
2861 }
2862
2863 let info = analyze_workspace_details(&crates);
2864 assert_eq!(info["app"].metrics.ce, 1);
2865 assert_eq!(info["dep"].metrics.ca, 1);
2866 }
2867
2868 #[test]
2869 fn path_root_resolves_special_paths() {
2870 let defined = std::collections::HashMap::new();
2871 let mut ws = std::collections::HashSet::new();
2872 ws.insert("my_crate".to_string());
2873 let visitor = DetailVisitor {
2874 current: None,
2875 defined: &defined,
2876 crate_name: "my_crate",
2877 workspace_crates: &ws,
2878 all_defined: &std::collections::HashMap::new(),
2879 reexports: &std::collections::HashMap::new(),
2880 imports: std::collections::HashMap::new(),
2881 internal: std::collections::HashMap::new(),
2882 external: std::collections::HashMap::new(),
2883 methods: &std::collections::HashMap::new(),
2884 trait_bounds: &std::collections::HashMap::new(),
2885 };
2886 let p: syn::Path = syn::parse_str("self::Foo").unwrap();
2887 assert_eq!(visitor.path_root(&p), Some("my_crate".to_string()));
2888 let p: syn::Path = syn::parse_str("super::bar::Baz").unwrap();
2889 assert_eq!(visitor.path_root(&p), Some("my_crate".to_string()));
2890 let p: syn::Path = syn::parse_str("crate::Foo").unwrap();
2891 assert_eq!(visitor.path_root(&p), Some("my_crate".to_string()));
2892 }
2893
2894 #[test]
2895 fn infer_expr_type_special_paths() {
2896 use std::collections::{HashMap, HashSet};
2897 let mut defined = HashMap::new();
2898 defined.insert("Foo".to_string(), ClassKind::Struct);
2899 let mut ws = HashSet::new();
2900 ws.insert("my_crate".to_string());
2901 let visitor = DetailVisitor {
2902 current: Some("Current".to_string()),
2903 defined: &defined,
2904 crate_name: "my_crate",
2905 workspace_crates: &ws,
2906 all_defined: &HashMap::new(),
2907 reexports: &HashMap::new(),
2908 imports: HashMap::new(),
2909 internal: HashMap::new(),
2910 external: HashMap::new(),
2911 methods: &HashMap::new(),
2912 trait_bounds: &HashMap::new(),
2913 };
2914 let e: syn::Expr = syn::parse_str("self").unwrap();
2915 assert_eq!(
2916 visitor.infer_expr_type(&e),
2917 Some(("Current".to_string(), Some("my_crate".to_string())))
2918 );
2919 let e: syn::Expr = syn::parse_str("self::Foo").unwrap();
2920 assert_eq!(
2921 visitor.infer_expr_type(&e),
2922 Some(("Foo".to_string(), Some("my_crate".to_string())))
2923 );
2924 let e: syn::Expr = syn::parse_str("super::Foo").unwrap();
2925 assert_eq!(
2926 visitor.infer_expr_type(&e),
2927 Some(("Foo".to_string(), Some("my_crate".to_string())))
2928 );
2929 let e: syn::Expr = syn::parse_str("crate::Foo").unwrap();
2930 assert_eq!(
2931 visitor.infer_expr_type(&e),
2932 Some(("Foo".to_string(), Some("my_crate".to_string())))
2933 );
2934 }
2935
2936 #[test]
2937 fn detects_multiple_cycles() {
2938 let src_a = "use crate_b::B; pub struct A(B);";
2939 let src_b = "use crate_a::A; pub struct B(A);";
2940 let src_c = "use crate_d::D; pub struct C(D);";
2941 let src_d = "use crate_c::C; pub struct D(C);";
2942
2943 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2944 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2945 let file_c: syn::File = syn::parse_str(src_c).unwrap();
2946 let file_d: syn::File = syn::parse_str(src_d).unwrap();
2947
2948 let crates = vec![
2949 ("crate_a".to_string(), vec![file_a]),
2950 ("crate_b".to_string(), vec![file_b]),
2951 ("crate_c".to_string(), vec![file_c]),
2952 ("crate_d".to_string(), vec![file_d]),
2953 ];
2954
2955 let info = analyze_workspace_details(&crates);
2956 let mut cycles = dependency_cycles(&info);
2957 cycles.sort_by(|a, b| a[0].cmp(&b[0]));
2958 assert_eq!(cycles.len(), 2);
2959 assert!(cycles[0].contains(&"crate_a".to_string()));
2960 assert!(cycles[0].contains(&"crate_b".to_string()));
2961 assert!(cycles[1].contains(&"crate_c".to_string()));
2962 assert!(cycles[1].contains(&"crate_d".to_string()));
2963 }
2964
2965 #[test]
2966 fn detects_no_cycles() {
2967 let src_a = "use crate_b::B; pub struct A(B);";
2968 let src_b = "pub struct B;";
2969 let src_c = "use crate_b::B; pub struct C(B);";
2970
2971 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2972 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2973 let file_c: syn::File = syn::parse_str(src_c).unwrap();
2974
2975 let crates = vec![
2976 ("crate_a".to_string(), vec![file_a]),
2977 ("crate_b".to_string(), vec![file_b]),
2978 ("crate_c".to_string(), vec![file_c]),
2979 ];
2980
2981 let info = analyze_workspace_details(&crates);
2982 let cycles = dependency_cycles(&info);
2983 assert!(cycles.is_empty());
2984 }
2985
2986 #[test]
2987 fn where_clause_dependency() {
2988 let src_a = "pub trait Foo {}";
2989 let src_b = r#"
2990 use crate_a::Foo;
2991 pub fn bar<T>(_: T)
2992 where
2993 T: Foo,
2994 {}
2995 "#;
2996
2997 let file_a: syn::File = syn::parse_str(src_a).unwrap();
2998 let file_b: syn::File = syn::parse_str(src_b).unwrap();
2999
3000 let crates = vec![
3001 ("crate_a".to_string(), vec![file_a.clone()]),
3002 ("crate_b".to_string(), vec![file_b.clone()]),
3003 ];
3004
3005 let info = analyze_workspace_details(&crates);
3006 let a_info = info.get("crate_a").unwrap();
3007 let b_info = info.get("crate_b").unwrap();
3008
3009 assert_eq!(b_info.metrics.ce, 1);
3010 assert_eq!(a_info.metrics.ca, 1);
3011 }
3012
3013 #[test]
3014 fn macro_method_generic_dependency() {
3015 let src_a = "pub struct A<T, E>(T, std::marker::PhantomData<E>);";
3016 let src_b = r#"
3017 use crate_a::A;
3018
3019 pub struct B(A<u8, u8>);
3020
3021 macro_rules! impl_invoke {
3022 ($that:expr, $req:expr) => {
3023 $that.invoke($req)
3024 };
3025 }
3026
3027 impl B {
3028 fn invoke(&self, _req: ()) -> () {
3029 ()
3030 }
3031 }
3032
3033 fn get_foo(b: &B, req: ()) -> () {
3034 impl_invoke!(b, req)
3035 }
3036 "#;
3037
3038 let file_a: syn::File = syn::parse_str(src_a).unwrap();
3039 let file_b: syn::File = syn::parse_str(src_b).unwrap();
3040
3041 let crates = vec![
3042 ("crate_a".to_string(), vec![file_a.clone()]),
3043 ("crate_b".to_string(), vec![file_b.clone()]),
3044 ];
3045
3046 let info = analyze_workspace_details(&crates);
3047 let a_info = info.get("crate_a").unwrap();
3048 let b_info = info.get("crate_b").unwrap();
3049
3050 assert_eq!(b_info.metrics.ce, 1);
3051 assert_eq!(a_info.metrics.ca, 1);
3052 }
3053}