Skip to main content

codelore_rca/metrics/
cyclomatic.rs

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/// The `Cyclomatic` metric.
10#[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    /// Merges a second `Cyclomatic` metric into the first one
60    pub fn merge(&mut self, other: &Stats) {
61        //Calculate minimum and maximum values
62        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    /// Returns the `Cyclomatic` metric value
70    pub fn cyclomatic(&self) -> f64 {
71        self.cyclomatic
72    }
73    /// Returns the sum
74    pub fn cyclomatic_sum(&self) -> f64 {
75        self.cyclomatic_sum
76    }
77
78    /// Returns the `Cyclomatic` metric average value
79    ///
80    /// This value is computed dividing the `Cyclomatic` value for the
81    /// number of spaces.
82    pub fn cyclomatic_average(&self) -> f64 {
83        self.cyclomatic_sum() / self.n as f64
84    }
85    /// Returns the `Cyclomatic` maximum value
86    pub fn cyclomatic_max(&self) -> f64 {
87        self.cyclomatic_max
88    }
89    /// Returns the `Cyclomatic` minimum value
90    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                // nspace = 2 (func and unit)
230                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                // nspace = 2 (func and unit)
254                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                // nspace = 2 (func and unit)
282                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                // nspace = 2 (func and unit)
318                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                // nspace = 2 (func and unit)
350                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                // nspace = 2 (func and unit)
392                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 to handle the case of min and max when merge happen before the final value of one module are set.
407    /// In this case the min value should be 3 because the unit space has 2 branches and a complexity of 3
408    /// while the function sumOfPrimes has a complexity of 4.
409    #[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                // nspace = 2 (func and unit)
438                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                // nspace = 4 (unit, class and 2 methods)
476                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                // nspace = 5 (unit, class and 3 methods)
529                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    // As reported here:
544    // https://github.com/sebastianbergmann/php-code-coverage/issues/607
545    // An anonymous class declaration is not considered when computing the Cyclomatic Complexity metric for Java
546    // Only the complexity of the anonymous class content is considered for the computation
547    #[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                // nspace = 8 (unit, 2 classes and 5 methods)
577                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}