tangled
alpha
login
or
join now
desertthunder.dev
/
mccabre
code complexity & repetition analysis tool
1
fork
atom
overview
issues
pulls
pipelines
cyclomatic complexity & loc metrics
desertthunder.dev
2 months ago
c3f42a3d
2e8923a1
+431
3 changed files
expand all
collapse all
unified
split
crates
core
src
complexity
cyclomatic.rs
loc.rs
mod.rs
+287
crates/core/src/complexity/cyclomatic.rs
···
1
1
+
use crate::Result;
2
2
+
use crate::tokenizer::{Language, Token, TokenType, Tokenizer};
3
3
+
use serde::{Deserialize, Serialize};
4
4
+
5
5
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6
6
+
pub enum Severity {
7
7
+
Low,
8
8
+
Moderate,
9
9
+
High,
10
10
+
VeryHigh,
11
11
+
}
12
12
+
13
13
+
/// Cyclomatic Complexity metrics for a file
14
14
+
#[derive(Debug, Clone, Serialize, Deserialize)]
15
15
+
pub struct CyclomaticMetrics {
16
16
+
/// Overall file complexity
17
17
+
pub file_complexity: usize,
18
18
+
/// Individual function complexities (if we can detect them)
19
19
+
pub functions: Vec<FunctionComplexity>,
20
20
+
}
21
21
+
22
22
+
#[derive(Debug, Clone, Serialize, Deserialize)]
23
23
+
pub struct FunctionComplexity {
24
24
+
/// Function name (if identifiable)
25
25
+
pub name: String,
26
26
+
/// Cyclomatic complexity value
27
27
+
pub complexity: usize,
28
28
+
/// Line number where function starts
29
29
+
pub line: usize,
30
30
+
}
31
31
+
32
32
+
impl CyclomaticMetrics {
33
33
+
/// Calculate cyclomatic complexity from source code
34
34
+
///
35
35
+
/// Uses the simplified formula: CC = number of decision points + 1
36
36
+
/// Decision points include: if, else if, while, for, loop, match/switch, case, catch, &&, ||, ?
37
37
+
pub fn calculate(source: &str, language: Language) -> Result<Self> {
38
38
+
let tokens = Tokenizer::new(source, language).tokenize()?;
39
39
+
let decision_points = tokens.iter().filter(|t| t.token_type.is_decision_point()).count();
40
40
+
let file_complexity = if decision_points == 0 { 1 } else { decision_points + 1 };
41
41
+
let functions = Self::detect_functions(&tokens, language);
42
42
+
43
43
+
Ok(CyclomaticMetrics { file_complexity, functions })
44
44
+
}
45
45
+
46
46
+
/// Attempt to detect function boundaries and calculate per-function complexity
47
47
+
///
48
48
+
/// Look for function patterns:
49
49
+
/// - Rust: "fn" identifier "(" ... ")" "{"
50
50
+
/// - JS/TS: "function" identifier "(" ... ")" "{"
51
51
+
/// - Go: "func" identifier "(" ... ")" "{"
52
52
+
/// - Java/C++: type identifier "(" ... ")" "{"
53
53
+
fn detect_functions(tokens: &[Token], _language: Language) -> Vec<FunctionComplexity> {
54
54
+
let mut functions = Vec::new();
55
55
+
let mut i = 0;
56
56
+
57
57
+
while i < tokens.len() {
58
58
+
let is_function_keyword = if let TokenType::Identifier(name) = &tokens[i].token_type {
59
59
+
name == "fn" || name == "func" || name == "function"
60
60
+
} else {
61
61
+
false
62
62
+
};
63
63
+
64
64
+
if is_function_keyword {
65
65
+
let mut name = "anonymous".to_string();
66
66
+
let line = tokens[i].line;
67
67
+
68
68
+
if i + 1 < tokens.len()
69
69
+
&& let TokenType::Identifier(id) = &tokens[i + 1].token_type
70
70
+
{
71
71
+
name = id.clone();
72
72
+
}
73
73
+
74
74
+
let body_start = Self::find_next_token(tokens, i, TokenType::LeftBrace);
75
75
+
76
76
+
if let Some(body_start_idx) = body_start
77
77
+
&& let Some(body_end_idx) = Self::find_matching_brace(tokens, body_start_idx)
78
78
+
{
79
79
+
let decision_points = tokens[body_start_idx..=body_end_idx]
80
80
+
.iter()
81
81
+
.filter(|t| t.token_type.is_decision_point())
82
82
+
.count();
83
83
+
84
84
+
let complexity = if decision_points == 0 { 1 } else { decision_points + 1 };
85
85
+
86
86
+
functions.push(FunctionComplexity { name, complexity, line });
87
87
+
88
88
+
i = body_end_idx + 1;
89
89
+
continue;
90
90
+
}
91
91
+
}
92
92
+
93
93
+
i += 1;
94
94
+
}
95
95
+
96
96
+
functions
97
97
+
}
98
98
+
99
99
+
/// Find the next token of a specific type
100
100
+
fn find_next_token(tokens: &[Token], start: usize, token_type: TokenType) -> Option<usize> {
101
101
+
tokens[start..]
102
102
+
.iter()
103
103
+
.position(|t| std::mem::discriminant(&t.token_type) == std::mem::discriminant(&token_type))
104
104
+
.map(|pos| start + pos)
105
105
+
}
106
106
+
107
107
+
/// Find the matching closing brace for an opening brace
108
108
+
fn find_matching_brace(tokens: &[Token], open_idx: usize) -> Option<usize> {
109
109
+
let mut depth = 0;
110
110
+
111
111
+
for (offset, token) in tokens[open_idx..].iter().enumerate() {
112
112
+
match token.token_type {
113
113
+
TokenType::LeftBrace => depth += 1,
114
114
+
TokenType::RightBrace => {
115
115
+
depth -= 1;
116
116
+
if depth == 0 {
117
117
+
return Some(open_idx + offset);
118
118
+
}
119
119
+
}
120
120
+
_ => {}
121
121
+
}
122
122
+
}
123
123
+
124
124
+
None
125
125
+
}
126
126
+
127
127
+
/// Get severity level based on complexity threshold
128
128
+
/// Standard thresholds from literature:
129
129
+
/// 1-10: Simple, low risk
130
130
+
/// 11-20: More complex, moderate risk
131
131
+
/// 21-50: Complex, high risk
132
132
+
/// 50+: Very complex, very high risk
133
133
+
pub fn severity(&self) -> Severity {
134
134
+
match self.file_complexity {
135
135
+
1..=10 => Severity::Low,
136
136
+
11..=20 => Severity::Moderate,
137
137
+
21..=50 => Severity::High,
138
138
+
_ => Severity::VeryHigh,
139
139
+
}
140
140
+
}
141
141
+
}
142
142
+
143
143
+
#[cfg(test)]
144
144
+
mod tests {
145
145
+
use super::*;
146
146
+
147
147
+
#[test]
148
148
+
fn test_simple_function() {
149
149
+
let source = r#"
150
150
+
fn simple() {
151
151
+
let x = 5;
152
152
+
return x;
153
153
+
}
154
154
+
"#;
155
155
+
let metrics = CyclomaticMetrics::calculate(source, Language::Rust).unwrap();
156
156
+
assert_eq!(metrics.file_complexity, 1);
157
157
+
assert_eq!(metrics.severity(), Severity::Low);
158
158
+
}
159
159
+
160
160
+
#[test]
161
161
+
fn test_single_if() {
162
162
+
let source = r#"
163
163
+
fn check(x: i32) {
164
164
+
if x > 5 {
165
165
+
println!("big");
166
166
+
}
167
167
+
}
168
168
+
"#;
169
169
+
let metrics = CyclomaticMetrics::calculate(source, Language::Rust).unwrap();
170
170
+
assert_eq!(metrics.file_complexity, 2);
171
171
+
}
172
172
+
173
173
+
#[test]
174
174
+
fn test_multiple_decision_points() {
175
175
+
let source = r#"
176
176
+
fn complex(x: i32, y: i32) {
177
177
+
if x > 0 && y > 0 {
178
178
+
while x < 10 {
179
179
+
x += 1;
180
180
+
}
181
181
+
} else if x < 0 {
182
182
+
for i in 0..5 {
183
183
+
println!("{}", i);
184
184
+
}
185
185
+
}
186
186
+
}
187
187
+
"#;
188
188
+
let metrics = CyclomaticMetrics::calculate(source, Language::Rust).unwrap();
189
189
+
assert_eq!(metrics.file_complexity, 6);
190
190
+
}
191
191
+
192
192
+
#[test]
193
193
+
fn test_ternary_operator() {
194
194
+
let source = r#"
195
195
+
let x = condition ? true_value : false_value;
196
196
+
let y = a && b ? c : d;
197
197
+
"#;
198
198
+
let metrics = CyclomaticMetrics::calculate(source, Language::JavaScript).unwrap();
199
199
+
assert_eq!(metrics.file_complexity, 4);
200
200
+
}
201
201
+
202
202
+
#[test]
203
203
+
fn test_switch_case() {
204
204
+
let source = r#"
205
205
+
switch (x) {
206
206
+
case 1:
207
207
+
break;
208
208
+
case 2:
209
209
+
break;
210
210
+
default:
211
211
+
break;
212
212
+
}
213
213
+
"#;
214
214
+
let metrics = CyclomaticMetrics::calculate(source, Language::JavaScript).unwrap();
215
215
+
assert!(metrics.file_complexity >= 4);
216
216
+
}
217
217
+
218
218
+
#[test]
219
219
+
fn test_function_detection_rust() {
220
220
+
let source = r#"
221
221
+
fn simple() {
222
222
+
let x = 5;
223
223
+
}
224
224
+
225
225
+
fn complex() {
226
226
+
if true {
227
227
+
while false {
228
228
+
loop { break; }
229
229
+
}
230
230
+
}
231
231
+
}
232
232
+
"#;
233
233
+
let metrics = CyclomaticMetrics::calculate(source, Language::Rust).unwrap();
234
234
+
235
235
+
if !metrics.functions.is_empty() {
236
236
+
for func in &metrics.functions {
237
237
+
assert!(func.complexity >= 1);
238
238
+
assert!(!func.name.is_empty());
239
239
+
}
240
240
+
}
241
241
+
}
242
242
+
243
243
+
#[test]
244
244
+
fn test_javascript_function() {
245
245
+
let source = r#"
246
246
+
function hello() {
247
247
+
if (x > 0) {
248
248
+
return true;
249
249
+
}
250
250
+
return false;
251
251
+
}
252
252
+
"#;
253
253
+
let metrics = CyclomaticMetrics::calculate(source, Language::JavaScript).unwrap();
254
254
+
assert!(!metrics.functions.is_empty());
255
255
+
assert_eq!(metrics.file_complexity, 2);
256
256
+
}
257
257
+
258
258
+
#[test]
259
259
+
fn test_severity_levels() {
260
260
+
assert_eq!(
261
261
+
CyclomaticMetrics { file_complexity: 5, functions: vec![] }.severity(),
262
262
+
Severity::Low
263
263
+
);
264
264
+
assert_eq!(
265
265
+
CyclomaticMetrics { file_complexity: 15, functions: vec![] }.severity(),
266
266
+
Severity::Moderate
267
267
+
);
268
268
+
assert_eq!(
269
269
+
CyclomaticMetrics { file_complexity: 25, functions: vec![] }.severity(),
270
270
+
Severity::High
271
271
+
);
272
272
+
assert_eq!(
273
273
+
CyclomaticMetrics { file_complexity: 100, functions: vec![] }.severity(),
274
274
+
Severity::VeryHigh
275
275
+
);
276
276
+
}
277
277
+
278
278
+
#[test]
279
279
+
fn test_logical_operators() {
280
280
+
let source = r#"
281
281
+
if (a && b && c) { }
282
282
+
if (x || y || z) { }
283
283
+
"#;
284
284
+
let metrics = CyclomaticMetrics::calculate(source, Language::JavaScript).unwrap();
285
285
+
assert_eq!(metrics.file_complexity, 7);
286
286
+
}
287
287
+
}
+139
crates/core/src/complexity/loc.rs
···
1
1
+
use crate::Result;
2
2
+
use crate::tokenizer::{Language, TokenType, Tokenizer};
3
3
+
use serde::{Deserialize, Serialize};
4
4
+
5
5
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6
6
+
enum LineKind {
7
7
+
Code,
8
8
+
Comment,
9
9
+
Blank,
10
10
+
}
11
11
+
12
12
+
/// Lines of Code metrics for a single file
13
13
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14
14
+
pub struct LocMetrics {
15
15
+
/// Total number of lines in the file
16
16
+
pub physical: usize,
17
17
+
/// Number of non-blank, non-comment lines
18
18
+
pub logical: usize,
19
19
+
/// Number of comment lines
20
20
+
pub comments: usize,
21
21
+
/// Number of blank lines
22
22
+
pub blank: usize,
23
23
+
}
24
24
+
25
25
+
impl LocMetrics {
26
26
+
pub fn calculate(source: &str, language: Language) -> Result<Self> {
27
27
+
let tokens = Tokenizer::new(source, language).tokenize()?;
28
28
+
let physical = if source.is_empty() { 0 } else { source.split('\n').count() };
29
29
+
let mut line_types = vec![LineKind::Blank; physical];
30
30
+
31
31
+
for token in &tokens {
32
32
+
let line_idx = token.line.saturating_sub(1);
33
33
+
if line_idx >= line_types.len() {
34
34
+
continue;
35
35
+
}
36
36
+
37
37
+
match token.token_type {
38
38
+
_ if token.token_type.is_significant() => {
39
39
+
line_types[line_idx] = LineKind::Code;
40
40
+
}
41
41
+
TokenType::Comment => {
42
42
+
if line_types[line_idx] != LineKind::Code {
43
43
+
line_types[line_idx] = LineKind::Comment;
44
44
+
}
45
45
+
}
46
46
+
_ => {}
47
47
+
}
48
48
+
}
49
49
+
50
50
+
for (idx, line) in source.lines().enumerate() {
51
51
+
if line.trim().is_empty() && idx < line_types.len() {
52
52
+
line_types[idx] = LineKind::Blank;
53
53
+
}
54
54
+
}
55
55
+
56
56
+
let comments = line_types.iter().filter(|&&t| t == LineKind::Comment).count();
57
57
+
let blank = line_types.iter().filter(|&&t| t == LineKind::Blank).count();
58
58
+
let logical = physical - comments - blank;
59
59
+
60
60
+
Ok(LocMetrics { physical, logical, comments, blank })
61
61
+
}
62
62
+
}
63
63
+
64
64
+
#[cfg(test)]
65
65
+
mod tests {
66
66
+
use super::*;
67
67
+
68
68
+
#[test]
69
69
+
fn test_empty_file() {
70
70
+
let source = "";
71
71
+
let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
72
72
+
assert_eq!(metrics.physical, 0);
73
73
+
assert_eq!(metrics.logical, 0);
74
74
+
}
75
75
+
76
76
+
#[test]
77
77
+
fn test_simple_code() {
78
78
+
let source = r#"
79
79
+
fn main() {
80
80
+
println!("Hello");
81
81
+
}
82
82
+
"#;
83
83
+
let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
84
84
+
assert_eq!(metrics.physical, 5);
85
85
+
assert!(metrics.logical >= 2);
86
86
+
assert!(metrics.blank >= 1);
87
87
+
}
88
88
+
89
89
+
#[test]
90
90
+
fn test_comments() {
91
91
+
let source = r#"
92
92
+
// This is a comment
93
93
+
/* Multi-line
94
94
+
comment */
95
95
+
let x = 5; // inline comment
96
96
+
"#;
97
97
+
let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
98
98
+
assert!(metrics.comments >= 2);
99
99
+
assert!(metrics.logical >= 1);
100
100
+
}
101
101
+
102
102
+
#[test]
103
103
+
fn test_blank_lines() {
104
104
+
let source = r#"
105
105
+
106
106
+
107
107
+
fn test() {}
108
108
+
109
109
+
110
110
+
"#;
111
111
+
let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
112
112
+
assert!(metrics.blank >= 4);
113
113
+
assert!(metrics.logical >= 1);
114
114
+
}
115
115
+
116
116
+
#[test]
117
117
+
fn test_javascript() {
118
118
+
let source = r#"
119
119
+
function hello() {
120
120
+
console.log("Hello");
121
121
+
}
122
122
+
"#;
123
123
+
let metrics = LocMetrics::calculate(source, Language::JavaScript).unwrap();
124
124
+
assert_eq!(metrics.physical, 5);
125
125
+
assert!(metrics.logical >= 2);
126
126
+
}
127
127
+
128
128
+
#[test]
129
129
+
fn test_all_comments() {
130
130
+
let source = r#"
131
131
+
// Comment 1
132
132
+
// Comment 2
133
133
+
/* Comment 3 */
134
134
+
"#;
135
135
+
let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
136
136
+
assert!(metrics.comments >= 3);
137
137
+
assert_eq!(metrics.logical, 0);
138
138
+
}
139
139
+
}
+5
crates/core/src/complexity/mod.rs
···
1
1
+
pub mod cyclomatic;
2
2
+
pub mod loc;
3
3
+
4
4
+
pub use cyclomatic::{CyclomaticMetrics, FunctionComplexity, Severity};
5
5
+
pub use loc::LocMetrics;