Skip to main content

cargo_anatomy/
lib.rs

1//! Utilities for analyzing Rust crates and computing package metrics.
2//!
3//! The library parses Rust source files and builds a dependency graph of the
4//! types that appear within a crate. From this graph a number of classic
5//! software metrics are derived, such as efferent/afferent coupling and
6//! relational cohesion. These building blocks are used by the accompanying
7//! `cargo-anatomy` binary, but the functions are generic enough to be consumed
8//! by other tools as well.
9use 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/// Wrap an error with file and line information.
19///
20/// This helper is mainly used by the [`loc_try!`] macro so that any propagated
21/// error retains its originating call site, which simplifies debugging.
22///
23/// # Examples
24///
25/// ```
26/// use cargo_anatomy::error_with_location;
27///
28/// fn might_fail() -> Result<(), Box<dyn std::error::Error>> {
29///     Err(error_with_location("boom"))
30/// }
31/// ```
32#[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/// Try expression and attach location info on error.
47///
48/// This macro behaves similarly to the `?` operator but ensures that any error
49/// returned is first wrapped with [`error_with_location`]. It is primarily
50/// intended for internal use.
51#[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/// Represents the kind of item defined within a crate.
79///
80/// The variants correspond to common Rust constructs that can participate in
81/// dependency relationships.
82#[derive(Debug, Serialize, Clone)]
83pub enum ClassKind {
84    /// A `struct` definition.
85    Struct,
86    /// An `enum` definition.
87    Enum,
88    /// A `trait` definition.
89    Trait,
90    /// A `type` alias.
91    TypeAlias,
92    /// A `macro_rules!` or other macro definition.
93    Macro,
94}
95
96/// Metadata about a type defined in a crate.
97#[derive(Debug, Serialize, Clone)]
98pub struct ClassInfo {
99    /// Name of the item as it appears in source.
100    pub name: String,
101    /// Kind of the item.
102    pub kind: ClassKind,
103}
104
105/// Specifies whether a crate belongs to the current workspace or is an
106/// external dependency.
107#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
108pub enum CrateKind {
109    /// The crate lives in the workspace being analysed.
110    Workspace,
111    /// The crate is an external dependency.
112    External,
113}
114
115/// Quantitative metrics describing the coupling and cohesion of a crate.
116///
117/// Values are derived from the relationships between types and follow the
118/// definitions popularised by Robert C. Martin. Each field is documented with
119/// the formula used to compute it.
120#[derive(Debug, Serialize, Clone)]
121pub struct Metrics {
122    /// Number of internal type relationships.
123    pub r: usize,
124    /// Total number of type definitions.
125    pub n: usize,
126    /// Relational cohesion calculated as `(r + 1) / n`.
127    pub h: f64,
128    /// Afferent coupling – external types depending on this crate.
129    pub ca: usize,
130    /// Efferent coupling – this crate's types depending on other workspace crates.
131    pub ce: usize,
132    /// Abstraction ratio of traits to total types.
133    pub a: f64,
134    /// Instability ratio `ce / (ce + ca)`.
135    pub i: f64,
136    /// Distance from the main sequence.
137    pub d: f64,
138    /// Normalised distance from the main sequence.
139    pub d_prime: f64,
140}
141
142/// Qualitative evaluation of a crate's level of abstraction.
143#[derive(Debug, Serialize, Clone)]
144#[serde(rename_all = "lowercase")]
145pub enum AbstractionEval {
146    /// More than the configured fraction of items are traits.
147    Abstract,
148    /// A mix of concrete and abstract items.
149    Mixed,
150    /// Dominated by concrete types.
151    Concrete,
152}
153
154/// Qualitative evaluation of relational cohesion.
155#[derive(Debug, Serialize, Clone)]
156#[serde(rename_all = "lowercase")]
157pub enum CohesionEval {
158    /// Cohesion exceeds the configured high threshold.
159    High,
160    /// Cohesion does not exceed the high threshold.
161    Low,
162}
163
164/// Qualitative evaluation of stability based on dependency ratios.
165#[derive(Debug, Serialize, Clone)]
166#[serde(rename_all = "lowercase")]
167pub enum StabilityEval {
168    /// Few outgoing dependencies, considered stable.
169    Stable,
170    /// Balance of incoming and outgoing dependencies.
171    Moderate,
172    /// Many outgoing dependencies, considered unstable.
173    Unstable,
174}
175
176/// Qualitative evaluation of the normalised distance from the main sequence.
177#[derive(Debug, Serialize, Clone)]
178#[serde(rename_all = "lowercase")]
179pub enum DistanceEval {
180    /// Falls within the good range near the main sequence.
181    Good,
182    /// Neither particularly good nor problematic.
183    Balanced,
184    /// Far from the main sequence on the stable side.
185    Painful,
186    /// Far from the main sequence on the unstable side.
187    Useless,
188}
189
190/// Human readable evaluation for a set of metrics.
191#[derive(Debug, Serialize, Clone)]
192pub struct Evaluation {
193    /// Result of evaluating abstraction.
194    pub a: AbstractionEval,
195    /// Result of evaluating cohesion.
196    pub h: CohesionEval,
197    /// Result of evaluating instability.
198    pub i: StabilityEval,
199    /// Result of evaluating distance from the main sequence.
200    pub d_prime: DistanceEval,
201}
202
203/// Metrics accompanied by their qualitative evaluation, used for output.
204#[derive(Debug, Serialize, Clone)]
205pub struct MetricsResult {
206    /// Raw quantitative metrics.
207    pub metrics: Metrics,
208    /// Human readable evaluation of those metrics.
209    pub evaluation: Evaluation,
210}
211
212/// Groups together threshold values used when evaluating metrics.
213#[derive(Debug, Clone, Deserialize, Serialize, Default)]
214pub struct EvaluationThresholds {
215    /// Thresholds governing abstraction classification.
216    #[serde(default)]
217    pub abstraction: AbstractionThresholds,
218    /// Thresholds for relational cohesion.
219    #[serde(default)]
220    pub cohesion: CohesionThresholds,
221    /// Thresholds for stability.
222    #[serde(default)]
223    pub instability: InstabilityThresholds,
224    /// Thresholds for distance from the main sequence.
225    #[serde(default)]
226    pub distance: DistanceThresholds,
227}
228
229/// Top-level structure for configuration files.
230#[derive(Debug, Clone, Deserialize, Serialize, Default)]
231pub struct Config {
232    /// Threshold configuration for metric evaluation.
233    #[serde(default)]
234    pub evaluation: EvaluationThresholds,
235}
236
237/// Thresholds used when categorising a crate as abstract or concrete.
238#[derive(Debug, Clone, Deserialize, Serialize)]
239pub struct AbstractionThresholds {
240    /// Minimum ratio of traits considered abstract.
241    #[serde(default = "default_abstract_min")]
242    pub abstract_min: f64,
243    /// Maximum ratio of traits considered concrete.
244    #[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/// Thresholds used to evaluate relational cohesion.
266#[derive(Debug, Clone, Deserialize, Serialize)]
267pub struct CohesionThresholds {
268    /// Values greater than this are considered highly cohesive.
269    #[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/// Thresholds used to categorise crate stability.
286#[derive(Debug, Clone, Deserialize, Serialize)]
287pub struct InstabilityThresholds {
288    /// Minimum ratio considered unstable.
289    #[serde(default = "default_unstable_min")]
290    pub unstable_min: f64,
291    /// Maximum ratio considered stable.
292    #[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/// Thresholds for evaluating distance from the main sequence.
314#[derive(Debug, Clone, Deserialize, Serialize)]
315pub struct DistanceThresholds {
316    /// Maximum normalised distance considered good.
317    #[serde(default = "default_good_max")]
318    pub good_max: f64,
319    /// Minimum normalised distance considered bad.
320    #[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
341/// Evaluate a set of metrics using the library's default thresholds.
342///
343/// This is a convenience wrapper around [`evaluate_metrics_with`].
344/// Thresholds loosely follow the metrics described in Robert C. Martin's
345/// *Agile Software Development*.
346pub fn evaluate_metrics(m: &Metrics) -> Evaluation {
347    evaluate_metrics_with(m, &EvaluationThresholds::default())
348}
349
350/// Evaluate metrics using custom thresholds.
351pub 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/// Detailed analysis results for a single crate.
395#[derive(Debug, Serialize, Clone)]
396pub struct CrateDetail {
397    /// Whether the crate is part of the workspace or external.
398    pub kind: CrateKind,
399    /// Raw metrics computed for the crate.
400    pub metrics: Metrics,
401    /// Qualitative evaluation of the metrics.
402    pub evaluation: Evaluation,
403    /// All types defined within the crate.
404    pub classes: Vec<ClassInfo>,
405    /// Mapping of type names to the types they depend on within the crate.
406    pub internal_depends_on: HashMap<String, Vec<String>>,
407    /// Inverse mapping of [`internal_depends_on`].
408    pub internal_depended_by: HashMap<String, Vec<String>>,
409    /// External dependencies keyed by crate and type.
410    pub external_depends_on: HashMap<String, HashMap<String, Vec<String>>>,
411    /// External dependents keyed by crate and type.
412    pub external_depended_by: HashMap<String, HashMap<String, Vec<String>>>,
413}
414
415/// Traverse parsed syntax trees and collect all type definitions, counting traits.
416///
417/// Returns a mapping from type name to [`ClassKind`] together with the number of
418/// trait definitions encountered. Items marked as tests are ignored.
419pub 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}
466/// Map method names to their return types for each `impl` block or trait.
467///
468/// The returned map uses `(impl_name, method_name)` as the key where
469/// `impl_name` is the type or trait that the method belongs to and the value
470/// is the return type. Test code is ignored.
471pub 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}
579/// Collect trait inheritance information for each trait.
580///
581/// Returns a map from trait name to the list of traits it declares as
582/// supertraits. Test code is ignored.
583pub 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
610/// Collect re-exported items from workspace crates.
611///
612/// Returns a map from the re-exported name to the originating crate and the
613/// original identifier.
614pub 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}
694/// Parse all Rust source files belonging to the given package.
695fn 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
735/// Parse all Rust source files belonging to the given package.
736///
737/// Every library or binary target's source directory is scanned (or `src/` when
738/// no targets declare a path) and any `.rs` files found are parsed into
739/// [`syn::File`] structures. Files located under a `tests` directory are skipped
740/// since Cargo treats integration tests as separate crates.
741/// Returns the parsed files.
742pub 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}
752/// Parse a package's source files and compute metrics.
753///
754/// The provided `workspace_types` should contain class names from all crates in
755/// the workspace so that internal and external references are classified
756/// correctly. Returns the computed [`Metrics`] for the package.
757pub 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}
764/// Analyze parsed files to produce package metrics.
765///
766/// `workspace_types` should contain all type names defined in the workspace so
767/// references can be counted as internal or external.
768pub 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; // not computed in this function
787    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
825/// Analyse multiple crates together so cross-crate dependencies can be counted.
826///
827/// Each tuple contains the crate name and its parsed source files.
828/// Only the [`Metrics`] for each crate are returned.
829pub fn analyze_workspace(crates: &[(String, Vec<File>)]) -> HashMap<String, Metrics> {
830    analyze_workspace_with_thresholds(crates, &EvaluationThresholds::default())
831}
832
833/// Analyse multiple crates together using custom evaluation thresholds.
834///
835/// Returns only the [`Metrics`] for each crate.
836pub 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
846/// Produce full [`CrateDetail`] information for multiple crates.
847///
848/// This performs a deeper analysis than [`analyze_workspace`] by tracking
849/// type-level dependencies between crates.
850pub fn analyze_workspace_details(crates: &[(String, Vec<File>)]) -> HashMap<String, CrateDetail> {
851    analyze_workspace_details_with_thresholds(crates, &EvaluationThresholds::default())
852}
853
854/// Produce full [`CrateDetail`] information for multiple crates using custom thresholds.
855pub 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
1076/// Detect cycles in the crate dependency graph.
1077///
1078/// The returned vector contains one entry per cycle; each entry is a list of
1079/// crate names in the order they appear in the cycle. Internally uses
1080/// Tarjan's strongly connected components algorithm.
1081pub fn dependency_cycles(details: &HashMap<String, CrateDetail>) -> Vec<Vec<String>> {
1082    // Build adjacency list of crate -> crates it depends on
1083    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    /// Map of imported identifiers to their originating crate and original name.
1204    ///
1205    /// The key is the local identifier introduced by a `use` statement. The
1206    /// value is a tuple of `(root crate, original identifier)` where either
1207    /// element may be `None` if it cannot be resolved.
1208    imports: HashMap<String, (Option<String>, Option<String>)>,
1209    internal: HashMap<String, HashSet<String>>, // from -> to
1210    external: HashMap<String, HashMap<String, HashSet<String>>>, // from -> crate -> types
1211    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                    // When `first` is `None`, the use statement is importing a
1323                    // crate without an alias (e.g. `use foo;`). In this case
1324                    // `path_root` can resolve the crate directly, so we only
1325                    // track names when they appear as part of a path with a
1326                    // prefix.
1327                    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); // HashMap is outside workspace
1671        assert!((metrics.h - ((2.0 + 1.0) / 3.0)).abs() < 1e-6);
1672        assert!((metrics.a - (1.0 / 3.0)).abs() < 1e-6);
1673        // With no cross-crate dependencies, instability is defined as zero
1674        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}