code_split_complexity/
lib.rs1use 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, TsxParser,
8 TypescriptParser, metrics,
9};
10use walkdir::WalkDir;
11
12pub fn analyze(root: &Path, builder: &mut GraphBuilder) -> Result<usize> {
18 analyze_extensions(root, builder, &["rs"])
19}
20
21pub fn analyze_python(root: &Path, builder: &mut GraphBuilder) -> Result<usize> {
23 analyze_extensions(root, builder, &["py"])
24}
25
26pub fn analyze_js(root: &Path, builder: &mut GraphBuilder) -> Result<usize> {
28 analyze_extensions(root, builder, &["js", "jsx", "ts", "tsx"])
29}
30
31fn analyze_extensions(
32 root: &Path,
33 builder: &mut GraphBuilder,
34 extensions: &[&str],
35) -> Result<usize> {
36 let mut file_index: HashMap<String, usize> = HashMap::new();
37
38 for (i, node) in builder.nodes().iter().enumerate() {
39 match node.kind {
40 NodeKind::File => {
45 file_index.insert(node.path.clone(), i);
46 }
47 NodeKind::Module if node.line.is_none() => {
48 file_index.entry(node.path.clone()).or_insert(i);
49 }
50 _ => {}
51 }
52 }
53
54 let mut annotated = 0usize;
55
56 for entry in WalkDir::new(root)
57 .into_iter()
58 .filter_map(|e| e.ok())
59 .filter(|e| {
60 e.file_type().is_file()
61 && e.path()
62 .extension()
63 .and_then(|x| x.to_str())
64 .is_some_and(|x| extensions.contains(&x))
65 })
66 {
67 let path = entry.path();
68 let Ok(src) = std::fs::read(path) else {
69 continue;
70 };
71 let canonical = path.to_string_lossy().into_owned();
72
73 let Some(space) = parse_metrics(path, src) else {
74 continue;
75 };
76
77 if let Some(&idx) = file_index.get(&canonical) {
78 builder.nodes_mut()[idx].complexity = Some(complexity_from(&space));
79 annotated += 1;
80 }
81 }
82
83 Ok(annotated)
84}
85
86fn parse_metrics(path: &Path, src: Vec<u8>) -> Option<FuncSpace> {
87 match path.extension().and_then(|e| e.to_str()) {
88 Some("rs") => metrics(&RustParser::new(src, path, None), path),
89 Some("py") => metrics(&PythonParser::new(src, path, None), path),
90 Some("js") | Some("jsx") => metrics(&JavascriptParser::new(src, path, None), path),
91 Some("ts") => metrics(&TypescriptParser::new(src, path, None), path),
92 Some("tsx") => metrics(&TsxParser::new(src, path, None), path),
93 _ => None,
94 }
95}
96
97fn complexity_from(s: &FuncSpace) -> Complexity {
98 let m = &s.metrics;
99 let sloc = m.loc.sloc();
100 let vol = m.halstead.volume();
101
102 Complexity {
103 cyclomatic: m.cyclomatic.cyclomatic(),
104 cognitive: m.cognitive.cognitive(),
105 exits: m.nexits.exit(),
106 args: if m.nargs.fn_args() > 0.0 {
108 m.nargs.fn_args()
109 } else {
110 m.nargs.closure_args()
111 },
112 functions: m.nom.functions(),
113 closures: m.nom.closures(),
114 coupling: None, maintainability: Some(Maintainability {
116 mi: m.mi.mi_original(),
117 mi_sei: m.mi.mi_sei(),
118 }),
119 loc: if sloc > 0.0 {
120 Some(Loc {
121 source: sloc,
122 logical: m.loc.lloc(),
123 comments: m.loc.cloc(),
124 blank: m.loc.blank(),
125 })
126 } else {
127 None
128 },
129 halstead: if vol > 0.0 {
130 Some(Halstead {
131 length: m.halstead.length(),
132 vocabulary: (m.halstead.u_operators() + m.halstead.u_operands()),
133 volume: vol,
134 effort: m.halstead.effort(),
135 time: m.halstead.time(),
136 bugs: m.halstead.bugs(),
137 })
138 } else {
139 None
140 },
141 }
142}