1use serde::Serialize;
2use serde::ser::{SerializeStruct, Serializer};
3use std::fmt;
4
5use crate::checker::Checker;
6use crate::macros::implement_metric_trait;
7use crate::*;
8
9#[derive(Debug, Clone)]
11pub struct Stats {
12 cyclomatic_sum: f64,
13 cyclomatic: f64,
14 n: usize,
15 cyclomatic_max: f64,
16 cyclomatic_min: f64,
17}
18
19impl Default for Stats {
20 fn default() -> Self {
21 Self {
22 cyclomatic_sum: 0.,
23 cyclomatic: 1.,
24 n: 1,
25 cyclomatic_max: 0.,
26 cyclomatic_min: f64::MAX,
27 }
28 }
29}
30
31impl Serialize for Stats {
32 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
33 where
34 S: Serializer,
35 {
36 let mut st = serializer.serialize_struct("cyclomatic", 4)?;
37 st.serialize_field("sum", &self.cyclomatic_sum())?;
38 st.serialize_field("average", &self.cyclomatic_average())?;
39 st.serialize_field("min", &self.cyclomatic_min())?;
40 st.serialize_field("max", &self.cyclomatic_max())?;
41 st.end()
42 }
43}
44
45impl fmt::Display for Stats {
46 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47 write!(
48 f,
49 "sum: {}, average: {}, min: {}, max: {}",
50 self.cyclomatic_sum(),
51 self.cyclomatic_average(),
52 self.cyclomatic_min(),
53 self.cyclomatic_max()
54 )
55 }
56}
57
58impl Stats {
59 pub fn merge(&mut self, other: &Stats) {
61 self.cyclomatic_max = self.cyclomatic_max.max(other.cyclomatic_max);
63 self.cyclomatic_min = self.cyclomatic_min.min(other.cyclomatic_min);
64
65 self.cyclomatic_sum += other.cyclomatic_sum;
66 self.n += other.n;
67 }
68
69 pub fn cyclomatic(&self) -> f64 {
71 self.cyclomatic
72 }
73 pub fn cyclomatic_sum(&self) -> f64 {
75 self.cyclomatic_sum
76 }
77
78 pub fn cyclomatic_average(&self) -> f64 {
83 self.cyclomatic_sum() / self.n as f64
84 }
85 pub fn cyclomatic_max(&self) -> f64 {
87 self.cyclomatic_max
88 }
89 pub fn cyclomatic_min(&self) -> f64 {
91 self.cyclomatic_min
92 }
93 #[inline(always)]
94 pub(crate) fn compute_sum(&mut self) {
95 self.cyclomatic_sum += self.cyclomatic;
96 }
97 #[inline(always)]
98 pub(crate) fn compute_minmax(&mut self) {
99 self.cyclomatic_max = self.cyclomatic_max.max(self.cyclomatic);
100 self.cyclomatic_min = self.cyclomatic_min.min(self.cyclomatic);
101 self.compute_sum();
102 }
103}
104
105pub trait Cyclomatic
106where
107 Self: Checker,
108{
109 fn compute(node: &Node, stats: &mut Stats);
110}
111
112impl Cyclomatic for PythonCode {
113 fn compute(node: &Node, stats: &mut Stats) {
114 use Python::*;
115
116 match node.kind_id().into() {
117 If | Elif | For | While | Except | With | Assert | And | Or => {
118 stats.cyclomatic += 1.;
119 }
120 Else => {
121 if node.has_ancestors(
122 |node| matches!(node.kind_id().into(), ForStatement | WhileStatement),
123 |node| node.kind_id() == ElseClause,
124 ) {
125 stats.cyclomatic += 1.;
126 }
127 }
128 _ => {}
129 }
130 }
131}
132
133impl Cyclomatic for JavascriptCode {
134 fn compute(node: &Node, stats: &mut Stats) {
135 use Javascript::*;
136
137 match node.kind_id().into() {
138 If | For | While | Case | Catch | TernaryExpression | AMPAMP | PIPEPIPE => {
139 stats.cyclomatic += 1.;
140 }
141 _ => {}
142 }
143 }
144}
145
146impl Cyclomatic for TypescriptCode {
147 fn compute(node: &Node, stats: &mut Stats) {
148 use Typescript::*;
149
150 match node.kind_id().into() {
151 If | For | While | Case | Catch | TernaryExpression | AMPAMP | PIPEPIPE => {
152 stats.cyclomatic += 1.;
153 }
154 _ => {}
155 }
156 }
157}
158
159impl Cyclomatic for TsxCode {
160 fn compute(node: &Node, stats: &mut Stats) {
161 use Tsx::*;
162
163 match node.kind_id().into() {
164 If | For | While | Case | Catch | TernaryExpression | AMPAMP | PIPEPIPE => {
165 stats.cyclomatic += 1.;
166 }
167 _ => {}
168 }
169 }
170}
171
172impl Cyclomatic for RustCode {
173 fn compute(node: &Node, stats: &mut Stats) {
174 use Rust::*;
175
176 match node.kind_id().into() {
177 If | For | While | Loop | MatchArm | MatchArm2 | TryExpression | AMPAMP | PIPEPIPE => {
178 stats.cyclomatic += 1.;
179 }
180 _ => {}
181 }
182 }
183}
184
185impl Cyclomatic for CppCode {
186 fn compute(node: &Node, stats: &mut Stats) {
187 use Cpp::*;
188
189 match node.kind_id().into() {
190 If | For | While | Case | Catch | ConditionalExpression | AMPAMP | PIPEPIPE => {
191 stats.cyclomatic += 1.;
192 }
193 _ => {}
194 }
195 }
196}
197
198impl Cyclomatic for JavaCode {
199 fn compute(node: &Node, stats: &mut Stats) {
200 use Java::*;
201
202 match node.kind_id().into() {
203 If | For | While | Case | Catch | TernaryExpression | AMPAMP | PIPEPIPE => {
204 stats.cyclomatic += 1.;
205 }
206 _ => {}
207 }
208 }
209}
210
211implement_metric_trait!(Cyclomatic, KotlinCode, PreprocCode, CcommentCode);
212
213#[cfg(test)]
214mod tests {
215 use crate::tools::check_metrics;
216
217 use super::*;
218
219 #[test]
220 fn python_simple_function() {
221 check_metrics::<PythonParser>(
222 "def f(a, b): # +2 (+1 unit space)
223 if a and b: # +2 (+1 and)
224 return 1
225 if c and d: # +2 (+1 and)
226 return 1",
227 "foo.py",
228 |metric| {
229 insta::assert_json_snapshot!(
231 metric.cyclomatic,
232 @r###"
233 {
234 "sum": 6.0,
235 "average": 3.0,
236 "min": 1.0,
237 "max": 5.0
238 }"###
239 );
240 },
241 );
242 }
243
244 #[test]
245 fn python_1_level_nesting() {
246 check_metrics::<PythonParser>(
247 "def f(a, b): # +2 (+1 unit space)
248 if a: # +1
249 for i in range(b): # +1
250 return 1",
251 "foo.py",
252 |metric| {
253 insta::assert_json_snapshot!(
255 metric.cyclomatic,
256 @r###"
257 {
258 "sum": 4.0,
259 "average": 2.0,
260 "min": 1.0,
261 "max": 3.0
262 }"###
263 );
264 },
265 );
266 }
267
268 #[test]
269 fn rust_1_level_nesting() {
270 check_metrics::<RustParser>(
271 "fn f() { // +2 (+1 unit space)
272 if true { // +1
273 match true {
274 true => println!(\"test\"), // +1
275 false => println!(\"test\"), // +1
276 }
277 }
278 }",
279 "foo.rs",
280 |metric| {
281 insta::assert_json_snapshot!(
283 metric.cyclomatic,
284 @r###"
285 {
286 "sum": 5.0,
287 "average": 2.5,
288 "min": 1.0,
289 "max": 4.0
290 }"###
291 );
292 },
293 );
294 }
295
296 #[test]
297 fn c_switch() {
298 check_metrics::<CppParser>(
299 "void f() { // +2 (+1 unit space)
300 switch (1) {
301 case 1: // +1
302 printf(\"one\");
303 break;
304 case 2: // +1
305 printf(\"two\");
306 break;
307 case 3: // +1
308 printf(\"three\");
309 break;
310 default:
311 printf(\"all\");
312 break;
313 }
314 }",
315 "foo.c",
316 |metric| {
317 insta::assert_json_snapshot!(
319 metric.cyclomatic,
320 @r###"
321 {
322 "sum": 5.0,
323 "average": 2.5,
324 "min": 1.0,
325 "max": 4.0
326 }"###
327 );
328 },
329 );
330 }
331
332 #[test]
333 fn c_real_function() {
334 check_metrics::<CppParser>(
335 "int sumOfPrimes(int max) { // +2 (+1 unit space)
336 int total = 0;
337 OUT: for (int i = 1; i <= max; ++i) { // +1
338 for (int j = 2; j < i; ++j) { // +1
339 if (i % j == 0) { // +1
340 continue OUT;
341 }
342 }
343 total += i;
344 }
345 return total;
346 }",
347 "foo.c",
348 |metric| {
349 insta::assert_json_snapshot!(
351 metric.cyclomatic,
352 @r###"
353 {
354 "sum": 5.0,
355 "average": 2.5,
356 "min": 1.0,
357 "max": 4.0
358 }"###
359 );
360 },
361 );
362 }
363
364 #[test]
365 fn c_unit_before() {
366 check_metrics::<CppParser>(
367 "
368 int a=42;
369 if(a==42) //+2(+1 unit space)
370 {
371
372 }
373 if(a==34) //+1
374 {
375
376 }
377 int sumOfPrimes(int max) { // +1
378 int total = 0;
379 OUT: for (int i = 1; i <= max; ++i) { // +1
380 for (int j = 2; j < i; ++j) { // +1
381 if (i % j == 0) { // +1
382 continue OUT;
383 }
384 }
385 total += i;
386 }
387 return total;
388 }",
389 "foo.c",
390 |metric| {
391 insta::assert_json_snapshot!(
393 metric.cyclomatic,
394 @r###"
395 {
396 "sum": 7.0,
397 "average": 3.5,
398 "min": 3.0,
399 "max": 4.0
400 }"###
401 );
402 },
403 );
404 }
405
406 #[test]
410 fn c_unit_after() {
411 check_metrics::<CppParser>(
412 "
413 int sumOfPrimes(int max) { // +1
414 int total = 0;
415 OUT: for (int i = 1; i <= max; ++i) { // +1
416 for (int j = 2; j < i; ++j) { // +1
417 if (i % j == 0) { // +1
418 continue OUT;
419 }
420 }
421 total += i;
422 }
423 return total;
424 }
425
426 int a=42;
427 if(a==42) //+2(+1 unit space)
428 {
429
430 }
431 if(a==34) //+1
432 {
433
434 }",
435 "foo.c",
436 |metric| {
437 insta::assert_json_snapshot!(
439 metric.cyclomatic,
440 @r###"
441 {
442 "sum": 7.0,
443 "average": 3.5,
444 "min": 3.0,
445 "max": 4.0
446 }"###
447 );
448 },
449 );
450 }
451
452 #[test]
453 fn java_simple_class() {
454 check_metrics::<JavaParser>(
455 "
456 public class Example { // +2 (+1 unit space)
457 int a = 10;
458 boolean b = (a > 5) ? true : false; // +1
459 boolean c = b && true; // +1
460
461 public void m1() { // +1
462 if (a % 2 == 0) { // +1
463 b = b || c; // +1
464 }
465 }
466 public void m2() { // +1
467 while (a > 3) { // +1
468 m1();
469 a--;
470 }
471 }
472 }",
473 "foo.java",
474 |metric| {
475 insta::assert_json_snapshot!(
477 metric.cyclomatic,
478 @r###"
479 {
480 "sum": 9.0,
481 "average": 2.25,
482 "min": 1.0,
483 "max": 3.0
484 }"###
485 );
486 },
487 );
488 }
489
490 #[test]
491 fn java_real_class() {
492 check_metrics::<JavaParser>(
493 "
494 public class Matrix { // +2 (+1 unit space)
495 private int[][] m = new int[5][5];
496
497 public void init() { // +1
498 for (int i = 0; i < m.length; i++) { // +1
499 for (int j = 0; j < m[i].length; j++) { // +1
500 m[i][j] = i * j;
501 }
502 }
503 }
504 public int compute(int i, int j) { // +1
505 try {
506 return m[i][j] / m[j][i];
507 } catch (ArithmeticException e) { // +1
508 return -1;
509 } catch (ArrayIndexOutOfBoundsException e) { // +1
510 return -2;
511 }
512 }
513 public void print(int result) { // +1
514 switch (result) {
515 case -1: // +1
516 System.out.println(\"Division by zero\");
517 break;
518 case -2: // +1
519 System.out.println(\"Wrong index number\");
520 break;
521 default:
522 System.out.println(\"The result is \" + result);
523 }
524 }
525 }",
526 "foo.java",
527 |metric| {
528 insta::assert_json_snapshot!(
530 metric.cyclomatic,
531 @r###"
532 {
533 "sum": 11.0,
534 "average": 2.2,
535 "min": 1.0,
536 "max": 3.0
537 }"###
538 );
539 },
540 );
541 }
542
543 #[test]
548 fn java_anonymous_class() {
549 check_metrics::<JavaParser>(
550 "
551 abstract class A { // +2 (+1 unit space)
552 public abstract boolean m1(int n); // +1
553 public abstract boolean m2(int n); // +1
554 }
555 public class B { // +1
556
557 public void test() { // +1
558 A a = new A() {
559 public boolean m1(int n) { // +1
560 if (n % 2 == 0) { // +1
561 return true;
562 }
563 return false;
564 }
565 public boolean m2(int n) { // +1
566 if (n % 5 == 0) { // +1
567 return true;
568 }
569 return false;
570 }
571 };
572 }
573 }",
574 "foo.java",
575 |metric| {
576 insta::assert_json_snapshot!(
578 metric.cyclomatic,
579 @r###"
580 {
581 "sum": 10.0,
582 "average": 1.25,
583 "min": 1.0,
584 "max": 2.0
585 }"###
586 );
587 },
588 );
589 }
590}