rust_code_analysis/metrics/
cyclomatic.rs

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