Skip to main content

rust_code_analysis_code_split/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 MozjsCode {
134    fn compute(node: &Node, stats: &mut Stats) {
135        use Mozjs::*;
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 JavascriptCode {
147    fn compute(node: &Node, stats: &mut Stats) {
148        use Javascript::*;
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 TypescriptCode {
160    fn compute(node: &Node, stats: &mut Stats) {
161        use Typescript::*;
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 TsxCode {
173    fn compute(node: &Node, stats: &mut Stats) {
174        use Tsx::*;
175
176        match node.kind_id().into() {
177            If | For | While | Case | Catch | TernaryExpression | AMPAMP | PIPEPIPE => {
178                stats.cyclomatic += 1.;
179            }
180            _ => {}
181        }
182    }
183}
184
185impl Cyclomatic for RustCode {
186    fn compute(node: &Node, stats: &mut Stats) {
187        use Rust::*;
188
189        match node.kind_id().into() {
190            If | For | While | Loop | MatchArm | MatchArm2 | TryExpression | AMPAMP | PIPEPIPE => {
191                stats.cyclomatic += 1.;
192            }
193            _ => {}
194        }
195    }
196}
197
198impl Cyclomatic for CppCode {
199    fn compute(node: &Node, stats: &mut Stats) {
200        use Cpp::*;
201
202        match node.kind_id().into() {
203            If | For | While | Case | Catch | ConditionalExpression | AMPAMP | PIPEPIPE => {
204                stats.cyclomatic += 1.;
205            }
206            _ => {}
207        }
208    }
209}
210
211impl Cyclomatic for JavaCode {
212    fn compute(node: &Node, stats: &mut Stats) {
213        use Java::*;
214
215        match node.kind_id().into() {
216            If | For | While | Case | Catch | TernaryExpression | AMPAMP | PIPEPIPE => {
217                stats.cyclomatic += 1.;
218            }
219            _ => {}
220        }
221    }
222}
223
224implement_metric_trait!(Cyclomatic, KotlinCode, PreprocCode, CcommentCode);
225
226#[cfg(test)]
227mod tests {
228    use crate::tools::check_metrics;
229
230    use super::*;
231
232    #[test]
233    fn python_simple_function() {
234        check_metrics::<PythonParser>(
235            "def f(a, b): # +2 (+1 unit space)
236                if a and b:  # +2 (+1 and)
237                   return 1
238                if c and d: # +2 (+1 and)
239                   return 1",
240            "foo.py",
241            |metric| {
242                // nspace = 2 (func and unit)
243                insta::assert_json_snapshot!(
244                    metric.cyclomatic,
245                    @r###"
246                    {
247                      "sum": 6.0,
248                      "average": 3.0,
249                      "min": 1.0,
250                      "max": 5.0
251                    }"###
252                );
253            },
254        );
255    }
256
257    #[test]
258    fn python_1_level_nesting() {
259        check_metrics::<PythonParser>(
260            "def f(a, b): # +2 (+1 unit space)
261                if a:  # +1
262                    for i in range(b):  # +1
263                        return 1",
264            "foo.py",
265            |metric| {
266                // nspace = 2 (func and unit)
267                insta::assert_json_snapshot!(
268                    metric.cyclomatic,
269                    @r###"
270                    {
271                      "sum": 4.0,
272                      "average": 2.0,
273                      "min": 1.0,
274                      "max": 3.0
275                    }"###
276                );
277            },
278        );
279    }
280
281    #[test]
282    fn rust_1_level_nesting() {
283        check_metrics::<RustParser>(
284            "fn f() { // +2 (+1 unit space)
285                 if true { // +1
286                     match true {
287                         true => println!(\"test\"), // +1
288                         false => println!(\"test\"), // +1
289                     }
290                 }
291             }",
292            "foo.rs",
293            |metric| {
294                // nspace = 2 (func and unit)
295                insta::assert_json_snapshot!(
296                    metric.cyclomatic,
297                    @r###"
298                    {
299                      "sum": 5.0,
300                      "average": 2.5,
301                      "min": 1.0,
302                      "max": 4.0
303                    }"###
304                );
305            },
306        );
307    }
308
309    #[test]
310    fn c_switch() {
311        check_metrics::<CppParser>(
312            "void f() { // +2 (+1 unit space)
313                 switch (1) {
314                     case 1: // +1
315                         printf(\"one\");
316                         break;
317                     case 2: // +1
318                         printf(\"two\");
319                         break;
320                     case 3: // +1
321                         printf(\"three\");
322                         break;
323                     default:
324                         printf(\"all\");
325                         break;
326                 }
327             }",
328            "foo.c",
329            |metric| {
330                // nspace = 2 (func and unit)
331                insta::assert_json_snapshot!(
332                    metric.cyclomatic,
333                    @r###"
334                    {
335                      "sum": 5.0,
336                      "average": 2.5,
337                      "min": 1.0,
338                      "max": 4.0
339                    }"###
340                );
341            },
342        );
343    }
344
345    #[test]
346    fn c_real_function() {
347        check_metrics::<CppParser>(
348            "int sumOfPrimes(int max) { // +2 (+1 unit space)
349                 int total = 0;
350                 OUT: for (int i = 1; i <= max; ++i) { // +1
351                   for (int j = 2; j < i; ++j) { // +1
352                       if (i % j == 0) { // +1
353                          continue OUT;
354                       }
355                   }
356                   total += i;
357                 }
358                 return total;
359            }",
360            "foo.c",
361            |metric| {
362                // nspace = 2 (func and unit)
363                insta::assert_json_snapshot!(
364                    metric.cyclomatic,
365                    @r###"
366                    {
367                      "sum": 5.0,
368                      "average": 2.5,
369                      "min": 1.0,
370                      "max": 4.0
371                    }"###
372                );
373            },
374        );
375    }
376
377    #[test]
378    fn c_unit_before() {
379        check_metrics::<CppParser>(
380            "
381            int a=42;
382            if(a==42) //+2(+1 unit space)
383            {
384
385            }
386            if(a==34) //+1
387            {
388
389            }
390            int sumOfPrimes(int max) { // +1
391                 int total = 0;
392                 OUT: for (int i = 1; i <= max; ++i) { // +1
393                   for (int j = 2; j < i; ++j) { // +1
394                       if (i % j == 0) { // +1
395                          continue OUT;
396                       }
397                   }
398                   total += i;
399                 }
400                 return total;
401            }",
402            "foo.c",
403            |metric| {
404                // nspace = 2 (func and unit)
405                insta::assert_json_snapshot!(
406                    metric.cyclomatic,
407                    @r###"
408                    {
409                      "sum": 7.0,
410                      "average": 3.5,
411                      "min": 3.0,
412                      "max": 4.0
413                    }"###
414                );
415            },
416        );
417    }
418
419    /// Test to handle the case of min and max when merge happen before the final value of one module are set.
420    /// In this case the min value should be 3 because the unit space has 2 branches and a complexity of 3
421    /// while the function sumOfPrimes has a complexity of 4.
422    #[test]
423    fn c_unit_after() {
424        check_metrics::<CppParser>(
425            "
426            int sumOfPrimes(int max) { // +1
427                 int total = 0;
428                 OUT: for (int i = 1; i <= max; ++i) { // +1
429                   for (int j = 2; j < i; ++j) { // +1
430                       if (i % j == 0) { // +1
431                          continue OUT;
432                       }
433                   }
434                   total += i;
435                 }
436                 return total;
437            }
438
439            int a=42;
440            if(a==42) //+2(+1 unit space)
441            {
442
443            }
444            if(a==34) //+1
445            {
446
447            }",
448            "foo.c",
449            |metric| {
450                // nspace = 2 (func and unit)
451                insta::assert_json_snapshot!(
452                    metric.cyclomatic,
453                    @r###"
454                    {
455                      "sum": 7.0,
456                      "average": 3.5,
457                      "min": 3.0,
458                      "max": 4.0
459                    }"###
460                );
461            },
462        );
463    }
464
465    #[test]
466    fn java_simple_class() {
467        check_metrics::<JavaParser>(
468            "
469            public class Example { // +2 (+1 unit space)
470                int a = 10;
471                boolean b = (a > 5) ? true : false; // +1
472                boolean c = b && true; // +1
473
474                public void m1() { // +1
475                    if (a % 2 == 0) { // +1
476                        b = b || c; // +1
477                    }
478                }
479                public void m2() { // +1
480                    while (a > 3) { // +1
481                        m1();
482                        a--;
483                    }
484                }
485            }",
486            "foo.java",
487            |metric| {
488                // nspace = 4 (unit, class and 2 methods)
489                insta::assert_json_snapshot!(
490                    metric.cyclomatic,
491                    @r###"
492                    {
493                      "sum": 9.0,
494                      "average": 2.25,
495                      "min": 1.0,
496                      "max": 3.0
497                    }"###
498                );
499            },
500        );
501    }
502
503    #[test]
504    fn java_real_class() {
505        check_metrics::<JavaParser>(
506            "
507            public class Matrix { // +2 (+1 unit space)
508                private int[][] m = new int[5][5];
509
510                public void init() { // +1
511                    for (int i = 0; i < m.length; i++) { // +1
512                        for (int j = 0; j < m[i].length; j++) { // +1
513                            m[i][j] = i * j;
514                        }
515                    }
516                }
517                public int compute(int i, int j) { // +1
518                    try {
519                        return m[i][j] / m[j][i];
520                    } catch (ArithmeticException e) { // +1
521                        return -1;
522                    } catch (ArrayIndexOutOfBoundsException e) { // +1
523                        return -2;
524                    }
525                }
526                public void print(int result) { // +1
527                    switch (result) {
528                        case -1: // +1
529                            System.out.println(\"Division by zero\");
530                            break;
531                        case -2: // +1
532                            System.out.println(\"Wrong index number\");
533                            break;
534                        default:
535                            System.out.println(\"The result is \" + result);
536                    }
537                }
538            }",
539            "foo.java",
540            |metric| {
541                // nspace = 5 (unit, class and 3 methods)
542                insta::assert_json_snapshot!(
543                    metric.cyclomatic,
544                    @r###"
545                    {
546                      "sum": 11.0,
547                      "average": 2.2,
548                      "min": 1.0,
549                      "max": 3.0
550                    }"###
551                );
552            },
553        );
554    }
555
556    // As reported here:
557    // https://github.com/sebastianbergmann/php-code-coverage/issues/607
558    // An anonymous class declaration is not considered when computing the Cyclomatic Complexity metric for Java
559    // Only the complexity of the anonymous class content is considered for the computation
560    #[test]
561    fn java_anonymous_class() {
562        check_metrics::<JavaParser>(
563            "
564            abstract class A { // +2 (+1 unit space)
565                public abstract boolean m1(int n); // +1
566                public abstract boolean m2(int n); // +1
567            }
568            public class B { // +1
569
570                public void test() { // +1
571                    A a = new A() {
572                        public boolean m1(int n) { // +1
573                            if (n % 2 == 0) { // +1
574                                return true;
575                            }
576                            return false;
577                        }
578                        public boolean m2(int n) { // +1
579                            if (n % 5 == 0) { // +1
580                                return true;
581                            }
582                            return false;
583                        }
584                    };
585                }
586            }",
587            "foo.java",
588            |metric| {
589                // nspace = 8 (unit, 2 classes and 5 methods)
590                insta::assert_json_snapshot!(
591                    metric.cyclomatic,
592                    @r###"
593                    {
594                      "sum": 10.0,
595                      "average": 1.25,
596                      "min": 1.0,
597                      "max": 2.0
598                    }"###
599                );
600            },
601        );
602    }
603}