//! Counts the independent things inside a Rust source file. //! //! A file is read with `syn`, its top-level items become graph nodes, and an //! edge is drawn from A to B when A's body names something B defines. The //! connected components of that graph are the pieces the file could be split //! into without either piece importing from the other. Names that resolve //! outside the file are recorded against the module they come from, which is //! what a split-out file would have to import. pub mod cluster; pub mod item; pub mod report; pub mod scan; /// Parse and analyse one file's text. pub fn analyse_source( path: &str, source: &str, peel: usize, limits: cluster::SplitLimits, ) -> syn::Result { Ok(cluster::analyse( path, item::parse(source, limits.explode_impls_over)?, peel, limits, )) } #[cfg(test)] mod tests { use super::*; fn clusters(src: &str) -> Vec> { let a = analyse_source("t.rs", src, 0, Default::default()).expect("fixture parses"); a.clusters .iter() .map(|c| c.items.iter().map(|&i| a.items[i].name.clone()).collect()) .collect() } #[test] fn unconnected_functions_are_separate_clusters() { let out = clusters("fn a() { let x = 1; }\nfn b() { let y = 2; }\n"); assert_eq!(out.len(), 2); } #[test] fn a_call_joins_two_functions() { let out = clusters("fn a() { b(); }\nfn b() {}\n"); assert_eq!(out, vec![vec!["a".to_string(), "b".to_string()]]); } #[test] fn a_test_module_is_its_own_cluster() { let src = r#" fn a() { b(); } fn b() {} #[cfg(test)] mod tests { use super::*; #[test] fn works() { a(); } } "#; let a = analyse_source("t.rs", src, 0, Default::default()).expect("fixture parses"); assert_eq!( a.scores.cluster_count, 1, "tests do not join the code graph" ); let test_clusters: Vec<_> = a.clusters.iter().filter(|c| c.is_test).collect(); assert_eq!(test_clusters.len(), 1); assert!(a.scores.test_lines > 0); } #[test] fn an_impl_joins_its_type() { let src = "struct Zone;\nimpl Zone { fn name(&self) -> &str { \"z\" } }\nfn other() {}\n"; let out = clusters(src); assert_eq!(out.len(), 2); assert!(out .iter() .any(|c| c.len() == 2 && c.contains(&"Zone".to_string()))); } #[test] fn a_foreign_trait_impl_stands_alone() { let src = "impl std::fmt::Display for String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) } }\nfn a() {}\n"; let a = analyse_source("t.rs", src, 0, Default::default()).expect("fixture parses"); assert_eq!(a.scores.cluster_count, 2); assert!(a.clusters.iter().any(|c| c.is_foreign_impl)); } #[test] fn imports_follow_the_cluster_that_uses_them() { let src = "use std::collections::HashMap;\nfn a() -> HashMap { HashMap::new() }\nfn b() {}\n"; let a = analyse_source("t.rs", src, 0, Default::default()).expect("fixture parses"); let user = a .clusters .iter() .find(|c| c.items.iter().any(|&i| a.items[i].name == "a")) .expect("cluster for a"); assert!(user.imports.contains("use std::collections::HashMap;")); let other = a .clusters .iter() .find(|c| c.items.iter().any(|&i| a.items[i].name == "b")) .expect("cluster for b"); assert!(other.imports.is_empty()); } #[test] fn contiguous_clusters_do_not_count_as_interleaved() { let contiguous = "fn a1() { a2(); }\nfn a2() {}\nfn b1() { b2(); }\nfn b2() {}\n"; let woven = "fn a1() { a2(); }\nfn b1() { b2(); }\nfn a2() {}\nfn b2() {}\n"; let c = analyse_source("t.rs", contiguous, 0, Default::default()).unwrap(); let w = analyse_source("t.rs", woven, 0, Default::default()).unwrap(); assert_eq!(c.scores.cluster_count, 2); assert_eq!(w.scores.cluster_count, 2); assert_eq!(c.scores.interleaving, 0.0); assert!(w.scores.interleaving > 0.0); } #[test] fn a_thin_link_between_two_halves_makes_two_cohesion_groups() { // One connected component, because `b3` calls `a1` once. The dense // wiring is inside each half, which is what the groups follow. let src = " fn a1() { a2(); a3(); } fn a2() { a1(); a3(); } fn a3() { a1(); a2(); } fn b1() { b2(); b3(); } fn b2() { b1(); b3(); } fn b3() { b1(); b2(); a1(); } "; let a = analyse_source("t.rs", src, 0, Default::default()).unwrap(); assert_eq!(a.scores.cluster_count, 1); assert_eq!(a.scores.group_count, 2); } #[test] fn a_long_impl_is_read_one_method_at_a_time() { let body = (0..8) .map(|i| format!("fn m{i}(&self) -> u32 {{ {i} }}\n{}", "// pad\n".repeat(12))) .collect::(); let src = format!("struct Big;\nimpl Big {{\n{body}}}\n"); let a = analyse_source("t.rs", &src, 0, Default::default()).unwrap(); let named: Vec<&str> = a.items.iter().map(|i| i.name.as_str()).collect(); assert!(named.contains(&"Big::m0"), "got {named:?}"); assert!(named.contains(&"Big::m7"), "got {named:?}"); } #[test] fn peeling_a_hub_reveals_the_clusters_behind_it() { let src = "struct Ctx;\nfn a(c: &Ctx) {}\nfn b(c: &Ctx) {}\nfn c_(c: &Ctx) {}\n"; let whole = analyse_source("t.rs", src, 0, Default::default()).unwrap(); assert_eq!(whole.scores.cluster_count, 1); let peeled = analyse_source("t.rs", src, 1, Default::default()).unwrap(); assert_eq!(peeled.peeled.len(), 1); assert_eq!(peeled.scores.cluster_count, 3); } }