1use crate::ir::Node;
28use crate::report::{FileReport, FunctionReport};
29
30struct Frame {
36 name: String,
37 kind: String,
38 line: u32,
39 cognitive: u32,
40 cyclomatic: u32,
41 nesting: u32,
42 children: Vec<FunctionReport>,
43}
44
45struct Engine {
46 stack: Vec<Frame>,
48}
49
50impl Engine {
51 fn new() -> Self {
52 let module = Frame {
53 name: "<module>".to_string(),
54 kind: "module".to_string(),
55 line: 1,
56 cognitive: 0,
57 cyclomatic: 0,
58 nesting: 0,
59 children: Vec::new(),
60 };
61 Self {
62 stack: vec![module],
63 }
64 }
65
66 fn top(&mut self) -> &mut Frame {
67 self.stack.last_mut().expect("stack never empty")
68 }
69
70 fn top_nesting(&self) -> u32 {
71 self.stack.last().expect("stack never empty").nesting
72 }
73
74 fn add_cognitive(&mut self, amount: u32) {
75 self.top().cognitive += amount;
76 }
77
78 fn add_cyclomatic(&mut self) {
79 self.top().cyclomatic += 1;
80 }
81
82 fn enter_nesting(&mut self) {
83 self.top().nesting += 1;
84 }
85
86 fn leave_nesting(&mut self) {
87 self.top().nesting -= 1;
88 }
89
90 fn walk(&mut self, nodes: &[Node]) {
92 for node in nodes {
93 self.visit(node);
94 }
95 }
96
97 fn nested(&mut self, add_cyclomatic: bool, body: &[Node]) {
102 let n = self.top_nesting();
103 self.add_cognitive(1 + n);
104 if add_cyclomatic {
105 self.add_cyclomatic();
106 }
107 self.enter_nesting();
108 self.walk(body);
109 self.leave_nesting();
110 }
111
112 fn visit(&mut self, node: &Node) {
113 match node {
114 Node::Function {
115 name,
116 kind,
117 line,
118 body,
119 } => self.visit_function(name, kind, *line, body),
120 Node::Branch {
121 test,
122 then,
123 alternate,
124 } => self.visit_branch(test, then, alternate),
125 Node::Conditional {
126 test,
127 then,
128 alternate,
129 } => {
130 let n = self.top_nesting();
131 self.add_cognitive(1 + n);
132 self.add_cyclomatic();
133 self.walk(test);
134 self.enter_nesting();
135 self.walk(then);
136 self.walk(alternate);
137 self.leave_nesting();
138 }
139 Node::Loop { body } => self.nested(true, body),
140 Node::Catch { body } => self.nested(true, body),
141 Node::Switch { cases } => {
142 let n = self.top_nesting();
143 self.add_cognitive(1 + n);
144 self.enter_nesting();
145 for case in cases {
146 if !case.is_default {
147 self.add_cyclomatic(); }
149 self.walk(&case.body);
150 }
151 self.leave_nesting();
152 }
153 Node::Jump { labeled } => {
154 if *labeled {
155 self.add_cognitive(1);
156 }
157 }
158 Node::Logical { operands, .. } => self.visit_logical(operands),
159 Node::Call { callee } => self.visit_call(callee.as_deref()),
160 Node::Group(children) => self.walk(children),
161 }
162 }
163
164 fn visit_function(&mut self, name: &str, kind: &str, line: u32, body: &[Node]) {
165 self.stack.push(Frame {
166 name: name.to_string(),
167 kind: kind.to_string(),
168 line,
169 cognitive: 0,
170 cyclomatic: 1, nesting: 0,
172 children: Vec::new(),
173 });
174 self.walk(body);
175 let frame = self.stack.pop().expect("function frame");
176 let report = FunctionReport {
177 name: frame.name,
178 kind: frame.kind,
179 line: frame.line,
180 cognitive: frame.cognitive,
181 cyclomatic: frame.cyclomatic,
182 children: frame.children,
183 };
184 self.top().children.push(report);
185 }
186
187 fn visit_branch(&mut self, test: &[Node], then: &[Node], alternate: &Option<Box<Node>>) {
190 let n = self.top_nesting();
191 self.add_cognitive(1 + n);
192 self.add_cyclomatic();
193 self.walk(test);
194 self.enter_nesting();
195 self.walk(then);
196 self.leave_nesting();
197 self.visit_alternate(alternate);
198 }
199
200 fn visit_alternate(&mut self, alternate: &Option<Box<Node>>) {
201 let Some(node) = alternate else { return };
202 match node.as_ref() {
203 Node::Branch {
205 test,
206 then,
207 alternate,
208 } => {
209 self.add_cognitive(1);
210 self.add_cyclomatic();
211 self.walk(test);
212 self.enter_nesting();
213 self.walk(then);
214 self.leave_nesting();
215 self.visit_alternate(alternate);
216 }
217 other => {
219 self.add_cognitive(1);
220 self.enter_nesting();
221 self.visit(other);
222 self.leave_nesting();
223 }
224 }
225 }
226
227 fn visit_logical(&mut self, operands: &[Node]) {
230 self.add_cognitive(1);
231 for _ in 1..operands.len() {
232 self.add_cyclomatic();
233 }
234 self.walk(operands);
235 }
236
237 fn visit_call(&mut self, callee: Option<&str>) {
238 if let Some(name) = callee
239 && let Some(top) = self.stack.last()
240 && top.kind != "module"
241 && top.name == name
242 {
243 self.add_cognitive(1); }
245 }
246}
247
248fn sum_tree(fns: &[FunctionReport], cog: &mut u32, cyc: &mut u32) {
250 for f in fns {
251 *cog += f.cognitive;
252 *cyc += f.cyclomatic;
253 sum_tree(&f.children, cog, cyc);
254 }
255}
256
257pub fn analyze(path: &str, nodes: &[Node], parse_errors: Vec<String>) -> FileReport {
261 let mut engine = Engine::new();
262 engine.walk(nodes);
263 let module = engine.stack.pop().expect("module frame");
264
265 let functions = module.children;
266 let mut cognitive = module.cognitive;
267 let mut cyclomatic = module.cyclomatic;
268 sum_tree(&functions, &mut cognitive, &mut cyclomatic);
269
270 FileReport {
271 path: path.to_string(),
272 cognitive,
273 cyclomatic,
274 functions,
275 parse_errors,
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::ir::{LogicalOp, Node, SwitchCase};
283
284 fn func(name: &str, kind: &str, body: Vec<Node>) -> Node {
285 Node::Function {
286 name: name.into(),
287 kind: kind.into(),
288 line: 1,
289 body,
290 }
291 }
292 fn find<'a>(fns: &'a [FunctionReport], name: &str) -> Option<&'a FunctionReport> {
293 for f in fns {
294 if f.name == name {
295 return Some(f);
296 }
297 if let Some(found) = find(&f.children, name) {
298 return Some(found);
299 }
300 }
301 None
302 }
303 fn cog(report: &FileReport, name: &str) -> u32 {
304 find(&report.functions, name).expect("function").cognitive
305 }
306 fn cyc(report: &FileReport, name: &str) -> u32 {
307 find(&report.functions, name).expect("function").cyclomatic
308 }
309
310 #[test]
312 fn sonar_sum_of_primes_is_7() {
313 let inner_if = Node::Branch {
314 test: vec![],
315 then: vec![Node::Jump { labeled: true }],
316 alternate: None,
317 };
318 let inner_for = Node::Loop {
319 body: vec![inner_if],
320 };
321 let outer_for = Node::Loop {
322 body: vec![inner_for],
323 };
324 let f = func("sumOfPrimes", "function", vec![outer_for]);
325 let r = analyze("t", &[f], vec![]);
326 assert_eq!(cog(&r, "sumOfPrimes"), 7);
328 }
329
330 #[test]
331 fn sonar_get_words_is_1() {
332 let sw = Node::Switch {
333 cases: vec![
334 SwitchCase {
335 is_default: false,
336 body: vec![],
337 },
338 SwitchCase {
339 is_default: false,
340 body: vec![],
341 },
342 SwitchCase {
343 is_default: true,
344 body: vec![],
345 },
346 ],
347 };
348 let r = analyze("t", &[func("getWords", "function", vec![sw])], vec![]);
349 assert_eq!(cog(&r, "getWords"), 1);
350 assert_eq!(cyc(&r, "getWords"), 3);
352 }
353
354 #[test]
355 fn nested_if_adds_nesting() {
356 let deep = Node::Branch {
357 test: vec![],
358 then: vec![Node::Branch {
359 test: vec![],
360 then: vec![Node::Branch {
361 test: vec![],
362 then: vec![],
363 alternate: None,
364 }],
365 alternate: None,
366 }],
367 alternate: None,
368 };
369 let r = analyze("t", &[func("f", "function", vec![deep])], vec![]);
370 assert_eq!(cog(&r, "f"), 6); }
372
373 #[test]
374 fn else_if_else_are_flat() {
375 let chain = Node::Branch {
377 test: vec![],
378 then: vec![],
379 alternate: Some(Box::new(Node::Branch {
380 test: vec![],
381 then: vec![],
382 alternate: Some(Box::new(Node::Group(vec![]))),
383 })),
384 };
385 let r = analyze("t", &[func("f", "function", vec![chain])], vec![]);
386 assert_eq!(cog(&r, "f"), 3); }
388
389 #[test]
390 fn logical_sequences() {
391 let inner_and = Node::Logical {
393 op: LogicalOp::And,
394 operands: vec![
395 Node::Group(vec![]),
396 Node::Group(vec![]),
397 Node::Group(vec![]),
398 ],
399 };
400 let outer_or = Node::Logical {
401 op: LogicalOp::Or,
402 operands: vec![inner_and, Node::Group(vec![])],
403 };
404 let branch = Node::Branch {
405 test: vec![outer_or],
406 then: vec![],
407 alternate: None,
408 };
409 let r = analyze("t", &[func("f", "function", vec![branch])], vec![]);
410 assert_eq!(cog(&r, "f"), 3);
411 assert_eq!(cyc(&r, "f"), 5);
413 }
414
415 #[test]
416 fn recursion_adds_one() {
417 let body = vec![
419 Node::Branch {
420 test: vec![],
421 then: vec![],
422 alternate: None,
423 },
424 Node::Call {
425 callee: Some("fib".into()),
426 },
427 ];
428 let r = analyze("t", &[func("fib", "function", body)], vec![]);
429 assert_eq!(cog(&r, "fib"), 2);
430 }
431
432 #[test]
433 fn nested_function_is_independent_unit() {
434 let inner = func(
435 "inner",
436 "function",
437 vec![Node::Branch {
438 test: vec![],
439 then: vec![],
440 alternate: None,
441 }],
442 );
443 let outer = func("outer", "function", vec![inner]);
444 let r = analyze("t", &[outer], vec![]);
445 assert_eq!(cog(&r, "outer"), 0); assert_eq!(cog(&r, "inner"), 1);
447 }
448
449 #[test]
450 fn file_total_sums_all_functions() {
451 let a = func(
452 "a",
453 "function",
454 vec![Node::Branch {
455 test: vec![],
456 then: vec![],
457 alternate: None,
458 }],
459 );
460 let b = func(
461 "b",
462 "function",
463 vec![Node::Branch {
464 test: vec![],
465 then: vec![],
466 alternate: None,
467 }],
468 );
469 let r = analyze("t", &[a, b], vec![]);
470 assert_eq!(r.cognitive, 2);
471 }
472}