fallow_engine/health/
branching.rs1use fallow_types::discover::DiscoveredFile;
10use fallow_types::extract::{FileBranching, ModuleInfo};
11use rustc_hash::FxHashMap;
12use std::path::PathBuf;
13
14pub type BranchingByFile = FxHashMap<PathBuf, FileBranching>;
20
21#[must_use]
31pub fn branching_by_file(files: &[DiscoveredFile], modules: &[ModuleInfo]) -> BranchingByFile {
32 let mut by_file = BranchingByFile::default();
33 for module in modules {
34 if module.complexity.is_empty() {
35 continue;
36 }
37 let Some(file) = files.get(module.file_id.0 as usize) else {
38 continue;
39 };
40 let totals = FileBranching::from_units(&module.complexity);
41 if totals.functions == 0 {
42 continue;
43 }
44 by_file.insert(file.path.clone(), totals);
45 }
46 by_file
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use fallow_types::discover::FileId;
53 use fallow_types::extract::{
54 ComplexityContribution, ComplexityContributionKind, ComplexityMetric, FunctionComplexity,
55 };
56
57 fn file(id: u32, path: &str) -> DiscoveredFile {
58 DiscoveredFile {
59 id: FileId(id),
60 path: PathBuf::from(path),
61 size_bytes: 0,
62 }
63 }
64
65 fn unit(name: &str, cyclomatic: u16, branches: u16) -> FunctionComplexity {
66 FunctionComplexity {
67 name: name.to_string(),
68 is_private_member: false,
69 context_named: None,
70 line: 1,
71 col: 0,
72 cyclomatic,
73 cognitive: 0,
74 line_count: 10,
75 param_count: 0,
76 react_hook_count: 0,
77 react_jsx_max_depth: 0,
78 react_prop_count: 0,
79 source_hash: None,
80 contributions: std::iter::repeat_n(
81 ComplexityContribution {
82 line: 1,
83 col: 0,
84 metric: ComplexityMetric::Cyclomatic,
85 kind: ComplexityContributionKind::If,
86 weight: 1,
87 nesting: 0,
88 literal_membership: false,
89 },
90 usize::from(branches),
91 )
92 .collect(),
93 }
94 }
95
96 fn module(file_id: u32, units: Vec<FunctionComplexity>) -> ModuleInfo {
97 ModuleInfo {
98 complexity: units,
99 ..ModuleInfo::empty(FileId(file_id))
100 }
101 }
102
103 #[test]
104 fn aggregates_each_module_under_its_path() {
105 let files = vec![file(0, "/p/a.ts"), file(1, "/p/b.ts")];
106 let modules = vec![
107 module(0, vec![unit("one", 3, 2), unit("two", 2, 1)]),
108 module(1, vec![unit("three", 1, 0)]),
109 ];
110
111 let by_file = branching_by_file(&files, &modules);
112
113 let a = by_file[&PathBuf::from("/p/a.ts")];
114 assert_eq!(a.branch_points, 3);
115 assert_eq!(a.functions, 2);
116 assert_eq!(a.peak_cyclomatic, 3);
117 assert_eq!(a.implied_cyclomatic(), 5, "2 functions carrying 3 branches");
118
119 let b = by_file[&PathBuf::from("/p/b.ts")];
120 assert_eq!(b.branch_points, 0);
121 assert_eq!(b.functions, 1);
122 }
123
124 #[test]
125 fn omits_modules_with_no_units() {
126 let files = vec![file(0, "/p/a.ts")];
127 let modules = vec![module(0, Vec::new())];
128 assert!(branching_by_file(&files, &modules).is_empty());
129 }
130
131 #[test]
132 fn omits_modules_whose_only_units_are_synthetic() {
133 let files = vec![file(0, "/p/a.vue")];
134 let modules = vec![module(0, vec![unit("<template>", 4, 3)])];
135 assert!(
136 branching_by_file(&files, &modules).is_empty(),
137 "a synthetic template unit is excluded, so the file has nothing to report"
138 );
139 }
140
141 #[test]
142 fn reports_a_module_whose_only_unit_is_module_scope() {
143 let files = vec![file(0, "/p/config.ts")];
144 let modules = vec![module(0, vec![unit("<module>", 3, 2)])];
145
146 let totals = branching_by_file(&files, &modules)[&PathBuf::from("/p/config.ts")];
147
148 assert_eq!(totals.branch_points, 2, "module-scope branching is counted");
149 assert_eq!(totals.functions, 1);
150 assert!(
151 !totals.has_synthetic_units,
152 "the module unit is not a template unit and excludes nothing"
153 );
154 }
155
156 #[test]
157 fn skips_a_module_whose_file_id_is_out_of_range() {
158 let files = vec![file(0, "/p/a.ts")];
159 let modules = vec![module(7, vec![unit("one", 2, 1)])];
160 assert!(branching_by_file(&files, &modules).is_empty());
161 }
162}