Skip to main content

rust_code_analysis_code_split/metrics/
abc.rs

1use serde::Serialize;
2use serde::ser::{SerializeStruct, Serializer};
3use std::fmt;
4
5use crate::checker::Checker;
6use crate::macros::implement_metric_trait;
7use crate::node::Node;
8use crate::*;
9
10/// The `ABC` metric.
11///
12/// The `ABC` metric measures the size of a source code by counting
13/// the number of Assignments (`A`), Branches (`B`) and Conditions (`C`).
14/// The metric defines an ABC score as a vector of three elements (`<A,B,C>`).
15/// The ABC score can be represented by its individual components (`A`, `B` and `C`)
16/// or by the magnitude of the vector (`|<A,B,C>| = sqrt(A^2 + B^2 + C^2)`).
17///
18/// Official paper and definition:
19///
20/// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
21///
22/// <https://www.softwarerenovation.com/Articles.aspx>
23#[derive(Debug, Clone)]
24pub struct Stats {
25    assignments: f64,
26    assignments_sum: f64,
27    assignments_min: f64,
28    assignments_max: f64,
29    branches: f64,
30    branches_sum: f64,
31    branches_min: f64,
32    branches_max: f64,
33    conditions: f64,
34    conditions_sum: f64,
35    conditions_min: f64,
36    conditions_max: f64,
37    space_count: usize,
38    declaration: Vec<DeclKind>,
39}
40
41#[derive(Debug, Clone)]
42enum DeclKind {
43    Var,
44    Const,
45}
46
47impl Default for Stats {
48    fn default() -> Self {
49        Self {
50            assignments: 0.,
51            assignments_sum: 0.,
52            assignments_min: f64::MAX,
53            assignments_max: 0.,
54            branches: 0.,
55            branches_sum: 0.,
56            branches_min: f64::MAX,
57            branches_max: 0.,
58            conditions: 0.,
59            conditions_sum: 0.,
60            conditions_min: f64::MAX,
61            conditions_max: 0.,
62            space_count: 1,
63            declaration: Vec::new(),
64        }
65    }
66}
67
68impl Serialize for Stats {
69    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
70    where
71        S: Serializer,
72    {
73        let mut st = serializer.serialize_struct("abc", 13)?;
74        st.serialize_field("assignments", &self.assignments_sum())?;
75        st.serialize_field("branches", &self.branches_sum())?;
76        st.serialize_field("conditions", &self.conditions_sum())?;
77        st.serialize_field("magnitude", &self.magnitude_sum())?;
78        st.serialize_field("assignments_average", &self.assignments_average())?;
79        st.serialize_field("branches_average", &self.branches_average())?;
80        st.serialize_field("conditions_average", &self.conditions_average())?;
81        st.serialize_field("assignments_min", &self.assignments_min())?;
82        st.serialize_field("assignments_max", &self.assignments_max())?;
83        st.serialize_field("branches_min", &self.branches_min())?;
84        st.serialize_field("branches_max", &self.branches_max())?;
85        st.serialize_field("conditions_min", &self.conditions_min())?;
86        st.serialize_field("conditions_max", &self.conditions_max())?;
87        st.end()
88    }
89}
90
91impl fmt::Display for Stats {
92    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93        write!(
94            f,
95            "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
96            assignments_average: {}, branches_average: {}, conditions_average: {}, \
97            assignments_min: {}, assignments_max: {}, \
98            branches_min: {}, branches_max: {}, \
99            conditions_min: {}, conditions_max: {}",
100            self.assignments_sum(),
101            self.branches_sum(),
102            self.conditions_sum(),
103            self.magnitude_sum(),
104            self.assignments_average(),
105            self.branches_average(),
106            self.conditions_average(),
107            self.assignments_min(),
108            self.assignments_max(),
109            self.branches_min(),
110            self.branches_max(),
111            self.conditions_min(),
112            self.conditions_max()
113        )
114    }
115}
116
117impl Stats {
118    /// Merges a second `Abc` metric into the first one.
119    pub fn merge(&mut self, other: &Stats) {
120        // Calculates minimum and maximum values
121        self.assignments_min = self.assignments_min.min(other.assignments_min);
122        self.assignments_max = self.assignments_max.max(other.assignments_max);
123        self.branches_min = self.branches_min.min(other.branches_min);
124        self.branches_max = self.branches_max.max(other.branches_max);
125        self.conditions_min = self.conditions_min.min(other.conditions_min);
126        self.conditions_max = self.conditions_max.max(other.conditions_max);
127
128        self.assignments_sum += other.assignments_sum;
129        self.branches_sum += other.branches_sum;
130        self.conditions_sum += other.conditions_sum;
131
132        self.space_count += other.space_count;
133    }
134
135    /// Returns the `Abc` assignments metric value.
136    pub fn assignments(&self) -> f64 {
137        self.assignments
138    }
139
140    /// Returns the `Abc` assignments sum metric value.
141    pub fn assignments_sum(&self) -> f64 {
142        self.assignments_sum
143    }
144
145    /// Returns the `Abc` assignments average value.
146    ///
147    /// This value is computed dividing the `Abc`
148    /// assignments value for the number of spaces.
149    pub fn assignments_average(&self) -> f64 {
150        self.assignments_sum() / self.space_count as f64
151    }
152
153    /// Returns the `Abc` assignments minimum value.
154    pub fn assignments_min(&self) -> f64 {
155        self.assignments_min
156    }
157
158    /// Returns the `Abc` assignments maximum value.
159    pub fn assignments_max(&self) -> f64 {
160        self.assignments_max
161    }
162
163    /// Returns the `Abc` branches metric value.
164    pub fn branches(&self) -> f64 {
165        self.branches
166    }
167
168    /// Returns the `Abc` branches sum metric value.
169    pub fn branches_sum(&self) -> f64 {
170        self.branches_sum
171    }
172
173    /// Returns the `Abc` branches average value.
174    ///
175    /// This value is computed dividing the `Abc`
176    /// branches value for the number of spaces.
177    pub fn branches_average(&self) -> f64 {
178        self.branches_sum() / self.space_count as f64
179    }
180
181    /// Returns the `Abc` branches minimum value.
182    pub fn branches_min(&self) -> f64 {
183        self.branches_min
184    }
185
186    /// Returns the `Abc` branches maximum value.
187    pub fn branches_max(&self) -> f64 {
188        self.branches_max
189    }
190
191    /// Returns the `Abc` conditions metric value.
192    pub fn conditions(&self) -> f64 {
193        self.conditions
194    }
195
196    /// Returns the `Abc` conditions sum metric value.
197    pub fn conditions_sum(&self) -> f64 {
198        self.conditions_sum
199    }
200
201    /// Returns the `Abc` conditions average value.
202    ///
203    /// This value is computed dividing the `Abc`
204    /// conditions value for the number of spaces.
205    pub fn conditions_average(&self) -> f64 {
206        self.conditions_sum() / self.space_count as f64
207    }
208
209    /// Returns the `Abc` conditions minimum value.
210    pub fn conditions_min(&self) -> f64 {
211        self.conditions_min
212    }
213
214    /// Returns the `Abc` conditions maximum value.
215    pub fn conditions_max(&self) -> f64 {
216        self.conditions_max
217    }
218
219    /// Returns the `Abc` magnitude metric value.
220    pub fn magnitude(&self) -> f64 {
221        (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
222    }
223
224    /// Returns the `Abc` magnitude sum metric value.
225    pub fn magnitude_sum(&self) -> f64 {
226        (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
227            .sqrt()
228    }
229
230    #[inline(always)]
231    pub(crate) fn compute_sum(&mut self) {
232        self.assignments_sum += self.assignments;
233        self.branches_sum += self.branches;
234        self.conditions_sum += self.conditions;
235    }
236
237    #[inline(always)]
238    pub(crate) fn compute_minmax(&mut self) {
239        self.assignments_min = self.assignments_min.min(self.assignments);
240        self.assignments_max = self.assignments_max.max(self.assignments);
241        self.branches_min = self.branches_min.min(self.branches);
242        self.branches_max = self.branches_max.max(self.branches);
243        self.conditions_min = self.conditions_min.min(self.conditions);
244        self.conditions_max = self.conditions_max.max(self.conditions);
245        self.compute_sum();
246    }
247}
248
249pub trait Abc
250where
251    Self: Checker,
252{
253    fn compute(node: &Node, stats: &mut Stats);
254}
255
256// Inspects the content of Java parenthesized expressions
257// and `Not` operators to find unary conditional expressions
258fn java_inspect_container(container_node: &Node, conditions: &mut f64) {
259    use Java::*;
260
261    let mut node = *container_node;
262    let mut node_kind = node.kind_id().into();
263
264    // Initializes the flag to true if the container is known to contain a boolean value
265    let mut has_boolean_content = match node.parent().unwrap().kind_id().into() {
266        BinaryExpression | IfStatement | WhileStatement | DoStatement | ForStatement => true,
267        TernaryExpression => node
268            .previous_sibling()
269            .is_none_or(|prev_node| !matches!(prev_node.kind_id().into(), QMARK | COLON)),
270        _ => false,
271    };
272
273    // Looks inside parenthesized expressions and `Not` operators to find what they contain
274    loop {
275        // Checks if the node is a parenthesized expression or a `Not` operator
276        // The child node of index 0 contains the unary expression operator (we look for the `!` operator)
277        let is_parenthesised_exp = matches!(node_kind, ParenthesizedExpression);
278        let is_not_operator = matches!(node_kind, UnaryExpression)
279            && matches!(node.child(0).unwrap().kind_id().into(), BANG);
280
281        // Stops the exploration if the node is neither
282        // a parenthesized expression nor a `Not` operator
283        if !is_parenthesised_exp && !is_not_operator {
284            break;
285        }
286
287        // Sets the flag to true if a `Not` operator is found
288        // This is used to prove if a variable or a value returned by a method is actually boolean
289        // e.g. `return (!x);`
290        if !has_boolean_content && is_not_operator {
291            has_boolean_content = true;
292        }
293
294        // Parenthesized expressions and `Not` operators nodes
295        // always store their expressions in the children nodes of index one
296        // https://github.com/tree-sitter/tree-sitter-java/blob/master/src/grammar.json#L2472
297        // https://github.com/tree-sitter/tree-sitter-java/blob/master/src/grammar.json#L2150
298        node = node.child(1).unwrap();
299        node_kind = node.kind_id().into();
300
301        // Stops the exploration when the content is found
302        if matches!(node_kind, MethodInvocation | Identifier | True | False) {
303            if has_boolean_content {
304                *conditions += 1.;
305            }
306            break;
307        }
308    }
309}
310
311// Inspects a list of elements and counts any unary conditional expression found
312fn java_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
313    use Java::*;
314
315    let list_kind = list_node.kind_id().into();
316    let mut cursor = list_node.cursor();
317
318    // Scans the immediate children nodes of the argument node
319    if cursor.goto_first_child() {
320        loop {
321            // Gets the current child node and its kind
322            let node = cursor.node();
323            let node_kind = node.kind_id().into();
324
325            // Checks if the node is a unary condition
326            if matches!(node_kind, MethodInvocation | Identifier | True | False)
327                && matches!(list_kind, BinaryExpression)
328                && !matches!(list_kind, ArgumentList)
329            {
330                *conditions += 1.;
331            } else {
332                // Checks if the node is a unary condition container
333                java_inspect_container(&node, conditions);
334            }
335
336            // Moves the cursor to the next sibling node of the current node
337            // Exits the scan if there is no next sibling node
338            if !cursor.goto_next_sibling() {
339                break;
340            }
341        }
342    }
343}
344
345implement_metric_trait!(
346    Abc,
347    PythonCode,
348    MozjsCode,
349    JavascriptCode,
350    TypescriptCode,
351    TsxCode,
352    RustCode,
353    CppCode,
354    PreprocCode,
355    CcommentCode,
356    KotlinCode
357);
358
359// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
360// Source: https://www.softwarerenovation.com/Articles.aspx
361// ABC Java rules: (page 8, figure 4)
362// ABC Java example: (page 15, listing 4)
363impl Abc for JavaCode {
364    fn compute(node: &Node, stats: &mut Stats) {
365        use Java::*;
366
367        match node.kind_id().into() {
368            STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | LTLTEQ | GTGTEQ | AMPEQ | PIPEEQ
369            | CARETEQ | GTGTGTEQ | PLUSPLUS | DASHDASH => {
370                stats.assignments += 1.;
371            }
372            FieldDeclaration | LocalVariableDeclaration => {
373                stats.declaration.push(DeclKind::Var);
374            }
375            Final => {
376                if let Some(DeclKind::Var) = stats.declaration.last() {
377                    stats.declaration.push(DeclKind::Const);
378                }
379            }
380            SEMI => {
381                if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
382                    stats.declaration.clear();
383                }
384            }
385            EQ => {
386                // Excludes constant declarations
387                stats
388                    .declaration
389                    .last()
390                    .map(|decl| {
391                        if matches!(decl, DeclKind::Var) {
392                            stats.assignments += 1.;
393                        }
394                    })
395                    .unwrap_or_else(|| {
396                        stats.assignments += 1.;
397                    });
398            }
399            MethodInvocation | New => {
400                stats.branches += 1.;
401            }
402            GTEQ | LTEQ | EQEQ | BANGEQ | Else | Case | Default | QMARK | Try | Catch => {
403                stats.conditions += 1.;
404            }
405            GT | LT => {
406                // Excludes `<` and `>` used for generic types
407                if let Some(parent) = node.parent()
408                    && !matches!(parent.kind_id().into(), TypeArguments)
409                {
410                    stats.conditions += 1.;
411                }
412            }
413            // Counts unary conditions in elements separated by `&&` or `||` boolean operators
414            AMPAMP | PIPEPIPE => {
415                if let Some(parent) = node.parent() {
416                    java_count_unary_conditions(&parent, &mut stats.conditions);
417                }
418            }
419            // Counts unary conditions among method arguments
420            ArgumentList => {
421                java_count_unary_conditions(node, &mut stats.conditions);
422            }
423            // Counts unary conditions inside assignments
424            VariableDeclarator | AssignmentExpression => {
425                // The child node of index 2 contains the right operand of an assignment operation
426                if let Some(right_operand) = node.child(2)
427                    && matches!(
428                        right_operand.kind_id().into(),
429                        ParenthesizedExpression | UnaryExpression
430                    )
431                {
432                    java_inspect_container(&right_operand, &mut stats.conditions);
433                }
434            }
435            // Counts unary conditions inside if and while statements
436            IfStatement | WhileStatement => {
437                // The child node of index 1 contains the condition
438                if let Some(condition) = node.child(1)
439                    && matches!(condition.kind_id().into(), ParenthesizedExpression)
440                {
441                    java_inspect_container(&condition, &mut stats.conditions);
442                }
443            }
444            // Counts unary conditions do-while statements
445            DoStatement => {
446                // The child node of index 3 contains the condition
447                if let Some(condition) = node.child(3)
448                    && matches!(condition.kind_id().into(), ParenthesizedExpression)
449                {
450                    java_inspect_container(&condition, &mut stats.conditions);
451                }
452            }
453            // Counts unary conditions inside for statements
454            ForStatement => {
455                // The child node of index 3 contains the `condition` when
456                // the initialization expression is a variable declaration
457                // e.g. `for ( int i=0; `condition`; ... ) {}`
458                if let Some(condition) = node.child(3) {
459                    match condition.kind_id().into() {
460                        SEMI => {
461                            // The child node of index 4 contains the `condition` when
462                            // the initialization expression is not a variable declaration
463                            // e.g. `for ( i=0; `condition`; ... ) {}`
464                            if let Some(cond) = node.child(4) {
465                                match cond.kind_id().into() {
466                                    MethodInvocation | Identifier | True | False | SEMI
467                                    | RPAREN => {
468                                        stats.conditions += 1.;
469                                    }
470                                    ParenthesizedExpression | UnaryExpression => {
471                                        java_inspect_container(&cond, &mut stats.conditions);
472                                    }
473                                    _ => {}
474                                }
475                            }
476                        }
477                        MethodInvocation | Identifier | True | False => {
478                            stats.conditions += 1.;
479                        }
480                        ParenthesizedExpression | UnaryExpression => {
481                            java_inspect_container(&condition, &mut stats.conditions);
482                        }
483                        _ => {}
484                    }
485                }
486            }
487            // Counts unary conditions inside return statements
488            ReturnStatement => {
489                // The child node of index 1 contains the return value
490                if let Some(value) = node.child(1)
491                    && matches!(
492                        value.kind_id().into(),
493                        ParenthesizedExpression | UnaryExpression
494                    )
495                {
496                    java_inspect_container(&value, &mut stats.conditions)
497                }
498            }
499            // Counts unary conditions inside implicit return statements in lambda expressions
500            LambdaExpression => {
501                // The child node of index 2 contains the return value
502                if let Some(value) = node.child(2)
503                    && matches!(
504                        value.kind_id().into(),
505                        ParenthesizedExpression | UnaryExpression
506                    )
507                {
508                    java_inspect_container(&value, &mut stats.conditions)
509                }
510            }
511            // Counts unary conditions inside ternary expressions
512            TernaryExpression => {
513                // The child node of index 0 contains the condition
514                if let Some(condition) = node.child(0) {
515                    match condition.kind_id().into() {
516                        MethodInvocation | Identifier | True | False => {
517                            stats.conditions += 1.;
518                        }
519                        ParenthesizedExpression | UnaryExpression => {
520                            java_inspect_container(&condition, &mut stats.conditions);
521                        }
522                        _ => {}
523                    }
524                }
525                // The child node of index 2 contains the first expression
526                if let Some(expression) = node.child(2)
527                    && matches!(
528                        expression.kind_id().into(),
529                        ParenthesizedExpression | UnaryExpression
530                    )
531                {
532                    java_inspect_container(&expression, &mut stats.conditions);
533                }
534                // The child node of index 4 contains the second expression
535                if let Some(expression) = node.child(4)
536                    && matches!(
537                        expression.kind_id().into(),
538                        ParenthesizedExpression | UnaryExpression
539                    )
540                {
541                    java_inspect_container(&expression, &mut stats.conditions);
542                }
543            }
544            _ => {}
545        }
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use crate::tools::check_metrics;
552
553    use super::*;
554
555    // Constant declarations are not counted as assignments
556    #[test]
557    fn java_constant_declarations() {
558        check_metrics::<JavaParser>(
559            "class A {
560                private final int X1 = 0, Y1 = 0;
561                public final float PI = 3.14f;
562                final static String HELLO = \"Hello,\";
563                protected String world = \" world!\";   // +1a
564                public float e = 2.718f;                // +1a
565                private int x2 = 1, y2 = 2;             // +2a
566
567                void m() {
568                    final int Z1 = 0, Z2 = 0, Z3 = 0;
569                    final float T = 0.0f;
570                    int z1 = 1, z2 = 2, z3 = 3;         // +3a
571                    float t = 60.0f;                    // +1a
572                }
573            }",
574            "foo.java",
575            |metric| {
576                // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
577                // space count: 3 (1 unit, 1 class and 1 method)
578                insta::assert_json_snapshot!(
579                    metric.abc,
580                    @r###"
581                    {
582                      "assignments": 8.0,
583                      "branches": 0.0,
584                      "conditions": 0.0,
585                      "magnitude": 8.0,
586                      "assignments_average": 2.6666666666666665,
587                      "branches_average": 0.0,
588                      "conditions_average": 0.0,
589                      "assignments_min": 0.0,
590                      "assignments_max": 4.0,
591                      "branches_min": 0.0,
592                      "branches_max": 0.0,
593                      "conditions_min": 0.0,
594                      "conditions_max": 0.0
595                    }"###
596                );
597            },
598        );
599    }
600
601    // "In computer science, conditionals (that is, conditional statements, conditional expressions
602    // and conditional constructs,) are programming language commands for handling decisions."
603    // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
604    // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
605    // Variables, method invocations and true or false values used inside
606    // variable declarations and assignment expressions are not counted as conditions
607    #[test]
608    fn java_declarations_with_conditions() {
609        check_metrics::<JavaParser>(
610            "
611            boolean a = (1 > 2);            // +1a +1c
612            boolean b = 3 > 4;              // +1a +1c
613            boolean c = (1 > 2) && 3 > 4;   // +1a +2c
614            boolean d = b && (x > 5) || c;  // +1a +3c
615            boolean e = !d;                 // +1a +1c
616            boolean f = ((!false));         // +1a +1c
617            boolean g = !(!(true));         // +1a +1c
618            boolean h = true;               // +1a
619            boolean i = (false);            // +1a
620            boolean j = (((((true)))));     // +1a
621            boolean k = (((((m())))));      // +1a +1b
622            boolean l = (((((!m())))));     // +1a +1b +1c
623            boolean m = (!(!((m()))));      // +1a +1b +1c
624            List<String> n = null;          // +1a (< and > used for generic types are not counted as conditions)
625            ",
626            "foo.java",
627          |metric| {
628                // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
629                // space count: 1 (1 unit)
630                insta::assert_json_snapshot!(
631                    metric.abc,
632                    @r###"
633                    {
634                      "assignments": 14.0,
635                      "branches": 3.0,
636                      "conditions": 12.0,
637                      "magnitude": 18.681541692269406,
638                      "assignments_average": 14.0,
639                      "branches_average": 3.0,
640                      "conditions_average": 12.0,
641                      "assignments_min": 14.0,
642                      "assignments_max": 14.0,
643                      "branches_min": 3.0,
644                      "branches_max": 3.0,
645                      "conditions_min": 12.0,
646                      "conditions_max": 12.0
647                    }"###
648                );
649            },
650        );
651    }
652
653    // Conditions can be found in assignment expressions
654    #[test]
655    fn java_assignments_with_conditions() {
656        check_metrics::<JavaParser>(
657            "
658            a = 2 < 1;                  // +1a +1c
659            b = (4 >= 3) && 2 <= 1;     // +1a +2c
660            c = a || (x != 10) && b;    // +1a +3c
661            d = !false;                 // +1a +1c
662            e = (!false);               // +1a +1c
663            f = !(false);               // +1a +1c
664            g = (!(((true))));          // +1a +1c
665            h = ((true));               // +1a
666            i = !m();                   // +1a +1b +1c
667            j = !((m()));               // +1a +1b +1c
668            k = (!(m()));               // +1a +1b +1c
669            l = ((!(m())));             // +1a +1b +1c
670            m = !B.<Integer>m(2);       // +1a +1b +1c
671            n = !((B.<Integer>m(4)));   // +1a +1b +1c
672            ",
673            "foo.java",
674            |metric| {
675                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
676                // space count: 1 (1 unit)
677                insta::assert_json_snapshot!(
678                    metric.abc,
679                    @r###"
680                    {
681                      "assignments": 14.0,
682                      "branches": 6.0,
683                      "conditions": 16.0,
684                      "magnitude": 22.090722034374522,
685                      "assignments_average": 14.0,
686                      "branches_average": 6.0,
687                      "conditions_average": 16.0,
688                      "assignments_min": 14.0,
689                      "assignments_max": 14.0,
690                      "branches_min": 6.0,
691                      "branches_max": 6.0,
692                      "conditions_min": 16.0,
693                      "conditions_max": 16.0
694                    }"###
695                );
696            },
697        );
698    }
699
700    // Conditions can be found in method arguments
701    #[test]
702    fn java_methods_arguments_with_conditions() {
703        check_metrics::<JavaParser>(
704            "
705            m1(a);                                  // +1b
706            m2(a, b);                               // +1b
707            m3(true, (false), (((true))));          // +1b
708            m3(m1(false), m1(true), m1(false));     // +4b
709            m1(!a);                                 // +1b +1c
710            m2((((a))), (!b));                      // +1b +1c
711            m3(!(a), b, !!!c);                      // +1b +2c
712            m3(a, !b, m2(!a, !m2(!b, !m1(!c))));    // +4b +6c
713            ",
714            "foo.java",
715            |metric| {
716                // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
717                // space count: 1 (1 unit)
718                insta::assert_json_snapshot!(
719                    metric.abc,
720                    @r###"
721                    {
722                      "assignments": 0.0,
723                      "branches": 14.0,
724                      "conditions": 10.0,
725                      "magnitude": 17.204650534085253,
726                      "assignments_average": 0.0,
727                      "branches_average": 14.0,
728                      "conditions_average": 10.0,
729                      "assignments_min": 0.0,
730                      "assignments_max": 0.0,
731                      "branches_min": 14.0,
732                      "branches_max": 14.0,
733                      "conditions_min": 10.0,
734                      "conditions_max": 10.0
735                    }"###
736                );
737            },
738        );
739    }
740
741    // "A unary conditional expression is an implicit condition that uses no relational operators."
742    // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
743    // https://www.softwarerenovation.com/Articles.aspx (page 5)
744    #[test]
745    fn java_if_single_conditions() {
746        check_metrics::<JavaParser>(
747            "
748            if ( a < 0 ) {}             // +1c
749            if ( ((a != 0)) ) {}        // +1c
750            if ( !(a > 0) ) {}          // +1c
751            if ( !(((a == 0))) ) {}     // +1c
752            if ( b.m1() ) {}            // +1b +1c
753            if ( !b.m1() ) {}           // +1b +1c
754            if ( !!b.m2() ) {}          // +1b +1c
755            if ( (!(b.m1())) ) {}       // +1b +1c
756            if ( (!(!b.m1())) ) {}      // +1b +1c
757            if ( ((b.m2())) ) {}        // +1b +1c
758            if ( ((b.m().m1())) ) {}    // +2b +1c
759            if ( c ) {}                 // +1c
760            if ( !c ) {}                // +1c
761            if ( !!!!!!!!!!c ) {}       // +1c
762            if ( (((c))) ) {}           // +1c
763            if ( (((!c))) ) {}          // +1c
764            if ( ((!(c))) ) {}          // +1c
765            if ( true ) {}              // +1c
766            if ( !true ) {}             // +1c
767            if ( ((false)) ) {}         // +1c
768            if ( !(!(false)) ) {}       // +1c
769            if ( !!!false ) {}          // +1c
770            ",
771            "foo.java",
772            |metric| {
773                // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
774                // space count: 1 (1 unit)
775                insta::assert_json_snapshot!(
776                    metric.abc,
777                    @r###"
778                    {
779                      "assignments": 0.0,
780                      "branches": 8.0,
781                      "conditions": 22.0,
782                      "magnitude": 23.40939982143925,
783                      "assignments_average": 0.0,
784                      "branches_average": 8.0,
785                      "conditions_average": 22.0,
786                      "assignments_min": 0.0,
787                      "assignments_max": 0.0,
788                      "branches_min": 8.0,
789                      "branches_max": 8.0,
790                      "conditions_min": 22.0,
791                      "conditions_max": 22.0
792                    }"###
793                );
794            },
795        );
796    }
797
798    #[test]
799    fn java_if_multiple_conditions() {
800        check_metrics::<JavaParser>(
801            "
802            if ( a || b || c || d ) {}              // +4c
803            if ( a || b && c && d ) {}              // +4c
804            if ( x < y && a == b ) {}               // +2c
805            if ( ((z < (x + y))) ) {}               // +1c
806            if ( a || ((((b))) && c) ) {}           // +3c
807            if ( a && ((((a == b))) && c) ) {}      // +3c
808            if ( a || ((((a == b))) || ((c))) ) {}  // +3c
809            if ( x < y && B.m() ) {}                // +1b +2c
810            if ( x < y && !(((B.m()))) ) {}         // +1b +2c
811            if ( !(x < y) && !B.m() ) {}            // +1b +2c
812            if ( !!!(!!!(a)) && B.m() ||            // +1b +2c
813                 !B.m() && (((x > 4))) ) {}         // +1b +2c
814            ",
815            "foo.java",
816            |metric| {
817                // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
818                // space count: 1 (1 unit)
819                insta::assert_json_snapshot!(
820                    metric.abc,
821                    @r###"
822                    {
823                      "assignments": 0.0,
824                      "branches": 5.0,
825                      "conditions": 30.0,
826                      "magnitude": 30.4138126514911,
827                      "assignments_average": 0.0,
828                      "branches_average": 5.0,
829                      "conditions_average": 30.0,
830                      "assignments_min": 0.0,
831                      "assignments_max": 0.0,
832                      "branches_min": 5.0,
833                      "branches_max": 5.0,
834                      "conditions_min": 30.0,
835                      "conditions_max": 30.0
836                    }"###
837                );
838            },
839        );
840    }
841
842    #[test]
843    fn java_while_and_do_while_conditions() {
844        check_metrics::<JavaParser>(
845            "
846            while ( (!(!(!(a)))) ) {}                   // +1c
847            while ( b || 1 > 2 ) {}                     // +2c
848            while ( x.m() && (((c))) ) {}               // +1b +2c
849            do {} while ( !!!(((!!!a))) );              // +1c
850            do {} while ( a || (b && c) );              // +3c
851            do {} while ( !x.m() && 1 > 2 || !true );   // +1b +3c
852            ",
853            "foo.java",
854            |metric| {
855                // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
856                // space count: 1 (1 unit)
857                insta::assert_json_snapshot!(
858                    metric.abc,
859                    @r###"
860                    {
861                      "assignments": 0.0,
862                      "branches": 2.0,
863                      "conditions": 12.0,
864                      "magnitude": 12.165525060596439,
865                      "assignments_average": 0.0,
866                      "branches_average": 2.0,
867                      "conditions_average": 12.0,
868                      "assignments_min": 0.0,
869                      "assignments_max": 0.0,
870                      "branches_min": 2.0,
871                      "branches_max": 2.0,
872                      "conditions_min": 12.0,
873                      "conditions_max": 12.0
874                    }"###
875                );
876            },
877        );
878    }
879
880    // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
881    // "These are cases where a single variable/field/value is treated as a boolean value.
882    // Examples include `if (x)` and `return !ready`."
883    // According to this definition, unary conditional expressions are counted also in function return values.
884    // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
885    // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
886    #[test]
887    fn java_return_with_conditions() {
888        check_metrics::<JavaParser>(
889            "class A {
890                boolean m1() {
891                    return !(z >= 0);       // +1c
892                }
893                boolean m2() {
894                    return (((!x)));        // +1c
895                }
896                boolean m3() {
897                    return x && y;          // +2c
898                }
899                boolean m4() {
900                    return y || (z < 0);    // +2c
901                }
902                boolean m5() {
903                    return x || y ?         // +3c (two unary conditions and one ?)
904                        true : false;
905                }
906            }",
907            "foo.java",
908            |metric| {
909                // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
910                // space count: 7 (1 unit, 1 class and 5 methods)
911                insta::assert_json_snapshot!(
912                    metric.abc,
913                    @r###"
914                    {
915                      "assignments": 0.0,
916                      "branches": 0.0,
917                      "conditions": 9.0,
918                      "magnitude": 9.0,
919                      "assignments_average": 0.0,
920                      "branches_average": 0.0,
921                      "conditions_average": 1.2857142857142858,
922                      "assignments_min": 0.0,
923                      "assignments_max": 0.0,
924                      "branches_min": 0.0,
925                      "branches_max": 0.0,
926                      "conditions_min": 0.0,
927                      "conditions_max": 3.0
928                    }"###
929                );
930            },
931        );
932    }
933
934    // Variables, method invocations, and true or false values
935    // inside return statements are not counted as conditions
936    #[test]
937    fn java_return_without_conditions() {
938        check_metrics::<JavaParser>(
939            "class A {
940                boolean m1() {
941                    return x;
942                }
943                boolean m2() {
944                    return (x);
945                }
946                boolean m3() {
947                    return y.m();   // +1b
948                }
949                boolean m4() {
950                    return false;
951                }
952                void m5() {
953                    return;
954                }
955            }",
956            "foo.java",
957            |metric| {
958                // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
959                // space count: 7 (1 unit, 1 class and 5 methods)
960                insta::assert_json_snapshot!(
961                    metric.abc,
962                    @r###"
963                    {
964                      "assignments": 0.0,
965                      "branches": 1.0,
966                      "conditions": 0.0,
967                      "magnitude": 1.0,
968                      "assignments_average": 0.0,
969                      "branches_average": 0.14285714285714285,
970                      "conditions_average": 0.0,
971                      "assignments_min": 0.0,
972                      "assignments_max": 0.0,
973                      "branches_min": 0.0,
974                      "branches_max": 1.0,
975                      "conditions_min": 0.0,
976                      "conditions_max": 0.0
977                    }"###
978                );
979            },
980        );
981    }
982
983    // Variables, method invocations, and true or false values
984    // in lambda expression return values are not counted as conditions
985    #[test]
986    fn java_lambda_expressions_return_with_conditions() {
987        check_metrics::<JavaParser>(
988            "
989            Predicate<Boolean> p1 = a -> a;                         // +1a
990            Predicate<Boolean> p2 = b -> true;                      // +1a
991            Predicate<Boolean> p3 = c -> m();                       // +1a
992            Predicate<Integer> p4 = d -> d > 10;                    // +1a +1c
993            Predicate<Boolean> p5 = (e) -> !e;                      // +1a +1c
994            Predicate<Boolean> p6 = (f) -> !((!f));                 // +1a +1c
995            Predicate<Boolean> p7 = (g) -> !g && true;              // +1a +2c
996            BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
997            BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
998                return j || k;                                      // +1a +2c
999            };
1000            ",
1001            "foo.java",
1002            |metric| {
1003                // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1004                // space count: 1 (1 unit)
1005                insta::assert_json_snapshot!(
1006                    metric.abc,
1007                    @r###"
1008                    {
1009                      "assignments": 9.0,
1010                      "branches": 1.0,
1011                      "conditions": 9.0,
1012                      "magnitude": 12.767145334803704,
1013                      "assignments_average": 9.0,
1014                      "branches_average": 1.0,
1015                      "conditions_average": 9.0,
1016                      "assignments_min": 9.0,
1017                      "assignments_max": 9.0,
1018                      "branches_min": 1.0,
1019                      "branches_max": 1.0,
1020                      "conditions_min": 9.0,
1021                      "conditions_max": 9.0
1022                    }"###
1023                );
1024            },
1025        );
1026    }
1027
1028    #[test]
1029    fn java_for_with_variable_declaration() {
1030        check_metrics::<JavaParser>(
1031            "
1032            for ( int i1 = 0; !(!(!(!a))); i1++ ) {}                // +2a +1c
1033            for ( int i2 = 0; !B.m(); i2++ ) {}                     // +2a +1b +1c
1034            for ( int i3 = 0; a || false; i3++ ) {}                 // +2a +2c
1035            for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {}  // +2a +1b +3c
1036            for ( int i5 = 0; true; i5++ ) {}                       // +2a +1c
1037            ",
1038            "foo.java",
1039            |metric| {
1040                // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1041                // space count: 1 (1 unit)
1042                insta::assert_json_snapshot!(
1043                    metric.abc,
1044                    @r###"
1045                    {
1046                      "assignments": 10.0,
1047                      "branches": 2.0,
1048                      "conditions": 8.0,
1049                      "magnitude": 12.96148139681572,
1050                      "assignments_average": 10.0,
1051                      "branches_average": 2.0,
1052                      "conditions_average": 8.0,
1053                      "assignments_min": 10.0,
1054                      "assignments_max": 10.0,
1055                      "branches_min": 2.0,
1056                      "branches_max": 2.0,
1057                      "conditions_min": 8.0,
1058                      "conditions_max": 8.0
1059                    }"###
1060                );
1061            },
1062        );
1063    }
1064
1065    #[test]
1066    fn java_for_without_variable_declaration() {
1067        check_metrics::<JavaParser>(
1068            "class A{
1069                void m1() {
1070                    for (i = 0; x < y; i++) {}          // +2a +1c
1071                    for (i = 0; ((x < y)); i++) {}      // +2a +1c
1072                    for (i = 0; !(!(x < y)); i++) {}    // +2a +1c
1073                    for (i = 0; true; i++) {}           // +2a +1c
1074                }
1075                void m2() {
1076                    for ( ; true; ) {}  // +1c
1077                }
1078                void m3() {
1079                    for ( ; ; ) {}      // +1c (one implicit unary condition set to true)
1080                }
1081            }",
1082            "foo.java",
1083            |metric| {
1084                // magnitude: sqrt(64 + 0 + 36) = sqrt(100)
1085                // space count: 5 (1 unit, 1 class and 3 methods)
1086                insta::assert_json_snapshot!(
1087                    metric.abc,
1088                    @r###"
1089                    {
1090                      "assignments": 8.0,
1091                      "branches": 0.0,
1092                      "conditions": 6.0,
1093                      "magnitude": 10.0,
1094                      "assignments_average": 1.6,
1095                      "branches_average": 0.0,
1096                      "conditions_average": 1.2,
1097                      "assignments_min": 0.0,
1098                      "assignments_max": 8.0,
1099                      "branches_min": 0.0,
1100                      "branches_max": 0.0,
1101                      "conditions_min": 0.0,
1102                      "conditions_max": 4.0
1103                    }"###
1104                );
1105            },
1106        );
1107    }
1108
1109    // Variables, method invocations, and true or false values
1110    // in ternary expression return values are not counted as conditions
1111    #[test]
1112    fn java_ternary_conditions() {
1113        check_metrics::<JavaParser>(
1114            "
1115            a = true;                                   // +1a
1116            b = a ? true : false;                       // +1a +2c
1117            c = ((((a)))) ? !false : !b;                // +1a +4c
1118            d = !this.m() ? !!a : (false);              // +1a +1b +3c
1119            e = !(a) && b ? ((c)) : !d;                 // +1a +4c
1120            if ( this.m() ? a : !this.m() ) {}          // +2b +3c
1121            if ( x > 0 ? !(false) : this.m() ) {}       // +1b +3c
1122            if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {}   // +5c
1123            ",
1124            "foo.java",
1125            |metric| {
1126                // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1127                //  space count: 1 (1 unit)
1128                insta::assert_json_snapshot!(
1129                    metric.abc,
1130                    @r###"
1131                    {
1132                      "assignments": 5.0,
1133                      "branches": 4.0,
1134                      "conditions": 24.0,
1135                      "magnitude": 24.839484696748443,
1136                      "assignments_average": 5.0,
1137                      "branches_average": 4.0,
1138                      "conditions_average": 24.0,
1139                      "assignments_min": 5.0,
1140                      "assignments_max": 5.0,
1141                      "branches_min": 4.0,
1142                      "branches_max": 4.0,
1143                      "conditions_min": 24.0,
1144                      "conditions_max": 24.0
1145                    }"###
1146                );
1147            },
1148        );
1149    }
1150}