1use std::collections::HashMap;
2use std::path::Path;
3
4use anyhow::Result;
5use code_split_core::{Complexity, GraphBuilder, Halstead, Loc, Maintainability, NodeKind};
6use rust_code_analysis::{
7 FuncSpace, JavascriptParser, ParserTrait, PythonParser, RustParser, SpaceKind, TsxParser,
8 TypescriptParser, metrics,
9};
10use walkdir::WalkDir;
11
12pub fn analyze(root: &Path, builder: &mut GraphBuilder) -> Result<usize> {
17 analyze_extensions(root, builder, &["rs"])
18}
19
20pub fn analyze_python(root: &Path, builder: &mut GraphBuilder) -> Result<usize> {
22 analyze_extensions(root, builder, &["py"])
23}
24
25pub fn analyze_js(root: &Path, builder: &mut GraphBuilder) -> Result<usize> {
27 analyze_extensions(root, builder, &["js", "jsx", "ts", "tsx"])
28}
29
30fn analyze_extensions(
31 root: &Path,
32 builder: &mut GraphBuilder,
33 extensions: &[&str],
34) -> Result<usize> {
35 let mut file_index: HashMap<String, usize> = HashMap::new();
36 let mut fn_index: HashMap<(String, String), usize> = HashMap::new();
37 let mut fn_line_index: HashMap<(String, usize), usize> = HashMap::new();
39
40 for (i, node) in builder.nodes().iter().enumerate() {
41 match node.kind {
42 NodeKind::File => {
46 file_index.insert(node.path.clone(), i);
47 }
48 NodeKind::Module if node.line.is_none() => {
49 file_index.entry(node.path.clone()).or_insert(i);
50 }
51 NodeKind::Fn | NodeKind::Method => {
52 fn_index.insert((node.path.clone(), node.name.clone()), i);
53 if let Some(line) = node.line {
54 fn_line_index.insert((node.path.clone(), line as usize), i);
55 }
56 }
57 _ => {}
58 }
59 }
60
61 let mut annotated = 0usize;
62
63 for entry in WalkDir::new(root)
64 .into_iter()
65 .filter_map(|e| e.ok())
66 .filter(|e| {
67 e.file_type().is_file()
68 && e.path()
69 .extension()
70 .and_then(|x| x.to_str())
71 .is_some_and(|x| extensions.contains(&x))
72 })
73 {
74 let path = entry.path();
75 let Ok(src) = std::fs::read(path) else {
76 continue;
77 };
78 let canonical = path.to_string_lossy().into_owned();
79
80 let Some(space) = parse_metrics(path, src) else {
81 continue;
82 };
83
84 if let Some(&idx) = file_index.get(&canonical) {
85 builder.nodes_mut()[idx].complexity = Some(complexity_from(&space));
86 annotated += 1;
87 }
88 annotated += collect_fns(&space, &canonical, builder, &fn_index, &fn_line_index);
89 }
90
91 Ok(annotated)
92}
93
94fn parse_metrics(path: &Path, src: Vec<u8>) -> Option<FuncSpace> {
95 match path.extension().and_then(|e| e.to_str()) {
96 Some("rs") => metrics(&RustParser::new(src, path, None), path),
97 Some("py") => metrics(&PythonParser::new(src, path, None), path),
98 Some("js") | Some("jsx") => metrics(&JavascriptParser::new(src, path, None), path),
99 Some("ts") => metrics(&TypescriptParser::new(src, path, None), path),
100 Some("tsx") => metrics(&TsxParser::new(src, path, None), path),
101 _ => None,
102 }
103}
104
105fn collect_fns(
106 space: &FuncSpace,
107 file: &str,
108 builder: &mut GraphBuilder,
109 fn_index: &HashMap<(String, String), usize>,
110 fn_line_index: &HashMap<(String, usize), usize>,
111) -> usize {
112 let mut count = 0;
113 if matches!(space.kind, SpaceKind::Function) {
114 let name = space.name.as_deref().unwrap_or("?");
115 let bare = name.split("::").last().unwrap_or(name);
116
117 let idx = fn_line_index
118 .get(&(file.to_owned(), space.start_line))
119 .copied()
120 .or_else(|| fn_index.get(&(file.to_owned(), bare.to_owned())).copied());
121
122 if let Some(idx) = idx {
123 builder.nodes_mut()[idx].complexity = Some(complexity_from(space));
124 count += 1;
125 }
126 }
127 for child in &space.spaces {
128 count += collect_fns(child, file, builder, fn_index, fn_line_index);
129 }
130 count
131}
132
133fn complexity_from(s: &FuncSpace) -> Complexity {
134 let m = &s.metrics;
135 let sloc = m.loc.sloc();
136 let vol = m.halstead.volume();
137
138 Complexity {
139 cyclomatic: m.cyclomatic.cyclomatic(),
140 cognitive: m.cognitive.cognitive(),
141 exits: m.nexits.exit(),
142 args: if m.nargs.fn_args() > 0.0 {
144 m.nargs.fn_args()
145 } else {
146 m.nargs.closure_args()
147 },
148 functions: m.nom.functions(),
149 closures: m.nom.closures(),
150 coupling: None, maintainability: Some(Maintainability {
152 mi: m.mi.mi_original(),
153 mi_sei: m.mi.mi_sei(),
154 }),
155 loc: if sloc > 0.0 {
156 Some(Loc {
157 source: sloc,
158 logical: m.loc.lloc(),
159 comments: m.loc.cloc(),
160 blank: m.loc.blank(),
161 })
162 } else {
163 None
164 },
165 halstead: if vol > 0.0 {
166 Some(Halstead {
167 length: m.halstead.length(),
168 vocabulary: (m.halstead.u_operators() + m.halstead.u_operands()),
169 volume: vol,
170 effort: m.halstead.effort(),
171 time: m.halstead.time(),
172 bugs: m.halstead.bugs(),
173 })
174 } else {
175 None
176 },
177 }
178}