tokmd_analysis/
source_complexity.rs1#[path = "source_complexity/mask.rs"]
8mod mask;
9
10use mask::RustCodeMask;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct RustFunctionComplexitySummary {
15 pub total_complexity: u32,
17 pub max_complexity: u32,
19 pub function_count: usize,
21 pub max_function_length: usize,
23}
24
25pub trait SourceAnalyzer {
27 fn analyze_rust(&self, content: &str) -> RustFunctionComplexitySummary;
29}
30
31#[derive(Debug, Default, Clone, Copy)]
33pub struct RustAnalyzer;
34
35impl SourceAnalyzer for RustAnalyzer {
36 fn analyze_rust(&self, content: &str) -> RustFunctionComplexitySummary {
37 analyze_rust_function_complexity(content)
38 }
39}
40
41pub fn analyze_rust_function_complexity(content: &str) -> RustFunctionComplexitySummary {
46 let mut total_complexity: u32 = 0;
47 let mut max_complexity: u32 = 0;
48 let mut function_count: usize = 0;
49 let mut max_function_length: usize = 0;
50
51 let mut in_function = false;
52 let mut brace_depth: i32 = 0;
53 let mut function_brace_depth: i32 = 0;
54 let mut function_start_line: usize = 0;
55 let mut current_complexity: u32 = 1;
56 let mut mask = RustCodeMask::default();
57
58 for (line_idx, line) in content.lines().enumerate() {
59 let code_line = mask.code_only_line(line);
60 let trimmed = code_line.trim();
61
62 if trimmed.is_empty() {
63 continue;
64 }
65
66 let is_fn_start = !in_function && is_rust_fn_start(trimmed);
67
68 if is_fn_start {
69 in_function = true;
70 function_start_line = line_idx;
71 function_brace_depth = brace_depth;
72 current_complexity = 1;
73 }
74
75 for c in code_line.chars() {
76 if c == '{' {
77 brace_depth += 1;
78 } else if c == '}' {
79 brace_depth -= 1;
80 if in_function && brace_depth == function_brace_depth {
81 let function_length = line_idx - function_start_line + 1;
82 max_function_length = max_function_length.max(function_length);
83 total_complexity += current_complexity;
84 max_complexity = max_complexity.max(current_complexity);
85 function_count += 1;
86 in_function = false;
87 current_complexity = 1;
88 }
89 }
90 }
91
92 if in_function {
93 for kw in ["if ", "while ", "for ", "loop ", "match ", "&&", "||", "?"] {
94 let mut search_line = trimmed;
95 while let Some(pos) = search_line.find(kw) {
96 current_complexity += 1;
97 search_line = &search_line[pos + kw.len()..];
98 }
99 }
100
101 current_complexity += trimmed.matches("=>").count() as u32;
102 }
103 }
104
105 if in_function {
106 function_count += 1;
107 total_complexity += current_complexity;
108 max_complexity = max_complexity.max(current_complexity);
109 }
110
111 RustFunctionComplexitySummary {
112 total_complexity,
113 max_complexity,
114 function_count,
115 max_function_length,
116 }
117}
118
119fn is_rust_fn_start(trimmed: &str) -> bool {
120 let Some(fn_pos) = trimmed.find("fn ") else {
121 return false;
122 };
123
124 let mut rest = trimmed[..fn_pos].trim();
125 if rest.is_empty() {
126 return true;
127 }
128
129 while !rest.is_empty() {
130 rest = rest.trim_start();
131 if rest.is_empty() {
132 break;
133 }
134 if rest.starts_with("pub(") {
135 let Some(close) = rest.find(')') else {
136 return false;
137 };
138 rest = &rest[close + 1..];
139 } else if let Some(next) = rest.strip_prefix("pub") {
140 rest = next;
141 } else if let Some(next) = rest.strip_prefix("async") {
142 rest = next;
143 } else if let Some(next) = rest.strip_prefix("unsafe") {
144 rest = next;
145 } else if let Some(next) = rest.strip_prefix("const") {
146 rest = next;
147 } else if rest.starts_with("extern") {
148 rest = rest["extern".len()..].trim_start();
149 if rest.starts_with('"') {
150 let Some(close) = rest[1..].find('"') else {
151 return false;
152 };
153 rest = &rest[close + 2..];
154 }
155 } else {
156 return false;
157 }
158 }
159
160 true
161}
162
163#[cfg(test)]
164mod tests {
165 use super::{RustAnalyzer, SourceAnalyzer, analyze_rust_function_complexity};
166
167 #[test]
168 fn rust_complexity_counts_else_if_once() {
169 let analysis = analyze_rust_function_complexity(
170 r#"
171fn branchy(x: i32) -> i32 {
172 if x > 0 {
173 1
174 } else if x < 0 {
175 -1
176 } else if x == 0 {
177 0
178 } else {
179 42
180 }
181}
182"#,
183 );
184
185 assert_eq!(analysis.function_count, 1);
186 assert_eq!(analysis.total_complexity, 4);
187 assert_eq!(analysis.max_complexity, 4);
188 }
189
190 #[test]
191 fn rust_complexity_ignores_decisions_in_strings_and_comments() {
192 let analysis = analyze_rust_function_complexity(
193 r###"
194fn only_real_branch(flag: bool) {
195 let _normal = "if while for loop match && || ? => { }";
196 let _raw = r##"if while for loop match && || ? => { }"##;
197 let _char = '?';
198 /* if outer /* while nested */ match ignored => */
199 if flag {
200 println!("ok"); // else if ignored && ||
201 }
202}
203"###,
204 );
205
206 assert_eq!(analysis.function_count, 1);
207 assert_eq!(analysis.total_complexity, 2);
208 assert_eq!(analysis.max_complexity, 2);
209 }
210
211 #[test]
212 fn rust_complexity_counts_code_before_trailing_comment() {
213 let analysis = analyze_rust_function_complexity(
214 r#"
215fn branch_before_comment(flag: bool) {
216 if flag { return; } // if ignored && ||
217}
218"#,
219 );
220
221 assert_eq!(analysis.function_count, 1);
222 assert_eq!(analysis.total_complexity, 2);
223 assert_eq!(analysis.max_complexity, 2);
224 }
225
226 #[test]
227 fn rust_complexity_counts_match_arms() {
228 let analysis = analyze_rust_function_complexity(
229 r#"
230fn classify(x: i32) -> i32 {
231 match x {
232 0 => 0,
233 1 => 1,
234 _ => 2,
235 }
236}
237"#,
238 );
239
240 assert_eq!(analysis.function_count, 1);
241 assert_eq!(analysis.max_complexity, 5);
242 }
243
244 #[test]
245 fn rust_analyzer_trait_delegates_to_function_scoped_analysis() {
246 let analyzer = RustAnalyzer;
247 let analysis = analyzer.analyze_rust(
248 r#"
249fn first() {
250 if true {}
251}
252
253fn second() {
254 while false {}
255 for _ in 0..1 {}
256}
257"#,
258 );
259
260 assert_eq!(analysis.function_count, 2);
261 assert_eq!(analysis.total_complexity, 5);
262 assert_eq!(analysis.max_complexity, 3);
263 }
264}