Skip to main content

big_code_analysis/metrics/
nexits.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8// Metric counts (token, function, branch, argument, etc.) are stored as
9// `usize` and crossed with `f64` averages, ratios, and Halstead scores
10// across the cyclomatic / MI / Halstead computations. The `usize as f64`
11// and `f64 as usize` casts are intentional and snapshot-anchored — every
12// site is bounded by the count it came from. Allowing the lints at the
13// module level keeps the metric arithmetic legible.
14#![allow(
15    clippy::cast_precision_loss,
16    clippy::cast_possible_truncation,
17    clippy::cast_sign_loss
18)]
19
20use std::fmt;
21
22use crate::checker::Checker;
23use crate::macros::implement_metric_trait;
24use crate::*;
25
26/// The `NExit` metric.
27///
28/// This metric counts the number of possible exit points
29/// from a function/method.
30#[derive(Debug, Clone, PartialEq)]
31#[non_exhaustive]
32pub struct Stats {
33    exit: usize,
34    exit_sum: usize,
35    total_space_functions: usize,
36    exit_min: usize,
37    exit_max: usize,
38}
39
40impl Default for Stats {
41    fn default() -> Self {
42        Self {
43            exit: 0,
44            exit_sum: 0,
45            total_space_functions: 1,
46            exit_min: usize::MAX,
47            exit_max: 0,
48        }
49    }
50}
51
52impl fmt::Display for Stats {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        write!(
55            f,
56            "sum: {}, average: {} min: {}, max: {}",
57            self.nexits_sum(),
58            self.nexits_average(),
59            self.nexits_min(),
60            self.nexits_max()
61        )
62    }
63}
64
65impl Stats {
66    /// Merges a second `NExit` metric into the first one
67    pub fn merge(&mut self, other: &Stats) {
68        self.exit_max = self.exit_max.max(other.exit_max);
69        self.exit_min = self.exit_min.min(other.exit_min);
70        self.exit_sum += other.exit_sum;
71    }
72
73    /// Returns the `NExit` metric value
74    #[must_use]
75    pub fn nexits(&self) -> u64 {
76        self.exit as u64
77    }
78    /// Returns the `NExit` metric sum value
79    #[must_use]
80    pub fn nexits_sum(&self) -> u64 {
81        self.exit_sum as u64
82    }
83    /// Returns the `NExit` metric minimum value.
84    ///
85    /// Collapses the `usize::MAX` sentinel that `Stats::default()` plants
86    /// into `exit_min` to `0`, so a never-observed space
87    /// serializes to a meaningful number rather than `1.8446744e19`.
88    #[must_use]
89    pub fn nexits_min(&self) -> u64 {
90        if self.exit_min == usize::MAX {
91            0
92        } else {
93            self.exit_min as u64
94        }
95    }
96    /// Returns the `NExit` metric maximum value
97    #[must_use]
98    pub fn nexits_max(&self) -> u64 {
99        self.exit_max as u64
100    }
101
102    /// Returns the `NExit` metric average value
103    ///
104    /// This value is computed dividing the `NExit` value
105    /// for the total number of functions/closures in a space.
106    ///
107    /// The per-function divisor (shared with `cyclomatic`/`cognitive`/
108    /// `nargs`, #512) is guarded with `.max(1)` via the shared `average`
109    /// helper, so a space with no counted functions (or one where `Nom`
110    /// was not selected) degrades to `sum / 1` instead of producing
111    /// `inf`/`NaN` (#428).
112    #[must_use]
113    pub fn nexits_average(&self) -> f64 {
114        crate::metrics::average(self.nexits_sum() as f64, self.total_space_functions)
115    }
116    #[inline]
117    pub(crate) fn compute_sum(&mut self) {
118        self.exit_sum += self.exit;
119    }
120    #[inline]
121    pub(crate) fn compute_minmax(&mut self) {
122        self.exit_max = self.exit_max.max(self.exit);
123        self.exit_min = self.exit_min.min(self.exit);
124        self.compute_sum();
125    }
126    pub(crate) fn finalize(&mut self, total_space_functions: usize) {
127        self.total_space_functions = total_space_functions;
128    }
129}
130
131#[doc(hidden)]
132/// Per-language computation of the exit-point count.
133pub(crate) trait Exit
134where
135    Self: Checker,
136{
137    /// Walk `node` and update `stats` with this metric for the language
138    /// implementing the trait.
139    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats);
140}
141
142// Bumps `stats.exit` whenever the current node matches any of the
143// supplied per-language token variants. Mirrors the `js_cognitive!` /
144// `impl_cyclomatic_c_family!` shape used elsewhere in `src/metrics/`.
145macro_rules! impl_exit_match_kinds {
146    ($code:ty, $lang:ident, [$($kind:ident),+ $(,)?]) => {
147        impl Exit for $code {
148            fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
149                if matches!(node.kind_id().into(), $($lang::$kind)|+) {
150                    stats.exit += 1;
151                }
152            }
153        }
154    };
155}
156
157// `Python::Yield` is the yield-expression node (kind text "yield"); Python
158// has no dedicated yield-statement variant. Counting it as an exit mirrors
159// `CsharpCode` / `PhpCode`: generator suspension hands control back to the
160// caller, so the function does leave even though it may later resume.
161impl_exit_match_kinds!(PythonCode, Python, [ReturnStatement, RaiseStatement, Yield]);
162// JS-family generators: `yield` / `yield*` parse as `YieldExpression`.
163// Counted for the same reason as Python — see comment above.
164impl_exit_match_kinds!(
165    MozjsCode,
166    Mozjs,
167    [ReturnStatement, ThrowStatement, YieldExpression]
168);
169impl_exit_match_kinds!(
170    JavascriptCode,
171    Javascript,
172    [ReturnStatement, ThrowStatement, YieldExpression]
173);
174impl_exit_match_kinds!(
175    TypescriptCode,
176    Typescript,
177    [ReturnStatement, ThrowStatement, YieldExpression]
178);
179impl_exit_match_kinds!(
180    TsxCode,
181    Tsx,
182    [ReturnStatement, ThrowStatement, YieldExpression]
183);
184impl_exit_match_kinds!(CppCode, Cpp, [ReturnStatement, ThrowStatement]);
185impl_exit_match_kinds!(MozcppCode, Mozcpp, [ReturnStatement, ThrowStatement]);
186// C has no exceptions: `return` is the only exit kind (no `throw`).
187impl_exit_match_kinds!(CCode, C, [ReturnStatement]);
188// Objective-C adds `@throw` on top of C's `return` (the `throw_statement`
189// node), mirroring the C++ exit set.
190impl_exit_match_kinds!(ObjcCode, Objc, [ReturnStatement, ThrowStatement]);
191// Java's `yield` is the Java-14+ switch-expression yield statement
192// (an unambiguous statement node, distinct from a labeled `break`).
193// It hands the switch-expression value back as an explicit exit, so it
194// counts identically to Groovy and C#. Implicit final-expression
195// returns are not counted — only explicit return / throw / yield.
196impl_exit_match_kinds!(
197    JavaCode,
198    Java,
199    [ReturnStatement, ThrowStatement, YieldStatement]
200);
201// Groovy's `yield` is the Java-14+ switch-expression yield, identical
202// to Java's. Implicit-return-from-closure is NOT counted as an exit
203// (consistent with Java) — only explicit return / throw / yield count.
204impl_exit_match_kinds!(
205    GroovyCode,
206    Groovy,
207    [ReturnStatement, ThrowStatement, YieldStatement]
208);
209
210impl Exit for RustCode {
211    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
212        // Count only explicit `return` and `?` (TryExpression). The
213        // implicit final-expression path is NOT an exit — peer-language
214        // impls have the same convention. See #243 for the prior bug
215        // that added a spurious +1 for every function with a return
216        // type.
217        if matches!(
218            node.kind_id().into(),
219            Rust::ReturnExpression | Rust::TryExpression
220        ) {
221            stats.exit += 1;
222        }
223    }
224}
225
226impl Exit for CsharpCode {
227    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
228        if matches!(
229            node.kind_id().into(),
230            Csharp::ReturnStatement
231                | Csharp::YieldStatement
232                | Csharp::ThrowStatement
233                | Csharp::ThrowExpression
234        ) {
235            stats.exit += 1;
236        }
237    }
238}
239
240impl Exit for GoCode {
241    // Go has no dedicated `panic` node: `panic(...)` is the built-in
242    // abrupt-exit call (it unwinds the stack like `throw`/`raise` in the
243    // exception languages), parsed as a `call_expression` whose `function`
244    // field is a bare `identifier` spelling `panic`. Count it as an exit
245    // alongside `return`. Matching the bare identifier (not a
246    // `selector_expression`) means a package-qualified call like
247    // `foo.panic()` — a user function, not the built-in — is not counted,
248    // mirroring how Bash matches the bare builtin command name.
249    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
250        if matches!(node.kind_id().into(), Go::ReturnStatement) {
251            stats.exit += 1;
252        } else if node.kind_id() == Go::CallExpression
253            && let Some(function) = node.child_by_field_name("function")
254            && function.kind_id() == Go::Identifier
255            && function.utf8_text(code) == Some("panic")
256        {
257            stats.exit += 1;
258        }
259    }
260}
261
262impl Exit for PerlCode {
263    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
264        if node.kind_id() == Perl::ReturnExpression {
265            stats.exit += 1;
266        }
267    }
268}
269
270impl Exit for KotlinCode {
271    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
272        if matches!(
273            node.kind_id().into(),
274            Kotlin::ReturnExpression | Kotlin::ThrowExpression
275        ) {
276            stats.exit += 1;
277        }
278    }
279}
280
281impl Exit for LuaCode {
282    // Lua has no `throw`/`raise` keyword: the abrupt-exit primitives are the
283    // built-in `error(...)` (raises a Lua error that unwinds to the nearest
284    // `pcall`) and `os.exit(...)` (terminates the process). Both parse as a
285    // `function_call` whose `name` field is the callee. `error(...)` is a
286    // bare `identifier`; `os.exit(...)` is a `dot_index_expression` with text
287    // `os.exit`. Count them as exits alongside `return`. Matching the exact
288    // callee text means a user call such as `foo()` or `myError()` is not
289    // counted, mirroring how Bash/Elixir match the bare builtin name.
290    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
291        if node.kind_id() == Lua::ReturnStatement {
292            stats.exit += 1;
293        } else if node.kind_id() == Lua::FunctionCall
294            && let Some(name) = node.child_by_field_name("name")
295            && matches!(name.utf8_text(code), Some("error" | "os.exit"))
296        {
297            stats.exit += 1;
298        }
299    }
300}
301
302impl Exit for BashCode {
303    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
304        // Bash has no `return_statement` node: `return` and `exit` are
305        // ordinary builtins parsed as `Bash::Command` whose `name` field
306        // points at a `Bash::CommandName`. Identify them by comparing the
307        // command-name text against the literal builtins.
308        if matches!(node.kind_id().into(), Bash::Command)
309            && let Some(name) = node.child_by_field_name("name")
310            && matches!(name.utf8_text(code), Some("return" | "exit"))
311        {
312            stats.exit += 1;
313        }
314    }
315}
316
317impl Exit for TclCode {
318    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
319        // Tcl has no return keyword node; `return` is a generic Command whose
320        // name field is a simple_word with text "return".
321        if node.kind_id() == Tcl::Command
322            && let Some(name) = node.child_by_field_name("name")
323            && name.kind_id() == Tcl::SimpleWord
324            && name.utf8_text(code) == Some("return")
325        {
326            stats.exit += 1;
327        }
328    }
329}
330
331impl Exit for IrulesCode {
332    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
333        // Like Tcl, iRules has no `return` keyword node — `return` is a
334        // generic Command (it is not among the grammar's `_builtin`
335        // commands). The bare name word can surface as either `simple_word`
336        // or `concat_word` depending on context, so match on the name text
337        // rather than a fixed kind. A multi-value `return $a $b` still has a
338        // single `name` field and is counted once. iRules flow commands
339        // (`event disable`, `TCP::close`, `reject`, `drop`) are deliberately
340        // not counted as exits in v1.
341        if node.kind_id() == Irules::Command
342            && let Some(name) = node.child_by_field_name("name")
343            && name.utf8_text(code) == Some("return")
344        {
345            stats.exit += 1;
346        }
347    }
348}
349
350impl Exit for PhpCode {
351    // tree-sitter-php 0.24.2's `exit_statement` rule covers `exit` only
352    // (with or without parentheses); `die(...)` is grammar-classified as
353    // a `function_call_expression` and therefore is NOT counted here.
354    // Detecting `die` would require inspecting call-expression callee
355    // text — brittle and likely to false-match user-defined `die`
356    // functions. Modern PHP idiom favors `throw new Exception()` over
357    // `die`, so leaving this asymmetric is acceptable.
358    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
359        if matches!(
360            node.kind_id().into(),
361            Php::ReturnStatement | Php::YieldExpression | Php::ThrowExpression | Php::ExitStatement
362        ) {
363            stats.exit += 1;
364        }
365    }
366}
367
368// Real defaults — no functions to return from. Audited in #188.
369implement_metric_trait!(Exit, PreprocCode, CcommentCode);
370
371impl Exit for RubyCode {
372    // Ruby's `return` is the only dedicated grammar node for an
373    // intra-function exit. `yield` passes control to the block but does
374    // not exit the enclosing method; `raise`/`exit` are ordinary method
375    // calls without grammar nodes. tree-sitter-ruby exposes the
376    // `return_statement` rule under two aliased visible kinds
377    // (`Return`, `Return2`); the `Return3` token is the bare `return`
378    // keyword inside those nodes and is not counted on its own.
379    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
380        if matches!(node.kind_id().into(), Ruby::Return | Ruby::Return2) {
381            stats.exit += 1;
382        }
383    }
384}
385
386impl Exit for ElixirCode {
387    // Elixir has no `return` statement: the last expression in a function
388    // body is the return value. Early-exit happens through `throw`,
389    // `raise`, `reraise`, or `exit`, all of which surface as `Call`
390    // nodes whose target is an `Identifier` whose text spells the
391    // keyword. Mirrors the Bash/Tcl pattern of comparing target text.
392    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
393        if node.kind_id() == Elixir::Call
394            && let Some(target) = node.child_by_field_name("target")
395            && target.kind_id() == Elixir::Identifier
396            && matches!(
397                target.utf8_text(code),
398                Some("throw" | "raise" | "reraise" | "exit")
399            )
400        {
401            stats.exit += 1;
402        }
403    }
404}
405
406#[cfg(test)]
407#[allow(
408    clippy::float_cmp,
409    clippy::cast_precision_loss,
410    clippy::cast_possible_truncation,
411    clippy::cast_sign_loss,
412    clippy::similar_names,
413    clippy::doc_markdown,
414    clippy::needless_raw_string_hashes,
415    clippy::too_many_lines
416)]
417mod tests {
418    use crate::tools::check_metrics;
419
420    use super::*;
421
422    /// A `Stats::default()` that never sees an
423    /// observation must not leak the `usize::MAX` sentinel for
424    /// `exit_min`. The getter collapses the sentinel to `0.0` so
425    /// JSON never emits `1.8446744e19`.
426    #[test]
427    fn exit_empty_file_min_is_zero() {
428        let stats = Stats::default();
429        assert_eq!(stats.nexits_min(), 0);
430    }
431
432    #[test]
433    fn python_no_exit() {
434        check_metrics::<PythonParser>("a = 42", "foo.py", |metric| {
435            // 0 functions
436            insta::assert_json_snapshot!(
437                metric.nexits,
438                @r#"
439            {
440              "sum": 0,
441              "average": 0.0,
442              "min": 0,
443              "max": 0
444            }
445            "#
446            );
447        });
448    }
449
450    #[test]
451    fn rust_no_exit() {
452        check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| {
453            // 0 functions
454            insta::assert_json_snapshot!(
455                metric.nexits,
456                @r#"
457            {
458              "sum": 0,
459              "average": 0.0,
460              "min": 0,
461              "max": 0
462            }
463            "#
464            );
465        });
466    }
467
468    #[test]
469    fn rust_question_mark() {
470        check_metrics::<RustParser>("let _ = a? + b? + c?;", "foo.rs", |metric| {
471            // 0 functions
472            insta::assert_json_snapshot!(
473                metric.nexits,
474                @r#"
475            {
476              "sum": 3,
477              "average": 3.0,
478              "min": 3,
479              "max": 3
480            }
481            "#
482            );
483        });
484    }
485
486    // Regression for #243: `Exit for RustCode` used to add 1 whenever
487    // a function_item with an explicit `-> T` was visited. Because the
488    // spaces traversal pushes a new State *before* Exit::compute runs
489    // for that function_item, every Rust function with an explicit
490    // return type was getting one extra exit on top of its real
491    // `return` / `?` exits. The fix drops the spurious clause; this
492    // test pins exit == 1 for a function with one explicit return.
493    #[test]
494    fn rust_explicit_return_with_return_type() {
495        check_metrics::<RustParser>("fn foo() -> i32 { return 1; }", "foo.rs", |metric| {
496            // 1 explicit return / 1 space
497            insta::assert_json_snapshot!(
498                metric.nexits,
499                @r#"
500            {
501              "sum": 1,
502              "average": 1.0,
503              "min": 0,
504              "max": 1
505            }
506            "#
507            );
508        });
509    }
510
511    // Regression for #243: an implicit final-expression return must
512    // NOT count as an exit — matching every other language's
513    // convention (Java, C++, Go, etc. don't count implicit returns).
514    #[test]
515    fn rust_implicit_return_not_counted() {
516        check_metrics::<RustParser>("fn foo() -> i32 { 0 }", "foo.rs", |metric| {
517            // 0 explicit exits / 1 space
518            insta::assert_json_snapshot!(
519                metric.nexits,
520                @r#"
521            {
522              "sum": 0,
523              "average": 0.0,
524              "min": 0,
525              "max": 0
526            }
527            "#
528            );
529        });
530    }
531
532    // Regression for #243: a function with both an explicit return on
533    // one branch and an implicit final expression should count only
534    // the explicit return.
535    #[test]
536    fn rust_mixed_explicit_and_implicit_return() {
537        check_metrics::<RustParser>(
538            "fn foo(x: bool) -> i32 { if x { return 1; } 0 }",
539            "foo.rs",
540            |metric| {
541                // 1 explicit return; the implicit `0` is not an exit
542                insta::assert_json_snapshot!(
543                    metric.nexits,
544                    @r#"
545                {
546                  "sum": 1,
547                  "average": 1.0,
548                  "min": 0,
549                  "max": 1
550                }
551                "#
552                );
553            },
554        );
555    }
556
557    // Regression for #243: `?` inside a function body is the only
558    // implicit-exit form that does count, and the function having an
559    // explicit `Result` return type must not double it.
560    #[test]
561    fn rust_question_mark_in_function() {
562        check_metrics::<RustParser>(
563            "fn foo() -> Result<i32, ()> { Ok(do_thing()?) }",
564            "foo.rs",
565            |metric| {
566                // 1 `?` operator, no explicit `return`
567                insta::assert_json_snapshot!(
568                    metric.nexits,
569                    @r#"
570                {
571                  "sum": 1,
572                  "average": 1.0,
573                  "min": 0,
574                  "max": 1
575                }
576                "#
577                );
578            },
579        );
580    }
581
582    // Regression for #243: a unit-returning function with no
583    // explicit `return` or `?` must report 0 exits.
584    #[test]
585    fn rust_unit_return_no_exit() {
586        check_metrics::<RustParser>("fn foo() { let _x = 1; }", "foo.rs", |metric| {
587            // 0 exits / 1 space
588            insta::assert_json_snapshot!(
589                metric.nexits,
590                @r#"
591            {
592              "sum": 0,
593              "average": 0.0,
594              "min": 0,
595              "max": 0
596            }
597            "#
598            );
599        });
600    }
601
602    #[test]
603    fn c_no_exit() {
604        check_metrics::<CParser>("int a = 42;", "foo.c", |metric| {
605            // 0 functions
606            insta::assert_json_snapshot!(
607                metric.nexits,
608                @r#"
609            {
610              "sum": 0,
611              "average": 0.0,
612              "min": 0,
613              "max": 0
614            }
615            "#
616            );
617        });
618    }
619
620    /// Multiple `return` statements across `if` / `else` branches.  Every
621    /// `Cpp::ReturnStatement` adds +1 — there is no early-out collapse.
622    #[test]
623    fn c_multiple_returns_in_branches() {
624        check_metrics::<CParser>(
625            "int f(int x) {
626                 if (x < 0) {
627                     return -1;
628                 } else if (x == 0) {
629                     return 0;
630                 } else {
631                     return 1;
632                 }
633             }",
634            "foo.c",
635            |metric| {
636                // 1 function, 3 returns
637                assert_eq!(metric.nexits.nexits_sum(), 3);
638                assert_eq!(metric.nexits.nexits_max(), 3);
639                insta::assert_json_snapshot!(
640                    metric.nexits,
641                    @r#"
642                {
643                  "sum": 3,
644                  "average": 3.0,
645                  "min": 0,
646                  "max": 3
647                }
648                "#
649                );
650            },
651        );
652    }
653
654    /// The raison d'être of `LANG::C` (#721): C code that uses C++
655    /// keywords (`new`, `class`, `delete`) as plain identifiers parses
656    /// cleanly through `tree-sitter-c`, where the C++ grammar
657    /// ERROR-cascades. The load-bearing assertion is `!root.has_error()`:
658    /// the C++ grammar errors on this input yet *still* recovers a
659    /// function node and two `return`s, so a metric-count assertion alone
660    /// does not distinguish the two grammars — only the error-free parse
661    /// does. C has no `throw`, so `return` is the sole exit kind.
662    #[test]
663    fn c_keyword_identifiers_parse_and_returns_count() {
664        use std::path::PathBuf;
665
666        let source = "int process(int new, int class) {
667                 int delete = new + class;
668                 if (delete > 0) {
669                     return delete;
670                 }
671                 return 0;
672             }";
673        let parser = CParser::new(source.as_bytes().to_vec(), &PathBuf::from("foo.c"), None);
674        assert!(
675            !parser.root().has_error(),
676            "C grammar must parse C++-keyword identifiers without an error cascade"
677        );
678
679        check_metrics::<CParser>(source, "foo.c", |metric| {
680            assert_eq!(metric.nom.functions_sum(), 1);
681            assert_eq!(metric.nexits.nexits_sum(), 2);
682        });
683    }
684
685    /// `return` statements inside `try` and `catch` blocks both count;
686    /// the impl matches `Cpp::ReturnStatement` regardless of enclosing
687    /// scope.  C++-only: bare C has no `try`/`catch`.
688    #[test]
689    fn cpp_return_in_try_catch() {
690        check_metrics::<CppParser>(
691            "int f(int x) {
692                 try {
693                     if (x == 0) {
694                         return 1;
695                     }
696                     return 2;
697                 } catch (...) {
698                     return -1;
699                 }
700             }",
701            "foo.cpp",
702            |metric| {
703                // 1 function, 3 returns (2 in try, 1 in catch); no
704                // `throw` here, so the return-only path stays at 3.
705                assert_eq!(metric.nexits.nexits_sum(), 3);
706                assert_eq!(metric.nexits.nexits_max(), 3);
707                insta::assert_json_snapshot!(
708                    metric.nexits,
709                    @r#"
710                {
711                  "sum": 3,
712                  "average": 3.0,
713                  "min": 0,
714                  "max": 3
715                }
716                "#
717                );
718            },
719        );
720    }
721
722    /// Early `return` inside a loop body is counted separately from the
723    /// trailing return — every reachable `return` is an exit.
724    #[test]
725    fn c_early_return_in_loop() {
726        check_metrics::<CParser>(
727            "int find(int* a, int n, int target) {
728                 for (int i = 0; i < n; ++i) {
729                     if (a[i] == target) {
730                         return i;
731                     }
732                 }
733                 return -1;
734             }",
735            "foo.c",
736            |metric| {
737                // 1 function, 2 returns
738                assert_eq!(metric.nexits.nexits_sum(), 2);
739                assert_eq!(metric.nexits.nexits_max(), 2);
740                insta::assert_json_snapshot!(
741                    metric.nexits,
742                    @r#"
743                {
744                  "sum": 2,
745                  "average": 2.0,
746                  "min": 0,
747                  "max": 2
748                }
749                "#
750                );
751            },
752        );
753    }
754
755    /// `void` function with no explicit `return` — exit count is 0.
756    /// The implicit fall-through return is intentionally not modelled.
757    #[test]
758    fn c_void_no_explicit_return() {
759        check_metrics::<CParser>(
760            "void greet(const char* who) {
761                 printf(\"hi %s\\n\", who);
762             }",
763            "foo.c",
764            |metric| {
765                // 1 function with zero ReturnStatement nodes.
766                assert_eq!(metric.nexits.nexits_sum(), 0);
767                assert_eq!(metric.nexits.nexits_max(), 0);
768                insta::assert_json_snapshot!(
769                    metric.nexits,
770                    @r#"
771                {
772                  "sum": 0,
773                  "average": 0.0,
774                  "min": 0,
775                  "max": 0
776                }
777                "#
778                );
779            },
780        );
781    }
782
783    #[test]
784    fn javascript_no_exit() {
785        check_metrics::<JavascriptParser>("var a = 42;", "foo.js", |metric| {
786            // 0 functions
787            insta::assert_json_snapshot!(
788                metric.nexits,
789                @r#"
790            {
791              "sum": 0,
792              "average": 0.0,
793              "min": 0,
794              "max": 0
795            }
796            "#
797            );
798        });
799    }
800
801    #[test]
802    fn javascript_simple_function() {
803        check_metrics::<JavascriptParser>(
804            "function f(a, b) {
805                 if (a) {
806                     return a;
807                 }
808                 return b;
809             }",
810            "foo.js",
811            |metric| {
812                // 1 function with 2 return statements
813                insta::assert_json_snapshot!(
814                    metric.nexits,
815                    @r#"
816                {
817                  "sum": 2,
818                  "average": 2.0,
819                  "min": 0,
820                  "max": 2
821                }
822                "#
823                );
824            },
825        );
826    }
827
828    #[test]
829    fn javascript_nested_functions() {
830        check_metrics::<JavascriptParser>(
831            "function outer() {
832                 function inner() {
833                     return 1;
834                 }
835                 return inner();
836             }",
837            "foo.js",
838            |metric| {
839                // 2 functions, each with 1 return
840                insta::assert_json_snapshot!(
841                    metric.nexits,
842                    @r#"
843                {
844                  "sum": 2,
845                  "average": 1.0,
846                  "min": 0,
847                  "max": 1
848                }
849                "#
850                );
851            },
852        );
853    }
854
855    #[test]
856    fn python_simple_function() {
857        check_metrics::<PythonParser>(
858            "def f(a, b):
859                 if a:
860                     return a",
861            "foo.py",
862            |metric| {
863                // 1 function
864                insta::assert_json_snapshot!(
865                    metric.nexits,
866                    @r#"
867                {
868                  "sum": 1,
869                  "average": 1.0,
870                  "min": 0,
871                  "max": 1
872                }
873                "#
874                );
875            },
876        );
877    }
878
879    #[test]
880    fn python_more_functions() {
881        check_metrics::<PythonParser>(
882            "def f(a, b):
883                 if a:
884                     return a
885            def f(a, b):
886                 if b:
887                     return b",
888            "foo.py",
889            |metric| {
890                // 2 functions
891                insta::assert_json_snapshot!(
892                    metric.nexits,
893                    @r#"
894                {
895                  "sum": 2,
896                  "average": 1.0,
897                  "min": 0,
898                  "max": 1
899                }
900                "#
901                );
902            },
903        );
904    }
905
906    #[test]
907    fn python_nested_functions() {
908        check_metrics::<PythonParser>(
909            "def f(a, b):
910                 def foo(a):
911                     if a:
912                         return 1
913                 bar = lambda a: lambda b: b or True or True
914                 return bar(foo(a))(a)",
915            "foo.py",
916            |metric| {
917                // 2 functions + 2 lambdas = 4
918                insta::assert_json_snapshot!(
919                    metric.nexits,
920                    @r#"
921                {
922                  "sum": 2,
923                  "average": 0.5,
924                  "min": 0,
925                  "max": 1
926                }
927                "#
928                );
929            },
930        );
931    }
932
933    #[test]
934    fn java_no_exit() {
935        check_metrics::<JavaParser>("int a = 42;", "foo.java", |metric| {
936            // 0 functions
937            insta::assert_json_snapshot!(
938                metric.nexits,
939                @r#"
940            {
941              "sum": 0,
942              "average": 0.0,
943              "min": 0,
944              "max": 0
945            }
946            "#
947            );
948        });
949    }
950
951    #[test]
952    fn java_simple_function() {
953        check_metrics::<JavaParser>(
954            "class A {
955              public int sum(int x, int y) {
956                return x + y;
957              }
958            }",
959            "foo.java",
960            |metric| {
961                // 1 exit / 1 space
962                insta::assert_json_snapshot!(
963                    metric.nexits,
964                    @r#"
965                {
966                  "sum": 1,
967                  "average": 1.0,
968                  "min": 0,
969                  "max": 1
970                }
971                "#
972                );
973            },
974        );
975    }
976
977    #[test]
978    fn go_no_return() {
979        check_metrics::<GoParser>(
980            "package main
981            func f() {
982                x := 1
983                _ = x
984            }",
985            "foo.go",
986            |metric| {
987                // No return_statement → exit_sum = 0.
988                insta::assert_json_snapshot!(
989                    metric.nexits,
990                    @r#"
991                {
992                  "sum": 0,
993                  "average": 0.0,
994                  "min": 0,
995                  "max": 0
996                }
997                "#
998                );
999            },
1000        );
1001    }
1002
1003    #[test]
1004    fn go_single_return() {
1005        check_metrics::<GoParser>(
1006            "package main
1007            func f() int {
1008                return 1
1009            }",
1010            "foo.go",
1011            |metric| {
1012                insta::assert_json_snapshot!(
1013                    metric.nexits,
1014                    @r#"
1015                {
1016                  "sum": 1,
1017                  "average": 1.0,
1018                  "min": 0,
1019                  "max": 1
1020                }
1021                "#
1022                );
1023            },
1024        );
1025    }
1026
1027    #[test]
1028    fn go_multiple_returns() {
1029        check_metrics::<GoParser>(
1030            "package main
1031            func f(x int) int {
1032                if x > 0 {
1033                    return 1
1034                }
1035                if x < 0 {
1036                    return -1
1037                }
1038                return 0
1039            }",
1040            "foo.go",
1041            |metric| {
1042                // 3 distinct return_statements across branches.
1043                insta::assert_json_snapshot!(
1044                    metric.nexits,
1045                    @r#"
1046                {
1047                  "sum": 3,
1048                  "average": 3.0,
1049                  "min": 0,
1050                  "max": 3
1051                }
1052                "#
1053                );
1054            },
1055        );
1056    }
1057
1058    #[test]
1059    fn go_naked_return() {
1060        check_metrics::<GoParser>(
1061            "package main
1062            func f() (x int) {
1063                x = 1
1064                return
1065            }",
1066            "foo.go",
1067            |metric| {
1068                // Bare `return` with named results is still a return_statement.
1069                insta::assert_json_snapshot!(
1070                    metric.nexits,
1071                    @r#"
1072                {
1073                  "sum": 1,
1074                  "average": 1.0,
1075                  "min": 0,
1076                  "max": 1
1077                }
1078                "#
1079                );
1080            },
1081        );
1082    }
1083
1084    #[test]
1085    fn go_multivalue_return() {
1086        check_metrics::<GoParser>(
1087            "package main
1088            func f() (int, error) {
1089                return 0, nil
1090            }",
1091            "foo.go",
1092            |metric| {
1093                // `return a, b` is one return_statement (Go has no comma operator).
1094                insta::assert_json_snapshot!(
1095                    metric.nexits,
1096                    @r#"
1097                {
1098                  "sum": 1,
1099                  "average": 1.0,
1100                  "min": 0,
1101                  "max": 1
1102                }
1103                "#
1104                );
1105            },
1106        );
1107    }
1108
1109    #[test]
1110    fn go_panic_counts_as_exit() {
1111        check_metrics::<GoParser>(
1112            "package main
1113            func f() {
1114                panic(\"boom\")
1115            }",
1116            "foo.go",
1117            |metric| {
1118                // panic(...) is the built-in abrupt-exit call, counted like
1119                // throw/raise — one exit even though there is no `return`.
1120                insta::assert_json_snapshot!(
1121                    metric.nexits,
1122                    @r#"
1123                {
1124                  "sum": 1,
1125                  "average": 1.0,
1126                  "min": 0,
1127                  "max": 1
1128                }
1129                "#
1130                );
1131            },
1132        );
1133    }
1134
1135    #[test]
1136    fn go_panic_and_return_both_count() {
1137        check_metrics::<GoParser>(
1138            "package main
1139            func f(x int) int {
1140                if x < 0 {
1141                    panic(\"negative\")
1142                }
1143                return x
1144            }",
1145            "foo.go",
1146            |metric| {
1147                // panic(...) + return are both abrupt exits → 2.
1148                insta::assert_json_snapshot!(
1149                    metric.nexits,
1150                    @r#"
1151                {
1152                  "sum": 2,
1153                  "average": 2.0,
1154                  "min": 0,
1155                  "max": 2
1156                }
1157                "#
1158                );
1159            },
1160        );
1161    }
1162
1163    #[test]
1164    fn go_package_qualified_panic_is_not_exit() {
1165        check_metrics::<GoParser>(
1166            "package main
1167            func f() {
1168                foo.panic()
1169            }",
1170            "foo.go",
1171            |metric| {
1172                // `foo.panic()` is a user method on package `foo`, not the
1173                // built-in `panic` — its callee is a selector_expression, not
1174                // a bare identifier, so it must not be counted.
1175                insta::assert_json_snapshot!(
1176                    metric.nexits,
1177                    @r#"
1178                {
1179                  "sum": 0,
1180                  "average": 0.0,
1181                  "min": 0,
1182                  "max": 0
1183                }
1184                "#
1185                );
1186            },
1187        );
1188    }
1189
1190    #[test]
1191    fn java_split_function() {
1192        check_metrics::<JavaParser>(
1193            "class A {
1194              public int multiply(int x, int y) {
1195                if(x == 0 || y == 0){
1196                    return 0;
1197                }
1198                return x * y;
1199              }
1200            }",
1201            "foo.java",
1202            |metric| {
1203                // 2 exit / space 1
1204                insta::assert_json_snapshot!(
1205                    metric.nexits,
1206                    @r#"
1207                {
1208                  "sum": 2,
1209                  "average": 2.0,
1210                  "min": 0,
1211                  "max": 2
1212                }
1213                "#
1214                );
1215            },
1216        );
1217    }
1218
1219    #[test]
1220    fn csharp_no_exit() {
1221        check_metrics::<CsharpParser>("int a = 42;", "foo.cs", |metric| {
1222            insta::assert_json_snapshot!(
1223                metric.nexits,
1224                @r#"
1225            {
1226              "sum": 0,
1227              "average": 0.0,
1228              "min": 0,
1229              "max": 0
1230            }
1231            "#
1232            );
1233        });
1234    }
1235
1236    #[test]
1237    fn csharp_simple_function() {
1238        check_metrics::<CsharpParser>(
1239            "class A {
1240              public int Sum(int x, int y) {
1241                return x + y;
1242              }
1243            }",
1244            "foo.cs",
1245            |metric| {
1246                insta::assert_json_snapshot!(
1247                    metric.nexits,
1248                    @r#"
1249                {
1250                  "sum": 1,
1251                  "average": 1.0,
1252                  "min": 0,
1253                  "max": 1
1254                }
1255                "#
1256                );
1257            },
1258        );
1259    }
1260
1261    #[test]
1262    fn csharp_split_function() {
1263        check_metrics::<CsharpParser>(
1264            "class A {
1265              public int Multiply(int x, int y) {
1266                if (x == 0 || y == 0) {
1267                    return 0;
1268                }
1269                return x * y;
1270              }
1271            }",
1272            "foo.cs",
1273            |metric| {
1274                insta::assert_json_snapshot!(
1275                    metric.nexits,
1276                    @r#"
1277                {
1278                  "sum": 2,
1279                  "average": 2.0,
1280                  "min": 0,
1281                  "max": 2
1282                }
1283                "#
1284                );
1285            },
1286        );
1287    }
1288
1289    #[test]
1290    fn csharp_yield_and_throw() {
1291        check_metrics::<CsharpParser>(
1292            "class A {
1293              public IEnumerable<int> Gen() {
1294                yield return 1;
1295                yield break;
1296              }
1297              public int Bad(int x) {
1298                if (x < 0) throw new System.Exception();
1299                return x;
1300              }
1301            }",
1302            "foo.cs",
1303            |metric| {
1304                // 2 yields + 1 throw + 1 return = 4 across two methods.
1305                insta::assert_json_snapshot!(
1306                    metric.nexits,
1307                    @r#"
1308                {
1309                  "sum": 4,
1310                  "average": 2.0,
1311                  "min": 0,
1312                  "max": 2
1313                }
1314                "#
1315                );
1316            },
1317        );
1318    }
1319
1320    #[test]
1321    fn perl_no_exit() {
1322        check_metrics::<PerlParser>(
1323            "sub f {
1324                print 'hi';
1325            }",
1326            "foo.pl",
1327            |metric| {
1328                insta::assert_json_snapshot!(
1329                    metric.nexits,
1330                    @r#"
1331                {
1332                  "sum": 0,
1333                  "average": 0.0,
1334                  "min": 0,
1335                  "max": 0
1336                }
1337                "#
1338                );
1339            },
1340        );
1341    }
1342
1343    #[test]
1344    fn perl_no_function_no_exit() {
1345        check_metrics::<PerlParser>("my $x = 1;\nprint $x;\n", "foo.pl", |metric| {
1346            insta::assert_json_snapshot!(metric.nexits, @r#"
1347            {
1348              "sum": 0,
1349              "average": 0.0,
1350              "min": 0,
1351              "max": 0
1352            }
1353            "#);
1354        });
1355    }
1356
1357    #[test]
1358    fn perl_multiple_returns() {
1359        check_metrics::<PerlParser>(
1360            "sub f {
1361                return 1 if $_[0];
1362                return 0;
1363            }",
1364            "foo.pl",
1365            |metric| {
1366                insta::assert_json_snapshot!(
1367                    metric.nexits,
1368                    @r#"
1369                {
1370                  "sum": 2,
1371                  "average": 2.0,
1372                  "min": 0,
1373                  "max": 2
1374                }
1375                "#
1376                );
1377            },
1378        );
1379    }
1380
1381    #[test]
1382    fn tsx_function_with_returns() {
1383        check_metrics::<TsxParser>(
1384            "function clamp(val: number, min: number, max: number) {
1385                 if (val < min) {
1386                     return min;
1387                 }
1388                 if (val > max) {
1389                     return max;
1390                 }
1391                 return val;
1392             }",
1393            "foo.tsx",
1394            |metric| {
1395                insta::assert_json_snapshot!(
1396                    metric.nexits,
1397                    @r#"
1398                {
1399                  "sum": 3,
1400                  "average": 3.0,
1401                  "min": 0,
1402                  "max": 3
1403                }
1404                "#
1405                );
1406            },
1407        );
1408    }
1409
1410    #[test]
1411    fn typescript_no_exit() {
1412        check_metrics::<TypescriptParser>("const x: number = 42;", "foo.ts", |metric| {
1413            insta::assert_json_snapshot!(
1414                metric.nexits,
1415                @r#"
1416            {
1417              "sum": 0,
1418              "average": 0.0,
1419              "min": 0,
1420              "max": 0
1421            }
1422            "#
1423            );
1424        });
1425    }
1426
1427    #[test]
1428    fn typescript_function_with_returns() {
1429        check_metrics::<TypescriptParser>(
1430            "function safeDivide(a: number, b: number): number | null {
1431                 if (b === 0) {
1432                     return null;
1433                 }
1434                 return a / b;
1435             }",
1436            "foo.ts",
1437            |metric| {
1438                insta::assert_json_snapshot!(
1439                    metric.nexits,
1440                    @r#"
1441                {
1442                  "sum": 2,
1443                  "average": 2.0,
1444                  "min": 0,
1445                  "max": 2
1446                }
1447                "#
1448                );
1449            },
1450        );
1451    }
1452
1453    #[test]
1454    fn mozjs_no_exit() {
1455        check_metrics::<MozjsParser>("var a = 42;", "foo.js", |metric| {
1456            insta::assert_json_snapshot!(
1457                metric.nexits,
1458                @r#"
1459            {
1460              "sum": 0,
1461              "average": 0.0,
1462              "min": 0,
1463              "max": 0
1464            }
1465            "#
1466            );
1467        });
1468    }
1469
1470    #[test]
1471    fn mozjs_function_with_returns() {
1472        check_metrics::<MozjsParser>(
1473            "function f(a, b) {
1474                 if (a) {
1475                     return a;
1476                 }
1477                 return b;
1478             }",
1479            "foo.js",
1480            |metric| {
1481                insta::assert_json_snapshot!(
1482                    metric.nexits,
1483                    @r#"
1484                {
1485                  "sum": 2,
1486                  "average": 2.0,
1487                  "min": 0,
1488                  "max": 2
1489                }
1490                "#
1491                );
1492            },
1493        );
1494    }
1495
1496    #[test]
1497    fn kotlin_exit_return_and_throw() {
1498        check_metrics::<KotlinParser>(
1499            "fun divide(a: Int, b: Int): Int {
1500                if (b == 0) {
1501                    throw IllegalArgumentException(\"zero\")
1502                }
1503                return a / b
1504            }",
1505            "foo.kt",
1506            |metric| {
1507                insta::assert_json_snapshot!(
1508                    metric.nexits,
1509                    @r#"
1510                {
1511                  "sum": 2,
1512                  "average": 2.0,
1513                  "min": 0,
1514                  "max": 2
1515                }
1516                "#
1517                );
1518            },
1519        );
1520    }
1521
1522    #[test]
1523    fn lua_no_exit() {
1524        check_metrics::<LuaParser>(
1525            "local function f(x)
1526  local y = x + 1
1527end",
1528            "foo.lua",
1529            |metric| {
1530                insta::assert_json_snapshot!(
1531                    metric.nexits,
1532                    @r#"
1533                {
1534                  "sum": 0,
1535                  "average": 0.0,
1536                  "min": 0,
1537                  "max": 0
1538                }
1539                "#
1540                );
1541            },
1542        );
1543    }
1544
1545    #[test]
1546    fn lua_return() {
1547        check_metrics::<LuaParser>(
1548            "local function f(x)
1549  if x > 0 then
1550    return x
1551  end
1552  return 0
1553end",
1554            "foo.lua",
1555            |metric| {
1556                insta::assert_json_snapshot!(
1557                    metric.nexits,
1558                    @r#"
1559                {
1560                  "sum": 2,
1561                  "average": 2.0,
1562                  "min": 0,
1563                  "max": 2
1564                }
1565                "#
1566                );
1567            },
1568        );
1569    }
1570
1571    #[test]
1572    fn lua_error_counts_as_exit() {
1573        check_metrics::<LuaParser>(
1574            "local function f(x)
1575  error(\"bad\")
1576end",
1577            "foo.lua",
1578            |metric| {
1579                // error(...) raises a Lua error that unwinds the stack — a
1580                // built-in abrupt exit, counted like throw/raise.
1581                insta::assert_json_snapshot!(
1582                    metric.nexits,
1583                    @r#"
1584                {
1585                  "sum": 1,
1586                  "average": 1.0,
1587                  "min": 0,
1588                  "max": 1
1589                }
1590                "#
1591                );
1592            },
1593        );
1594    }
1595
1596    #[test]
1597    fn lua_os_exit_counts_as_exit() {
1598        check_metrics::<LuaParser>(
1599            "local function f()
1600  os.exit(1)
1601end",
1602            "foo.lua",
1603            |metric| {
1604                // os.exit(...) terminates the process — its callee is a
1605                // dot_index_expression spelling `os.exit`, counted as an exit.
1606                insta::assert_json_snapshot!(
1607                    metric.nexits,
1608                    @r#"
1609                {
1610                  "sum": 1,
1611                  "average": 1.0,
1612                  "min": 0,
1613                  "max": 1
1614                }
1615                "#
1616                );
1617            },
1618        );
1619    }
1620
1621    #[test]
1622    fn lua_error_and_return_both_count() {
1623        check_metrics::<LuaParser>(
1624            "local function f(x)
1625  if x < 0 then
1626    error(\"negative\")
1627  end
1628  return x
1629end",
1630            "foo.lua",
1631            |metric| {
1632                // error(...) + return are both abrupt exits → 2.
1633                insta::assert_json_snapshot!(
1634                    metric.nexits,
1635                    @r#"
1636                {
1637                  "sum": 2,
1638                  "average": 2.0,
1639                  "min": 0,
1640                  "max": 2
1641                }
1642                "#
1643                );
1644            },
1645        );
1646    }
1647
1648    #[test]
1649    fn lua_user_call_is_not_exit() {
1650        check_metrics::<LuaParser>(
1651            "local function f()
1652  foo()
1653  myError(\"x\")
1654end",
1655            "foo.lua",
1656            |metric| {
1657                // Neither `foo()` nor a user `myError(...)` is the built-in
1658                // `error`/`os.exit`, so neither is counted.
1659                insta::assert_json_snapshot!(
1660                    metric.nexits,
1661                    @r#"
1662                {
1663                  "sum": 0,
1664                  "average": 0.0,
1665                  "min": 0,
1666                  "max": 0
1667                }
1668                "#
1669                );
1670            },
1671        );
1672    }
1673
1674    #[test]
1675    fn bash_no_exit() {
1676        check_metrics::<BashParser>("echo \"no exits\"", "foo.sh", |metric| {
1677            insta::assert_json_snapshot!(
1678                metric.nexits,
1679                @r#"
1680            {
1681              "sum": 0,
1682              "average": 0.0,
1683              "min": 0,
1684              "max": 0
1685            }
1686            "#
1687            );
1688        });
1689    }
1690
1691    #[test]
1692    fn bash_explicit_return() {
1693        check_metrics::<BashParser>(
1694            "f() {
1695                 if [ -z \"$1\" ]; then
1696                     return 1
1697                 fi
1698                 echo ok
1699             }",
1700            "foo.sh",
1701            |metric| {
1702                insta::assert_json_snapshot!(
1703                    metric.nexits,
1704                    @r#"
1705                {
1706                  "sum": 1,
1707                  "average": 1.0,
1708                  "min": 0,
1709                  "max": 1
1710                }
1711                "#
1712                );
1713            },
1714        );
1715    }
1716
1717    #[test]
1718    fn bash_explicit_exit() {
1719        check_metrics::<BashParser>(
1720            "f() {
1721                 exit 0
1722             }",
1723            "foo.sh",
1724            |metric| {
1725                insta::assert_json_snapshot!(
1726                    metric.nexits,
1727                    @r#"
1728                {
1729                  "sum": 1,
1730                  "average": 1.0,
1731                  "min": 0,
1732                  "max": 1
1733                }
1734                "#
1735                );
1736            },
1737        );
1738    }
1739
1740    #[test]
1741    fn bash_multiple_exits() {
1742        check_metrics::<BashParser>(
1743            "f() {
1744                 if [ \"$1\" = die ]; then
1745                     exit 1
1746                 fi
1747                 return 0
1748             }",
1749            "foo.sh",
1750            |metric| {
1751                insta::assert_json_snapshot!(
1752                    metric.nexits,
1753                    @r#"
1754                {
1755                  "sum": 2,
1756                  "average": 2.0,
1757                  "min": 0,
1758                  "max": 2
1759                }
1760                "#
1761                );
1762            },
1763        );
1764    }
1765
1766    #[test]
1767    fn bash_returnish_names_are_not_exits() {
1768        // `returncode=1` is a `variable_assignment`, not a Command. The
1769        // function `returns` is invoked via a Command whose CommandName is
1770        // the literal "returns" — it must NOT be matched as a return/exit
1771        // builtin (whole-token match, no prefix collision).
1772        check_metrics::<BashParser>(
1773            "returncode=1
1774             returns() {
1775                 echo named
1776             }
1777             returns",
1778            "foo.sh",
1779            |metric| {
1780                insta::assert_json_snapshot!(
1781                    metric.nexits,
1782                    @r#"
1783                {
1784                  "sum": 0,
1785                  "average": 0.0,
1786                  "min": 0,
1787                  "max": 0
1788                }
1789                "#
1790                );
1791            },
1792        );
1793    }
1794
1795    #[test]
1796    fn tcl_no_exit() {
1797        check_metrics::<TclParser>(
1798            "proc f {x} {
1799    puts $x
1800}",
1801            "foo.tcl",
1802            |metric| {
1803                insta::assert_json_snapshot!(
1804                    metric.nexits,
1805                    @r#"
1806                {
1807                  "sum": 0,
1808                  "average": 0.0,
1809                  "min": 0,
1810                  "max": 0
1811                }
1812                "#
1813                );
1814            },
1815        );
1816    }
1817
1818    #[test]
1819    fn tcl_return() {
1820        check_metrics::<TclParser>(
1821            "proc f {x} {
1822    return $x
1823}",
1824            "foo.tcl",
1825            |metric| {
1826                assert_eq!(metric.nexits.nexits_sum(), 1);
1827                assert_eq!(metric.nexits.nexits_max(), 1);
1828                insta::assert_json_snapshot!(metric.nexits);
1829            },
1830        );
1831    }
1832
1833    #[test]
1834    fn tcl_multiple_returns() {
1835        check_metrics::<TclParser>(
1836            "proc f {x} {
1837    if {$x > 0} {
1838        return positive
1839    }
1840    return nonpositive
1841}",
1842            "foo.tcl",
1843            |metric| {
1844                assert_eq!(metric.nexits.nexits_sum(), 2);
1845                assert_eq!(metric.nexits.nexits_max(), 2);
1846                insta::assert_json_snapshot!(metric.nexits);
1847            },
1848        );
1849    }
1850
1851    #[test]
1852    fn typescript_multiple_returns() {
1853        check_metrics::<TypescriptParser>(
1854            "function classify(n: number): string {
1855             if (n > 0) {
1856                 return 'positive';
1857             } else if (n < 0) {
1858                 return 'negative';
1859             }
1860             return 'zero';
1861         }",
1862            "foo.ts",
1863            |metric| {
1864                assert_eq!(metric.nexits.nexits_sum(), 3);
1865                assert_eq!(metric.nexits.nexits_max(), 3);
1866                insta::assert_json_snapshot!(metric.nexits);
1867            },
1868        );
1869    }
1870
1871    #[test]
1872    fn typescript_nested_functions() {
1873        check_metrics::<TypescriptParser>(
1874            "function outer(): number {
1875             function inner(): number {
1876                 return 42;
1877             }
1878             return inner();
1879         }",
1880            "foo.ts",
1881            |metric| {
1882                // outer has 1 return, inner has 1 return → sum=2, max=1
1883                assert_eq!(metric.nexits.nexits_sum(), 2);
1884                assert_eq!(metric.nexits.nexits_max(), 1);
1885                insta::assert_json_snapshot!(metric.nexits);
1886            },
1887        );
1888    }
1889
1890    #[test]
1891    fn tsx_no_exit() {
1892        check_metrics::<TsxParser>(
1893            "function f(): void {
1894             console.log('hello');
1895         }",
1896            "foo.tsx",
1897            |metric| {
1898                assert_eq!(metric.nexits.nexits_sum(), 0);
1899                assert_eq!(metric.nexits.nexits_max(), 0);
1900                insta::assert_json_snapshot!(metric.nexits);
1901            },
1902        );
1903    }
1904
1905    #[test]
1906    fn tsx_multiple_returns() {
1907        check_metrics::<TsxParser>(
1908            "function classify(n: number): string {
1909             if (n > 0) {
1910                 return 'positive';
1911             } else if (n < 0) {
1912                 return 'negative';
1913             }
1914             return 'zero';
1915         }",
1916            "foo.tsx",
1917            |metric| {
1918                assert_eq!(metric.nexits.nexits_sum(), 3);
1919                assert_eq!(metric.nexits.nexits_max(), 3);
1920                insta::assert_json_snapshot!(metric.nexits);
1921            },
1922        );
1923    }
1924
1925    #[test]
1926    fn kotlin_multiple_returns() {
1927        check_metrics::<KotlinParser>(
1928            "fun classify(n: Int): String {
1929             if (n > 0) {
1930                 return \"positive\"
1931             } else if (n < 0) {
1932                 return \"negative\"
1933             }
1934             return \"zero\"
1935         }",
1936            "foo.kt",
1937            |metric| {
1938                assert_eq!(metric.nexits.nexits_sum(), 3);
1939                assert_eq!(metric.nexits.nexits_max(), 3);
1940                insta::assert_json_snapshot!(metric.nexits);
1941            },
1942        );
1943    }
1944
1945    #[test]
1946    fn kotlin_no_exit() {
1947        check_metrics::<KotlinParser>(
1948            "fun f(): Unit {
1949             println(\"hello\")
1950         }",
1951            "foo.kt",
1952            |metric| {
1953                assert_eq!(metric.nexits.nexits_sum(), 0);
1954                assert_eq!(metric.nexits.nexits_max(), 0);
1955                insta::assert_json_snapshot!(metric.nexits);
1956            },
1957        );
1958    }
1959
1960    #[test]
1961    fn mozjs_nested_functions() {
1962        check_metrics::<MozjsParser>(
1963            "function outer() {
1964             function inner() {
1965                 return 42;
1966             }
1967             return inner();
1968         }",
1969            "foo.js",
1970            |metric| {
1971                // outer has 1 return, inner has 1 return → sum=2, max=1
1972                assert_eq!(metric.nexits.nexits_sum(), 2);
1973                assert_eq!(metric.nexits.nexits_max(), 1);
1974                insta::assert_json_snapshot!(metric.nexits);
1975            },
1976        );
1977    }
1978
1979    #[test]
1980    fn php_no_exit() {
1981        check_metrics::<PhpParser>("<?php $a = 42;", "foo.php", |metric| {
1982            insta::assert_json_snapshot!(
1983                metric.nexits,
1984                @r#"
1985            {
1986              "sum": 0,
1987              "average": 0.0,
1988              "min": 0,
1989              "max": 0
1990            }
1991            "#
1992            );
1993        });
1994    }
1995
1996    #[test]
1997    fn php_yield_throw() {
1998        // Generator yields and a throw expression in statement position both
1999        // count as exits.
2000        check_metrics::<PhpParser>(
2001            "<?php
2002            function gen() {
2003                yield 1;
2004                yield 2;
2005                throw new \\Exception('x');
2006            }",
2007            "foo.php",
2008            |metric| {
2009                // 3 exits (2 yields + 1 throw) inside one function space.
2010                insta::assert_json_snapshot!(
2011                    metric.nexits,
2012                    @r#"
2013                {
2014                  "sum": 3,
2015                  "average": 3.0,
2016                  "min": 0,
2017                  "max": 3
2018                }
2019                "#
2020                );
2021            },
2022        );
2023    }
2024
2025    #[test]
2026    fn php_exit_statement() {
2027        // `exit_statement` covers both `exit;` (bare) and `exit(N);` (with
2028        // optional argument). `die` is NOT in the `exit_statement` rule of
2029        // tree-sitter-php 0.24.2 — `die(...)` parses as a function call —
2030        // so we only count `exit` here.
2031        check_metrics::<PhpParser>(
2032            "<?php
2033            function bail(int $code): void {
2034                if ($code === 1) {
2035                    exit(1);
2036                }
2037                exit;
2038            }",
2039            "foo.php",
2040            |metric| {
2041                // 2 exit_statements inside one function space.
2042                insta::assert_json_snapshot!(
2043                    metric.nexits,
2044                    @r#"
2045                {
2046                  "sum": 2,
2047                  "average": 2.0,
2048                  "min": 0,
2049                  "max": 2
2050                }
2051                "#
2052                );
2053            },
2054        );
2055    }
2056
2057    #[test]
2058    fn elixir_no_exit() {
2059        // Plain function returning a value has no early-exit calls. The
2060        // `average` is `null` because Elixir's only function space is
2061        // the Unit; there is no per-function aggregation to average
2062        // over.
2063        check_metrics::<ElixirParser>(
2064            "defmodule Foo do\n  def add(a, b) do\n    a + b\n  end\nend\n",
2065            "foo.ex",
2066            |metric| {
2067                assert_eq!(metric.nexits.nexits_sum(), 0);
2068                insta::assert_json_snapshot!(
2069                    metric.nexits,
2070                    @r#"
2071                {
2072                  "sum": 0,
2073                  "average": 0.0,
2074                  "min": 0,
2075                  "max": 0
2076                }
2077                "#
2078                );
2079            },
2080        );
2081    }
2082
2083    #[test]
2084    fn elixir_raise_throw_exit() {
2085        // `raise`/`throw`/`exit` are recognised by inspecting the `target`
2086        // field text of `Call` nodes — there is no dedicated AST kind.
2087        check_metrics::<ElixirParser>(
2088            "defmodule Foo do\n  def bad(x) do\n    raise \"first\"\n    throw(:second)\n    exit(:third)\n  end\nend\n",
2089            "foo.ex",
2090            |metric| {
2091                assert_eq!(metric.nexits.nexits_sum(), 3);
2092                insta::assert_json_snapshot!(
2093                    metric.nexits,
2094                    @r#"
2095                {
2096                  "sum": 3,
2097                  "average": 3.0,
2098                  "min": 0,
2099                  "max": 3
2100                }
2101                "#
2102                );
2103            },
2104        );
2105    }
2106
2107    #[test]
2108    fn elixir_reraise_counts() {
2109        // `reraise` is the Elixir variant of `raise` that re-throws an
2110        // existing exception while preserving the stacktrace; we count
2111        // it as an exit alongside `raise`.
2112        check_metrics::<ElixirParser>(
2113            "defmodule Foo do\n  def wrap(stack) do\n    reraise(\"oops\", stack)\n  end\nend\n",
2114            "foo.ex",
2115            |metric| {
2116                assert_eq!(metric.nexits.nexits_sum(), 1);
2117            },
2118        );
2119    }
2120
2121    #[test]
2122    fn elixir_lookalike_call_is_not_exit() {
2123        // Only the exact identifiers `throw`/`raise`/`reraise`/`exit` are
2124        // exits; a user-defined `throw_event` or remote-call must NOT
2125        // count. This guards against future text-match regressions.
2126        check_metrics::<ElixirParser>(
2127            "defmodule Foo do\n  def f do\n    throw_event(:click)\n    Logger.raise_alert()\n    exit_code = 0\n    exit_code\n  end\nend\n",
2128            "foo.ex",
2129            |metric| {
2130                assert_eq!(metric.nexits.nexits_sum(), 0);
2131            },
2132        );
2133    }
2134
2135    #[test]
2136    fn ruby_no_exit() {
2137        // Function body without any `return` produces zero exits.
2138        check_metrics::<RubyParser>("def foo\n  a = 1\n  a + 1\nend\n", "foo.rb", |metric| {
2139            assert_eq!(metric.nexits.nexits_sum(), 0);
2140        });
2141    }
2142
2143    #[test]
2144    fn ruby_multiple_returns() {
2145        // Four explicit `return` statements (no modifier sugar) — one
2146        // per branch. Anchors the headline sum.
2147        check_metrics::<RubyParser>(
2148            "def kind(x)\n  return :zero if x == 0\n  if x > 0\n    return :pos\n  elsif x < 0\n    return :neg\n  end\n  return :unknown\nend\n",
2149            "foo.rb",
2150            |metric| {
2151                assert_eq!(metric.nexits.nexits_sum(), 4);
2152            },
2153        );
2154    }
2155
2156    #[test]
2157    fn ruby_explicit_returns() {
2158        // Each `return` (statement or modifier-wrapped) contributes one
2159        // exit. `yield` is intentionally NOT counted (it does not exit
2160        // the method).
2161        check_metrics::<RubyParser>(
2162            "def foo(x)\n  return 0 if x.nil?\n  yield x\n  return x * 2\nend\n",
2163            "foo.rb",
2164            |metric| {
2165                assert_eq!(metric.nexits.nexits_sum(), 2);
2166                insta::assert_json_snapshot!(metric.nexits);
2167            },
2168        );
2169    }
2170
2171    #[test]
2172    fn python_return_and_raise() {
2173        // `raise` exits the function (stack unwinds)
2174        // just like `return`. Mirrors the C# / Kotlin / PHP / Elixir
2175        // behaviour. One `raise` + one `return` => 2 exits.
2176        check_metrics::<PythonParser>(
2177            "def parse(s):
2178                 if not s:
2179                     raise ValueError(\"empty\")
2180                 return int(s)",
2181            "foo.py",
2182            |metric| {
2183                assert_eq!(metric.nexits.nexits_sum(), 2);
2184                insta::assert_json_snapshot!(
2185                    metric.nexits,
2186                    @r#"
2187                {
2188                  "sum": 2,
2189                  "average": 2.0,
2190                  "min": 0,
2191                  "max": 2
2192                }
2193                "#
2194                );
2195            },
2196        );
2197    }
2198
2199    #[test]
2200    fn javascript_return_and_throw() {
2201        // `throw` is a function exit.
2202        check_metrics::<JavascriptParser>(
2203            "function parseLength(s) {
2204                 if (s === null) throw new Error('null');
2205                 return s.length;
2206             }",
2207            "foo.js",
2208            |metric| {
2209                assert_eq!(metric.nexits.nexits_sum(), 2);
2210                insta::assert_json_snapshot!(
2211                    metric.nexits,
2212                    @r#"
2213                {
2214                  "sum": 2,
2215                  "average": 2.0,
2216                  "min": 0,
2217                  "max": 2
2218                }
2219                "#
2220                );
2221            },
2222        );
2223    }
2224
2225    #[test]
2226    fn mozjs_return_and_throw() {
2227        // Same shape as plain JavaScript.
2228        check_metrics::<MozjsParser>(
2229            "function parseLength(s) {
2230                 if (s === null) throw new Error('null');
2231                 return s.length;
2232             }",
2233            "foo.js",
2234            |metric| {
2235                assert_eq!(metric.nexits.nexits_sum(), 2);
2236                insta::assert_json_snapshot!(
2237                    metric.nexits,
2238                    @r#"
2239                {
2240                  "sum": 2,
2241                  "average": 2.0,
2242                  "min": 0,
2243                  "max": 2
2244                }
2245                "#
2246                );
2247            },
2248        );
2249    }
2250
2251    #[test]
2252    fn typescript_return_and_throw() {
2253        check_metrics::<TypescriptParser>(
2254            "function parseLength(s: string | null): number {
2255                 if (s === null) throw new Error('null');
2256                 return s.length;
2257             }",
2258            "foo.ts",
2259            |metric| {
2260                assert_eq!(metric.nexits.nexits_sum(), 2);
2261                insta::assert_json_snapshot!(
2262                    metric.nexits,
2263                    @r#"
2264                {
2265                  "sum": 2,
2266                  "average": 2.0,
2267                  "min": 0,
2268                  "max": 2
2269                }
2270                "#
2271                );
2272            },
2273        );
2274    }
2275
2276    #[test]
2277    fn tsx_return_and_throw() {
2278        check_metrics::<TsxParser>(
2279            "function parseLength(s: string | null): number {
2280                 if (s === null) throw new Error('null');
2281                 return s.length;
2282             }",
2283            "foo.tsx",
2284            |metric| {
2285                assert_eq!(metric.nexits.nexits_sum(), 2);
2286                insta::assert_json_snapshot!(
2287                    metric.nexits,
2288                    @r#"
2289                {
2290                  "sum": 2,
2291                  "average": 2.0,
2292                  "min": 0,
2293                  "max": 2
2294                }
2295                "#
2296                );
2297            },
2298        );
2299    }
2300
2301    #[test]
2302    fn java_return_and_throw() {
2303        // `throw` exits the method.
2304        check_metrics::<JavaParser>(
2305            "class A {
2306                 int parseLength(String s) {
2307                     if (s == null) throw new NullPointerException();
2308                     return s.length();
2309                 }
2310             }",
2311            "foo.java",
2312            |metric| {
2313                assert_eq!(metric.nexits.nexits_sum(), 2);
2314                insta::assert_json_snapshot!(
2315                    metric.nexits,
2316                    @r#"
2317                {
2318                  "sum": 2,
2319                  "average": 2.0,
2320                  "min": 0,
2321                  "max": 2
2322                }
2323                "#
2324                );
2325            },
2326        );
2327    }
2328
2329    #[test]
2330    fn java_yield_in_switch_expression() {
2331        // Java-14+ switch-expression `yield` is an explicit exit. Each
2332        // `yield` counts as one, alongside the enclosing `return`.
2333        check_metrics::<JavaParser>(
2334            "class A {
2335                int describe(int n) {
2336                    return switch (n) {
2337                        case 0: yield 100;
2338                        default: yield 200;
2339                    };
2340                }
2341            }",
2342            "foo.java",
2343            |metric| {
2344                assert_eq!(metric.nexits.nexits_sum(), 3);
2345            },
2346        );
2347    }
2348
2349    #[test]
2350    fn groovy_no_exit() {
2351        // No functions at all — `nexits.sum` is 0.
2352        check_metrics::<GroovyParser>("int a = 42", "foo.groovy", |metric| {
2353            assert_eq!(metric.nexits.nexits_sum(), 0);
2354        });
2355    }
2356
2357    #[test]
2358    fn groovy_simple_function() {
2359        // One explicit return in a top-level function.
2360        check_metrics::<GroovyParser>(
2361            "int answer() {
2362                return 42
2363            }",
2364            "foo.groovy",
2365            |metric| {
2366                assert_eq!(metric.nexits.nexits_sum(), 1);
2367            },
2368        );
2369    }
2370
2371    #[test]
2372    fn groovy_return_and_throw() {
2373        check_metrics::<GroovyParser>(
2374            "class A {
2375                int parseLength(String s) {
2376                    if (s == null) throw new NullPointerException()
2377                    return s.length()
2378                }
2379            }",
2380            "foo.groovy",
2381            |metric| {
2382                assert_eq!(metric.nexits.nexits_sum(), 2);
2383            },
2384        );
2385    }
2386
2387    #[test]
2388    fn groovy_yield_in_switch_expression() {
2389        // Groovy inherits Java-14+ switch-expression `yield`. Each
2390        // explicit `yield` counts as one exit.
2391        check_metrics::<GroovyParser>(
2392            "class A {
2393                int describe(int n) {
2394                    return switch (n) {
2395                        case 0: yield 100;
2396                        default: yield 200;
2397                    }
2398                }
2399            }",
2400            "foo.groovy",
2401            |metric| {
2402                assert_eq!(metric.nexits.nexits_sum(), 3);
2403            },
2404        );
2405    }
2406
2407    #[test]
2408    fn groovy_implicit_return_not_counted() {
2409        // Groovy allows implicit return of the last expression in a
2410        // closure / function body. The Exit metric only counts
2411        // *explicit* `return` / `yield` / `throw` — consistent with
2412        // Java's docstring.
2413        check_metrics::<GroovyParser>("int identity(int x) { x }", "foo.groovy", |metric| {
2414            assert_eq!(metric.nexits.nexits_sum(), 0);
2415        });
2416    }
2417
2418    #[test]
2419    fn cpp_return_and_throw() {
2420        // `throw` exits the function.
2421        check_metrics::<CppParser>(
2422            "int parseLength(const char* s) {
2423                 if (s == nullptr) throw std::invalid_argument(\"null\");
2424                 return 0;
2425             }",
2426            "foo.cpp",
2427            |metric| {
2428                assert_eq!(metric.nexits.nexits_sum(), 2);
2429                insta::assert_json_snapshot!(
2430                    metric.nexits,
2431                    @r#"
2432                {
2433                  "sum": 2,
2434                  "average": 2.0,
2435                  "min": 0,
2436                  "max": 2
2437                }
2438                "#
2439                );
2440            },
2441        );
2442    }
2443
2444    #[test]
2445    fn python_yield_counts_as_exit() {
2446        // Generator suspension via `yield` hands control back to the
2447        // caller — the function does leave its frame, just resumably.
2448        // Mirrors the long-standing C# / PHP behaviour. Two yields plus
2449        // one return == 3 exits inside the one generator function.
2450        check_metrics::<PythonParser>(
2451            "def gen():
2452                 yield 1
2453                 yield 2
2454                 return",
2455            "foo.py",
2456            |metric| {
2457                assert_eq!(metric.nexits.nexits_sum(), 3);
2458                insta::assert_json_snapshot!(
2459                    metric.nexits,
2460                    @r#"
2461                {
2462                  "sum": 3,
2463                  "average": 3.0,
2464                  "min": 0,
2465                  "max": 3
2466                }
2467                "#
2468                );
2469            },
2470        );
2471    }
2472
2473    #[test]
2474    fn javascript_yield_counts_as_exit() {
2475        // `function*` generator: each `yield` is an exit edge, same as
2476        // Python/C#/PHP. Two yields + one return == 3.
2477        check_metrics::<JavascriptParser>(
2478            "function* gen() {
2479                 yield 1;
2480                 yield 2;
2481                 return;
2482             }",
2483            "foo.js",
2484            |metric| {
2485                assert_eq!(metric.nexits.nexits_sum(), 3);
2486                insta::assert_json_snapshot!(
2487                    metric.nexits,
2488                    @r#"
2489                {
2490                  "sum": 3,
2491                  "average": 3.0,
2492                  "min": 0,
2493                  "max": 3
2494                }
2495                "#
2496                );
2497            },
2498        );
2499    }
2500
2501    #[test]
2502    fn mozjs_yield_counts_as_exit() {
2503        // Same shape as plain JavaScript.
2504        check_metrics::<MozjsParser>(
2505            "function* gen() {
2506                 yield 1;
2507                 yield 2;
2508                 return;
2509             }",
2510            "foo.js",
2511            |metric| {
2512                assert_eq!(metric.nexits.nexits_sum(), 3);
2513                insta::assert_json_snapshot!(
2514                    metric.nexits,
2515                    @r#"
2516                {
2517                  "sum": 3,
2518                  "average": 3.0,
2519                  "min": 0,
2520                  "max": 3
2521                }
2522                "#
2523                );
2524            },
2525        );
2526    }
2527
2528    #[test]
2529    fn typescript_yield_counts_as_exit() {
2530        check_metrics::<TypescriptParser>(
2531            "function* gen(): Generator<number> {
2532                 yield 1;
2533                 yield 2;
2534                 return;
2535             }",
2536            "foo.ts",
2537            |metric| {
2538                assert_eq!(metric.nexits.nexits_sum(), 3);
2539                insta::assert_json_snapshot!(
2540                    metric.nexits,
2541                    @r#"
2542                {
2543                  "sum": 3,
2544                  "average": 3.0,
2545                  "min": 0,
2546                  "max": 3
2547                }
2548                "#
2549                );
2550            },
2551        );
2552    }
2553
2554    #[test]
2555    fn tsx_yield_counts_as_exit() {
2556        check_metrics::<TsxParser>(
2557            "function* gen(): Generator<number> {
2558                 yield 1;
2559                 yield 2;
2560                 return;
2561             }",
2562            "foo.tsx",
2563            |metric| {
2564                assert_eq!(metric.nexits.nexits_sum(), 3);
2565                insta::assert_json_snapshot!(
2566                    metric.nexits,
2567                    @r#"
2568                {
2569                  "sum": 3,
2570                  "average": 3.0,
2571                  "min": 0,
2572                  "max": 3
2573                }
2574                "#
2575                );
2576            },
2577        );
2578    }
2579
2580    #[test]
2581    fn python_yield_forms_count_as_exit() {
2582        // tree-sitter-python emits a single `Python::Yield` node kind for
2583        // every yield form: bare `yield`, `yield value`, and `yield from
2584        // iter`. The match arm therefore covers all three with no extra
2585        // variants needed. Three yield forms == 3 exits.
2586        check_metrics::<PythonParser>(
2587            "def gen():
2588                 yield
2589                 yield 1
2590                 yield from range(3)",
2591            "foo.py",
2592            |metric| {
2593                assert_eq!(metric.nexits.nexits_sum(), 3);
2594                insta::assert_json_snapshot!(
2595                    metric.nexits,
2596                    @r#"
2597                {
2598                  "sum": 3,
2599                  "average": 3.0,
2600                  "min": 0,
2601                  "max": 3
2602                }
2603                "#
2604                );
2605            },
2606        );
2607    }
2608
2609    #[test]
2610    fn javascript_yield_delegate_counts_as_exit() {
2611        // Delegating yield (`yield*`) parses as the same
2612        // `Javascript::YieldExpression` node as plain `yield`, so the
2613        // existing match arm covers it. Two regular yields + one
2614        // delegate == 3 exits.
2615        check_metrics::<JavascriptParser>(
2616            "function* gen() {
2617                 yield 1;
2618                 yield* other();
2619                 yield 2;
2620             }",
2621            "foo.js",
2622            |metric| {
2623                assert_eq!(metric.nexits.nexits_sum(), 3);
2624                insta::assert_json_snapshot!(
2625                    metric.nexits,
2626                    @r#"
2627                {
2628                  "sum": 3,
2629                  "average": 3.0,
2630                  "min": 0,
2631                  "max": 3
2632                }
2633                "#
2634                );
2635            },
2636        );
2637    }
2638
2639    #[test]
2640    fn mozjs_yield_delegate_counts_as_exit() {
2641        check_metrics::<MozjsParser>(
2642            "function* gen() {
2643                 yield 1;
2644                 yield* other();
2645                 yield 2;
2646             }",
2647            "foo.js",
2648            |metric| {
2649                assert_eq!(metric.nexits.nexits_sum(), 3);
2650                insta::assert_json_snapshot!(
2651                    metric.nexits,
2652                    @r#"
2653                {
2654                  "sum": 3,
2655                  "average": 3.0,
2656                  "min": 0,
2657                  "max": 3
2658                }
2659                "#
2660                );
2661            },
2662        );
2663    }
2664
2665    #[test]
2666    fn typescript_yield_delegate_counts_as_exit() {
2667        check_metrics::<TypescriptParser>(
2668            "function* gen(): Generator<number> {
2669                 yield 1;
2670                 yield* other();
2671                 yield 2;
2672             }",
2673            "foo.ts",
2674            |metric| {
2675                assert_eq!(metric.nexits.nexits_sum(), 3);
2676                insta::assert_json_snapshot!(
2677                    metric.nexits,
2678                    @r#"
2679                {
2680                  "sum": 3,
2681                  "average": 3.0,
2682                  "min": 0,
2683                  "max": 3
2684                }
2685                "#
2686                );
2687            },
2688        );
2689    }
2690
2691    #[test]
2692    fn tsx_yield_delegate_counts_as_exit() {
2693        check_metrics::<TsxParser>(
2694            "function* gen(): Generator<number> {
2695                 yield 1;
2696                 yield* other();
2697                 yield 2;
2698             }",
2699            "foo.tsx",
2700            |metric| {
2701                assert_eq!(metric.nexits.nexits_sum(), 3);
2702                insta::assert_json_snapshot!(
2703                    metric.nexits,
2704                    @r#"
2705                {
2706                  "sum": 3,
2707                  "average": 3.0,
2708                  "min": 0,
2709                  "max": 3
2710                }
2711                "#
2712                );
2713            },
2714        );
2715    }
2716
2717    /// A handler with no `return` has zero exits (iRules has no `return`
2718    /// keyword node; `return` is a generic command matched by name).
2719    #[test]
2720    fn irules_no_exit() {
2721        check_metrics::<IrulesParser>(
2722            "when HTTP_REQUEST {
2723    set x 1
2724    log local0. $x
2725}
2726",
2727            "foo.irule",
2728            |metric| {
2729                assert_eq!(metric.nexits.nexits_sum(), 0);
2730            },
2731        );
2732    }
2733
2734    /// A `return` command contributes one exit.
2735    #[test]
2736    fn irules_return() {
2737        check_metrics::<IrulesParser>(
2738            "when HTTP_REQUEST {
2739    if { [HTTP::uri] eq \"/\" } {
2740        return
2741    }
2742    log local0. \"served\"
2743}
2744",
2745            "foo.irule",
2746            |metric| {
2747                assert_eq!(metric.nexits.nexits_sum(), 1);
2748            },
2749        );
2750    }
2751
2752    /// A multi-value `return` (`return [list ...]`) is a single command and
2753    /// counts once, not once per returned value.
2754    #[test]
2755    fn irules_multi_value_return_counts_once() {
2756        check_metrics::<IrulesParser>(
2757            "proc pair { a b } {
2758    return [list $a $b]
2759}
2760",
2761            "foo.irule",
2762            |metric| {
2763                assert_eq!(metric.nexits.nexits_sum(), 1);
2764            },
2765        );
2766    }
2767
2768    /// Objective-C method with no `return` and no `@throw` has zero exit
2769    /// points.
2770    #[test]
2771    fn objc_no_exit() {
2772        check_metrics::<ObjcParser>(
2773            "@implementation Foo
2774- (void)bar {
2775    [self doWork];
2776}
2777@end
2778",
2779            "foo.m",
2780            |metric| {
2781                assert_eq!(metric.nexits.nexits_sum(), 0);
2782                insta::assert_json_snapshot!(metric.nexits, @r#"
2783                {
2784                  "sum": 0,
2785                  "average": 0.0,
2786                  "min": 0,
2787                  "max": 0
2788                }
2789                "#);
2790            },
2791        );
2792    }
2793
2794    /// Objective-C exit set is `return_statement` + `@throw`
2795    /// (`throw_statement`): a method with one of each counts 2.
2796    #[test]
2797    fn objc_return_and_throw() {
2798        check_metrics::<ObjcParser>(
2799            "@implementation Foo
2800- (int)bar:(int)x {
2801    if (x < 0) {
2802        @throw [NSException exceptionWithName:@\"e\" reason:@\"r\" userInfo:nil];
2803    }
2804    return x;
2805}
2806@end
2807",
2808            "foo.m",
2809            |metric| {
2810                assert_eq!(metric.nexits.nexits_sum(), 2);
2811                insta::assert_json_snapshot!(metric.nexits, @r#"
2812                {
2813                  "sum": 2,
2814                  "average": 2.0,
2815                  "min": 0,
2816                  "max": 2
2817                }
2818                "#);
2819            },
2820        );
2821    }
2822}