1use std::path::Path;
39
40use cccc_core::engine;
41use cccc_core::ir::{LogicalOp, Node, SwitchCase};
42use cccc_core::report::FileReport;
43use syn::spanned::Spanned;
44use syn::visit::{self, Visit};
45use syn::{
46 BinOp, Expr, ExprBinary, ExprBreak, ExprCall, ExprClosure, ExprContinue, ExprForLoop, ExprIf,
47 ExprLoop, ExprMatch, ExprMethodCall, ExprWhile, ImplItemFn, ItemFn, Local, Pat, TraitItemFn,
48};
49
50pub const DEFAULT_EXTS: &[&str] = &["rs"];
52
53pub fn analyze_source(path: &Path, source: &str) -> FileReport {
57 let (nodes, parse_errors) = to_ir(path, source);
58 engine::analyze(&path.display().to_string(), &nodes, parse_errors)
59}
60
61pub fn to_ir(_path: &Path, source: &str) -> (Vec<Node>, Vec<String>) {
66 match syn::parse_file(source) {
67 Ok(file) => {
68 let mut builder = Builder::new();
69 for item in &file.items {
70 builder.visit_item(item);
71 }
72 (builder.finish(), Vec::new())
73 }
74 Err(e) => (Vec::new(), vec![e.to_string()]),
75 }
76}
77
78struct Builder {
80 stack: Vec<Vec<Node>>,
83 pending_name: Option<String>,
85}
86
87impl Builder {
88 fn new() -> Self {
89 Self {
90 stack: vec![Vec::new()], pending_name: None,
92 }
93 }
94
95 fn finish(mut self) -> Vec<Node> {
97 self.stack.pop().expect("module collector")
98 }
99
100 fn emit(&mut self, node: Node) {
102 self.stack.last_mut().expect("collector").push(node);
103 }
104
105 fn collect<F: FnOnce(&mut Self)>(&mut self, f: F) -> Vec<Node> {
107 self.stack.push(Vec::new());
108 f(self);
109 self.stack.pop().expect("collector")
110 }
111
112 fn emit_function<F: FnOnce(&mut Self)>(
114 &mut self,
115 name: String,
116 kind: &'static str,
117 line: u32,
118 walk: F,
119 ) {
120 let body = self.collect(walk);
121 self.emit(Node::Function {
122 name,
123 kind: kind.to_string(),
124 line,
125 body,
126 });
127 }
128
129 fn lower_if(&mut self, it: &ExprIf) -> Node {
131 let test = self.collect(|s| s.visit_expr(&it.cond));
132 let then = self.collect(|s| s.visit_block(&it.then_branch));
133 let alternate = it
134 .else_branch
135 .as_ref()
136 .map(|(_, alt)| Box::new(self.lower_alternate(alt)));
137 Node::Branch {
138 test,
139 then,
140 alternate,
141 }
142 }
143
144 fn lower_alternate(&mut self, expr: &Expr) -> Node {
146 match expr {
147 Expr::If(elif) => self.lower_if(elif),
148 other => Node::Group(self.collect(|s| s.visit_expr(other))),
149 }
150 }
151
152 fn collect_logical(&mut self, expr: &ExprBinary, op: LogicalOp, operands: &mut Vec<Node>) {
155 self.collect_logical_side(&expr.left, op, operands);
156 self.collect_logical_side(&expr.right, op, operands);
157 }
158
159 fn collect_logical_side(&mut self, side: &Expr, op: LogicalOp, operands: &mut Vec<Node>) {
160 match side {
161 Expr::Binary(inner) => match logical_op(&inner.op) {
162 Some(inner_op) if inner_op == op => self.collect_logical(inner, op, operands),
163 Some(inner_op) => {
164 let mut sub = Vec::new();
165 self.collect_logical(inner, inner_op, &mut sub);
166 operands.push(Node::Logical {
167 op: inner_op,
168 operands: sub,
169 });
170 }
171 None => operands.push(Node::Group(self.collect(|s| s.visit_expr(side)))),
172 },
173 Expr::Paren(p) => self.collect_logical_side(&p.expr, op, operands),
174 Expr::Group(g) => self.collect_logical_side(&g.expr, op, operands),
175 _ => operands.push(Node::Group(self.collect(|s| s.visit_expr(side)))),
176 }
177 }
178}
179
180impl<'ast> Visit<'ast> for Builder {
181 fn visit_item_fn(&mut self, it: &'ast ItemFn) {
182 let name = it.sig.ident.to_string();
183 let line = line_of(&it.sig.ident);
184 self.emit_function(name, "function", line, |s| visit::visit_item_fn(s, it));
185 }
186
187 fn visit_impl_item_fn(&mut self, it: &'ast ImplItemFn) {
188 let name = it.sig.ident.to_string();
189 let line = line_of(&it.sig.ident);
190 self.emit_function(name, "method", line, |s| visit::visit_impl_item_fn(s, it));
191 }
192
193 fn visit_trait_item_fn(&mut self, it: &'ast TraitItemFn) {
194 if it.default.is_some() {
197 let name = it.sig.ident.to_string();
198 let line = line_of(&it.sig.ident);
199 self.emit_function(name, "method", line, |s| visit::visit_trait_item_fn(s, it));
200 }
201 }
202
203 fn visit_local(&mut self, it: &'ast Local) {
204 if let Some(init) = &it.init
205 && matches!(&*init.expr, Expr::Closure(_))
206 {
207 self.pending_name = pat_name(&it.pat);
208 }
209 visit::visit_local(self, it);
210 }
211
212 fn visit_expr_closure(&mut self, it: &'ast ExprClosure) {
213 let name = self
214 .pending_name
215 .take()
216 .unwrap_or_else(|| "<closure>".to_string());
217 let line = line_of(it);
218 self.emit_function(name, "closure", line, |s| visit::visit_expr_closure(s, it));
219 }
220
221 fn visit_expr_if(&mut self, it: &'ast ExprIf) {
222 let node = self.lower_if(it);
223 self.emit(node);
224 }
225
226 fn visit_expr_match(&mut self, it: &'ast ExprMatch) {
227 let head = self.collect(|s| s.visit_expr(&it.expr));
230 for node in head {
231 self.emit(node);
232 }
233 let mut cases = Vec::new();
234 for arm in &it.arms {
235 let body = self.collect(|s| {
236 if let Some((_, guard)) = &arm.guard {
237 s.visit_expr(guard);
238 }
239 s.visit_expr(&arm.body);
240 });
241 cases.push(SwitchCase {
242 is_default: is_catch_all(&arm.pat),
243 body,
244 });
245 }
246 self.emit(Node::Switch { cases });
247 }
248
249 fn visit_expr_for_loop(&mut self, it: &'ast ExprForLoop) {
250 let body = self.collect(|s| visit::visit_expr_for_loop(s, it));
251 self.emit(Node::Loop { body });
252 }
253
254 fn visit_expr_while(&mut self, it: &'ast ExprWhile) {
255 let body = self.collect(|s| visit::visit_expr_while(s, it));
256 self.emit(Node::Loop { body });
257 }
258
259 fn visit_expr_loop(&mut self, it: &'ast ExprLoop) {
260 let body = self.collect(|s| visit::visit_expr_loop(s, it));
261 self.emit(Node::Loop { body });
262 }
263
264 fn visit_expr_break(&mut self, it: &'ast ExprBreak) {
265 self.emit(Node::Jump {
266 labeled: it.label.is_some(),
267 });
268 visit::visit_expr_break(self, it);
269 }
270
271 fn visit_expr_continue(&mut self, it: &'ast ExprContinue) {
272 self.emit(Node::Jump {
273 labeled: it.label.is_some(),
274 });
275 }
276
277 fn visit_expr_binary(&mut self, it: &'ast ExprBinary) {
278 match logical_op(&it.op) {
279 Some(op) => {
280 let mut operands = Vec::new();
281 self.collect_logical(it, op, &mut operands);
282 self.emit(Node::Logical { op, operands });
283 }
284 None => visit::visit_expr_binary(self, it),
285 }
286 }
287
288 fn visit_expr_call(&mut self, it: &'ast ExprCall) {
289 self.emit(Node::Call {
290 callee: call_path_name(&it.func),
291 });
292 visit::visit_expr_call(self, it);
293 }
294
295 fn visit_expr_method_call(&mut self, it: &'ast ExprMethodCall) {
296 self.emit(Node::Call {
297 callee: Some(it.method.to_string()),
298 });
299 visit::visit_expr_method_call(self, it);
300 }
301}
302
303fn line_of<T: Spanned>(node: &T) -> u32 {
305 node.span().start().line as u32
306}
307
308fn logical_op(op: &BinOp) -> Option<LogicalOp> {
311 match op {
312 BinOp::And(_) => Some(LogicalOp::And),
313 BinOp::Or(_) => Some(LogicalOp::Or),
314 _ => None,
315 }
316}
317
318fn is_catch_all(pat: &Pat) -> bool {
320 match pat {
321 Pat::Wild(_) => true,
322 Pat::Ident(p) => p.subpat.is_none(),
323 _ => false,
324 }
325}
326
327fn pat_name(pat: &Pat) -> Option<String> {
329 match pat {
330 Pat::Ident(p) => Some(p.ident.to_string()),
331 Pat::Type(p) => pat_name(&p.pat),
332 _ => None,
333 }
334}
335
336fn call_path_name(func: &Expr) -> Option<String> {
339 match func {
340 Expr::Path(p) => p.path.segments.last().map(|s| s.ident.to_string()),
341 _ => None,
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 fn analyze(src: &str) -> FileReport {
350 analyze_source(Path::new("test.rs"), src)
351 }
352
353 fn find<'a>(
354 fns: &'a [cccc_core::report::FunctionReport],
355 name: &str,
356 ) -> Option<&'a cccc_core::report::FunctionReport> {
357 for f in fns {
358 if f.name == name {
359 return Some(f);
360 }
361 if let Some(found) = find(&f.children, name) {
362 return Some(found);
363 }
364 }
365 None
366 }
367
368 fn cognitive_of(src: &str, name: &str) -> u32 {
369 find(&analyze(src).functions, name)
370 .unwrap_or_else(|| panic!("function {name} not found"))
371 .cognitive
372 }
373
374 fn cyclomatic_of(src: &str, name: &str) -> u32 {
375 find(&analyze(src).functions, name)
376 .unwrap_or_else(|| panic!("function {name} not found"))
377 .cyclomatic
378 }
379
380 #[test]
381 fn sonar_sum_of_primes_is_7() {
382 let src = r#"
383 fn sum_of_primes(max: u32) -> u32 {
384 let mut total = 0;
385 'out: for i in 1..=max {
386 for j in 2..i {
387 if i % j == 0 {
388 continue 'out;
389 }
390 }
391 total += i;
392 }
393 total
394 }
395 "#;
396 assert_eq!(cognitive_of(src, "sum_of_primes"), 7);
398 }
399
400 #[test]
401 fn sonar_get_words_is_1() {
402 let src = r#"
403 fn get_words(number: u32) -> &'static str {
404 match number {
405 1 => "one",
406 2 => "a couple",
407 _ => "lots",
408 }
409 }
410 "#;
411 assert_eq!(cognitive_of(src, "get_words"), 1);
412 assert_eq!(cyclomatic_of(src, "get_words"), 3);
414 }
415
416 #[test]
417 fn nested_if_adds_nesting() {
418 let src = r#"
419 fn f(a: bool, b: bool, c: bool) {
420 if a { if b { if c {} } }
421 }
422 "#;
423 assert_eq!(cognitive_of(src, "f"), 6);
424 }
425
426 #[test]
427 fn else_if_else_are_flat() {
428 let src = r#"
429 fn f(a: bool, b: bool) {
430 if a {} else if b {} else {}
431 }
432 "#;
433 assert_eq!(cognitive_of(src, "f"), 3);
434 }
435
436 #[test]
437 fn logical_sequences() {
438 let src = r#"
439 fn f(a: bool, b: bool, c: bool, d: bool) {
440 if a && b && c || d {}
441 }
442 "#;
443 assert_eq!(cognitive_of(src, "f"), 3);
445 assert_eq!(cyclomatic_of(src, "f"), 5);
447 }
448
449 #[test]
450 fn recursion_adds_one_per_call() {
451 let src = r#"
452 fn fib(n: u64) -> u64 {
453 if n < 2 { return n; }
454 fib(n - 1) + fib(n - 2)
455 }
456 "#;
457 assert_eq!(cognitive_of(src, "fib"), 3);
459 }
460
461 #[test]
462 fn method_recursion_is_detected() {
463 let src = r#"
464 struct S;
465 impl S {
466 fn walk(&self, n: u64) -> u64 {
467 if n == 0 { 0 } else { self.walk(n - 1) }
468 }
469 }
470 "#;
471 assert_eq!(cognitive_of(src, "walk"), 3);
473 }
474
475 #[test]
476 fn nested_function_is_independent_unit() {
477 let src = r#"
478 fn outer() {
479 fn inner() { if true {} }
480 }
481 "#;
482 assert_eq!(cognitive_of(src, "outer"), 0);
483 assert_eq!(cognitive_of(src, "inner"), 1);
484 }
485
486 #[test]
487 fn closure_is_its_own_unit() {
488 let src = r#"
489 fn host() {
490 let pick = |a: bool, b: bool| if a && b { 1 } else { 0 };
491 }
492 "#;
493 assert_eq!(cognitive_of(src, "host"), 0);
495 assert_eq!(cognitive_of(src, "pick"), 3);
497 }
498
499 #[test]
500 fn loops_all_count() {
501 let src = r#"
502 fn f() {
503 while true {}
504 for _ in 0..3 {}
505 loop { break; }
506 }
507 "#;
508 assert_eq!(cognitive_of(src, "f"), 3);
510 }
511
512 #[test]
513 fn cyclomatic_basic() {
514 let src = r#"
515 fn f(a: bool, b: bool) {
516 if a && b { for _ in 0..1 {} } else if b {}
517 }
518 "#;
519 assert_eq!(cyclomatic_of(src, "f"), 5);
521 }
522
523 #[test]
524 fn names_methods_and_closures() {
525 let src = r#"
526 fn free() {}
527 struct C;
528 impl C { fn method(&self) {} }
529 fn host() { let lambda = |x: u32| x + 1; }
530 "#;
531 let r = analyze(src);
532 assert_eq!(find(&r.functions, "free").unwrap().kind, "function");
533 assert_eq!(find(&r.functions, "method").unwrap().kind, "method");
534 assert_eq!(find(&r.functions, "lambda").unwrap().kind, "closure");
535 }
536
537 #[test]
538 fn file_total_sums_all_functions() {
539 let src = r#"
540 fn a() { if true {} }
541 fn b() { if true {} }
542 "#;
543 assert_eq!(analyze(src).cognitive, 2);
544 }
545
546 #[test]
547 fn parse_error_is_reported() {
548 let (nodes, errors) = to_ir(Path::new("bad.rs"), "fn f( {");
549 assert!(nodes.is_empty());
550 assert_eq!(errors.len(), 1);
551 }
552}