rust_code_analysis_code_split/metrics/
cyclomatic.rs1use 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 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 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 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 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 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 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 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]
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 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 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 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 #[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 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}