Skip to main content

codelore_rca/metrics/
exit.rs

1use serde::Serialize;
2use serde::ser::{SerializeStruct, Serializer};
3use std::fmt;
4
5use crate::checker::Checker;
6use crate::macros::implement_metric_trait;
7use crate::*;
8
9/// The `NExit` metric.
10///
11/// This metric counts the number of possible exit points
12/// from a function/method.
13#[derive(Debug, Clone)]
14pub struct Stats {
15    exit: usize,
16    exit_sum: usize,
17    total_space_functions: usize,
18    exit_min: usize,
19    exit_max: usize,
20}
21
22impl Default for Stats {
23    fn default() -> Self {
24        Self {
25            exit: 0,
26            exit_sum: 0,
27            total_space_functions: 1,
28            exit_min: usize::MAX,
29            exit_max: 0,
30        }
31    }
32}
33
34impl Serialize for Stats {
35    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
36    where
37        S: Serializer,
38    {
39        let mut st = serializer.serialize_struct("nexits", 4)?;
40        st.serialize_field("sum", &self.exit_sum())?;
41        st.serialize_field("average", &self.exit_average())?;
42        st.serialize_field("min", &self.exit_min())?;
43        st.serialize_field("max", &self.exit_max())?;
44        st.end()
45    }
46}
47
48impl fmt::Display for Stats {
49    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50        write!(
51            f,
52            "sum: {}, average: {} min: {}, max: {}",
53            self.exit_sum(),
54            self.exit_average(),
55            self.exit_min(),
56            self.exit_max()
57        )
58    }
59}
60
61impl Stats {
62    /// Merges a second `NExit` metric into the first one
63    pub fn merge(&mut self, other: &Stats) {
64        self.exit_max = self.exit_max.max(other.exit_max);
65        self.exit_min = self.exit_min.min(other.exit_min);
66        self.exit_sum += other.exit_sum;
67    }
68
69    /// Returns the `NExit` metric value
70    pub fn exit(&self) -> f64 {
71        self.exit as f64
72    }
73    /// Returns the `NExit` metric sum value
74    pub fn exit_sum(&self) -> f64 {
75        self.exit_sum as f64
76    }
77    /// Returns the `NExit` metric  minimum value
78    pub fn exit_min(&self) -> f64 {
79        self.exit_min as f64
80    }
81    /// Returns the `NExit` metric maximum value
82    pub fn exit_max(&self) -> f64 {
83        self.exit_max as f64
84    }
85
86    /// Returns the `NExit` metric average value
87    ///
88    /// This value is computed dividing the `NExit` value
89    /// for the total number of functions/closures in a space.
90    ///
91    /// If there are no functions in a code, its value is `NAN`.
92    pub fn exit_average(&self) -> f64 {
93        self.exit_sum() / self.total_space_functions as f64
94    }
95    #[inline(always)]
96    pub(crate) fn compute_sum(&mut self) {
97        self.exit_sum += self.exit;
98    }
99    #[inline(always)]
100    pub(crate) fn compute_minmax(&mut self) {
101        self.exit_max = self.exit_max.max(self.exit);
102        self.exit_min = self.exit_min.min(self.exit);
103        self.compute_sum();
104    }
105    pub(crate) fn finalize(&mut self, total_space_functions: usize) {
106        self.total_space_functions = total_space_functions;
107    }
108}
109
110pub trait Exit
111where
112    Self: Checker,
113{
114    fn compute(node: &Node, stats: &mut Stats);
115}
116
117impl Exit for PythonCode {
118    fn compute(node: &Node, stats: &mut Stats) {
119        if matches!(node.kind_id().into(), Python::ReturnStatement) {
120            stats.exit += 1;
121        }
122    }
123}
124
125impl Exit for JavascriptCode {
126    fn compute(node: &Node, stats: &mut Stats) {
127        if matches!(node.kind_id().into(), Javascript::ReturnStatement) {
128            stats.exit += 1;
129        }
130    }
131}
132
133impl Exit for TypescriptCode {
134    fn compute(node: &Node, stats: &mut Stats) {
135        if matches!(node.kind_id().into(), Typescript::ReturnStatement) {
136            stats.exit += 1;
137        }
138    }
139}
140
141impl Exit for TsxCode {
142    fn compute(node: &Node, stats: &mut Stats) {
143        if matches!(node.kind_id().into(), Tsx::ReturnStatement) {
144            stats.exit += 1;
145        }
146    }
147}
148
149impl Exit for RustCode {
150    fn compute(node: &Node, stats: &mut Stats) {
151        if matches!(
152            node.kind_id().into(),
153            Rust::ReturnExpression | Rust::TryExpression
154        ) || Self::is_func(node) && node.child_by_field_name("return_type").is_some()
155        {
156            stats.exit += 1;
157        }
158    }
159}
160
161impl Exit for CppCode {
162    fn compute(node: &Node, stats: &mut Stats) {
163        if matches!(node.kind_id().into(), Cpp::ReturnStatement) {
164            stats.exit += 1;
165        }
166    }
167}
168
169impl Exit for JavaCode {
170    fn compute(node: &Node, stats: &mut Stats) {
171        if matches!(node.kind_id().into(), Java::ReturnStatement) {
172            stats.exit += 1;
173        }
174    }
175}
176
177implement_metric_trait!(Exit, KotlinCode, PreprocCode, CcommentCode);
178
179#[cfg(test)]
180mod tests {
181    use crate::tools::check_metrics;
182
183    use super::*;
184
185    #[test]
186    fn python_no_exit() {
187        check_metrics::<PythonParser>("a = 42", "foo.py", |metric| {
188            // 0 functions
189            insta::assert_json_snapshot!(
190                metric.nexits,
191                @r###"
192                    {
193                      "sum": 0.0,
194                      "average": null,
195                      "min": 0.0,
196                      "max": 0.0
197                    }"###
198            );
199        });
200    }
201
202    #[test]
203    fn rust_no_exit() {
204        check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| {
205            // 0 functions
206            insta::assert_json_snapshot!(
207                metric.nexits,
208                @r###"
209                    {
210                      "sum": 0.0,
211                      "average": null,
212                      "min": 0.0,
213                      "max": 0.0
214                    }"###
215            );
216        });
217    }
218
219    #[test]
220    fn rust_question_mark() {
221        check_metrics::<RustParser>("let _ = a? + b? + c?;", "foo.rs", |metric| {
222            // 0 functions
223            insta::assert_json_snapshot!(
224                metric.nexits,
225                @r###"
226                    {
227                      "sum": 3.0,
228                      "average": null,
229                      "min": 3.0,
230                      "max": 3.0
231                    }"###
232            );
233        });
234    }
235
236    #[test]
237    fn c_no_exit() {
238        check_metrics::<CppParser>("int a = 42;", "foo.c", |metric| {
239            // 0 functions
240            insta::assert_json_snapshot!(
241                metric.nexits,
242                @r###"
243                    {
244                      "sum": 0.0,
245                      "average": null,
246                      "min": 0.0,
247                      "max": 0.0
248                    }"###
249            );
250        });
251    }
252
253    #[test]
254    fn javascript_no_exit() {
255        check_metrics::<JavascriptParser>("var a = 42;", "foo.js", |metric| {
256            // 0 functions
257            insta::assert_json_snapshot!(
258                metric.nexits,
259                @r###"
260                    {
261                      "sum": 0.0,
262                      "average": null,
263                      "min": 0.0,
264                      "max": 0.0
265                    }"###
266            );
267        });
268    }
269
270    #[test]
271    fn python_simple_function() {
272        check_metrics::<PythonParser>(
273            "def f(a, b):
274                 if a:
275                     return a",
276            "foo.py",
277            |metric| {
278                println!("{:?}", metric.nexits);
279                // 1 function
280                insta::assert_json_snapshot!(
281                    metric.nexits,
282                    @r###"
283                    {
284                      "sum": 1.0,
285                      "average": 1.0,
286                      "min": 0.0,
287                      "max": 1.0
288                    }"###
289                );
290            },
291        );
292    }
293
294    #[test]
295    fn python_more_functions() {
296        check_metrics::<PythonParser>(
297            "def f(a, b):
298                 if a:
299                     return a
300            def f(a, b):
301                 if b:
302                     return b",
303            "foo.py",
304            |metric| {
305                // 2 functions
306                insta::assert_json_snapshot!(
307                    metric.nexits,
308                    @r###"
309                    {
310                      "sum": 2.0,
311                      "average": 1.0,
312                      "min": 0.0,
313                      "max": 1.0
314                    }"###
315                );
316            },
317        );
318    }
319
320    #[test]
321    fn python_nested_functions() {
322        check_metrics::<PythonParser>(
323            "def f(a, b):
324                 def foo(a):
325                     if a:
326                         return 1
327                 bar = lambda a: lambda b: b or True or True
328                 return bar(foo(a))(a)",
329            "foo.py",
330            |metric| {
331                // 2 functions + 2 lambdas = 4
332                insta::assert_json_snapshot!(
333                    metric.nexits,
334                    @r###"
335                    {
336                      "sum": 2.0,
337                      "average": 0.5,
338                      "min": 0.0,
339                      "max": 1.0
340                    }"###
341                );
342            },
343        );
344    }
345
346    #[test]
347    fn java_no_exit() {
348        check_metrics::<JavaParser>("int a = 42;", "foo.java", |metric| {
349            // 0 functions
350            insta::assert_json_snapshot!(
351                metric.nexits,
352                @r###"
353                    {
354                      "sum": 0.0,
355                      "average": null,
356                      "min": 0.0,
357                      "max": 0.0
358                    }"###
359            );
360        });
361    }
362
363    #[test]
364    fn java_simple_function() {
365        check_metrics::<JavaParser>(
366            "class A {
367              public int sum(int x, int y) {
368                return x + y;
369              }
370            }",
371            "foo.java",
372            |metric| {
373                // 1 exit / 1 space
374                insta::assert_json_snapshot!(
375                    metric.nexits,
376                    @r###"
377                    {
378                      "sum": 1.0,
379                      "average": 1.0,
380                      "min": 0.0,
381                      "max": 1.0
382                    }"###
383                );
384            },
385        );
386    }
387
388    #[test]
389    fn java_split_function() {
390        check_metrics::<JavaParser>(
391            "class A {
392              public int multiply(int x, int y) {
393                if(x == 0 || y == 0){
394                    return 0;
395                }
396                return x * y;
397              }
398            }",
399            "foo.java",
400            |metric| {
401                // 2 exit / space 1
402                insta::assert_json_snapshot!(
403                    metric.nexits,
404                    @r###"
405                    {
406                      "sum": 2.0,
407                      "average": 2.0,
408                      "min": 0.0,
409                      "max": 2.0
410                    }"###
411                );
412            },
413        );
414    }
415}