Skip to main content

codelore_rca/metrics/
cognitive.rs

1use std::collections::HashMap;
2
3use serde::Serialize;
4use serde::ser::{SerializeStruct, Serializer};
5use std::fmt;
6
7use crate::checker::Checker;
8use crate::macros::implement_metric_trait;
9use crate::*;
10
11// TODO: Find a way to increment the cognitive complexity value
12// for recursive code. For some kind of languages, such as C++, it is pretty
13// hard to detect, just parsing the code, if a determined function is recursive
14// because the call graph of a function is solved at runtime.
15// So a possible solution could be searching for a crate which implements
16// a light language interpreter, computing the call graph, and then detecting
17// if there are cycles. At this point, it is possible to figure out if a
18// function is recursive or not.
19
20/// The `Cognitive Complexity` metric.
21#[derive(Debug, Clone)]
22pub struct Stats {
23    structural: usize,
24    structural_sum: usize,
25    structural_min: usize,
26    structural_max: usize,
27    nesting: usize,
28    max_nesting: usize,
29    total_nesting: usize,
30    bool_ops: usize,
31    total_space_functions: usize,
32    boolean_seq: BoolSequence,
33}
34
35impl Default for Stats {
36    fn default() -> Self {
37        Self {
38            structural: 0,
39            structural_sum: 0,
40            structural_min: usize::MAX,
41            structural_max: 0,
42            nesting: 0,
43            max_nesting: 0,
44            total_nesting: 0,
45            bool_ops: 0,
46            total_space_functions: 1,
47            boolean_seq: BoolSequence::default(),
48        }
49    }
50}
51
52impl Serialize for Stats {
53    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
54    where
55        S: Serializer,
56    {
57        let mut st = serializer.serialize_struct("cognitive", 4)?;
58        st.serialize_field("sum", &self.cognitive_sum())?;
59        st.serialize_field("average", &self.cognitive_average())?;
60        st.serialize_field("min", &self.cognitive_min())?;
61        st.serialize_field("max", &self.cognitive_max())?;
62        st.end()
63    }
64}
65
66impl fmt::Display for Stats {
67    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
68        write!(
69            f,
70            "sum: {}, average: {}, min:{}, max: {}",
71            self.cognitive(),
72            self.cognitive_average(),
73            self.cognitive_min(),
74            self.cognitive_max()
75        )
76    }
77}
78
79impl Stats {
80    /// Merges a second `Cognitive Complexity` metric into the first one
81    pub fn merge(&mut self, other: &Stats) {
82        self.structural_min = self.structural_min.min(other.structural_min);
83        self.structural_max = self.structural_max.max(other.structural_max);
84        self.structural_sum += other.structural_sum;
85    }
86
87    /// Returns the `Cognitive Complexity` metric value
88    pub fn cognitive(&self) -> f64 {
89        self.structural as f64
90    }
91    /// Returns the `Cognitive Complexity` sum metric value
92    pub fn cognitive_sum(&self) -> f64 {
93        self.structural_sum as f64
94    }
95
96    /// Returns the `Cognitive Complexity` minimum metric value
97    pub fn cognitive_min(&self) -> f64 {
98        self.structural_min as f64
99    }
100    /// Returns the `Cognitive Complexity` maximum metric value
101    pub fn cognitive_max(&self) -> f64 {
102        self.structural_max as f64
103    }
104
105    /// Returns the `Cognitive Complexity` metric average value
106    ///
107    /// This value is computed dividing the `Cognitive Complexity` value
108    /// for the total number of functions/closures in a space.
109    ///
110    /// If there are no functions in a code, its value is `NAN`.
111    pub fn cognitive_average(&self) -> f64 {
112        self.cognitive_sum() / self.total_space_functions as f64
113    }
114
115    /// Returns the maximum control-flow nesting depth reached in this space.
116    ///
117    /// Counted as the deepest level of nested branching/looping constructs
118    /// (e.g. three nested `if`s yield `3`); `0` when the space contains no
119    /// such construct. Derived from the same nesting the cognitive pass
120    /// already tracks.
121    pub fn max_nesting(&self) -> f64 {
122        self.max_nesting as f64
123    }
124
125    /// Returns the sum of the nesting depth of every branching/looping
126    /// construct in this space.
127    pub fn total_nesting(&self) -> f64 {
128        self.total_nesting as f64
129    }
130
131    /// Returns the number of boolean-operator sequences in this space.
132    ///
133    /// A run of the same operator (`a && b && c`) counts once; switching
134    /// operators (`a && b || c`) starts a new sequence. This is the same
135    /// boolean-sequence accounting the cognitive metric folds into its score.
136    pub fn bool_ops(&self) -> f64 {
137        self.bool_ops as f64
138    }
139    #[inline(always)]
140    pub(crate) fn compute_sum(&mut self) {
141        self.structural_sum += self.structural;
142    }
143    #[inline(always)]
144    pub(crate) fn compute_minmax(&mut self) {
145        self.structural_min = self.structural_min.min(self.structural);
146        self.structural_max = self.structural_max.max(self.structural);
147        self.compute_sum();
148    }
149
150    pub(crate) fn finalize(&mut self, total_space_functions: usize) {
151        self.total_space_functions = total_space_functions;
152    }
153}
154
155pub trait Cognitive
156where
157    Self: Checker,
158{
159    fn compute(
160        node: &Node,
161        stats: &mut Stats,
162        nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
163    );
164}
165
166fn compute_booleans<T: std::cmp::PartialEq + std::convert::From<u16>>(
167    node: &Node,
168    stats: &mut Stats,
169    typs1: T,
170    typs2: T,
171) {
172    for child in node.children() {
173        if typs1 == child.kind_id().into() || typs2 == child.kind_id().into() {
174            let prev_structural = stats.structural;
175            stats.structural = stats
176                .boolean_seq
177                .eval_based_on_prev(child.kind_id(), stats.structural);
178            // `bool_ops` rides the cognitive counter's sequence detection: the
179            // delta is exactly 1 when `eval_based_on_prev` counted a new boolean
180            // sequence (first operator, or a switch to a different operator) and
181            // 0 when it extended the current run. This coupling is deliberate —
182            // if `eval_based_on_prev`'s return semantics ever change, this line
183            // must change with it.
184            stats.bool_ops += stats.structural - prev_structural;
185        }
186    }
187}
188
189#[derive(Debug, Default, Clone)]
190struct BoolSequence {
191    boolean_op: Option<u16>,
192}
193
194impl BoolSequence {
195    fn reset(&mut self) {
196        self.boolean_op = None;
197    }
198
199    fn not_operator(&mut self, not_id: u16) {
200        self.boolean_op = Some(not_id);
201    }
202
203    fn eval_based_on_prev(&mut self, bool_id: u16, structural: usize) -> usize {
204        if let Some(prev) = self.boolean_op {
205            if prev != bool_id {
206                // The boolean operator is different from the previous one, so
207                // the counter is incremented.
208                structural + 1
209            } else {
210                // The boolean operator is equal to the previous one, so
211                // the counter is not incremented.
212                structural
213            }
214        } else {
215            // Save the first boolean operator in a sequence of
216            // logical operators and increment the counter.
217            self.boolean_op = Some(bool_id);
218            structural + 1
219        }
220    }
221}
222
223#[inline(always)]
224fn increment(stats: &mut Stats) {
225    stats.structural += stats.nesting + 1;
226}
227
228#[inline(always)]
229fn increment_by_one(stats: &mut Stats) {
230    stats.structural += 1;
231}
232
233fn get_nesting_from_map(
234    node: &Node,
235    nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
236) -> (usize, usize, usize) {
237    if let Some(parent) = node.parent() {
238        if let Some(n) = nesting_map.get(&parent.id()) {
239            *n
240        } else {
241            (0, 0, 0)
242        }
243    } else {
244        (0, 0, 0)
245    }
246}
247
248fn increment_function_depth<T: std::cmp::PartialEq + std::convert::From<u16>>(
249    depth: &mut usize,
250    node: &Node,
251    stop: T,
252) {
253    // Increase depth function nesting if needed
254    let mut child = *node;
255    while let Some(parent) = child.parent() {
256        if stop == parent.kind_id().into() {
257            *depth += 1;
258            break;
259        }
260        child = parent;
261    }
262}
263
264#[inline(always)]
265fn increase_nesting(stats: &mut Stats, nesting: &mut usize, depth: usize, lambda: usize) {
266    stats.nesting = *nesting + depth + lambda;
267    // Track nesting depth as a 1-based count: an outermost construct
268    // (`stats.nesting == 0`) sits at depth 1.
269    let depth_count = stats.nesting + 1;
270    stats.max_nesting = stats.max_nesting.max(depth_count);
271    stats.total_nesting += depth_count;
272    increment(stats);
273    *nesting += 1;
274    stats.boolean_seq.reset();
275}
276
277impl Cognitive for PythonCode {
278    fn compute(
279        node: &Node,
280        stats: &mut Stats,
281        nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
282    ) {
283        use Python::*;
284
285        // Get nesting of the parent
286        let (mut nesting, mut depth, mut lambda) = get_nesting_from_map(node, nesting_map);
287
288        match node.kind_id().into() {
289            IfStatement
290            | ForStatement
291            | WhileStatement
292            | MatchStatement
293            | ConditionalExpression => {
294                increase_nesting(stats, &mut nesting, depth, lambda);
295            }
296            ElifClause => {
297                // No nesting increment for them because their cost has already
298                // been paid by the if construct
299                increment_by_one(stats);
300                // Reset the boolean sequence
301                stats.boolean_seq.reset();
302            }
303            ElseClause => {
304                // No nesting increment: the branch cost has already been paid
305                // by the if construct. `finally` is not a branch and does not
306                // increment.
307                increment_by_one(stats);
308                // The else body starts a fresh boolean sequence, so its
309                // operators must not merge with the preceding branch
310                // condition's. Reset at the `else` boundary, exactly as
311                // `ElifClause` and the sibling languages' `Else` arms do.
312                stats.boolean_seq.reset();
313            }
314            ExceptClause => {
315                // `except` pays a structural increment plus the nesting penalty
316                // for the branches it sits inside, then raises the nesting level
317                // for its handler body. Establish the clause's own nesting level
318                // before the increment so it is read from here, not from a stale
319                // `stats.nesting` left by an unrelated preceding construct.
320                stats.nesting = nesting + depth + lambda;
321                increment(stats);
322                nesting += 1;
323            }
324            ExpressionList | ExpressionStatement | Tuple => {
325                stats.boolean_seq.reset();
326            }
327            NotOperator => {
328                stats.boolean_seq.not_operator(node.kind_id());
329            }
330            BooleanOperator => {
331                if node.count_specific_ancestors::<PythonParser>(
332                    |node| node.kind_id() == BooleanOperator,
333                    |node| node.kind_id() == Lambda,
334                ) == 0
335                {
336                    stats.structural += node.count_specific_ancestors::<PythonParser>(
337                        |node| node.kind_id() == Lambda,
338                        |node| {
339                            matches!(
340                                node.kind_id().into(),
341                                ExpressionList | IfStatement | ForStatement | WhileStatement
342                            )
343                        },
344                    );
345                }
346                compute_booleans::<language_python::Python>(node, stats, And, Or);
347            }
348            Lambda => {
349                // Increase lambda nesting
350                lambda += 1;
351            }
352            FunctionDefinition => {
353                // Increase depth function nesting if needed
354                increment_function_depth::<language_python::Python>(
355                    &mut depth,
356                    node,
357                    FunctionDefinition,
358                );
359            }
360            _ => {}
361        }
362        // Add node to nesting map
363        nesting_map.insert(node.id(), (nesting, depth, lambda));
364    }
365}
366
367impl Cognitive for RustCode {
368    fn compute(
369        node: &Node,
370        stats: &mut Stats,
371        nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
372    ) {
373        use Rust::*;
374        //TODO: Implement macros
375        let (mut nesting, mut depth, mut lambda) = get_nesting_from_map(node, nesting_map);
376
377        match node.kind_id().into() {
378            IfExpression => {
379                // Check if a node is not an else-if
380                if !Self::is_else_if(node) {
381                    increase_nesting(stats,&mut nesting, depth, lambda);
382                }
383            }
384            ForExpression | WhileExpression | LoopExpression | MatchExpression => {
385                increase_nesting(stats,&mut nesting, depth, lambda);
386            }
387            Else /*else-if also */ => {
388                increment_by_one(stats);
389                // An else-if condition starts a fresh boolean sequence, so its
390                // operators must not merge with the preceding branch
391                // condition's. Reset at the `else` boundary.
392                stats.boolean_seq.reset();
393            }
394            BreakExpression | ContinueExpression => {
395                if let Some(label_child) = node.child(1)
396                    && let Label = label_child.kind_id().into()
397                {
398                    increment_by_one(stats);
399                }
400            }
401            UnaryExpression => {
402                stats.boolean_seq.not_operator(node.kind_id());
403            }
404            BinaryExpression => {
405                compute_booleans::<language_rust::Rust>(node, stats, AMPAMP, PIPEPIPE);
406            }
407            FunctionItem  => {
408                nesting = 0;
409                // Increase depth function nesting if needed
410                increment_function_depth::<language_rust::Rust>(&mut depth, node, FunctionItem);
411            }
412            ClosureExpression => {
413                lambda += 1;
414            }
415            _ => {}
416        }
417        nesting_map.insert(node.id(), (nesting, depth, lambda));
418    }
419}
420
421impl Cognitive for CppCode {
422    fn compute(
423        node: &Node,
424        stats: &mut Stats,
425        nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
426    ) {
427        use Cpp::*;
428
429        //TODO: Implement macros
430        let (mut nesting, depth, mut lambda) = get_nesting_from_map(node, nesting_map);
431
432        match node.kind_id().into() {
433            IfStatement => {
434                if !Self::is_else_if(node) {
435                    increase_nesting(stats,&mut nesting, depth, lambda);
436                }
437            }
438            ForStatement | WhileStatement | DoStatement | SwitchStatement | CatchClause => {
439                increase_nesting(stats,&mut nesting, depth, lambda);
440            }
441            GotoStatement => {
442                increment_by_one(stats);
443            }
444            Else /* else-if also */ => {
445                increment_by_one(stats);
446                // An else-if condition starts a fresh boolean sequence, so its
447                // operators must not merge with the preceding branch
448                // condition's. Reset at the `else` boundary.
449                stats.boolean_seq.reset();
450            }
451            UnaryExpression2 => {
452                stats.boolean_seq.not_operator(node.kind_id());
453            }
454            BinaryExpression2 => {
455                compute_booleans::<language_cpp::Cpp>(node, stats, AMPAMP, PIPEPIPE);
456            }
457            LambdaExpression => {
458                lambda += 1;
459            }
460            _ => {}
461        }
462        nesting_map.insert(node.id(), (nesting, depth, lambda));
463    }
464}
465
466macro_rules! js_cognitive {
467    ($lang:ident) => {
468        fn compute(node: &Node, stats: &mut Stats, nesting_map: &mut HashMap<usize, (usize, usize, usize)>) {
469            use $lang::*;
470            let (mut nesting, mut depth, mut lambda) = get_nesting_from_map(node, nesting_map);
471
472            match node.kind_id().into() {
473                IfStatement => {
474                    if !Self::is_else_if(&node) {
475                        increase_nesting(stats,&mut nesting, depth, lambda);
476                    }
477                }
478                ForStatement | ForInStatement | WhileStatement | DoStatement | SwitchStatement | CatchClause | TernaryExpression => {
479                    increase_nesting(stats,&mut nesting, depth, lambda);
480                }
481                Else /* else-if also */ => {
482                    increment_by_one(stats);
483                    // An else-if condition starts a fresh boolean sequence, so
484                    // its operators must not merge with the preceding branch
485                    // condition's. Reset at the `else` boundary.
486                    stats.boolean_seq.reset();
487                }
488                ExpressionStatement => {
489                    // Reset the boolean sequence
490                    stats.boolean_seq.reset();
491                }
492                UnaryExpression => {
493                    stats.boolean_seq.not_operator(node.kind_id());
494                }
495                BinaryExpression => {
496                    compute_booleans::<$lang>(node, stats, AMPAMP, PIPEPIPE);
497                }
498                FunctionDeclaration => {
499                    // Reset lambda nesting at function for JS
500                    nesting = 0;
501                    lambda = 0;
502                    // Increase depth function nesting if needed
503                    increment_function_depth::<$lang>(&mut depth, node, FunctionDeclaration);
504                }
505                ArrowFunction => {
506                    lambda += 1;
507                }
508                _ => {}
509            }
510            nesting_map.insert(node.id(), (nesting, depth, lambda));
511        }
512    };
513}
514
515impl Cognitive for JavascriptCode {
516    js_cognitive!(Javascript);
517}
518
519impl Cognitive for TypescriptCode {
520    js_cognitive!(Typescript);
521}
522
523impl Cognitive for TsxCode {
524    js_cognitive!(Tsx);
525}
526
527impl Cognitive for JavaCode {
528    fn compute(
529        node: &Node,
530        stats: &mut Stats,
531        nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
532    ) {
533        use Java::*;
534
535        let (mut nesting, depth, mut lambda) = get_nesting_from_map(node, nesting_map);
536
537        match node.kind_id().into() {
538            IfStatement => {
539                if !Self::is_else_if(node) {
540                    increase_nesting(stats,&mut nesting, depth, lambda);
541                }
542            }
543            ForStatement | EnhancedForStatement | WhileStatement | DoStatement | SwitchBlock
544            | CatchClause | TernaryExpression => {
545                increase_nesting(stats,&mut nesting, depth, lambda);
546            }
547            Else /* else-if also */ => {
548                increment_by_one(stats);
549                // An else-if condition starts a fresh boolean sequence, so its
550                // operators must not merge with the preceding branch
551                // condition's. Reset at the `else` boundary.
552                stats.boolean_seq.reset();
553            }
554            UnaryExpression => {
555                stats.boolean_seq.not_operator(node.kind_id());
556            }
557            BinaryExpression => {
558                compute_booleans::<language_java::Java>(node, stats, AMPAMP, PIPEPIPE);
559            }
560            LambdaExpression => {
561                lambda += 1;
562            }
563            _ => {}
564        }
565        nesting_map.insert(node.id(), (nesting, depth, lambda));
566    }
567}
568
569implement_metric_trait!(Cognitive, PreprocCode, CcommentCode, KotlinCode);
570
571#[cfg(test)]
572mod tests {
573    use crate::tools::check_metrics;
574
575    use super::*;
576
577    #[test]
578    fn python_no_cognitive() {
579        check_metrics::<PythonParser>("a = 42", "foo.py", |metric| {
580            insta::assert_json_snapshot!(
581                metric.cognitive,
582                @r###"
583                    {
584                      "sum": 0.0,
585                      "average": null,
586                      "min": 0.0,
587                      "max": 0.0
588                    }"###
589            );
590        });
591    }
592
593    #[test]
594    fn rust_no_cognitive() {
595        check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| {
596            insta::assert_json_snapshot!(
597                metric.cognitive,
598                @r###"
599                    {
600                      "sum": 0.0,
601                      "average": null,
602                      "min": 0.0,
603                      "max": 0.0
604                    }"###
605            );
606        });
607    }
608
609    #[test]
610    fn c_no_cognitive() {
611        check_metrics::<CppParser>("int a = 42;", "foo.c", |metric| {
612            insta::assert_json_snapshot!(
613                metric.cognitive,
614                @r###"
615                    {
616                      "sum": 0.0,
617                      "average": null,
618                      "min": 0.0,
619                      "max": 0.0
620                    }"###
621            );
622        });
623    }
624
625    #[test]
626    fn mozjs_no_cognitive() {
627        check_metrics::<JavascriptParser>("var a = 42;", "foo.js", |metric| {
628            insta::assert_json_snapshot!(
629                metric.cognitive,
630                @r###"
631                    {
632                      "sum": 0.0,
633                      "average": null,
634                      "min": 0.0,
635                      "max": 0.0
636                    }"###
637            );
638        });
639    }
640
641    #[test]
642    fn python_simple_function() {
643        check_metrics::<PythonParser>(
644            "def f(a, b):
645                if a and b:  # +2 (+1 and)
646                   return 1
647                if c and d: # +2 (+1 and)
648                   return 1",
649            "foo.py",
650            |metric| {
651                insta::assert_json_snapshot!(
652                    metric.cognitive,
653                    @r###"
654                    {
655                      "sum": 4.0,
656                      "average": 4.0,
657                      "min": 0.0,
658                      "max": 4.0
659                    }"###
660                );
661            },
662        );
663    }
664
665    #[test]
666    fn python_expression_statement() {
667        // Boolean expressions containing `And` and `Or` operators were not
668        // considered in assignments
669        check_metrics::<PythonParser>(
670            "def f(a, b):
671                c = True and True",
672            "foo.py",
673            |metric| {
674                insta::assert_json_snapshot!(
675                    metric.cognitive,
676                    @r###"
677                    {
678                      "sum": 1.0,
679                      "average": 1.0,
680                      "min": 0.0,
681                      "max": 1.0
682                    }"###
683                );
684            },
685        );
686    }
687
688    #[test]
689    fn python_tuple() {
690        // Boolean expressions containing `And` and `Or` operators were not
691        // considered inside tuples
692        check_metrics::<PythonParser>(
693            "def f(a, b):
694                return \"%s%s\" % (a and \"Get\" or \"Set\", b)",
695            "foo.py",
696            |metric| {
697                insta::assert_json_snapshot!(
698                    metric.cognitive,
699                    @r###"
700                    {
701                      "sum": 2.0,
702                      "average": 2.0,
703                      "min": 0.0,
704                      "max": 2.0
705                    }"###
706                );
707            },
708        );
709    }
710
711    #[test]
712    fn python_elif_function() {
713        // Boolean expressions containing `And` and `Or` operators were not
714        // considered in `elif` statements
715        check_metrics::<PythonParser>(
716            "def f(a, b):
717                if a and b:  # +2 (+1 and)
718                   return 1
719                elif c and d: # +2 (+1 and)
720                   return 1",
721            "foo.py",
722            |metric| {
723                insta::assert_json_snapshot!(
724                    metric.cognitive,
725                    @r###"
726                    {
727                      "sum": 4.0,
728                      "average": 4.0,
729                      "min": 0.0,
730                      "max": 4.0
731                    }"###
732                );
733            },
734        );
735    }
736
737    #[test]
738    fn python_more_elifs_function() {
739        // Boolean expressions containing `And` and `Or` operators were not
740        // considered when there were more `elif` statements
741        check_metrics::<PythonParser>(
742            "def f(a, b):
743                if a and b:  # +2 (+1 and)
744                   return 1
745                elif c and d: # +2 (+1 and)
746                   return 1
747                elif e and f: # +2 (+1 and)
748                   return 1",
749            "foo.py",
750            |metric| {
751                insta::assert_json_snapshot!(
752                    metric.cognitive,
753                    @r###"
754                    {
755                      "sum": 6.0,
756                      "average": 6.0,
757                      "min": 0.0,
758                      "max": 6.0
759                    }"###
760                );
761            },
762        );
763    }
764
765    #[test]
766    fn rust_simple_function() {
767        check_metrics::<RustParser>(
768            "fn f() {
769                 if a && b { // +2 (+1 &&)
770                     println!(\"test\");
771                 }
772                 if c && d { // +2 (+1 &&)
773                     println!(\"test\");
774                 }
775             }",
776            "foo.rs",
777            |metric| {
778                insta::assert_json_snapshot!(
779                    metric.cognitive,
780                    @r###"
781                    {
782                      "sum": 4.0,
783                      "average": 4.0,
784                      "min": 0.0,
785                      "max": 4.0
786                    }"###
787                );
788            },
789        );
790    }
791
792    #[test]
793    fn c_simple_function() {
794        check_metrics::<CppParser>(
795            "void f() {
796                 if (a && b) { // +2 (+1 &&)
797                     printf(\"test\");
798                 }
799                 if (c && d) { // +2 (+1 &&)
800                     printf(\"test\");
801                 }
802             }",
803            "foo.c",
804            |metric| {
805                insta::assert_json_snapshot!(
806                    metric.cognitive,
807                    @r###"
808                    {
809                      "sum": 4.0,
810                      "average": 4.0,
811                      "min": 0.0,
812                      "max": 4.0
813                    }"###
814                );
815            },
816        );
817    }
818
819    #[test]
820    fn mozjs_simple_function() {
821        check_metrics::<JavascriptParser>(
822            "function f() {
823                 if (a && b) { // +2 (+1 &&)
824                     window.print(\"test\");
825                 }
826                 if (c && d) { // +2 (+1 &&)
827                     window.print(\"test\");
828                 }
829             }",
830            "foo.js",
831            |metric| {
832                insta::assert_json_snapshot!(
833                    metric.cognitive,
834                    @r###"
835                    {
836                      "sum": 4.0,
837                      "average": 4.0,
838                      "min": 0.0,
839                      "max": 4.0
840                    }"###
841                );
842            },
843        );
844    }
845
846    #[test]
847    fn python_sequence_same_booleans() {
848        check_metrics::<PythonParser>(
849            "def f(a, b):
850                if a and b and True:  # +2 (+1 sequence of and)
851                   return 1",
852            "foo.py",
853            |metric| {
854                insta::assert_json_snapshot!(
855                    metric.cognitive,
856                    @r###"
857                    {
858                      "sum": 2.0,
859                      "average": 2.0,
860                      "min": 0.0,
861                      "max": 2.0
862                    }"###
863                );
864            },
865        );
866    }
867
868    #[test]
869    fn rust_sequence_same_booleans() {
870        check_metrics::<RustParser>(
871            "fn f() {
872                 if a && b && true { // +2 (+1 sequence of &&)
873                     println!(\"test\");
874                 }
875             }",
876            "foo.rs",
877            |metric| {
878                insta::assert_json_snapshot!(
879                    metric.cognitive,
880                    @r###"
881                    {
882                      "sum": 2.0,
883                      "average": 2.0,
884                      "min": 0.0,
885                      "max": 2.0
886                    }"###
887                );
888            },
889        );
890
891        check_metrics::<RustParser>(
892            "fn f() {
893                 if a || b || c || d { // +2 (+1 sequence of ||)
894                     println!(\"test\");
895                 }
896             }",
897            "foo.rs",
898            |metric| {
899                insta::assert_json_snapshot!(
900                    metric.cognitive,
901                    @r###"
902                    {
903                      "sum": 2.0,
904                      "average": 2.0,
905                      "min": 0.0,
906                      "max": 2.0
907                    }"###
908                );
909            },
910        );
911    }
912
913    #[test]
914    fn c_sequence_same_booleans() {
915        check_metrics::<CppParser>(
916            "void f() {
917                 if (a && b && 1 == 1) { // +2 (+1 sequence of &&)
918                     printf(\"test\");
919                 }
920             }",
921            "foo.c",
922            |metric| {
923                insta::assert_json_snapshot!(
924                    metric.cognitive,
925                    @r###"
926                    {
927                      "sum": 2.0,
928                      "average": 2.0,
929                      "min": 0.0,
930                      "max": 2.0
931                    }"###
932                );
933            },
934        );
935
936        check_metrics::<CppParser>(
937            "void f() {
938                 if (a || b || c || d) { // +2 (+1 sequence of ||)
939                     printf(\"test\");
940                 }
941             }",
942            "foo.c",
943            |metric| {
944                insta::assert_json_snapshot!(
945                    metric.cognitive,
946                    @r###"
947                    {
948                      "sum": 2.0,
949                      "average": 2.0,
950                      "min": 0.0,
951                      "max": 2.0
952                    }"###
953                );
954            },
955        );
956    }
957
958    #[test]
959    fn mozjs_sequence_same_booleans() {
960        check_metrics::<JavascriptParser>(
961            "function f() {
962                 if (a && b && 1 == 1) { // +2 (+1 sequence of &&)
963                     window.print(\"test\");
964                 }
965             }",
966            "foo.js",
967            |metric| {
968                insta::assert_json_snapshot!(
969                    metric.cognitive,
970                    @r###"
971                    {
972                      "sum": 2.0,
973                      "average": 2.0,
974                      "min": 0.0,
975                      "max": 2.0
976                    }"###
977                );
978            },
979        );
980
981        check_metrics::<JavascriptParser>(
982            "function f() {
983                 if (a || b || c || d) { // +2 (+1 sequence of ||)
984                     window.print(\"test\");
985                 }
986             }",
987            "foo.js",
988            |metric| {
989                insta::assert_json_snapshot!(
990                    metric.cognitive,
991                    @r###"
992                    {
993                      "sum": 2.0,
994                      "average": 2.0,
995                      "min": 0.0,
996                      "max": 2.0
997                    }"###
998                );
999            },
1000        );
1001    }
1002
1003    #[test]
1004    fn rust_not_booleans() {
1005        check_metrics::<RustParser>(
1006            "fn f() {
1007                 if !a && !b { // +2 (+1 &&)
1008                     println!(\"test\");
1009                 }
1010             }",
1011            "foo.rs",
1012            |metric| {
1013                insta::assert_json_snapshot!(
1014                    metric.cognitive,
1015                    @r###"
1016                    {
1017                      "sum": 2.0,
1018                      "average": 2.0,
1019                      "min": 0.0,
1020                      "max": 2.0
1021                    }"###
1022                );
1023            },
1024        );
1025
1026        check_metrics::<RustParser>(
1027            "fn f() {
1028                 if a && !(b && c) { // +3 (+1 &&, +1 &&)
1029                     println!(\"test\");
1030                 }
1031             }",
1032            "foo.rs",
1033            |metric| {
1034                insta::assert_json_snapshot!(
1035                    metric.cognitive,
1036                    @r###"
1037                    {
1038                      "sum": 3.0,
1039                      "average": 3.0,
1040                      "min": 0.0,
1041                      "max": 3.0
1042                    }"###
1043                );
1044            },
1045        );
1046
1047        check_metrics::<RustParser>(
1048            "fn f() {
1049                 if !(a || b) && !(c || d) { // +4 (+1 ||, +1 &&, +1 ||)
1050                     println!(\"test\");
1051                 }
1052             }",
1053            "foo.rs",
1054            |metric| {
1055                insta::assert_json_snapshot!(
1056                    metric.cognitive,
1057                    @r###"
1058                    {
1059                      "sum": 4.0,
1060                      "average": 4.0,
1061                      "min": 0.0,
1062                      "max": 4.0
1063                    }"###
1064                );
1065            },
1066        );
1067    }
1068
1069    #[test]
1070    fn c_not_booleans() {
1071        check_metrics::<CppParser>(
1072            "void f() {
1073                 if (a && !(b && c)) { // +3 (+1 &&, +1 &&)
1074                     printf(\"test\");
1075                 }
1076             }",
1077            "foo.c",
1078            |metric| {
1079                insta::assert_json_snapshot!(
1080                    metric.cognitive,
1081                    @r###"
1082                    {
1083                      "sum": 3.0,
1084                      "average": 3.0,
1085                      "min": 0.0,
1086                      "max": 3.0
1087                    }"###
1088                );
1089            },
1090        );
1091
1092        check_metrics::<CppParser>(
1093            "void f() {
1094                 if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||)
1095                     printf(\"test\");
1096                 }
1097             }",
1098            "foo.c",
1099            |metric| {
1100                insta::assert_json_snapshot!(
1101                    metric.cognitive,
1102                    @r###"
1103                    {
1104                      "sum": 4.0,
1105                      "average": 4.0,
1106                      "min": 0.0,
1107                      "max": 4.0
1108                    }"###
1109                );
1110            },
1111        );
1112    }
1113
1114    #[test]
1115    fn mozjs_not_booleans() {
1116        check_metrics::<JavascriptParser>(
1117            "function f() {
1118                 if (a && !(b && c)) { // +3 (+1 &&, +1 &&)
1119                     window.print(\"test\");
1120                 }
1121             }",
1122            "foo.js",
1123            |metric| {
1124                insta::assert_json_snapshot!(
1125                    metric.cognitive,
1126                    @r###"
1127                    {
1128                      "sum": 3.0,
1129                      "average": 3.0,
1130                      "min": 0.0,
1131                      "max": 3.0
1132                    }"###
1133                );
1134            },
1135        );
1136
1137        check_metrics::<JavascriptParser>(
1138            "function f() {
1139                 if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||)
1140                     window.print(\"test\");
1141                 }
1142             }",
1143            "foo.js",
1144            |metric| {
1145                insta::assert_json_snapshot!(
1146                    metric.cognitive,
1147                    @r###"
1148                    {
1149                      "sum": 4.0,
1150                      "average": 4.0,
1151                      "min": 0.0,
1152                      "max": 4.0
1153                    }"###
1154                );
1155            },
1156        );
1157    }
1158
1159    #[test]
1160    fn python_sequence_different_booleans() {
1161        check_metrics::<PythonParser>(
1162            "def f(a, b):
1163                if a and b or True:  # +3 (+1 and, +1 or)
1164                   return 1",
1165            "foo.py",
1166            |metric| {
1167                insta::assert_json_snapshot!(
1168                    metric.cognitive,
1169                    @r###"
1170                    {
1171                      "sum": 3.0,
1172                      "average": 3.0,
1173                      "min": 0.0,
1174                      "max": 3.0
1175                    }"###
1176                );
1177            },
1178        );
1179    }
1180
1181    #[test]
1182    fn rust_sequence_different_booleans() {
1183        check_metrics::<RustParser>(
1184            "fn f() {
1185                 if a && b || true { // +3 (+1 &&, +1 ||)
1186                     println!(\"test\");
1187                 }
1188             }",
1189            "foo.rs",
1190            |metric| {
1191                insta::assert_json_snapshot!(
1192                    metric.cognitive,
1193                    @r###"
1194                    {
1195                      "sum": 3.0,
1196                      "average": 3.0,
1197                      "min": 0.0,
1198                      "max": 3.0
1199                    }"###
1200                );
1201            },
1202        );
1203    }
1204
1205    #[test]
1206    fn c_sequence_different_booleans() {
1207        check_metrics::<CppParser>(
1208            "void f() {
1209                 if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||)
1210                     printf(\"test\");
1211                 }
1212             }",
1213            "foo.c",
1214            |metric| {
1215                insta::assert_json_snapshot!(
1216                    metric.cognitive,
1217                    @r###"
1218                    {
1219                      "sum": 3.0,
1220                      "average": 3.0,
1221                      "min": 0.0,
1222                      "max": 3.0
1223                    }"###
1224                );
1225            },
1226        );
1227    }
1228
1229    #[test]
1230    fn mozjs_sequence_different_booleans() {
1231        check_metrics::<JavascriptParser>(
1232            "function f() {
1233                 if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||)
1234                     window.print(\"test\");
1235                 }
1236             }",
1237            "foo.js",
1238            |metric| {
1239                insta::assert_json_snapshot!(
1240                    metric.cognitive,
1241                    @r###"
1242                    {
1243                      "sum": 3.0,
1244                      "average": 3.0,
1245                      "min": 0.0,
1246                      "max": 3.0
1247                    }"###
1248                );
1249            },
1250        );
1251    }
1252
1253    #[test]
1254    fn python_formatted_sequence_different_booleans() {
1255        check_metrics::<PythonParser>(
1256            "def f(a, b):
1257                if (  # +1
1258                    a and b and  # +1
1259                    (c or d)  # +1
1260                ):
1261                   return 1",
1262            "foo.py",
1263            |metric| {
1264                insta::assert_json_snapshot!(
1265                    metric.cognitive,
1266                    @r###"
1267                    {
1268                      "sum": 3.0,
1269                      "average": 3.0,
1270                      "min": 0.0,
1271                      "max": 3.0
1272                    }"###
1273                );
1274            },
1275        );
1276    }
1277
1278    #[test]
1279    fn python_1_level_nesting() {
1280        check_metrics::<PythonParser>(
1281            "def f(a, b):
1282                if a:  # +1
1283                    for i in range(b):  # +2
1284                        return 1",
1285            "foo.py",
1286            |metric| {
1287                insta::assert_json_snapshot!(
1288                    metric.cognitive,
1289                    @r###"
1290                    {
1291                      "sum": 3.0,
1292                      "average": 3.0,
1293                      "min": 0.0,
1294                      "max": 3.0
1295                    }"###
1296                );
1297            },
1298        );
1299    }
1300
1301    #[test]
1302    fn rust_1_level_nesting() {
1303        check_metrics::<RustParser>(
1304            "fn f() {
1305                 if true { // +1
1306                     if true { // +2 (nesting = 1)
1307                         println!(\"test\");
1308                     } else if 1 == 1 { // +1
1309                         if true { // +3 (nesting = 2)
1310                             println!(\"test\");
1311                         }
1312                     } else { // +1
1313                         if true { // +3 (nesting = 2)
1314                             println!(\"test\");
1315                         }
1316                     }
1317                 }
1318             }",
1319            "foo.rs",
1320            |metric| {
1321                insta::assert_json_snapshot!(
1322                    metric.cognitive,
1323                    @r###"
1324                    {
1325                      "sum": 11.0,
1326                      "average": 11.0,
1327                      "min": 0.0,
1328                      "max": 11.0
1329                    }"###
1330                );
1331            },
1332        );
1333
1334        check_metrics::<RustParser>(
1335            "fn f() {
1336                 if true { // +1
1337                     match true { // +2 (nesting = 1)
1338                         true => println!(\"test\"),
1339                         false => println!(\"test\"),
1340                     }
1341                 }
1342             }",
1343            "foo.rs",
1344            |metric| {
1345                insta::assert_json_snapshot!(
1346                    metric.cognitive,
1347                    @r###"
1348                    {
1349                      "sum": 3.0,
1350                      "average": 3.0,
1351                      "min": 0.0,
1352                      "max": 3.0
1353                    }"###
1354                );
1355            },
1356        );
1357    }
1358
1359    #[test]
1360    fn c_1_level_nesting() {
1361        check_metrics::<CppParser>(
1362            "void f() {
1363                 if (1 == 1) { // +1
1364                     if (1 == 1) { // +2 (nesting = 1)
1365                         printf(\"test\");
1366                     } else if (1 == 1) { // +1
1367                         if (1 == 1) { // +3 (nesting = 2)
1368                             printf(\"test\");
1369                         }
1370                     } else { // +1
1371                         if (1 == 1) { // +3 (nesting = 2)
1372                             printf(\"test\");
1373                         }
1374                     }
1375                 }
1376             }",
1377            "foo.c",
1378            |metric| {
1379                insta::assert_json_snapshot!(
1380                    metric.cognitive,
1381                    @r###"
1382                    {
1383                      "sum": 11.0,
1384                      "average": 11.0,
1385                      "min": 0.0,
1386                      "max": 11.0
1387                    }"###
1388                );
1389            },
1390        );
1391    }
1392
1393    #[test]
1394    fn mozjs_1_level_nesting() {
1395        check_metrics::<JavascriptParser>(
1396            "function f() {
1397                 if (1 == 1) { // +1
1398                     if (1 == 1) { // +2 (nesting = 1)
1399                         window.print(\"test\");
1400                     } else if (1 == 1) { // +1
1401                         if (1 == 1) { // +3 (nesting = 2)
1402                             window.print(\"test\");
1403                         }
1404                     } else { // +1
1405                         if (1 == 1) { // +3 (nesting = 2)
1406                             window.print(\"test\");
1407                         }
1408                     }
1409                 }
1410             }",
1411            "foo.js",
1412            |metric| {
1413                insta::assert_json_snapshot!(
1414                    metric.cognitive,
1415                    @r###"
1416                    {
1417                      "sum": 11.0,
1418                      "average": 11.0,
1419                      "min": 0.0,
1420                      "max": 11.0
1421                    }"###
1422                );
1423            },
1424        );
1425    }
1426
1427    #[test]
1428    fn python_2_level_nesting() {
1429        check_metrics::<PythonParser>(
1430            "def f(a, b):
1431                if a:  # +1
1432                    for i in range(b):  # +2
1433                        if b:  # +3
1434                            return 1",
1435            "foo.py",
1436            |metric| {
1437                insta::assert_json_snapshot!(
1438                    metric.cognitive,
1439                    @r###"
1440                    {
1441                      "sum": 6.0,
1442                      "average": 6.0,
1443                      "min": 0.0,
1444                      "max": 6.0
1445                    }"###
1446                );
1447            },
1448        );
1449    }
1450
1451    #[test]
1452    fn rust_2_level_nesting() {
1453        check_metrics::<RustParser>(
1454            "fn f() {
1455                 if true { // +1
1456                     for i in 0..4 { // +2 (nesting = 1)
1457                         match true { // +3 (nesting = 2)
1458                             true => println!(\"test\"),
1459                             false => println!(\"test\"),
1460                         }
1461                     }
1462                 }
1463             }",
1464            "foo.rs",
1465            |metric| {
1466                insta::assert_json_snapshot!(
1467                    metric.cognitive,
1468                    @r###"
1469                    {
1470                      "sum": 6.0,
1471                      "average": 6.0,
1472                      "min": 0.0,
1473                      "max": 6.0
1474                    }"###
1475                );
1476            },
1477        );
1478    }
1479
1480    #[test]
1481    fn python_try_construct() {
1482        check_metrics::<PythonParser>(
1483            "def f(a, b):
1484                try:
1485                    for foo in bar:  # +1
1486                        return a
1487                except Exception:  # +1
1488                    if a < 0:  # +2
1489                        return a",
1490            "foo.py",
1491            |metric| {
1492                insta::assert_json_snapshot!(
1493                    metric.cognitive,
1494                    @r###"
1495                    {
1496                      "sum": 4.0,
1497                      "average": 4.0,
1498                      "min": 0.0,
1499                      "max": 4.0
1500                    }"###
1501                );
1502            },
1503        );
1504    }
1505
1506    #[test]
1507    fn mozjs_try_construct() {
1508        check_metrics::<JavascriptParser>(
1509            "function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
1510                 for (const collector of this.collectors) {
1511                     try {
1512                         collector._onChannelRedirect(oldChannel, newChannel, flags);
1513                     } catch (ex) {
1514                         console.error(
1515                             \"StackTraceCollector.onChannelRedirect threw an exception\",
1516                              ex
1517                         );
1518                     }
1519                 }
1520                 callback.onRedirectVerifyCallback(Cr.NS_OK);
1521             }",
1522            "foo.js",
1523            |metric| {
1524                insta::assert_json_snapshot!(
1525                    metric.cognitive,
1526                    @r###"
1527                    {
1528                      "sum": 3.0,
1529                      "average": 3.0,
1530                      "min": 0.0,
1531                      "max": 3.0
1532                    }"###
1533                );
1534            },
1535        );
1536    }
1537
1538    #[test]
1539    fn rust_break_continue() {
1540        // Only labeled break and continue statements are considered
1541        check_metrics::<RustParser>(
1542            "fn f() {
1543                 'tens: for ten in 0..3 { // +1
1544                     '_units: for unit in 0..=9 { // +2 (nesting = 1)
1545                         if unit % 2 == 0 { // +3 (nesting = 2)
1546                             continue;
1547                         } else if unit == 5 { // +1
1548                             continue 'tens; // +1
1549                         } else if unit == 6 { // +1
1550                             break;
1551                         } else { // +1
1552                             break 'tens; // +1
1553                         }
1554                     }
1555                 }
1556             }",
1557            "foo.rs",
1558            |metric| {
1559                insta::assert_json_snapshot!(
1560                    metric.cognitive,
1561                    @r###"
1562                    {
1563                      "sum": 11.0,
1564                      "average": 11.0,
1565                      "min": 0.0,
1566                      "max": 11.0
1567                    }"###
1568                );
1569            },
1570        );
1571    }
1572
1573    #[test]
1574    fn c_goto() {
1575        check_metrics::<CppParser>(
1576            "void f() {
1577             OUT: for (int i = 1; i <= max; ++i) { // +1
1578                      for (int j = 2; j < i; ++j) { // +2 (nesting = 1)
1579                          if (i % j == 0) { // +3 (nesting = 2)
1580                              goto OUT; // +1
1581                          }
1582                      }
1583                  }
1584             }",
1585            "foo.c",
1586            |metric| {
1587                insta::assert_json_snapshot!(
1588                    metric.cognitive,
1589                    @r###"
1590                    {
1591                      "sum": 7.0,
1592                      "average": 7.0,
1593                      "min": 0.0,
1594                      "max": 7.0
1595                    }"###
1596                );
1597            },
1598        );
1599    }
1600
1601    #[test]
1602    fn c_switch() {
1603        check_metrics::<CppParser>(
1604            "void f() {
1605                 switch (1) { // +1
1606                     case 1:
1607                         printf(\"one\");
1608                         break;
1609                     case 2:
1610                         printf(\"two\");
1611                         break;
1612                     case 3:
1613                         printf(\"three\");
1614                         break;
1615                     default:
1616                         printf(\"all\");
1617                         break;
1618                 }
1619             }",
1620            "foo.c",
1621            |metric| {
1622                insta::assert_json_snapshot!(
1623                    metric.cognitive,
1624                    @r###"
1625                    {
1626                      "sum": 1.0,
1627                      "average": 1.0,
1628                      "min": 0.0,
1629                      "max": 1.0
1630                    }"###
1631                );
1632            },
1633        );
1634    }
1635
1636    #[test]
1637    fn mozjs_switch() {
1638        check_metrics::<JavascriptParser>(
1639            "function f() {
1640                 switch (1) { // +1
1641                     case 1:
1642                         window.print(\"one\");
1643                         break;
1644                     case 2:
1645                         window.print(\"two\");
1646                         break;
1647                     case 3:
1648                         window.print(\"three\");
1649                         break;
1650                     default:
1651                         window.print(\"all\");
1652                         break;
1653                 }
1654             }",
1655            "foo.js",
1656            |metric| {
1657                insta::assert_json_snapshot!(
1658                    metric.cognitive,
1659                    @r###"
1660                    {
1661                      "sum": 1.0,
1662                      "average": 1.0,
1663                      "min": 0.0,
1664                      "max": 1.0
1665                    }"###
1666                );
1667            },
1668        );
1669    }
1670
1671    #[test]
1672    fn python_ternary_operator() {
1673        check_metrics::<PythonParser>(
1674            "def f(a, b):
1675                 if a % 2:  # +1
1676                     return 'c' if a else 'd'  # +2
1677                 return 'a' if a else 'b'  # +1",
1678            "foo.py",
1679            |metric| {
1680                insta::assert_json_snapshot!(
1681                    metric.cognitive,
1682                    @r###"
1683                    {
1684                      "sum": 4.0,
1685                      "average": 4.0,
1686                      "min": 0.0,
1687                      "max": 4.0
1688                    }"###
1689                );
1690            },
1691        );
1692    }
1693
1694    #[test]
1695    fn python_nested_functions_lambdas() {
1696        check_metrics::<PythonParser>(
1697            "def f(a, b):
1698                 def foo(a):
1699                     if a:  # +2 (+1 nesting)
1700                         return 1
1701                 # +3 (+1 for boolean sequence +2 for lambda nesting)
1702                 bar = lambda a: lambda b: b or True or True
1703                 return bar(foo(a))(a)",
1704            "foo.py",
1705            |metric| {
1706                // 2 functions + 2 lambdas = 4
1707                insta::assert_json_snapshot!(
1708                    metric.cognitive,
1709                    @r###"
1710                    {
1711                      "sum": 5.0,
1712                      "average": 1.25,
1713                      "min": 0.0,
1714                      "max": 3.0
1715                    }"###
1716                );
1717            },
1718        );
1719    }
1720
1721    #[test]
1722    fn python_real_function() {
1723        check_metrics::<PythonParser>(
1724            "def process_raw_constant(constant, min_word_length):
1725                 processed_words = []
1726                 raw_camelcase_words = []
1727                 for raw_word in re.findall(r'[a-z]+', constant):  # +1
1728                     word = raw_word.strip()
1729                         if (  # +2 (+1 if and +1 nesting)
1730                             len(word) >= min_word_length
1731                             and not (word.startswith('-') or word.endswith('-')) # +2 operators
1732                         ):
1733                             if is_camel_case_word(word):  # +3 (+1 if and +2 nesting)
1734                                 raw_camelcase_words.append(word)
1735                             else: # +1 else
1736                                 processed_words.append(word.lower())
1737                 return processed_words, raw_camelcase_words",
1738            "foo.py",
1739            |metric| {
1740                insta::assert_json_snapshot!(
1741                    metric.cognitive,
1742                    @r###"
1743                    {
1744                      "sum": 9.0,
1745                      "average": 9.0,
1746                      "min": 0.0,
1747                      "max": 9.0
1748                    }"###
1749                );
1750            },
1751        );
1752    }
1753
1754    #[test]
1755    fn rust_if_let_else_if_else() {
1756        check_metrics::<RustParser>(
1757            "pub fn create_usage_no_title(p: &Parser, used: &[&str]) -> String {
1758                 debugln!(\"usage::create_usage_no_title;\");
1759                 if let Some(u) = p.meta.usage_str { // +1
1760                     String::from(&*u)
1761                 } else if used.is_empty() { // +1
1762                     create_help_usage(p, true)
1763                 } else { // +1
1764                     create_smart_usage(p, used)
1765                }
1766            }",
1767            "foo.rs",
1768            |metric| {
1769                insta::assert_json_snapshot!(
1770                    metric.cognitive,
1771                    @r###"
1772                    {
1773                      "sum": 3.0,
1774                      "average": 3.0,
1775                      "min": 0.0,
1776                      "max": 3.0
1777                    }"###
1778                );
1779            },
1780        );
1781    }
1782
1783    #[test]
1784    fn typescript_if_else_if_else() {
1785        check_metrics::<TypescriptParser>(
1786            "function foo() {
1787                 if (this._closed) return Promise.resolve(); // +1
1788                 if (this._tempDirectory) { // +1
1789                     this.kill();
1790                 } else if (this.connection) { // +1
1791                     this.kill();
1792                 } else { // +1
1793                     throw new Error(`Error`);
1794                }
1795                helper.removeEventListeners(this._listeners);
1796                return this._processClosing;
1797            }",
1798            "foo.ts",
1799            |metric| {
1800                insta::assert_json_snapshot!(
1801                    metric.cognitive,
1802                    @r###"
1803                    {
1804                      "sum": 4.0,
1805                      "average": 4.0,
1806                      "min": 0.0,
1807                      "max": 4.0
1808                    }"###
1809                );
1810            },
1811        );
1812    }
1813
1814    #[test]
1815    fn rust_loop() {
1816        check_metrics::<RustParser>(
1817            "fn f(a: bool) {
1818                 loop { // +1
1819                     if a { // +2 (nesting = 1)
1820                         break;
1821                     }
1822                 }
1823             }",
1824            "foo.rs",
1825            |metric| {
1826                insta::assert_json_snapshot!(
1827                    metric.cognitive,
1828                    @r###"
1829                    {
1830                      "sum": 3.0,
1831                      "average": 3.0,
1832                      "min": 0.0,
1833                      "max": 3.0
1834                    }"###
1835                );
1836            },
1837        );
1838    }
1839
1840    #[test]
1841    fn javascript_if_else_if_else() {
1842        check_metrics::<JavascriptParser>(
1843            "function foo(a, b) {
1844                 if (a) { // +1
1845                     g();
1846                 } else if (b) { // +1
1847                     g();
1848                 } else { // +1
1849                     g();
1850                 }
1851             }",
1852            "foo.js",
1853            |metric| {
1854                insta::assert_json_snapshot!(
1855                    metric.cognitive,
1856                    @r###"
1857                    {
1858                      "sum": 3.0,
1859                      "average": 3.0,
1860                      "min": 0.0,
1861                      "max": 3.0
1862                    }"###
1863                );
1864            },
1865        );
1866    }
1867
1868    #[test]
1869    fn tsx_if_else_if_else() {
1870        check_metrics::<TsxParser>(
1871            "function foo(a, b) {
1872                 if (a) { // +1
1873                     g();
1874                 } else if (b) { // +1
1875                     g();
1876                 } else { // +1
1877                     g();
1878                 }
1879             }",
1880            "foo.tsx",
1881            |metric| {
1882                insta::assert_json_snapshot!(
1883                    metric.cognitive,
1884                    @r###"
1885                    {
1886                      "sum": 3.0,
1887                      "average": 3.0,
1888                      "min": 0.0,
1889                      "max": 3.0
1890                    }"###
1891                );
1892            },
1893        );
1894    }
1895
1896    #[test]
1897    fn java_enhanced_for() {
1898        check_metrics::<JavaParser>(
1899            "class X {
1900                void f(int[] xs) {
1901                    for (int x : xs) { // +1
1902                        if (x > 0) { // +2 (nesting = 1)
1903                            g(x);
1904                        }
1905                    }
1906                }
1907            }",
1908            "foo.java",
1909            |metric| {
1910                insta::assert_json_snapshot!(
1911                    metric.cognitive,
1912                    @r###"
1913                    {
1914                      "sum": 3.0,
1915                      "average": 3.0,
1916                      "min": 0.0,
1917                      "max": 3.0
1918                    }"###
1919                );
1920            },
1921        );
1922    }
1923
1924    #[test]
1925    fn java_ternary() {
1926        check_metrics::<JavaParser>(
1927            "class X {
1928                int f(boolean a) {
1929                    return a ? 1 : 2; // +1
1930                }
1931            }",
1932            "foo.java",
1933            |metric| {
1934                insta::assert_json_snapshot!(
1935                    metric.cognitive,
1936                    @r###"
1937                    {
1938                      "sum": 1.0,
1939                      "average": 1.0,
1940                      "min": 0.0,
1941                      "max": 1.0
1942                    }"###
1943                );
1944            },
1945        );
1946    }
1947
1948    #[test]
1949    fn java_else_if() {
1950        check_metrics::<JavaParser>(
1951            "class X {
1952                void f(int a) {
1953                    if (a == 1) { // +1
1954                        g();
1955                    } else if (a == 2) { // +1
1956                        g();
1957                    } else { // +1
1958                        g();
1959                    }
1960                }
1961            }",
1962            "foo.java",
1963            |metric| {
1964                insta::assert_json_snapshot!(
1965                    metric.cognitive,
1966                    @r###"
1967                    {
1968                      "sum": 3.0,
1969                      "average": 3.0,
1970                      "min": 0.0,
1971                      "max": 3.0
1972                    }"###
1973                );
1974            },
1975        );
1976    }
1977
1978    #[test]
1979    fn python_match() {
1980        check_metrics::<PythonParser>(
1981            "def f(x):
1982                match x:  # +1
1983                    case 1:
1984                        if x:  # +2 (nesting = 1)
1985                            g(x)",
1986            "foo.py",
1987            |metric| {
1988                insta::assert_json_snapshot!(
1989                    metric.cognitive,
1990                    @r###"
1991                    {
1992                      "sum": 3.0,
1993                      "average": 3.0,
1994                      "min": 0.0,
1995                      "max": 3.0
1996                    }"###
1997                );
1998            },
1999        );
2000    }
2001
2002    #[test]
2003    fn python_finally_no_increment() {
2004        check_metrics::<PythonParser>(
2005            "def f():
2006                try:
2007                    g()
2008                finally:  # +0 (finally is not a branch)
2009                    h()",
2010            "foo.py",
2011            |metric| {
2012                insta::assert_json_snapshot!(
2013                    metric.cognitive,
2014                    @r###"
2015                    {
2016                      "sum": 0.0,
2017                      "average": 0.0,
2018                      "min": 0.0,
2019                      "max": 0.0
2020                    }"###
2021                );
2022            },
2023        );
2024    }
2025
2026    #[test]
2027    fn java_no_cognitive() {
2028        check_metrics::<JavaParser>("int a = 42;", "foo.java", |metric| {
2029            insta::assert_json_snapshot!(
2030                metric.cognitive,
2031                @r###"
2032            {
2033              "sum": 0.0,
2034              "average": null,
2035              "min": 0.0,
2036              "max": 0.0
2037            }
2038            "###
2039            );
2040        });
2041    }
2042
2043    #[test]
2044    fn java_single_branch_function() {
2045        check_metrics::<JavaParser>(
2046            "class X {
2047                public static void print(boolean a){  
2048                if(a){ // +1
2049                  System.out.println(\"test1\");
2050                }
2051              }
2052            }",
2053            "foo.java",
2054            |metric| {
2055                insta::assert_json_snapshot!(
2056                    metric.cognitive,
2057                    @r###"
2058                {
2059                  "sum": 1.0,
2060                  "average": 1.0,
2061                  "min": 0.0,
2062                  "max": 1.0
2063                }
2064                "###
2065                );
2066            },
2067        );
2068    }
2069
2070    #[test]
2071    fn java_multiple_branch_function() {
2072        check_metrics::<JavaParser>(
2073            "class X {
2074              public static void print(boolean a, boolean b){  
2075                if(a){ // +1
2076                  System.out.println(\"test1\");
2077                }
2078                if(b){ // +1
2079                  System.out.println(\"test2\");
2080                }
2081                else { // +1
2082                  System.out.println(\"test3\");
2083                }
2084              }
2085            }",
2086            "foo.java",
2087            |metric| {
2088                insta::assert_json_snapshot!(
2089                    metric.cognitive,
2090                    @r###"
2091                {
2092                  "sum": 3.0,
2093                  "average": 3.0,
2094                  "min": 0.0,
2095                  "max": 3.0
2096                }
2097                "###
2098                );
2099            },
2100        );
2101    }
2102
2103    #[test]
2104    fn java_compound_conditions() {
2105        check_metrics::<JavaParser>(
2106            "class X {
2107              public static void print(boolean a, boolean b, boolean c, boolean d){  
2108                if(a && b){ // +2 (+1 &&)
2109                  System.out.println(\"test1\");
2110                }
2111                if(c && d){ // +2 (+1 &&)
2112                  System.out.println(\"test2\");
2113                }
2114              }
2115            }",
2116            "foo.java",
2117            |metric| {
2118                insta::assert_json_snapshot!(
2119                    metric.cognitive,
2120                    @r###"
2121                    {
2122                      "sum": 4.0,
2123                      "average": 4.0,
2124                      "min": 0.0,
2125                      "max": 4.0
2126                    }"###
2127                );
2128            },
2129        );
2130    }
2131
2132    #[test]
2133    fn java_switch_statement() {
2134        check_metrics::<JavaParser>(
2135            "class X {
2136              public static void print(boolean a, boolean b, boolean c, boolean d){
2137                switch(expr){ //+1
2138                  case 1:
2139                    System.out.println(\"test1\");
2140                    break;
2141                  case 2:
2142                    System.out.println(\"test2\");
2143                    break;
2144                  default:
2145                    System.out.println(\"test\");
2146                }
2147              }
2148            }",
2149            "foo.java",
2150            |metric| {
2151                insta::assert_json_snapshot!(
2152                    metric.cognitive,
2153                    @r###"
2154                    {
2155                      "sum": 1.0,
2156                      "average": 1.0,
2157                      "min": 0.0,
2158                      "max": 1.0
2159                    }"###
2160                );
2161            },
2162        );
2163    }
2164
2165    #[test]
2166    fn java_switch_expression() {
2167        check_metrics::<JavaParser>(
2168            "class X {
2169              public static void print(boolean a, boolean b, boolean c, boolean d){
2170                switch(expr){ // +1
2171                  case 1 -> System.out.println(\"test1\");
2172                  case 2 -> System.out.println(\"test2\");
2173                  default -> System.out.println(\"test\");
2174                }
2175              }
2176            }",
2177            "foo.java",
2178            |metric| {
2179                insta::assert_json_snapshot!(
2180                    metric.cognitive,
2181                    @r###"
2182                    {
2183                      "sum": 1.0,
2184                      "average": 1.0,
2185                      "min": 0.0,
2186                      "max": 1.0
2187                    }"###
2188                );
2189            },
2190        );
2191    }
2192
2193    #[test]
2194    fn java_not_booleans() {
2195        check_metrics::<JavaParser>(
2196            "class X {
2197              public static void print(boolean a, boolean b, boolean c, boolean d){
2198                if (a && !(b && c)) { // +3 (+1 &&, +1 &&)
2199                  printf(\"test\");
2200                }
2201              }
2202            }",
2203            "foo.java",
2204            |metric| {
2205                insta::assert_json_snapshot!(
2206                    metric.cognitive,
2207                    @r###"
2208                    {
2209                      "sum": 3.0,
2210                      "average": 3.0,
2211                      "min": 0.0,
2212                      "max": 3.0
2213                    }"###
2214                );
2215            },
2216        );
2217    }
2218
2219    #[test]
2220    fn rust_boolean_sequence_across_else_if() {
2221        // The `&&` in the else-if condition is its own boolean sequence, not a
2222        // continuation of the `if` condition's `&&`. Without a reset at the
2223        // `else` boundary the two runs merge and the else-if operator is
2224        // dropped, undercounting by 1.
2225        check_metrics::<RustParser>(
2226            "fn f() {
2227                 if a && b { // +2 (+1 if, +1 &&)
2228                     foo();
2229                 } else if c && d { // +2 (+1 else, +1 &&)
2230                     bar();
2231                 }
2232             }",
2233            "foo.rs",
2234            |metric| {
2235                insta::assert_json_snapshot!(
2236                    metric.cognitive,
2237                    @r###"
2238                    {
2239                      "sum": 4.0,
2240                      "average": 4.0,
2241                      "min": 0.0,
2242                      "max": 4.0
2243                    }"###
2244                );
2245            },
2246        );
2247    }
2248
2249    #[test]
2250    fn c_boolean_sequence_across_else_if() {
2251        check_metrics::<CppParser>(
2252            "void f() {
2253                 if (a && b) { // +2 (+1 if, +1 &&)
2254                     g();
2255                 } else if (c && d) { // +2 (+1 else, +1 &&)
2256                     h();
2257                 }
2258             }",
2259            "foo.c",
2260            |metric| {
2261                insta::assert_json_snapshot!(
2262                    metric.cognitive,
2263                    @r###"
2264                    {
2265                      "sum": 4.0,
2266                      "average": 4.0,
2267                      "min": 0.0,
2268                      "max": 4.0
2269                    }"###
2270                );
2271            },
2272        );
2273    }
2274
2275    #[test]
2276    fn java_boolean_sequence_across_else_if() {
2277        check_metrics::<JavaParser>(
2278            "class X {
2279                void f(boolean a, boolean b, boolean c, boolean d) {
2280                    if (a && b) { // +2 (+1 if, +1 &&)
2281                        g();
2282                    } else if (c && d) { // +2 (+1 else, +1 &&)
2283                        h();
2284                    }
2285                }
2286            }",
2287            "foo.java",
2288            |metric| {
2289                insta::assert_json_snapshot!(
2290                    metric.cognitive,
2291                    @r###"
2292                    {
2293                      "sum": 4.0,
2294                      "average": 4.0,
2295                      "min": 0.0,
2296                      "max": 4.0
2297                    }"###
2298                );
2299            },
2300        );
2301    }
2302
2303    #[test]
2304    fn javascript_boolean_sequence_across_else_if() {
2305        // Empty branch bodies isolate the `else` boundary: nothing between the
2306        // two conditions resets the boolean sequence, so a missing reset at
2307        // `else` would merge the two `&&` runs. (A non-empty body would mask
2308        // the defect via the `ExpressionStatement` reset.)
2309        check_metrics::<JavascriptParser>(
2310            "function f(a, b, c, d) {
2311                 if (a && b) { // +2 (+1 if, +1 &&)
2312                 } else if (c && d) { // +2 (+1 else, +1 &&)
2313                 }
2314             }",
2315            "foo.js",
2316            |metric| {
2317                insta::assert_json_snapshot!(
2318                    metric.cognitive,
2319                    @r###"
2320                    {
2321                      "sum": 4.0,
2322                      "average": 4.0,
2323                      "min": 0.0,
2324                      "max": 4.0
2325                    }"###
2326                );
2327            },
2328        );
2329    }
2330
2331    #[test]
2332    fn typescript_boolean_sequence_across_else_if() {
2333        check_metrics::<TypescriptParser>(
2334            "function f(a, b, c, d) {
2335                 if (a && b) { // +2 (+1 if, +1 &&)
2336                 } else if (c && d) { // +2 (+1 else, +1 &&)
2337                 }
2338             }",
2339            "foo.ts",
2340            |metric| {
2341                insta::assert_json_snapshot!(
2342                    metric.cognitive,
2343                    @r###"
2344                    {
2345                      "sum": 4.0,
2346                      "average": 4.0,
2347                      "min": 0.0,
2348                      "max": 4.0
2349                    }"###
2350                );
2351            },
2352        );
2353    }
2354
2355    #[test]
2356    fn tsx_boolean_sequence_across_else_if() {
2357        check_metrics::<TsxParser>(
2358            "function f(a, b, c, d) {
2359                 if (a && b) { // +2 (+1 if, +1 &&)
2360                 } else if (c && d) { // +2 (+1 else, +1 &&)
2361                 }
2362             }",
2363            "foo.tsx",
2364            |metric| {
2365                insta::assert_json_snapshot!(
2366                    metric.cognitive,
2367                    @r###"
2368                    {
2369                      "sum": 4.0,
2370                      "average": 4.0,
2371                      "min": 0.0,
2372                      "max": 4.0
2373                    }"###
2374                );
2375            },
2376        );
2377    }
2378
2379    #[test]
2380    fn python_boolean_sequence_across_else_if() {
2381        // Python's `elif` is its own grammar node (`ElifClause`) and already
2382        // resets the boolean sequence, so an `elif` here would not exercise the
2383        // defect. The reset that was missing is on the plain `else`
2384        // (`ElseClause`): the `and` in the `else` body is its own boolean
2385        // sequence, not a continuation of the `if` condition's `and`. Without a
2386        // reset at the `else` boundary the two runs merge and the else-body
2387        // operator is dropped, undercounting by 1 — 3 where every sibling
2388        // language scores 4 for the same shape. `return` (not an expression
2389        // statement) keeps the body's operator off the `ExpressionStatement`
2390        // reset, isolating the `else` boundary the way the JS test's empty
2391        // bodies do.
2392        check_metrics::<PythonParser>(
2393            "def f(a, b, c, d):
2394                 if a and b:  # +2 (+1 if, +1 and)
2395                     pass
2396                 else:  # +1 else
2397                     return c and d  # +1 and",
2398            "foo.py",
2399            |metric| {
2400                insta::assert_json_snapshot!(
2401                    metric.cognitive,
2402                    @r###"
2403                    {
2404                      "sum": 4.0,
2405                      "average": 4.0,
2406                      "min": 0.0,
2407                      "max": 4.0
2408                    }"###
2409                );
2410            },
2411        );
2412    }
2413
2414    #[test]
2415    fn python_except_nested_in_if() {
2416        // A nested `except` must pay the nesting penalty for the branches it
2417        // sits inside. Here the handler is one level deep (inside the `if`), so
2418        // it costs +2 (+1 base, +1 nesting), not +1.
2419        check_metrics::<PythonParser>(
2420            "def f(a):
2421                if a:  # +1
2422                    try:
2423                        g()
2424                    except Exception:  # +2 (+1 base, +1 nesting)
2425                        h()",
2426            "foo.py",
2427            |metric| {
2428                insta::assert_json_snapshot!(
2429                    metric.cognitive,
2430                    @r###"
2431                    {
2432                      "sum": 3.0,
2433                      "average": 3.0,
2434                      "min": 0.0,
2435                      "max": 3.0
2436                    }"###
2437                );
2438            },
2439        );
2440    }
2441
2442    #[test]
2443    fn python_except_not_inflated_by_sibling_nesting() {
2444        // The handler sits at nesting level 0; the deeply nested loops in the
2445        // `try` body must not inflate its cost. `except` costs +1, giving 4.
2446        check_metrics::<PythonParser>(
2447            "def f():
2448                try:
2449                    for i in x:  # +1
2450                        for j in y:  # +2 (nesting = 1)
2451                            g()
2452                except Exception:  # +1 (base only, nesting = 0)
2453                    h()",
2454            "foo.py",
2455            |metric| {
2456                insta::assert_json_snapshot!(
2457                    metric.cognitive,
2458                    @r###"
2459                    {
2460                      "sum": 4.0,
2461                      "average": 4.0,
2462                      "min": 0.0,
2463                      "max": 4.0
2464                    }"###
2465                );
2466            },
2467        );
2468    }
2469}