Skip to main content

code_split_complexity/
lib.rs

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, TsxParser,
8    TypescriptParser, metrics,
9};
10use walkdir::WalkDir;
11
12/// Walk all source files under `root` whose extension is in `extensions`,
13/// compute complexity metrics via rust-code-analysis, and annotate the
14/// file-level nodes in the graph (`File` nodes, and — before the Rust
15/// module→file collapse — file-backed `Module` nodes with `line == None`).
16/// Returns the number of nodes annotated.
17pub fn analyze(root: &Path, builder: &mut GraphBuilder) -> Result<usize> {
18    analyze_extensions(root, builder, &["rs"])
19}
20
21/// Same as `analyze` but for Python source files.
22pub fn analyze_python(root: &Path, builder: &mut GraphBuilder) -> Result<usize> {
23    analyze_extensions(root, builder, &["py"])
24}
25
26/// Same as `analyze` but for JavaScript / TypeScript source files.
27pub 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            // `File` nodes (Python/JS) and file-backed `Module` nodes (Rust,
41            // `line == None`) both represent a whole source file. Inline modules
42            // (`line.is_some()`) share the enclosing file's path and must not
43            // receive file-level metrics.
44            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        // fn_args > 0 → args = fn_args; otherwise use closure_args
107        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, // filled later in annotate_hk
115        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}